[
  {
    "path": ".dockerignore",
    "content": "app/node_modules\napp/src-tauri\napp/.idea\napp/.vscode\napp/dist\napp/dev-dist\napp/dist-ssr\napp/target\napp/tauri.conf.json\napp/tauri.js\n\nscreenshot\n.vscode\n.idea\nconfig.yaml\nconfig.dev.yaml\n\n# current in ~/storage\naddition/generation/data/*\n!addition/generation/data/.gitkeep\n\naddition/article/data/*\n!addition/article/data/.gitkeep\nsdk\nlogs\n\nchat\nchat.exe\n\n# for reverse engine\nreverse\naccess.json\naccess/*.json\n\ndb\ncache\nconfig\n\nREADME.md\n.gitignore\nscreenshot\nLICENSE\n\n.github\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: 报告问题 | Bug Report\nabout: 使用简练详细的语言描述你遇到的问题 | Describe the issue you encountered in detail\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n[//]: # (方框内删除已有的空格，填 x 号)\n+ [ ] 我已确认目前没有类似 issue\n+ [ ] 我已确认我已升级到最新版本\n+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案\n+ [ ] 我理解并愿意跟进此 issue，协助测试和提供反馈\n+ [ ] 我将以礼貌和尊重的态度提问，不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)\n+ [ ] 我理解并认可上述内容，并理解项目维护者精力有限，**不遵循规则的 issue 可能会被无视或直接关闭**\n\n**问题描述**\n\n**复现步骤**\n\n**预期结果**\n\n**日志信息**\n\n**相关截图 (如果有)**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/channel_update.md",
    "content": "---\nname: 渠道更新 | Channel Update\nabout: 新大模型供应商格式增加、更新请求 | Request to add or update a new llm provider format\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n[//]: # (方框内删除已有的空格，填 x 号)\n+ [ ] 我已确认目前没有类似 issue\n+ [ ] 我已确认我已升级到最新版本\n+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案\n+ [ ] 我理解并愿意跟进此 issue，协助测试和提供反馈\n+ [ ] 我将以礼貌和尊重的态度提问，不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)\n+ [ ] 如果为新供应商格式，我已确认此供应商有一定的用户群体和知名度，借此以广告和推广类的名义的中转站点请求将被直接关闭\n+ [ ] 我理解并认可上述内容，并理解项目维护者精力有限，**不遵循规则的 issue 可能会被无视或直接关闭**\n\n**供应商名称**\n\n**描述**\n\n**供应商网址 / 截图 / 样例 (如果愿意提供)**"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Discord\n    url: https://discord.gg/rpzNSmqaF2\n    about: Join Discord Community\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: 功能请求 | Feature Request\nabout: 使用简练详细的语言描述希望加入的新功能 | Describe the feature you would like to request\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n[//]: # (方框内删除已有的空格，填 x 号)\n+ [ ] 我已确认目前没有类似 issue\n+ [ ] 我已确认我已升级到最新版本\n+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案\n+ [ ] 我理解并愿意跟进此 issue，协助测试和提供反馈\n+ [ ] 我将以礼貌和尊重的态度提问，不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)\n+ [ ] 我理解并认可上述内容，并理解项目维护者精力有限，**不遵循规则的 issue 可能会被无视或直接关闭**\n\n**功能描述**\n\n**相关截图 (如果有)**\n"
  },
  {
    "path": ".github/workflows/app.yaml",
    "content": "name: Release App\n\non:\n  workflow_dispatch:\n  release:\n    types: [published]\n\njobs:\n  create-release:\n    permissions:\n      contents: write\n    runs-on: ubuntu-latest\n    outputs:\n      release_id: ${{ steps.create-release.outputs.result }}\n\n    steps:\n      - uses: actions/checkout@v3\n      - name: setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - name: get version\n        run: |\n          cd app\n          echo \"PACKAGE_VERSION=$(node -p \"require('./src-tauri/tauri.conf.json').package.version\")\" >> $GITHUB_ENV\n      - name: create release\n        id: create-release\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { data } = await github.rest.repos.getLatestRelease({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n            })\n            return data.id\n\n  build-tauri:\n    needs: create-release\n    permissions:\n      contents: write\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - os: ubuntu-latest\n            arch: x86_64\n            rust_target: x86_64-unknown-linux-gnu\n          - os: macos-latest\n            arch: x86_64\n            rust_target: x86_64-apple-darwin\n          - os: macos-latest\n            arch: aarch64\n            rust_target: aarch64-apple-darwin\n          - os: windows-latest\n            arch: x86_64\n            rust_target: x86_64-pc-windows-msvc\n\n    runs-on: ${{ matrix.config.os }}\n    steps:\n      - uses: actions/checkout@v3\n      - name: setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - name: install Rust stable\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.config.rust_target }}\n      - uses: Swatinem/rust-cache@v2\n        with:\n          key: ${{ matrix.config.rust_target }}\n      - name: install dependencies (ubuntu only)\n        if: matrix.config.os == 'ubuntu-latest'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf\n      - name: install frontend dependencies\n        run: |\n          cd app\n          npm install -g pnpm\n          pnpm install\n      - uses: tauri-apps/tauri-action@v0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n        with:\n          releaseId: ${{ needs.create-release.outputs.release_id }}\n          projectPath: ./app\n\n  publish-release:\n    permissions:\n      contents: write\n    runs-on: ubuntu-latest\n    needs: [create-release, build-tauri]\n\n    steps:\n      - name: publish release\n        id: publish-release\n        uses: actions/github-script@v6\n        env:\n          release_id: ${{ needs.create-release.outputs.release_id }}\n        with:\n          script: |\n            github.rest.repos.updateRelease({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              release_id: process.env.release_id,\n              draft: false,\n              prerelease: false\n            })\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Build Test\non:\n  push:\n    branches:\n      - '*'\n  pull_request:\n    branches:\n      - '*'\njobs:\n  release:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [ 18.x ]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Use Golang\n        uses: actions/setup-go@v4\n        with:\n          go-version: '1.20'\n\n      - name: Build Backend\n        run: |\n          go build .\n\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Build Frontend\n        run: |\n          cd app\n          npm install -g pnpm\n          pnpm install\n          pnpm build\n      - name: Upload a Build Artifact\n        uses: actions/upload-artifact@v4.0.0\n        with:\n          name: Build result\n          path: app/dist\n"
  },
  {
    "path": ".github/workflows/docker-cd.yaml",
    "content": "name: Docker CD\n\non:\n  release:\n    types: [created]\n\njobs:\n\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n\n      - name: Build and push Docker images\n        uses: docker/build-push-action@v2\n        with:\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: programzmh/chatnio:stable\n"
  },
  {
    "path": ".github/workflows/docker-ci.yaml",
    "content": "name: Docker Image CI\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n        with:\n          platforms: \"arm64,amd64\"\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}\n\n      - name: Get version\n        run: echo \"VERSION=$(node -p \"require('./app/src/conf/version.json').version\")\" >> $GITHUB_ENV\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Build and push Docker images\n        uses: docker/build-push-action@v3\n        with:\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: |\n            programzmh/chatnio:latest\n            programzmh/chatnio:${{ env.VERSION }}\n          cache-from: |\n            type=registry,ref=programzmh/chatnio:buildcache\n            type=gha\n          cache-to: |\n            type=registry,ref=programzmh/chatnio:buildcache,mode=max\n            type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/issue-translator.yaml",
    "content": "name: Issue Translator\non: \n  issue_comment: \n    types: [created]\n  issues: \n    types: [opened]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: usthe/issues-translate-action@v2.7\n        with:\n          IS_MODIFY_TITLE: false\n          CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.\n"
  },
  {
    "path": ".gitignore",
    "content": "app/node_modules\n.vscode\n.idea\nconfig.yaml\nconfig.dev.yaml\nstorage\n\naddition/generation/data/*\n!addition/generation/data/.gitkeep\n\naddition/article/data/*\n!addition/article/data/.gitkeep\nsdk\nlogs\n\nchat\n*.exe\nchat.exe\n\n# for reverse engine\nreverse\naccess.json\naccess/*.json\n\ndb\nredis\nconfig\npresets\n\nkey/*\n!key/.gitkeep\n\n# for https://github.com/di-sukharev/opencommit\n.env"
  },
  {
    "path": "Dockerfile",
    "content": "# Author: ProgramZmh\n# License: Apache-2.0\n# Description: Dockerfile for chatnio\n\nFROM --platform=$TARGETPLATFORM golang:1.20-alpine AS backend\n\nWORKDIR /backend\nCOPY . .\n\n# Set go proxy to https://goproxy.cn (open for vps in China Mainland)\n# RUN go env -w GOPROXY=https://goproxy.cn,direct\nARG TARGETARCH\nARG TARGETOS\nENV GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on CGO_ENABLED=1\n\n# Install build dependencies\nRUN apk update && \\\n    apk add --no-cache \\\n    gcc \\\n    musl-dev \\\n    g++ \\\n    make \\\n    linux-headers\n\n# Build backend\nRUN go build -o chat -a -ldflags=\"-extldflags=-static\" .\n\nFROM node:18 AS frontend\n\nWORKDIR /app\nCOPY ./app .\n\nRUN npm install -g pnpm && \\\n    pnpm install && \\\n    pnpm run build && \\\n    rm -rf node_modules src\n\n\nFROM alpine\n\n# Install dependencies\nRUN apk upgrade --no-cache && \\\n    apk add --no-cache wget ca-certificates tzdata && \\\n    update-ca-certificates 2>/dev/null || true\n\n# Set timezone\nRUN echo \"Asia/Shanghai\" > /etc/timezone && \\\n    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime\n\nWORKDIR /\n\n# Copy dist\nCOPY --from=backend /backend/chat /chat\nCOPY --from=backend /backend/config.example.yaml /config.example.yaml\nCOPY --from=backend /backend/utils/templates /utils/templates\nCOPY --from=backend /backend/addition/article/template.docx /addition/article/template.docx\nCOPY --from=frontend /app/dist /app/dist\n\n# Volumes\nVOLUME [\"/config\", \"/logs\", \"/storage\"]\n\n# Expose port\nEXPOSE 8094\n\n# Run application\nCMD [\"./chat\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n![chatnio](/app/public/logo.png)\n\n# [🥳 CoAI.Dev](https://coai.dev)\n\n#### 🚀 Next Generation AIGC One-Stop Business Solution\n\n#### *\"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)\"*\n\n\nEnglish · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [Docs](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [Deployment Guide](https://coai.dev/docs/deploy)\n\n[![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369)\n\n<img alt=\"CoAI.Dev Preview\" src=\"./screenshot/coai.png\" width=\"100%\" style=\"border-radius: 8px\">\n\n</div>\n\n## 📝 Features\n1. 🤖️ **Rich Model Support**: Multi-model service provider support (OpenAI / Anthropic / Gemini / Midjourney and more than ten compatible formats & private LLM support)\n2. 🤯 **Beautiful UI Design**: UI compatible with PC / Pad / Mobile, following [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) design standards, rich and beautiful interface design and backend dashboard\n3. 🎃 **Complete Markdown Support**: Support for **LaTeX formulas** / **Mermaid mind maps** / table rendering / code highlighting / chart drawing / progress bars and other advanced Markdown syntax support\n4. 👀 **Multi-theme Support**: Support for multiple theme switching, including **Light Mode** for light themes and **Dark Mode** for dark themes. 👉 [Custom Color Scheme](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)\n5. 📚 **Internationalization Support**: Support for internationalization, multi-language switching 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 Welcome to contribute translations [Pull Request](https://github.com/coaidev/coai/pulls)\n6. 🎨 **Text-to-Image Support**: Support for multiple text-to-image models: **OpenAI DALL-E**✅ & **Midjourney** (support for **U/V/R** operations)✅ & Stable Diffusion✅ etc.\n7. 📡 **Powerful Conversation Sync**: **Zero-cost cross-device conversation sync support for users**, support for **conversation sharing** (link sharing & save as image & share management), **no need for WebDav / WebRTC and other dependencies and complex learning costs**\n8. 🎈 **Model Market & Preset System**: Support for customizable model market in the backend, providing model introductions, tags, and other parameters. Site owners can customize model introductions according to the situation. Also supports a preset system, including **custom presets** and **cloud synchronization** functions.\n9. 📖 **Rich File Parsing**: **Out-of-the-box**, supports file parsing for **all models** (PDF / Docx / Pptx / Excel / image formats parsing), **supports more cloud image storage solutions** (S3 / R2 / MinIO etc.), **supports OCR image recognition** 👉 See project [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) for details (supports Vercel / Docker one-click deployment)\n10. 🌏 **Full Model Internet Search**: Based on the [SearXNG](https://github.com/searxng/searxng) open-source engine, supports rich search engines such as Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant, supports safe search mode, content truncation, image proxy, test search availability, and other functions.\n11. 💕 **Progressive Web App (PWA)**: Supports PWA applications & desktop support (desktop based on [Tauri](https://github.com/tauri-apps/tauri))\n12. 🤩 **Comprehensive Backend Management**: Supports beautiful and rich dashboard, announcement & notification management, user management, subscription management, gift code & redemption code management, price setting, subscription setting, custom model market, custom site name & logo, SMTP email settings, and other functions\n13. 🤑 **Multiple Billing Methods**: Supports 💴 **Subscription** and 💴 **Elastic Billing** two billing methods. Elastic billing supports per-request billing / token billing / no billing / anonymous calls and **minimum request points** detection and other powerful features\n14. 🎉 **Innovative Model Caching**: Supports enabling model caching: i.e., under the same request parameter hash, if it has been requested before, it will directly return the cached result (hitting the cache will not be billed), reducing the number of requests. You can customize whether to cache models, cache time, multiple cache result numbers, and other advanced cache settings\n15. 🥪 **Additional Features** (Support Discontinued): 🍎 **AI Project Generator Function** / 📂 **Batch Article Generation Function** / 🥪 **AI Card Function** (Deprecated)\n16. 😎 **Excellent Channel Management**: Self-written excellent channel algorithm, supports ⚡ **multi-channel management**, supports 🥳**priority** setting for channel call order, supports 🥳**weight** setting for load balancing probability distribution of channels at the same priority, supports 🥳**user grouping**, 🥳**automatic retry on failure**, 🥳**model redirection**, 🥳**built-in upstream hiding**, 🥳**channel status management** and other powerful **enterprise-level functions**\n17. ⭐ **OpenAI API Distribution & Proxy System**: Supports calling various large models in **OpenAI API** standard format, integrates powerful channel management functions, only needs to deploy one site to achieve simultaneous development of B/C-end business💖\n18. 👌 **Quick Upstream Synchronization**: Channel settings, model market, price settings, and other settings can quickly synchronize with upstream sites, modify your site configuration based on this, quickly build your site, save time and effort, one-click synchronization, quick launch\n19. 👋 **SEO Optimization**: Supports SEO optimization, supports custom site name, site logo, and other SEO optimization settings to make search engines crawl faster, making your site stand out👋\n20. 🎫 **Multiple Redemption Code Systems**: Supports multiple redemption code systems, supports gift codes and redemption codes, supports batch generation, gift codes are suitable for promotional distribution, redemption codes are suitable for card sales, for gift codes of one type, a user can only redeem one code, which to some extent reduces the situation of one user redeeming multiple times in promotions😀\n21. 🥰 **Business-Friendly License**: Adopts the **Apache-2.0** open-source license, friendly for commercial secondary development & distribution (please also comply with the provisions of the Apache-2.0 license, do not use for illegal purposes)\n\n> ### ✨ CoAI.Dev Business\n>\n> ![Pro Version Preview](./screenshot/coai-pro.png)\n>\n> - ✅ Beautiful commercial-grade UI, elegant frontend interface and backend management\n> - ✅ Supports TTS & STT, plugin marketplace, RAG knowledge base and other rich features and modules\n> - ✅ More payment providers, more billing models and advanced order management\n> - ✅ Supports more authentication methods, including SMS login, OAuth login, etc.\n> - ✅ Supports model monitoring, channel health detection, fault alarm automatic channel switching\n> - ✅ Supports multi-tenant API Key distribution system, enterprise-level token permission management and visitor restrictions\n> - ✅ Supports security auditing, logging, model rate limiting, API Gateway and other advanced features\n> - ✅ Supports promotion rewards, professional data statistics, user profile analysis and other business analysis capabilities\n> - ✅ Supports Discord/Telegram/Feishu and other bot integration capabilities (extension modules)\n> - ...\n>\n> [👉 Learn More](https://www.coai.dev/docs/contact)\n\n\n## 🔨 Supported Models\n1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*\n2. Anthropic Claude *(✅ Vision ✅ Function Calling)*\n3. Google Gemini & PaLM2 *(✅ Vision)*\n4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*\n5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)*\n6. Zhipu AI ChatGLM *(✅ Vision)*\n7. Alibaba Tongyi Qwen\n8. Tencent Hunyuan\n9. Baichuan AI\n10. Moonshot AI (👉 OpenAI)\n11. DeepSeek AI (👉 OpenAI)\n12. ByteDance Skylark *(✅ Function Calling)*\n13. Groq Cloud AI\n14. OpenRouter (👉 OpenAI)\n15. 360 GPT\n16. LocalAI / Ollama (👉 OpenAI)\n\n## 👻 OpenAI Compatible API Proxy\n   - [x] Chat Completions _(/v1/chat/completions)_\n   - [x] Image Generation _(/v1/images)_\n   - [x] Model List _(/v1/models)_\n   - [x] Dashboard Billing _(/v1/billing)_\n\n\n## 📦 Deployment\n> [!TIP]\n> **After successful deployment, the admin account is `root`, with the default password `chatnio123456`**\n\n### ✨ Zeabur (One-Click)\n[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI)\n\n> Zeabur provides a certain free quota, you can use non-paid regions for one-click deployment, and also supports plan subscriptions and elastic billing for flexible expansion.\n> 1. Click `Deploy` to deploy, and enter the domain name you wish to bind, wait for the deployment to complete.\n> 2. After deployment is complete, please visit your domain name and log in to the backend management using the username `root` and password `chatnio123456`. Please follow the prompts to change the password in the chatnio backend in a timely manner.\n\n### 🐳 BTPanel (One-Click)\n\n1. Install Baota Panel by visiting [BTPanel](https://www.bt.cn/new/download.html) and install using the stable version script.\n2. Log in to the panel and click **Docker** on the left to enter Docker management.\n3. If prompted that Docker / Docker Compose is not installed, you can install according to the guide above.\n4. After installation is complete, enter **App Store**, search for `CoAI` and click **Install**.\n5. Configure basic application information such as your domain name, port, etc., and click **Confirm** (default configuration can be used).\n6. First-time installation may take 1-2 minutes to complete database initialization. If you encounter issues, please check the panel running logs for troubleshooting.\n7. Visit your configured domain or server `http://[ip]:[port]`, log in to the backend management using username `root` and password `chatnio123456`.\n\n### AlibabaCloud ComputeNest (One-Click)\n[![Deploy on AlibabaCloud ComputeNest International Edition](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest-en.svg)](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition)\n1. Access the CoAI service on [ComputeNest International Edition](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition). If you are in China, please visit [ComputeNest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版), and fill in the deployment parameters as prompted.\n2. Select the payment type, fill in the instance parameters and network parameters, and click **Next: Confirm Order**.\n3. After confirming the deployment parameters and checking the estimated price, click Create Now and wait for the service instance to be deployed.\n4. Click **Service Instance** on the left. After the service instance is deployed, click the instance ID to enter the details interface.\n5. Click the address in **Use Now** on the details interface to enter the CoAI interface. The default username is `root` and the password is `chatnio123456` to log in to the backend management.\n6. For more operation details and payment information, see：[Service Details](https://computenest.console.aliyun.com/service/detail/ap-southeast-1/service-27e11d3a5c9b40628505/1?type=user&isRecommend=true).\n\n\n### ⚡ Docker Compose Installation (Recommended)\n> [!NOTE]\n> After successful execution, the host machine mapping address is `http://localhost:8000`\n\n```shell\ngit clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git\ncd chatnio\ndocker-compose up -d # Run the service\n# To use the stable version, use docker-compose -f docker-compose.stable.yaml up -d instead\n# To use Watchtower for automatic updates, use docker-compose -f docker-compose.watch.yaml up -d instead\n```\n\nVersion update (_If Watchtower automatic updates are enabled, manual updates are not necessary_):\n```shell\ndocker-compose down \ndocker-compose pull\ndocker-compose up -d\n```\n\n> - MySQL database mount directory: ~/**db**\n> - Redis database mount directory: ~/**redis**\n> - Configuration file mount directory: ~/**config**\n\n### ⚡ Docker Installation (Lightweight runtime, commonly used for external _MYSQL/RDS_ services)\n> [!NOTE]\n> After successful execution, the host machine address is `http://localhost:8094`.\n> \n> To use the stable version, use `programzmh/chatnio:stable` instead of `programzmh/chatnio:latest`\n\n```shell\ndocker run -d --name chatnio \\\n   --network host \\\n   -v ~/config:/config \\\n   -v ~/logs:/logs \\\n   -v ~/storage:/storage \\\n   -e MYSQL_HOST=localhost \\\n   -e MYSQL_PORT=3306 \\\n   -e MYSQL_DB=chatnio \\\n   -e MYSQL_USER=root \\\n   -e MYSQL_PASSWORD=chatnio123456 \\\n   -e REDIS_HOST=localhost \\\n   -e REDIS_PORT=6379 \\\n   -e SECRET=secret \\\n   -e SERVE_STATIC=true \\\n   programzmh/chatnio:latest\n```\n\n> - *--network host* means using the host machine's network, allowing the Docker container to use the host's network. You can modify this as needed.\n> - SECRET: JWT secret key, generate a random string and modify accordingly\n> - SERVE_STATIC: Whether to enable static file serving (normally this doesn't need to be changed, see FAQ below for details)\n> - *-v ~/config:/config* mounts the configuration file, *-v ~/logs:/logs* mounts the host machine directory for log files, *-v ~/storage:/storage* mounts the directory for additional feature generated files\n> - MySQL and Redis services need to be configured. Please refer to the information above to modify the environment variables accordingly\n\nVersion update (_After enabling Watchtower, manual updates are not necessary. After execution, follow the steps above to run again_):\n\n```shell\ndocker stop chatnio\ndocker rm chatnio\ndocker pull programzmh/chatnio:latest\n```\n\n### ⚒ Compile and Install\n\n> [!NOTE]\n> After successful deployment, the default port is **8094**, and the access address is `http://localhost:8094`\n> \n> Config settings (~/config/**config.yaml**) can be overridden using environment variables. For example, the `MYSQL_HOST` environment variable can override the `mysql.host` configuration item\n\n```shell\ngit clone https://github.com/coaidev/coai.git\ncd chatnio\n\ncd app\nnpm install -g pnpm\npnpm install\npnpm build\n\ncd ..\ngo build -o chatnio\n\n# e.g. using nohup (you can also use systemd or other service manager)\nnohup ./chatnio > output.log & # using nohup to run in background\n```\n\n## 📦 Tech Stack\n\n- 🥗 Frontend: React + Redux + Radix UI + Tailwind CSS\n- 🍎 Backend: Golang + Gin + Redis + MySQL\n- 🍒 Application Technology: PWA + WebSocket\n\n## 🤯 Why Create This Project & Project Advantages\n\n- We found that most AIGC commercial sites on the market are frontend-oriented lightweight deployment projects with beautiful UI interface designs, such as the commercial version of [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web). Due to its personal privatization-oriented design, there are some limitations in secondary commercial development, presenting some issues, such as:\n  1. **Difficult conversation synchronization**, for example, requiring services like WebDav, high user learning costs, and difficulties in real-time cross-device synchronization.\n  2. **Insufficient billing**, for example, only supporting elastic billing or only subscription-based, unable to meet the needs of different users.\n  3. **Inconvenient file parsing**, for example, only supporting uploading images to an image hosting service first, then returning to the site to input the URL direct link in the input box, without built-in file parsing functionality.\n  4. **No support for conversation URL sharing**, for example, only supporting conversation screenshot sharing, unable to support conversation URL sharing (or only supporting tools like ShareGPT, which cannot promote the site).\n  5. **Insufficient channel management**, for example, the backend only supports OpenAI format channels, making it difficult to be compatible with other format channels. And only one channel can be filled in, unable to support multi-channel management.\n  6. **No API call support**, for example, only supporting user interface calls, unable to support API proxying and management.\n\n- Another type is API distribution-oriented sites with powerful distribution systems, such as projects based on [One API](https://github.com/songquanpeng/one-api).\nAlthough these projects support powerful API proxying and management, they lack interface design and some C-end features, such as:\n  1. **Insufficient user interface**, for example, only supporting API calls, without built-in user interface chat. User interface chat requires manually copying the key and going to other sites to use, which has a high learning cost for ordinary users.\n  2. **No subscription system**, for example, only supporting elastic billing, lacking billing design for C-end users, unable to meet different user needs, and not user-friendly in terms of cost perception for users without a foundation.\n  3. **Insufficient C-end features**, for example, only supporting API calls, not supporting conversation synchronization, conversation sharing, file parsing, and other functions.\n  4. **Insufficient load balancing**, the open-source version does not support the **weight** parameter, unable to achieve balanced load distribution probability for channels at the same priority ([New API](https://github.com/Calcium-Ion/new-api) also solves this pain point, with a more beautiful UI).\n\nTherefore, we hope to combine the advantages of these two types of projects to create a project that has both a powerful API distribution system and a rich user interface design,\nthus meeting the needs of C-end users while developing B-end business, improving user experience, reducing user learning costs, and increasing user stickiness.\n\nThus, **CoAI.Dev** was born. We hope to create a project that has both a powerful API distribution system and a rich user interface design, becoming the next-generation open-source AIGC project's one-stop commercial solution.\n\n\n## ❤ Donations\n\nIf you find this project helpful, you can give it a Star to show your support!\n"
  },
  {
    "path": "README_ja-JP.md",
    "content": "<div align=\"center\">\n\n![chatnio](/app/public/logo.png)\n\n# [🥳 CoAI.Dev](https://coai.dev)\n\n#### 🚀 次世代AIGCワンストップビジネスソリューション\n\n#### *\"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)\"*\n\n\nEnglish · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [ドキュメント](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [デプロイガイド](https://coai.dev/docs/deploy)\n\n[![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369)\n\n<img alt=\"CoAI.Dev Preview\" src=\"./screenshot/coai.png\" width=\"100%\" style=\"border-radius: 8px\">\n\n</div>\n\n## 📝 機能\n1. 🤖️ **豊富なモデルサポート**: 複数のモデルサービスプロバイダーのサポート (OpenAI / Anthropic / Gemini / Midjourney など十数種類の互換フォーマット & プライベートLLMサポート)\n2. 🤯 **美しいUIデザイン**: PC / Pad / モバイルに対応したUI、[Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) のデザイン基準に従った豊富で美しいインターフェースデザインとバックエンドダッシュボード\n3. 🎃 **完全なMarkdownサポート**: **LaTeX数式** / **Mermaidマインドマップ** / テーブルレンダリング / コードハイライト / チャート描画 / プログレスバーなどの高度なMarkdown構文サポート\n4. 👀 **マルチテーマサポート**: 複数のテーマ切り替えをサポート、ライトテーマの**ライトモード**とダークテーマの**ダークモード**を含む。 👉 [カスタムカラースキーム](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)\n5. 📚 **国際化サポート**: 国際化をサポートし、複数の言語切り替えをサポート 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 翻訳の貢献を歓迎します [Pull Request](https://github.com/coaidev/coai/pulls)\n6. 🎨 **テキストから画像へのサポート**: 複数のテキストから画像へのモデルをサポート: **OpenAI DALL-E**✅ & **Midjourney** ( **U/V/R** 操作をサポート)✅ & Stable Diffusion✅ など\n7. 📡 **強力な会話同期**: **ユーザーのゼロコストクロスデバイス会話同期サポート**、**会話共有** (リンク共有 & 画像として保存 & 共有管理) をサポート、**WebDav / WebRTCなどの依存関係や複雑な学習コストは不要**\n8. 🎈 **モデル市場 & プリセットシステム**: バックエンドでカスタマイズ可能なモデル市場をサポートし、モデルの紹介、タグなどのパラメータを提供。サイトオーナーは状況に応じてモデルの紹介をカスタマイズできます。また、**カスタムプリセット**と**クラウド同期**機能を含むプリセットシステムもサポート。\n9. 📖 **豊富なファイル解析**: **すぐに使える**、**すべてのモデル**のファイル解析をサポート (PDF / Docx / Pptx / Excel / 画像形式の解析)、**より多くのクラウド画像ストレージソリューション** (S3 / R2 / MinIO など) をサポート、**OCR画像認識**をサポート 👉 詳細はプロジェクト [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) を参照 (Vercel / Dockerのワンクリックデプロイをサポート)\n10. 🌏 **全モデルインターネット検索**: [SearXNG](https://github.com/searxng/searxng) オープンソースエンジンに基づき、Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant などの豊富な検索エンジン検索をサポート、安全検索モード、コンテンツ切り捨て、画像プロキシ、検索可用性テストなどの機能をサポート。\n11. 💕 **プログレッシブウェブアプリ (PWA)**: PWAアプリケーションをサポートし、デスクトップサポート (デスクトップは [Tauri](https://github.com/tauri-apps/tauri) に基づく)\n12. 🤩 **包括的なバックエンド管理**: 美しく豊富なダッシュボード、公告 & 通知管理、ユーザー管理、サブスクリプション管理、ギフトコード & 交換コード管理、価格設定、サブスクリプション設定、カスタムモデル市場、カスタムサイト名 & ロゴ、SMTPメール設定などの機能をサポート\n13. 🤑 **複数の課金方法**: 💴 **サブスクリプション** と 💴 **エラスティック課金** の2つの課金方法をサポート。エラスティック課金は、リクエストごとの課金 / トークン課金 / 無課金 / 匿名通話をサポートし、**最小リクエストポイント** 検出などの強力な機能をサポート\n14. 🎉 **革新的なモデルキャッシュ**: モデルキャッシュの有効化をサポート: 同じリクエストパラメータハッシュの下で、以前にリクエストされた場合、キャッシュ結果を直接返します (キャッシュヒットは課金されません)、リクエスト回数を減らします。キャッシュするモデル、キャッシュ時間、複数のキャッシュ結果数などの高度なキャッシュ設定をカスタマイズできます\n15. 🥪 **追加機能** (サポート終了): 🍎 **AIプロジェクトジェネレーター機能** / 📂 **バッチ記事生成機能** / 🥪 **AIカード機能** (廃止)\n16. 😎 **優れたチャネル管理**: 自作の優れたチャネルアルゴリズム、⚡ **マルチチャネル管理** をサポート、チャネル呼び出し順序の設定をサポート、同じ優先度のチャネルの負荷分散確率の設定をサポート、**ユーザーグループ化**、**失敗時の自動再試行**、**モデルリダイレクト**、**組み込みの上流非表示**、**チャネル状態管理** などの強力な**企業レベルの機能**をサポート\n17. ⭐ **OpenAI API分散 & プロキシシステム**: **OpenAI API** 標準フォーマットでさまざまな大規模モデルを呼び出すことをサポートし、強力なチャネル管理機能を統合。1つのサイトをデプロイするだけで、B/Cエンドビジネスの同時開発を実現💖\n18. 👌 **迅速な上流同期**: チャネル設定、モデル市場、価格設定などの設定を迅速に上流サイトと同期し、これに基づいてサイト設定を変更し、迅速にサイトを構築し、時間と労力を節約し、ワンクリック同期、迅速なオンライン化を実現\n19. 👋 **SEO最適化**: SEO最適化をサポートし、カスタムサイト名、サイトロゴなどのSEO最適化設定をサポートし、検索エンジンがより速くクロールできるようにし、サイトを際立たせます👋\n20. 🎫 **複数の交換コードシステム**: 複数の交換コードシステムをサポートし、ギフトコードと交換コードをサポートし、バッチ生成をサポート。ギフトコードはプロモーション配布に適しており、交換コードはカード販売に適しています。ギフトコードの1つのタイプの複数のコードは、1人のユーザーが1つのコードしか交換できないため、プロモーション中に1人のユーザーが複数回交換する状況をある程度減らします😀\n21. 🥰 **ビジネスフレンドリーなライセンス**: **Apache-2.0** オープンソースライセンスを採用し、商用の二次開発 & 配布にフレンドリー (Apache-2.0ライセンスの規定を遵守し、違法な目的で使用しないでください)\n\n> ### ✨ CoAI.Dev ビジネス版\n>\n> ![Pro Version Preview](./screenshot/coai-pro.png)\n>\n> - ✅ 美しい商用グレードのUI、エレガントなフロントエンドインターフェースとバックエンド管理\n> - ✅ TTS & STT、プラグインマーケットプレイス、RAGナレッジベースなどの豊富な機能とモジュールをサポート\n> - ✅ より多くの支払いプロバイダー、より多くの課金モデルと高度な注文管理をサポート\n> - ✅ SMSログイン、OAuthログインなど、より多くの認証方法をサポート\n> - ✅ モデル監視、チャネルの健康状態検出、障害アラーム自動チャネル切り替えをサポート\n> - ✅ マルチテナントAPIキー配布システム、企業レベルのトークン権限管理と訪問者制限をサポート\n> - ✅ セキュリティ監査、ログ記録、モデルレート制限、APIゲートウェイなどの高度な機能をサポート\n> - ✅ プロモーション報酬、プロフェッショナルなデータ統計、ユーザープロファイル分析などのビジネス分析能力をサポート\n> - ✅ Discord/Telegram/Feishuなどのボット統合機能をサポート (拡張モジュール)\n> - ...\n>\n> [👉 詳細はこちら](https://www.coai.dev/docs/contact)\n\n\n## 🔨 サポートされているモデル\n1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*\n2. Anthropic Claude *(✅ Vision ✅ Function Calling)*\n3. Google Gemini & PaLM2 *(✅ Vision)*\n4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*\n5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)*\n6. Zhipu AI ChatGLM *(✅ Vision)*\n7. Alibaba Tongyi Qwen\n8. Tencent Hunyuan\n9. Baichuan AI\n10. Moonshot AI (👉 OpenAI)\n11. DeepSeek AI (👉 OpenAI)\n12. ByteDance Skylark *(✅ Function Calling)*\n13. Groq Cloud AI\n14. OpenRouter (👉 OpenAI)\n15. 360 GPT\n16. LocalAI / Ollama (👉 OpenAI)\n\n## 👻 OpenAI互換APIプロキシ\n   - [x] Chat Completions _(/v1/chat/completions)_\n   - [x] Image Generation _(/v1/images)_\n   - [x] Model List _(/v1/models)_\n   - [x] Dashboard Billing _(/v1/billing)_\n\n\n## 📦 デプロイ\n> [!TIP]\n> **デプロイが成功した後、管理者アカウントは `root` で、デフォルトのパスワードは `chatnio123456` です**\n\n### ✨ Zeabur (ワンクリック)\n[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI)\n\n> Zeaburは一定の無料クォータを提供しており、非有料地域でのワンクリックデプロイをサポートし、プランサブスクリプションとエラスティック課金による柔軟な拡張もサポートしています。\n> 1. `Deploy` をクリックしてデプロイし、バインドしたいドメイン名を入力し、デプロイが完了するのを待ちます。\n> 2. デプロイが完了したら、ドメイン名にアクセスし、ユーザー名 `root` とパスワード `chatnio123456` を使用してバックエンド管理にログインします。チャットニオのバックエンドでパスワードを変更するように指示に従ってください。\n\n### ⚡ Docker Composeインストール (推奨)\n> [!NOTE]\n> 実行が成功した後、ホストマシンのマッピングアドレスは `http://localhost:8000` です\n\n```shell\ngit clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git\ncd chatnio\ndocker-compose up -d # サービスを実行\n# 安定版を使用するには、docker-compose -f docker-compose.stable.yaml up -d を使用してください\n# Watchtowerを使用して自動更新するには、docker-compose -f docker-compose.watch.yaml up -d を使用してください\n```\n\nバージョン更新 (_Watchtower自動更新が有効な場合、手動更新は不要です_):\n```shell\ndocker-compose down \ndocker-compose pull\ndocker-compose up -d\n```\n\n> - MySQLデータベースマウントディレクトリ: ~/**db**\n> - Redisデータベースマウントディレクトリ: ~/**redis**\n> - 設定ファイルマウントディレクトリ: ~/**config**\n\n### ⚡ Dockerインストール (軽量ランタイム、外部 _MYSQL/RDS_ サービスでよく使用されます)\n> [!NOTE]\n> 実行が成功した後、ホストマシンのアドレスは `http://localhost:8094` です。\n> \n> 安定版を使用するには、`programzmh/chatnio:stable` を `programzmh/chatnio:latest` の代わりに使用してください\n\n```shell\ndocker run -d --name chatnio \\\n   --network host \\\n   -v ~/config:/config \\\n   -v ~/logs:/logs \\\n   -v ~/storage:/storage \\\n   -e MYSQL_HOST=localhost \\\n   -e MYSQL_PORT=3306 \\\n   -e MYSQL_DB=chatnio \\\n   -e MYSQL_USER=root \\\n   -e MYSQL_PASSWORD=chatnio123456 \\\n   -e REDIS_HOST=localhost \\\n   -e REDIS_PORT=6379 \\\n   -e SECRET=secret \\\n   -e SERVE_STATIC=true \\\n   programzmh/chatnio:latest\n```\n\n> - *--network host* はホストマシンのネットワークを使用することを意味し、Dockerコンテナがホストのネットワークを使用できるようにします。必要に応じて変更できます。\n> - SECRET: JWTシークレットキー、ランダムな文字列を生成して適宜変更\n> - SERVE_STATIC: 静的ファイルの提供を有効にするかどうか (通常、これを変更する必要はありません。詳細はFAQを参照)\n> - *-v ~/config:/config* は設定ファイルをマウントし、*-v ~/logs:/logs* はログファイルのホストマシンディレクトリをマウントし、*-v ~/storage:/storage* は追加機能の生成ファイルのディレクトリをマウント\n> - MySQLとRedisサービスを設定する必要があります。上記の情報を参照して環境変数を適宜変更してください\n\nバージョン更新 (_Watchtowerを有効にした後、手動更新は不要です。実行後、上記の手順に従って再度実行してください_):\n\n```shell\ndocker stop chatnio\ndocker rm chatnio\ndocker pull programzmh/chatnio:latest\n```\n\n### ⚒ コンパイルとインストール\n\n> [!NOTE]\n> デプロイが成功した後、デフォルトのポートは **8094** で、アクセスアドレスは `http://localhost:8094` です\n> \n> 設定項目 (~/config/**config.yaml**) は環境変数を使用して上書きできます。例えば、`MYSQL_HOST` 環境変数は `mysql.host` 設定項目を上書きできます\n\n```shell\ngit clone https://github.com/coaidev/coai.git\ncd chatnio\n\ncd app\nnpm install -g pnpm\npnpm install\npnpm build\n\ncd ..\ngo build -o chatnio\n\n# 例: nohupを使用 (systemdや他のサービスマネージャーも使用できます)\nnohup ./chatnio > output.log & # バックグラウンドで実行するためにnohupを使用\n```\n\n## 📦 技術スタック\n\n- 🥗 フロントエンド: React + Redux + Radix UI + Tailwind CSS\n- 🍎 バックエンド: Golang + Gin + Redis + MySQL\n- 🍒 アプリケーション技術: PWA + WebSocket\n\n## 🤯 なぜこのプロジェクトを作成したのか & プロジェクトの利点\n\n- 市場にあるほとんどのAIGC商用サイトは、[Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) の商用版のように、美しいUIインターフェースデザインを持つフロントエンド指向の軽量デプロイプロジェクトです。個人のプライベート化指向の設計のため、二次商用開発にはいくつかの制限があり、いくつかの問題が発生します。例えば:\n  1. **会話の同期が難しい**: 例えば、WebDavなどのサービスが必要で、ユーザーの学習コストが高く、クロスデバイスのリアルタイム同期が困難です。\n  2. **課金が不十分**: 例えば、エラスティック課金のみをサポートするか、サブスクリプションのみをサポートし、異なるユーザーのニーズを満たすことができません。\n  3. **ファイル解析が不便**: 例えば、最初に画像ホスティングサービスに画像をアップロードし、サイトに戻って入力ボックスにURL直リンクを入力する必要があり、組み込みのファイル解析機能がありません。\n  4. **会話URL共有のサポートがない**: 例えば、会話のスクリーンショット共有のみをサポートし、会話URL共有をサポートしません (またはShareGPTなどのツールのみをサポートし、サイトのプロモーションには役立ちません)。\n  5. **チャネル管理が不十分**: 例えば、バックエンドはOpenAIフォーマットのチャネルのみをサポートし、他のフォーマットのチャネルとの互換性が難しいです。そして、1つのチャネルしか入力できず、マルチチャネル管理をサポートしません。\n  6. **API呼び出しのサポートがない**: 例えば、ユーザーインターフェース呼び出しのみをサポートし、APIプロキシと管理をサポートしません。\n\n- もう1つのタイプは、[One API](https://github.com/songquanpeng/one-api) に基づくプロジェクトのように、強力な分散システムを持つAPI分散指向のサイトです。\nこれらのプロジェクトは強力なAPIプロキシと管理をサポートしていますが、インターフェースデザインといくつかのCエンド機能が不足しています。例えば:\n  1. **ユーザーインターフェースが不十分**: 例えば、API呼び出しのみをサポートし、組み込みのユーザーインターフェースチャットがありません。ユーザーインターフェースチャットは、手動でキーをコピーして他のサイトに行って使用する必要があり、普通のユーザーにとって学習コストが高いです。\n  2. **サブスクリプションシステムがない**: 例えば、エラスティック課金のみをサポートし、Cエンドユーザーの課金設計が不足しており、異なるユーザーのニーズを満たすことができず、基礎のないユーザーにとってコスト感知がフレンドリーではありません。\n  3. **Cエンド機能が不十分**: 例えば、API呼び出しのみをサポートし、会話同期、会話共有、ファイル解析などの機能をサポートしません。\n  4. **負荷分散が不十分**: オープンソース版は**重み**パラメータをサポートしておらず、同じ優先度のチャネルの負荷分散確率を実現できません ([New API](https://github.com/Calcium-Ion/new-api) もこの痛点を解決し、UIもより美しいです)。\n\nしたがって、これら2つのタイプのプロジェクトの利点を組み合わせて、強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、\nCエンドユーザーのニーズを満たしながらBエンドビジネスを開発し、ユーザーエクスペリエンスを向上させ、ユーザーの学習コストを削減し、ユーザーの粘着性を高めることを目指しています。\n\nそのため、**CoAI.Dev** が誕生しました。強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、次世代のオープンソースAIGCプロジェクトのワンストップ商用ソリューションになることを目指しています。\n\n\n## ❤ 寄付\n\nこのプロジェクトが役立つと思われる場合は、Starをクリックしてサポートを示すことができます！\n"
  },
  {
    "path": "README_zh-CN.md",
    "content": "<div align=\"center\">\n\n![chatnio](/app/public/logo.png)\n\n# [🥳 CoAI.Dev](https://coai.dev)\n\n#### 🚀 下一代 AIGC 一站式商业解决方案\n#### *“ CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api) ”*\n\n\n[English](./README.md) · 简体中文 · [文档](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [部署文档](https://coai.dev/docs/deploy)\n\n[![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369)\n\n<img alt=\"CoAI.Dev Preview\" src=\"./screenshot/coai.png\" width=\"100%\" style=\"border-radius: 8px\">\n\n</div>\n\n## 📝 功能\n1. 🤖️ **丰富模型支持**: 多模型服务商支持 (OpenAI / Anthropic / Gemini / Midjourney 等十余种格式兼容 & 私有化 LLM 支持)\n2. 🤯 **美观 UI 设计**: UI 兼容 PC / Pad / 移动三端，遵循 [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) 设计规范，丰富美观的界面设计和后台仪表盘\n3. 🎃 **完整 Markdown 支持**: 支持 **LaTeX 公式** / **Mermaid 思维导图** / 表格渲染 / 代码高亮 / 图表绘制 / 进度条等进阶 Markdown 语法支持\n4. 👀 **多主题支持**: 支持多种主题切换，包含亮色主题的**明亮模式**和暗色主题的**深色模式**。 👉 [自定义配色](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)\n5. 📚 **国际化支持**: 支持国际化，支持多语言切换 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 欢迎贡献翻译 [Pull Request](https://github.com/coaidev/coai/pulls) \n6. 🎨 **文生图支持**: 支持多种文生图模型: **OpenAI DALL-E**✅ & **Midjourney** (支持 **U/V/R** 操作)✅ & Stable Diffusion✅ 等\n7. 📡 **强大对话同步**: **用户 0 成本对话跨端同步支持**，支持**对话分享** (支持链接分享 & 保存为图片 & 分享管理), **无需 WebDav / WebRTC 等依赖和复杂学习成本**\n8. 🎈 **模型市场 & 预设系统**: 支持后台可自定义的模型市场, 可提供模型介绍、标签等参数, 站长可根据情况自定义模型简介。同时支持预设系统，包含 **自定义预设** 和 **云端同步** 功能。\n9. 📖 **丰富文件解析**: **开箱即用**, 支持**所有模型**的文件解析 (PDF / Docx / Pptx / Excel / 图片等格式解析), **支持更多云端图片存储方案** (S3 / R2 / MinIO 等), **支持 OCR 图片识别** 👉 详情参见项目 [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) (支持 Vercel / Docker 一键部署)\n10. 🌏 **全模型联网搜索**: 基于 [SearXNG](https://github.com/searxng/searxng) 开源引擎, 支持 Google / Bing / DuckDuckGo / Yahoo / WikiPedia / Arxiv / Qwant 等丰富搜索引擎搜索, 支持安全搜索模式, 内容截断, 图片代理, 测试搜索可用性等功能。\n11. 💕 **渐进式 Web 应用 (PWA)**: 支持 PWA 应用 & 支持桌面端 (桌面端基于 [Tauri](https://github.com/tauri-apps/tauri))\n12. 🤩 **齐全后台管理**: 支持美观丰富的仪表盘, 公告 & 通知管理, 用户管理, 订阅管理, 礼品码 & 兑换码管理, 价格设定, 订阅设定, 自定义模型市场, 自定义站点名称 & Logo, SMTP 发件设置等功能\n13. 🤑 **多种计费方式**: 支持 💴 **订阅制** 和 💴 **弹性计费** 两种计费方式, 弹性计费支持 次数计费 / Token 计费 / 不计费 / 可匿名调用 和 **最小请求点数** 检测等强大功能\n14. 🎉 **创新模型缓存**: 支持开启模型缓存：即同一个请求入参 Hash 下, 如果之前已请求过, 将直接返回缓存结果 (击中缓存将不计费), 减少请求次数。可自行自定义是否缓存的模型、缓存时间、多种缓存结果数等高级缓存设置\n15. 🥪 **附加功能** (停止支持): 🍎 **AI 项目生成器功能** / 📂 **批量文章生成功能** / 🥪 **AI 卡片功能** (已废弃)\n16. 😎 **优秀渠道管理**: 自写优秀渠道算法, 支持⚡ **多渠道管理**, 支持🥳**优先级**设置渠道的调用顺序, 支持🥳**权重**设置同一优先级下的渠道均衡负载分配概率, 支持🥳**用户分组**, 🥳**失败自动重试**, 🥳**模型重定向**, 🥳**内置上游隐藏**, 🥳**渠道状态管理**等强大**企业级功能**\n17. ⭐ **OpenAI API 分发 & 中转系统**: 支持以 **OpenAI API** 标准格式调用各种大模型, 集成强大的渠道管理功能, 仅需部署一个站点即可实现同时发展 B/C 端业务💖\n18. 👌 **快速同步上游**: 渠道设置、模型市场、价格设定等设置都可快速同步上游站点，以此基础修改自己的站点配置，快速搭建自己的站点，省时省力，一键同步，快速上线\n19. 👋 **SEO 优化**: 支持 SEO 优化，支持自定义站点名称、站点 Logo 等 SEO 优化设设置使搜索引擎更快的爬取，你的站点与众不同👋\n20. 🎫 **多种兑换码体系**: 支持多种兑换码体系，支持礼品码和兑换码，支持批量生成，礼品码适合宣传分发，兑换码适合发卡销售，礼品码一个类型的多个码一个用户仅能兑换一个码，在宣传中一定程度上减少一个用户兑换多次的情况😀\n21. 🥰 **商用友好协议**: 采用 **Apache-2.0** 开源协议, 商用二开 & 分发友好 (也请遵守 Apache-2.0 协议的规定, 请勿用于违法用途)\n\n> ### ✨ CoAI.Dev 商业版\n> ![商业版预览](./screenshot/coai-pro.png)\n>\n> - ✅ 美观商业级 UI, 漂亮的前端界面与后台管理\n> - ✅ 支持 TTS & STT, 插件市场, RAG 知识库等丰富功能与模块\n> - ✅ 更多支付供应商, 更多计费模式和高级订单管理\n> - ✅ 支持更多鉴权方式，包括短信登录、OAuth 登录等\n> - ✅ 支持模型监控，渠道健康检测，故障告警自动渠道切换\n> - ✅ 支持多租户 API Key 分发系统, 企业级令牌权限管理与访问者限制\n> - ✅ 支持安全审核, 日志记录, 模型限速, API Gateway 等高级功能\n> - ✅ 支持推广奖励，专业数据统计，用户画像分析等商业分析能力\n> - ✅ 支持Discord/Telegram/飞书等机器人对接集成能力 (扩展模块)\n> - ...\n>\n> [👉 了解更多](https://www.coai.dev/docs/contact)\n\n\n## 🔨 支持模型\n1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*\n2. Anthropic Claude *(✅ Vision ✅ Function Calling)*\n3. Google Gemini & PaLM2 *(✅ Vision)*\n4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*\n5. 讯飞星火 SparkDesk *(✅ Vision ✅ Function Calling)*\n6. 智谱清言 ChatGLM *(✅ Vision)*\n7. 通义千问 Tongyi Qwen\n8. 腾讯混元 Tencent Hunyuan\n9. 百川大模型 Baichuan AI\n10. 月之暗面 Moonshot AI (👉 OpenAI)\n11. 深度求索 DeepSeek AI (👉 OpenAI)\n12. 字节云雀 ByteDance Skylark *(✅ Function Calling)*\n13. Groq Cloud AI\n14. OpenRouter (👉 OpenAI)\n15. 360 GPT\n16. LocalAI / Ollama (👉 OpenAI)\n\n## 👻 中转 OpenAI 兼容 API\n   - [x] Chat Completions _(/v1/chat/completions)_\n   - [x] Image Generation _(/v1/images)_\n   - [x] Model List _(/v1/models)_\n   - [x] Dashboard Billing _(/v1/billing)_\n\n\n## 📦 部署方式\n> [!TIP]\n> **部署成功后, 管理员账号为 `root`, 密码默认为 `chatnio123456`**\n\n### ✨ Zeabur (一键部署)\n[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI)\n\n> Zeabur 提供一定的免费额度, 可以使用非付费区域进行一键部署，同时也支持计划订阅和弹性计费等方式弹性扩展。\n> 1. 点击 `Deploy` 进行部署, 并输入你希望绑定的域名，等待部署完成。\n> 2. 部署完成后, 请访问你的域名, 并使用用户名 `root` 密码 `chatnio123456` 登录后台管理，请按照提示在 chatnio 后台及时修改密码。\n\n### 🐳 宝塔面板 (一键部署)\n\n1. 安装宝塔面板，前往 [宝塔面板官网](https://www.bt.cn/new/download.html) 进行安装，选择正式版脚本安装。\n2. 登录面板，点击左侧 **Docker** 进入 Docker 管理。\n3. 如提示未安装 Docker / Docker Compose， 可根据上方引导安装。\n4. 安装完成后，进入 **应用商城**，搜索 `CoAI` 并点击 **安装**。\n5. 配置应用基本信息，如您的域名，端口等配置，并点击 **确认** (可使用默认配置)。\n6. 首次安装可能需要等待 1-2 分钟完成数据库初始化。如遇到问题，请查看面板运行日志进行排查。\n7. 访问您配置的域名或服务器 `http://[ip]:[port]`，使用用户名 `root` 和密码 `chatnio123456` 登录后台管理。\n\n### 阿里云计算巢 (一键部署)\n [![Deploy on AlibabaCloud ComputeNest](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest.svg)](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版)\n1. 访问计算巢CoAI[部署链接](https://computenest.console.aliyun.com/service/instance/create/cn-hangzhou?type=user&ServiceName=CoAI社区版)，按提示填写部署参数\n2. 选择付费类型，填写实例参数与网络参数，点击 **确认订单**\n3. 确认部署参数并查看预估价格后，点击立即创建，等待服务实例部署完成\n4. 点击左侧 **服务实例** 等待服务实例部署完成后，点击实例ID进入到详情界面\n5. 点击详情界面**立即使用**中的链接，可进入CoAI社区版界面。默认用户名为`root`，密码`为chatnio123456` 登录后台管理。\n6. 更多操作详情与付费信息，参见：[服务详情](https://computenest.console.aliyun.com/service/detail/cn-hangzhou/service-bfbf676bd89d434691fc/1?type=user&isRecommend=true)\n\n### ⚡ Docker Compose 安装 (推荐)\n> [!NOTE]\n> 运行成功后, 宿主机映射地址为 `http://localhost:8000`\n\n ```shell\n git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git\n cd chatnio\n docker-compose up -d # 运行服务\n# 如需使用 stable 版本, 请使用 docker-compose -f docker-compose.stable.yaml up -d 替代\n# 如需使用 watchtower 自动更新, 请使用 docker-compose -f docker-compose.watch.yaml up -d 替代\n```\n   \n版本更新（_开启 Watchtower 自动更新的情况下, 无需手动更新_）：\n```shell\ndocker-compose down \ndocker-compose pull\ndocker-compose up -d\n```\n\n> - MySQL 数据库挂载目录项目 ~/**db**\n> - Redis 数据库挂载目录项目 ~/**redis**\n> - 配置文件挂载目录项目 ~/**config**\n\n### ⚡ Docker 安装 (轻量运行时, 常用于外置 _MYSQL/RDS_ 服务)\n> [!NOTE]\n> 运行成功后, 宿主机地址为 `http://localhost:8094`。\n> \n> 如需使用 stable 版本, 请使用 `programzmh/chatnio:stable` 替代 `programzmh/chatnio:latest`  \n\n```shell\ndocker run -d --name chatnio \\\n   --network host \\\n   -v ~/config:/config \\\n   -v ~/logs:/logs \\\n   -v ~/storage:/storage \\\n   -e MYSQL_HOST=localhost \\\n   -e MYSQL_PORT=3306 \\\n   -e MYSQL_DB=chatnio \\\n   -e MYSQL_USER=root \\\n   -e MYSQL_PASSWORD=chatnio123456 \\\n   -e REDIS_HOST=localhost \\\n   -e REDIS_PORT=6379 \\\n   -e SECRET=secret \\\n   -e SERVE_STATIC=true \\\n   programzmh/chatnio:latest\n```\n\n> - *--network host* 指使用宿主机网络, 使 Docker 容器使用宿主机的网络, 可自行修改\n> - SECRET: JWT 密钥, 自行生成随机字符串修改\n> - SERVE_STATIC: 是否启用静态文件服务 (正常情况下不需要更改此项, 详见下方常见问题解答)\n> - *-v ~/config:/config* 挂载配置文件, *-v ~/logs:/logs* 挂载日志文件的宿主机目录, *-v ~/storage:/storage* 挂载附加功能的生成文件\n> - 需配置 MySQL 和 Redis 服务, 请自行参考上方信息修改环境变量\n \n 版本更新 （_开启 Watchtower 后无需手动更新, 执行后按照上述步骤重新运行即可_）：\n ```shell\ndocker stop chatnio\ndocker rm chatnio\ndocker pull programzmh/chatnio:latest\n```\n\n### ⚒ 编译安装\n> [!NOTE]\n> 部署成功后, 默认端口为 **8094**, 访问地址为 `http://localhost:8094`\n> \n> Config 配置项 (~/config/**config.yaml**) 可以使用环境变量进行覆盖, 如 `MYSQL_HOST` 环境变量可覆盖 `mysql.host` 配置项\n\n```shell\ngit clone https://github.com/coaidev/coai.git\ncd chatnio\n\ncd app\nnpm install -g pnpm\npnpm install\npnpm build\n\ncd ..\ngo build -o chatnio\n\n# e.g. using nohup (you can also use systemd or other service manager)\nnohup ./chatnio > output.log & # using nohup to run in background\n```\n\n## ❓ 常见问题 Q&A\n1. **为什么我部署后的站点可以访问页面, 可以登录注册, 但是无法使用聊天 (一直在转圈)？**\n   - 聊天等此类功能通过 websocket 进行通信, 请确保你的服务支持 websocket。 (Tip: 中转通过 Http 实现, 无需 websocket 支持)\n   - 如果你使用了 Nginx, Apache 等反向代理, 请确保已配置 websocket 支持。\n   - 如果使用了端口映射, 端口转发, CDN, API Gateway 等服务, 请确保你的服务支持并开启 websocket。\n2. **我配置的 Midjourney Proxy 格式的渠道一直转圈或报错 `please provide available notify url`**\n   - 若为转圈，请确保你的 Midjourney Proxy 服务已正常运行, 并且已配置正确的上游地址。\n   - **Midjourney 要填渠道类型要用 Midjourney 而不是 OpenAI (不知道为什么很多人填成了 OpenAI 类型格式然后过来反馈为什么empty response, mj-chat 类除外)**\n   - 排查完这些问题后, 请查看你的系统设置中的**后端域名**是否已经配置并配置正确。如果不配置, 将导致 Midjourney Proxy 服务无法正常回调。\n3. **此项目有什么外部依赖？**\n   - MySQL: 存储用户信息, 对话记录, 管理员信息等持久化数据。\n   - Redis: 存储用户快速鉴权信息, IP 速率限制, 订阅配额, 邮箱验证码等数据。\n   - 环境未配置好的情况下, 会导致服务无法正常运行, 请确保你的 MySQL 和 Redis 服务已正常运行 (Docker 部署, 编译部署需自行搭建外部服务)。\n4. **我的机器为 ARM 架构, 该项目支持 ARM 架构吗？**\n   - 支持。CoAI.Dev 项目使用 BuildX 构建多架构镜像, 你可以直接使用 docker-compose 或 docker 运行, 无需额外配置。\n   - 如果你使用编译安装, 直接在 ARM 机器上编译即可, 无需欸外配置。如果你使用 x86 机器编译, 请使用 `GOARCH=arm64 go build -o chatnio` 进行交叉编译并上传至 ARM 机器上运行。\n5. **如何修改 Root 默认密码？**\n   - 请点击右上角头像或侧边栏底部用户框进入后台管理, 点击系统设置下常规设置操作栏的 修改 Root 密码 进行修改。或者选择在 用户管理 中选定 root 用户进行修改密码操作。\n6. **系统设置中的后端域名是什么？**\n   - 后端域名是指后端 API 服务的地址, 默认为你访问站点后加 `/api` 的地址, 如 `https://example.com/api` 。\n   - 如果设置为非 *SERVE_STATIC* 模式, 开启前后端分离部署, 请将后端域名设置为你的后端 API 服务地址, 如 `https://api.example.com`。\n   - 后端域名此处用于 Midjourney Proxy 服务的后端回调地址, 如无需使用 Midjourney Proxy 服务, 请忽略此设置。\n7. **如何配置支付方式？**\n   - CoAI.Dev 开源版支持发卡模式, 设置系统设置中的购买链接为你的发卡地址即可。卡密可通过用户管理中兑换码管理中批量生成。\n8. **礼品码和兑换码有什么区别？**\n   - 礼品码一种类型只能一个用户只能绑定一次, 而非 aff code, 发福利等方式可使用礼品码, 可在头像下拉菜单中的礼品码中兑换。\n   - 兑换码一种类型可以多个用户绑定, 可作为正常购买和发卡使用, 可在用户管理中的兑换码管理中批量生成, 在头像下拉菜单的点数（菜单第一个）内输入兑换码进行兑换。\n   - 一个例子：比如我发了一个类型为 *新年快乐* 的福利, 此时推荐使用礼品码, 假设发放 100 个 66 点数, 如果为兑换码, 手快的一个用户就批量把所有兑换码的 6600 点数都用完了, 而礼品码则可以保证每个用户只能使用一次 (获得 66 点数)。\n   - 而搭建发卡的时, 如果用礼品码, 因为一个类型只能兑换一次, 购买多个礼品码会导致兑换失败, 而兑换码则可以在此场景下使用。\n9. **该项目支持 Vercel 部署吗？**\n   - CoAI.Dev 本身并不支持 Vercel 部署, 但是你可以使用前后端分离模式,  Vercel 部署前端部分, 后端部分使用 Docker 部署或编译部署。\n10. **前后端分离部署模式是什么？**\n    - 正常情况下, 前后端在同一服务内, 后端地址为 `/api`。前后端分离部署指前端和后端分别部署在不同的服务上, 前端服务为静态文件服务, 后端服务为 API 服务。\n      - 举个例子, 前端使用 Nginx (或 Vercel 等) 部署, 部署的域名为 `https://www.chatnio.net`。\n      - 后端使用 Docker 部署, 部署的域名为 `https://api.chatnio.net`。\n    - 此种部署方式需自行打包前端, 配置环境变量 `VITE_BACKEND_ENDPOINT` 为你的后端地址, 如 `https://api.chatnio.net`。\n    - 配置后端环境变量的 `SERVE_STATIC=false` 使后端服务不提供静态文件服务。\n11. **弹性计费和订阅详解**\n    - 弹性计费, 即 `点数`, 其图标类似于**云**, 模型计费通用方式, 为了防止虚假汇率, 写死 10 点数 = 1 元, 汇率可以在计费规则中的 **应用内置模板** 中自定义汇率。\n    - 订阅, 即订阅计划, 为固定价格计费方式按次配额, 订阅计费扣取点数 (举例: 如果站点的用户想订阅 32 元的计划, 则需要保证点数大于等于 320 点数)\n    - 订阅是 Item 的组合, 每个 Item 都可设置涵盖的模型, 订阅配额 (-1 为无限使用), 名称, ID (用于区分不同的 Item), 图标等。可在后台的订阅管理中进行操作, 是否开启订阅, 订阅价格等, 修改每个订阅等级的 Item, 以及支持直接导入其他订阅等级的 Item。\n    - 订阅支持分层并写死为三个等级。 等级分别为: _普通用户 (0)_, _基础版订阅 (1)_, _标准版订阅 (2)_, _专业版订阅 (3)_, 订阅等级即为用户分组, 可在渠道管理中进行高级设置, 选择勾选可使用此模型的用户分组。\n    - 订阅配额设置, 可在订阅管理中进行操作, 是否支持中转 API (默认关闭)\n12. **可请求最小点数检测 `user quota is not enough` 详解**\n    - 为防止站点用户滥用站点模型, 当请求点数低于最小请求点数时将返回点数不足的错误信息, 大于等于最小请求点数时将正常请求。\n    - 模型的最小可请求点数规则: \n        - 不计费模型无限制\n        - 次数计费模型最小点数为该模型的 1 次请求点数 (e.g. 若一个模型的单次请求点数为 0.1 点数, 则最小请求点数为 0.1 点数)\n        - Token 弹性计费模型为 1K 输入 Tokens 价格 + 1K 输出 Tokens 价格 (e.g. 若一个模型的 1K 输入 Tokens 价格为 0.05 点数, 1K 输出 Tokens 价格 0.1 点数, 则最小请求点数为 0.15 点数)\n13. **为何我的 GPT-4-All 等逆向模型无法使用上传文件中的图片?**\n    - 上传模型图片为 Base64 格式, 如果逆向不支持 Base64 格式, 请使用 URL 直链而非上传文件做法。\n14. **如何开始域名严格跨域检测?**\n    - 正常情况下，后端对所有域名开放跨域。如果非特殊需求，无需开启严格跨域检测。\n    - 如果需要开启严格跨域检测，可以在后端环境变量中 并配置 `ALLOW_ORIGINS`, 如 `ALLOW_ORIGINS=chatnio.net,chatnio.app` （不需要加协议前缀, www 解析无需手动添加, 后端将自动识别并允许跨域）, 这样就会支持严格跨域检测 (如 *http://www.chatnio.app*, *https://chatnio.net* 等将会被允许, 其他域名将会被拒绝)。\n    - 即使在开启严格跨域检测的情况下, /v1 接口会被仍然允许所有域的跨域请求, 以保证中转 API 的正常使用。\n15. **模型映射功能是如何使用的？**\n    - 渠道内的模型映射格式为 `[from]>[to]`, 多个映射之间换行, **from** 为请求的模型, **to** 为真实向上游发送的模型并且需要上游真实支持\n    - 如: 我有一个逆向渠道, 填写 `gpt-4-all>gpt-4`, 则我的用户请求 **gpt-4-all** 模型到该渠道时, 后端则会模型映射至 **gpt-4** 向该渠道请求 **gpt-4**, 此时该渠道支持 2 个模型, **gpt-4** 和 **gpt-4-all** (本质上都为 **gpt-4**)\n    - 如果我不想让我的这个逆向渠道影响到 **gpt-4** 的渠道组, 可以加前缀 `!gpt-4-all>gpt-4`, 该渠道 **gpt-4** 则会被忽略, 此时该渠道将只支持 1 个模型, **gpt-4-all** (但本质上为 **gpt-4**)\n\n## 📦 技术栈\n- 🥗 前端: React + Redux + Radix UI + Tailwind CSS\n- 🍎 后端: Golang + Gin + Redis + MySQL\n- 🍒 应用技术: PWA + WebSocket\n\n## 🤯 为什么写此项目 & 项目优势\n我们发现，市面上的 AIGC 商业站点，大多数都是偏向于前端轻量部署的项目，有精美的 UI 界面设计，\n比如 [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) 的二开商业版本，\n由于其偏向个人私有化的设计，在二开商业化时有一定的局限性，呈现出一些问题，比如：\n  - **对话同步难**, 比如需要 WebDav 等服务，用户学习成本高，跨端实时同步困难。\n  - **计费不够完善**, 比如只支持弹性计费或只支持订阅制，无法满足不同用户的需求。\n  - **文件解析不便捷**, 比如只支持先在图床上传图片，返回站点后再在输入框中输入 URL 直链，无内置文件解析功能。\n  - **不支持对话 URL 分享**, 比如只支持对话截图分享，无法支持对话 URL 分享 (或仅支持 ShareGPT 等工具，无法对站点起到推广作用)。\n  - **渠道管理不够强大**, 比如后台仅支持 OpenAI 格式渠道，兼容其他格式渠道困难。且只能填入一个渠道，无法支持多渠道管理。\n  - **不支持 API 调用**, 比如只支持用户界面调用，无法支持 API 中转和管理。\n\n另一种是偏向于 API 分发的站点，有强大的分发系统，比如基于 [One API](https://github.com/songquanpeng/one-api) 等项目，\n这类项目虽然支持强大的 API 中转和管理，但是缺少界面设计，且缺少一些 C 端功能，比如：\n  - **用户界面不够丰富**, 比如只支持 API 调用，不内置用户界面聊天。用户界面聊天需要自行复制密钥并前往其他站点才能使用，这对于普通用户来说，学习成本较高。\n  - **没有订阅制**, 比如只支持弹性计费，缺少对 C 端用户的计费设计，无法满足用户的不同需求，对于无基础的用户来说，成本感知不够友好。\n  - **C 端功能不够丰富**, 比如只支持 API 调用，不支持对话同步，不支持对话分享，不支持文件解析等功能。\n  - **均衡负载不够强大**, 开源版不支持**权重**参数, 无法实现同优先级的渠道均衡负载分配概率 ([New API](https://github.com/Calcium-Ion/new-api) 也解决了此痛点, UI 也更美观)。\n\n因此，我们希望能够将这两种项目的优势结合起来，做出一个既有强大的 API 分发系统，又有丰富的用户界面设计的项目，\n这样既能满足 C 端用户的需求，又能发展 B 端业务，提高用户体验，降低用户学习成本，提高用户粘性。\n\n于是，**CoAI.Dev** 应运而生，我们希望能够做出一个既有强大的 API 分发系统，又有丰富的用户界面设计的项目，成为下一代开源 AIGC 项目的商业一站式解决方案。\n\n\n## ❤ 捐助\n如果您觉得这个项目对您有所帮助, 您可以点个 Star 支持一下！\n"
  },
  {
    "path": "adapter/adapter.go",
    "content": "package adapter\n\nimport (\n\t\"chat/adapter/azure\"\n\t\"chat/adapter/baichuan\"\n\t\"chat/adapter/bing\"\n\t\"chat/adapter/claude\"\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/adapter/coze\"\n\t\"chat/adapter/dashscope\"\n\t\"chat/adapter/deepseek\"\n\t\"chat/adapter/dify\"\n\t\"chat/adapter/hunyuan\"\n\t\"chat/adapter/midjourney\"\n\t\"chat/adapter/openai\"\n\t\"chat/adapter/palm2\"\n\t\"chat/adapter/skylark\"\n\t\"chat/adapter/slack\"\n\t\"chat/adapter/sparkdesk\"\n\t\"chat/adapter/zhinao\"\n\t\"chat/adapter/zhipuai\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\nvar channelFactories = map[string]adaptercommon.FactoryCreator{\n\tglobals.OpenAIChannelType:      openai.NewChatInstanceFromConfig,\n\tglobals.AzureOpenAIChannelType: azure.NewChatInstanceFromConfig,\n\tglobals.ClaudeChannelType:      claude.NewChatInstanceFromConfig,\n\tglobals.SlackChannelType:       slack.NewChatInstanceFromConfig,\n\tglobals.BingChannelType:        bing.NewChatInstanceFromConfig,\n\tglobals.PalmChannelType:        palm2.NewChatInstanceFromConfig,\n\tglobals.SparkdeskChannelType:   sparkdesk.NewChatInstanceFromConfig,\n\tglobals.ChatGLMChannelType:     zhipuai.NewChatInstanceFromConfig,\n\tglobals.QwenChannelType:        dashscope.NewChatInstanceFromConfig,\n\tglobals.HunyuanChannelType:     hunyuan.NewChatInstanceFromConfig,\n\tglobals.BaichuanChannelType:    baichuan.NewChatInstanceFromConfig,\n\tglobals.SkylarkChannelType:     skylark.NewChatInstanceFromConfig,\n\tglobals.ZhinaoChannelType:      zhinao.NewChatInstanceFromConfig,\n\tglobals.MidjourneyChannelType:  midjourney.NewChatInstanceFromConfig,\n\tglobals.DeepseekChannelType:    deepseek.NewChatInstanceFromConfig,\n\tglobals.DifyChannelType:        dify.NewChatInstanceFromConfig,\n\tglobals.CozeChannelType:        coze.NewChatInstanceFromConfig,\n\n\tglobals.MoonshotChannelType: openai.NewChatInstanceFromConfig, // openai format\n\tglobals.GroqChannelType:     openai.NewChatInstanceFromConfig, // openai format\n}\n\nfunc createChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error {\n\tprops.Model = conf.GetModelReflect(props.OriginalModel)\n\tprops.Proxy = conf.GetProxy()\n\n\tfactoryType := conf.GetType()\n\tif factory, ok := channelFactories[factoryType]; ok {\n\t\treturn factory(conf).CreateStreamChatRequest(props, hook)\n\t}\n\n\treturn fmt.Errorf(\"unknown channel type %s (channel #%d)\", conf.GetType(), conf.GetId())\n}\n\nfunc createVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error {\n\tprops.Model = conf.GetModelReflect(props.OriginalModel)\n\tprops.Proxy = conf.GetProxy()\n\n\tfactoryType := conf.GetType()\n\tif creator, ok := channelFactories[factoryType]; ok {\n\t\tinst := creator(conf)\n\t\tif v, ok := inst.(adaptercommon.VideoFactory); ok {\n\t\t\treturn v.CreateVideoRequest(props, hook)\n\t\t}\n\t\treturn fmt.Errorf(\"video request not supported by channel type %s (channel #%d)\", conf.GetType(), conf.GetId())\n\t}\n\n\treturn fmt.Errorf(\"unknown channel type %s (channel #%d)\", conf.GetType(), conf.GetId())\n}\n"
  },
  {
    "path": "adapter/azure/chat.go",
    "content": "package azure\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string {\n\tmodel := strings.ReplaceAll(props.Model, \".\", \"\")\n\tif props.Model == globals.GPT3TurboInstruct {\n\t\treturn fmt.Sprintf(\"%s/openai/deployments/%s/completions?api-version=%s\", c.GetResource(), model, c.GetEndpoint())\n\t}\n\treturn fmt.Sprintf(\"%s/openai/deployments/%s/chat/completions?api-version=%s\", c.GetResource(), model, c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {\n\tresult := \"\"\n\tfor _, message := range messages {\n\t\tresult += fmt.Sprintf(\"%s: %s\\n\", message.Role, message.Content)\n\t}\n\treturn result\n}\n\nfunc (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {\n\tif len(props.Message) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn props.Message[len(props.Message)-1].Content\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\tif props.Model == globals.GPT3TurboInstruct {\n\t\t// for completions\n\t\treturn CompletionRequest{\n\t\t\tPrompt:   c.GetCompletionPrompt(props.Message),\n\t\t\tMaxToken: props.MaxTokens,\n\t\t\tStream:   stream,\n\t\t}\n\t}\n\n\treturn ChatRequest{\n\t\tMessages:         formatMessages(props),\n\t\tMaxToken:         props.MaxTokens,\n\t\tStream:           stream,\n\t\tPresencePenalty:  props.PresencePenalty,\n\t\tFrequencyPenalty: props.FrequencyPenalty,\n\t\tTemperature:      props.Temperature,\n\t\tTopP:             props.TopP,\n\t\tTools:            props.Tools,\n\t\tToolChoice:       props.ToolChoice,\n\t}\n}\n\n// CreateChatRequest is the native http request body for openai\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tif globals.IsOpenAIDalleModel(props.Model) {\n\t\treturn c.CreateImage(props)\n\t}\n\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(props),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"openai error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"openai error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", fmt.Errorf(\"openai error: %s\", data.Error.Message)\n\t}\n\treturn data.Choices[0].Message.Content, nil\n}\n\n// CreateStreamChatRequest is the stream response body for openai\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tif globals.IsOpenAIDalleModel(props.Model) {\n\t\tif url, err := c.CreateImage(props); err != nil {\n\t\t\treturn err\n\t\t} else {\n\t\t\treturn callback(&globals.Chunk{Content: url})\n\t\t}\n\t}\n\n\tisCompletionType := props.Model == globals.GPT3TurboInstruct\n\n\tticks := 0\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(props),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tticks += 1\n\n\t\t\tpartial, err := c.ProcessLine(data, isCompletionType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(partial)\n\t\t},\n\t}, props.Proxy)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s (type: %s)\", form.Error.Message, form.Error.Type)\n\t\t\treturn errors.New(msg)\n\t\t}\n\t\treturn err.Error\n\t}\n\n\tif ticks == 0 {\n\t\treturn errors.New(\"no response\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/azure/image.go",
    "content": "package azure\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype ImageProps struct {\n\tModel  string\n\tPrompt string\n\tSize   ImageSize\n\tProxy  globals.ProxyConfig\n}\n\nfunc (c *ChatInstance) GetImageEndpoint(model string) string {\n\tmodel = strings.ReplaceAll(model, \".\", \"\")\n\treturn fmt.Sprintf(\"%s/openai/deployments/%s/images/generations?api-version=%s\", c.GetResource(), model, c.GetEndpoint())\n}\n\n// CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error\nfunc (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) {\n\tres, err := utils.Post(\n\t\tc.GetImageEndpoint(props.Model),\n\t\tc.GetHeader(), ImageRequest{\n\t\t\tPrompt: props.Prompt,\n\t\t\tSize: utils.Multi[ImageSize](\n\t\t\t\tprops.Model == globals.Dalle3 || props.Model == globals.GPTImage1,\n\t\t\t\tImageSize1024,\n\t\t\t\tImageSize512,\n\t\t\t),\n\t\t\tN: 1,\n\t\t}, props.Proxy)\n\tif err != nil || res == nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"openai error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ImageResponse](res)\n\tif data == nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"openai error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"openai error: %s\", data.Error.Message)\n\t}\n\n\t// for gpt-image-1, return base64 data if available\n\tif props.Model == globals.GPTImage1 && data.Data[0].B64Json != \"\" {\n\t\treturn \"\", data.Data[0].B64Json, nil\n\t}\n\n\treturn data.Data[0].Url, \"\", nil\n}\n\n// CreateImage will create a dalle image from prompt, return markdown of image\nfunc (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {\n\turl, b64Json, err := c.CreateImageRequest(ImageProps{\n\t\tModel:  props.Model,\n\t\tPrompt: c.GetLatestPrompt(props),\n\t\tProxy:  props.Proxy,\n\t})\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"safety\") {\n\t\t\treturn err.Error(), nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\tif b64Json != \"\" {\n\t\treturn utils.GetBase64ImageMarkdown(b64Json), nil\n\t}\n\n\treturn utils.GetImageMarkdown(url), nil\n}\n"
  },
  {
    "path": "adapter/azure/processor.go",
    "content": "package azure\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\nfunc formatMessages(props *adaptercommon.ChatProps) interface{} {\n\tif globals.IsVisionModel(props.Model) {\n\t\treturn utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {\n\t\t\tif message.Role == globals.User {\n\t\t\t\traw, urls := utils.ExtractImages(message.Content, true)\n\t\t\t\timages := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {\n\t\t\t\t\tobj, err := utils.NewImage(url)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tglobals.Info(fmt.Sprintf(\"cannot process image: %s (source: %s)\", err.Error(), utils.Extract(url, 24, \"...\")))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tprops.Buffer.AddImage(obj)\n\n\t\t\t\t\treturn &MessageContent{\n\t\t\t\t\t\tType: \"image_url\",\n\t\t\t\t\t\tImageUrl: &ImageUrl{\n\t\t\t\t\t\t\tUrl: url,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\treturn Message{\n\t\t\t\t\tRole: message.Role,\n\t\t\t\t\tContent: utils.Prepend(images, MessageContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: &raw,\n\t\t\t\t\t}),\n\t\t\t\t\tName:         message.Name,\n\t\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Message{\n\t\t\t\tRole: message.Role,\n\t\t\t\tContent: MessageContents{\n\t\t\t\t\tMessageContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: &message.Content,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tName:         message.Name,\n\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t}\n\t\t})\n\t}\n\n\treturn props.Message\n}\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\treturn utils.UnmarshalForm[ChatStreamResponse](data)\n}\n\nfunc processCompletionResponse(data string) *CompletionResponse {\n\treturn utils.UnmarshalForm[CompletionResponse](data)\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\treturn utils.UnmarshalForm[ChatStreamErrorResponse](data)\n}\n\nfunc getChoices(form *ChatStreamResponse) *globals.Chunk {\n\tif len(form.Choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoice := form.Choices[0].Delta\n\n\treturn &globals.Chunk{\n\t\tContent:      choice.Content,\n\t\tToolCall:     choice.ToolCalls,\n\t\tFunctionCall: choice.FunctionCall,\n\t}\n}\n\nfunc getCompletionChoices(form *CompletionResponse) string {\n\tif len(form.Choices) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn form.Choices[0].Text\n}\n\nfunc getRobustnessResult(chunk string) string {\n\texp := `\\\"content\\\":\\\"(.*?)\\\"`\n\tcompile, err := regexp.Compile(exp)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmatches := compile.FindStringSubmatch(chunk)\n\tif len(matches) > 1 {\n\t\treturn utils.ProcessRobustnessChar(matches[1])\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {\n\tif isCompletionType {\n\t\t// openai legacy support\n\t\tif completion := processCompletionResponse(data); completion != nil {\n\t\t\treturn &globals.Chunk{\n\t\t\t\tContent: getCompletionChoices(completion),\n\t\t\t}, nil\n\t\t}\n\n\t\tglobals.Warn(fmt.Sprintf(\"openai error: cannot parse completion response: %s\", data))\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse completion response\")\n\t}\n\n\tif form := processChatResponse(data); form != nil {\n\t\treturn getChoices(form), nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(fmt.Sprintf(\"openai error: %s (type: %s)\", form.Error.Message, form.Error.Type))\n\t}\n\n\tglobals.Warn(fmt.Sprintf(\"openai error: cannot parse chat completion response: %s\", data))\n\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse chat completion response\")\n}\n"
  },
  {
    "path": "adapter/azure/struct.go",
    "content": "package azure\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n\tResource string\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetResource() string {\n\treturn c.Resource\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t\t\"api-key\":      c.GetApiKey(),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string, resource string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t\tResource: resource,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparam := conf.SplitRandomSecret(2)\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tparam[0],\n\t\tparam[1],\n\t)\n}\n"
  },
  {
    "path": "adapter/azure/types.go",
    "content": "package azure\n\nimport \"chat/globals\"\n\ntype ImageUrl struct {\n\tUrl    string  `json:\"url\"`\n\tDetail *string `json:\"detail,omitempty\"`\n}\n\ntype MessageContent struct {\n\tType     string    `json:\"type\"`\n\tText     *string   `json:\"text,omitempty\"`\n\tImageUrl *ImageUrl `json:\"image_url,omitempty\"`\n}\n\ntype MessageContents []MessageContent\n\ntype Message struct {\n\tRole         string                `json:\"role\"`\n\tContent      MessageContents       `json:\"content\"`\n\tName         *string               `json:\"name,omitempty\"`\n\tFunctionCall *globals.FunctionCall `json:\"function_call,omitempty\"` // only `function` role\n\tToolCallId   *string               `json:\"tool_call_id,omitempty\"`  // only `tool` role\n\tToolCalls    *globals.ToolCalls    `json:\"tool_calls,omitempty\"`    // only `assistant` role\n}\n\n// ChatRequest is the request body for openai\ntype ChatRequest struct {\n\tModel               string                 `json:\"model\"`\n\tMessages            interface{}            `json:\"messages\"`\n\tMaxToken            *int                   `json:\"max_tokens,omitempty\"`\n\tMaxCompletionTokens *int                   `json:\"max_completion_tokens,omitempty\"`\n\tStream              bool                   `json:\"stream\"`\n\tPresencePenalty     *float32               `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty    *float32               `json:\"frequency_penalty,omitempty\"`\n\tTemperature         *float32               `json:\"temperature,omitempty\"`\n\tTopP                *float32               `json:\"top_p,omitempty\"`\n\tTools               *globals.FunctionTools `json:\"tools,omitempty\"`\n\tToolChoice          *interface{}           `json:\"tool_choice,omitempty\"` // string or object\n}\n\n// CompletionRequest is the request body for openai completion\ntype CompletionRequest struct {\n\tModel    string `json:\"model\"`\n\tPrompt   string `json:\"prompt\"`\n\tMaxToken *int   `json:\"max_tokens,omitempty\"`\n\tStream   bool   `json:\"stream\"`\n}\n\n// ChatResponse is the native http request body for openai\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tIndex        int             `json:\"index\"`\n\t\tMessage      globals.Message `json:\"message\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\n// ChatStreamResponse is the stream response body for openai\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tDelta        globals.Message `json:\"delta\"`\n\t\tIndex        int             `json:\"index\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n}\n\n// CompletionResponse is the native http request body / stream response body for openai completion\ntype CompletionResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tText  string `json:\"text\"`\n\t\tIndex int    `json:\"index\"`\n\t} `json:\"choices\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t\tType    string `json:\"type\"`\n\t} `json:\"error\"`\n}\n\ntype ImageSize string\n\n// ImageRequest is the request body for openai dalle image generation\ntype ImageRequest struct {\n\tModel  string    `json:\"model\"`\n\tPrompt string    `json:\"prompt\"`\n\tSize   ImageSize `json:\"size\"`\n\tN      int       `json:\"n\"`\n}\n\ntype ImageResponse struct {\n\tData []struct {\n\t\tUrl     string `json:\"url,omitempty\"`\n\t\tB64Json string `json:\"b64_json,omitempty\"`\n\t} `json:\"data\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n\tUsage *struct {\n\t\tInputTokens        int `json:\"input_tokens\"`\n\t\tInputTokensDetails struct {\n\t\t\tImageTokens int `json:\"image_tokens\"`\n\t\t\tTextTokens  int `json:\"text_tokens\"`\n\t\t} `json:\"input_tokens_details\"`\n\t\tOutputTokens int `json:\"output_tokens\"`\n\t\tTotalTokens  int `json:\"total_tokens\"`\n\t} `json:\"usage,omitempty\"`\n}\n\nvar (\n\tImageSize256  ImageSize = \"256x256\"\n\tImageSize512  ImageSize = \"512x512\"\n\tImageSize1024 ImageSize = \"1024x1024\"\n)\n"
  },
  {
    "path": "adapter/baichuan/chat.go",
    "content": "package baichuan\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v1/chat/completions\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetModel(model string) string {\n\tswitch model {\n\tcase globals.Baichuan53B:\n\t\treturn \"Baichuan2\"\n\tdefault:\n\t\treturn model\n\t}\n}\n\nfunc (c *ChatInstance) GetMessages(messages []globals.Message) []globals.Message {\n\tfor _, message := range messages {\n\t\tif message.Role == globals.System || message.Role == globals.Tool {\n\t\t\tmessage.Role = globals.User\n\t\t}\n\t}\n\n\treturn messages\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) ChatRequest {\n\treturn ChatRequest{\n\t\tModel:       c.GetModel(props.Model),\n\t\tMessages:    c.GetMessages(props.Message),\n\t\tStream:      stream,\n\t\tTopP:        props.TopP,\n\t\tTopK:        props.TopK,\n\t\tTemperature: props.Temperature,\n\t}\n}\n\n// CreateChatRequest is the native http request body for baichuan\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"baichuan error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"baichuan error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", fmt.Errorf(\"baichuan error: %s\", data.Error.Message)\n\t}\n\treturn data.Choices[0].Message.Content, nil\n}\n\n// CreateStreamChatRequest is the stream response body for baichuan\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tpartial, err := c.ProcessLine(data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(partial)\n\t\t},\n\t}, props.Proxy)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s (type: %s)\", form.Error.Message, form.Error.Type)\n\t\t\treturn errors.New(msg)\n\t\t}\n\t\treturn err.Error\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/baichuan/processor.go",
    "content": "package baichuan\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\treturn utils.UnmarshalForm[ChatStreamResponse](data)\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\treturn utils.UnmarshalForm[ChatStreamErrorResponse](data)\n}\n\nfunc getChoices(form *ChatStreamResponse) *globals.Chunk {\n\tif len(form.Choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoice := form.Choices[0].Delta\n\n\treturn &globals.Chunk{\n\t\tContent: choice.Content,\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) {\n\tif form := processChatResponse(data); form != nil {\n\t\treturn getChoices(form), nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(fmt.Sprintf(\"baichuan error: %s (type: %s)\", form.Error.Message, form.Error.Type))\n\t}\n\n\tglobals.Warn(fmt.Sprintf(\"baichuan error: cannot parse chat completion response: %s\", data))\n\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse chat completion response\")\n}\n"
  },
  {
    "path": "adapter/baichuan/struct.go",
    "content": "package baichuan\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/baichuan/types.go",
    "content": "package baichuan\n\nimport \"chat/globals\"\n\n// Baichuan AI API is similar to OpenAI API\n\ntype ChatRequest struct {\n\tModel             string            `json:\"model\"`\n\tMessages          []globals.Message `json:\"messages\"`\n\tStream            bool              `json:\"stream\"`\n\tTopP              *float32          `json:\"top_p,omitempty\"`\n\tTopK              *int              `json:\"top_k,omitempty\"`\n\tTemperature       *float32          `json:\"temperature,omitempty\"`\n\tWithSearchEnhance *bool             `json:\"with_search_enhance,omitempty\"`\n}\n\n// ChatResponse is the native http request body for baichuan\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tMessage struct {\n\t\t\tContent string `json:\"content\"`\n\t\t}\n\t} `json:\"choices\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\n// ChatStreamResponse is the stream response body for baichuan\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tDelta struct {\n\t\t\tContent string `json:\"content\"`\n\t\t}\n\t\tIndex int `json:\"index\"`\n\t} `json:\"choices\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t\tType    string `json:\"type\"`\n\t} `json:\"error\"`\n}\n"
  },
  {
    "path": "adapter/bing/chat.go",
    "content": "package bing\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {\n\tvar conn *utils.WebSocket\n\tif conn = utils.NewWebsocketClient(c.GetEndpoint()); conn == nil {\n\t\treturn fmt.Errorf(\"bing error: websocket connection failed\")\n\t}\n\tdefer conn.DeferClose()\n\n\tmodel := strings.TrimPrefix(props.Model, \"bing-\")\n\tprompt := props.Message[len(props.Message)-1].Content\n\tif err := conn.SendJSON(&ChatRequest{\n\t\tPrompt: prompt,\n\t\tHash:   c.Secret,\n\t\tModel:  model,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tform, err := utils.ReadForm[ChatResponse](conn)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"websocket: close 1000\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tglobals.Debug(fmt.Sprintf(\"bing error: %s\", err.Error()))\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := hook(&globals.Chunk{\n\t\t\tContent: form.Response,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "adapter/bing/struct.go",
    "content": "package bing\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tSecret   string\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn fmt.Sprintf(\"%s/chat\", c.Endpoint)\n}\n\nfunc NewChatInstance(endpoint, secret string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tSecret:   secret,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/bing/types.go",
    "content": "package bing\n\n// see https://github.com/Deeptrain-Community/chatnio-bing-service\n\ntype ChatRequest struct {\n\tPrompt string `json:\"prompt\"`\n\tHash   string `json:\"hash\"`\n\tModel  string `json:\"model\"`\n}\n\ntype ChatResponse struct {\n\tResponse string `json:\"response\"`\n}\n"
  },
  {
    "path": "adapter/claude/chat.go",
    "content": "package claude\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nconst defaultTokens = 2500\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v1/messages\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetChatHeaders() map[string]string {\n\treturn map[string]string{\n\t\t\"content-type\":      \"application/json\",\n\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\"x-api-key\":         c.GetApiKey(),\n\t}\n}\n\n// ConvertCompletionMessage converts the completion message to anthropic complete format (deprecated)\nfunc (c *ChatInstance) ConvertCompletionMessage(message []globals.Message) string {\n\tmapper := map[string]string{\n\t\tglobals.System:    \"Assistant\",\n\t\tglobals.User:      \"Human\",\n\t\tglobals.Assistant: \"Assistant\",\n\t}\n\n\tvar result string\n\tfor i, item := range message {\n\t\tif item.Role == globals.Tool {\n\t\t\tcontinue\n\t\t}\n\t\tif i == 0 && item.Role == globals.Assistant {\n\t\t\t// skip first assistant message\n\t\t\tcontinue\n\t\t}\n\n\t\tresult += fmt.Sprintf(\"\\n\\n%s: %s\", mapper[item.Role], item.Content)\n\t}\n\treturn fmt.Sprintf(\"%s\\n\\nAssistant:\", result)\n}\n\nfunc (c *ChatInstance) GetTokens(props *adaptercommon.ChatProps) int {\n\tif props.MaxTokens == nil || *props.MaxTokens <= 0 {\n\t\treturn defaultTokens\n\t}\n\n\treturn *props.MaxTokens\n}\n\nfunc (c *ChatInstance) ConvertMessages(props *adaptercommon.ChatProps) []globals.Message {\n\t// anthropic api: top message must be user message, only `user` and `assistant` role messages are allowd\n\tstart := false\n\n\tresult := make([]globals.Message, 0)\n\n\tfor _, message := range props.Message {\n\t\tif message.Role == globals.System {\n\t\t\tcontinue\n\t\t}\n\n\t\t// if is first message, set it to user message\n\t\tif !start {\n\t\t\tstart = true\n\t\t\tresult = append(result, globals.Message{\n\t\t\t\tRole:    globals.User,\n\t\t\t\tContent: message.Content,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\t// anthropic api does not allow multi-same role messages\n\t\tif len(result) > 0 && result[len(result)-1].Role == message.Role {\n\t\t\tresult[len(result)-1].Content += \"\\n\" + message.Content\n\t\t\tcontinue\n\t\t}\n\n\t\tresult = append(result, message)\n\t}\n\n\treturn result\n}\n\nfunc (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message {\n\tconverted := c.ConvertMessages(props)\n\treturn utils.Each(converted, func(message globals.Message) Message {\n\t\tif !globals.IsVisionModel(props.Model) || message.Role != globals.User {\n\t\t\treturn Message{\n\t\t\t\tRole:    message.Role,\n\t\t\t\tContent: message.Content,\n\t\t\t}\n\t\t}\n\n\t\tcontent, urls := utils.ExtractImages(message.Content, true)\n\t\timages := utils.EachNotNil(urls, func(url string) *MessageContent {\n\t\t\tobj, err := utils.NewImage(url)\n\t\t\tprops.Buffer.AddImage(obj)\n\t\t\tif err != nil {\n\t\t\t\tglobals.Info(fmt.Sprintf(\"cannot process image: %s (source: %s)\", err.Error(), utils.Extract(url, 24, \"...\")))\n\t\t\t}\n\n\t\t\ti := utils.NewImageContent(url)\n\t\t\treturn &MessageContent{\n\t\t\t\tType: \"image\",\n\t\t\t\tSource: &MessageImage{\n\t\t\t\t\tType:      \"base64\",\n\t\t\t\t\tMediaType: i.GetType(),\n\t\t\t\t\tData:      i.ToRawBase64(),\n\t\t\t\t},\n\t\t\t}\n\t\t})\n\n\t\treturn Message{\n\t\t\tRole: message.Role,\n\t\t\tContent: utils.Prepend(images, MessageContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: &content,\n\t\t\t}),\n\t\t}\n\t})\n}\n\nfunc (c *ChatInstance) GetSystemPrompt(props *adaptercommon.ChatProps) (prompt string) {\n\tfor _, message := range props.Message {\n\t\tif message.Role == globals.System {\n\t\t\tprompt += message.Content\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) *ChatBody {\n\tmessages := c.GetMessages(props)\n\treturn &ChatBody{\n\t\tMessages:    messages,\n\t\tMaxTokens:   c.GetTokens(props),\n\t\tModel:       props.Model,\n\t\tSystem:      c.GetSystemPrompt(props),\n\t\tStream:      stream,\n\t\tTemperature: props.Temperature,\n\t\tTopP:        props.TopP,\n\t\tTopK:        props.TopK,\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) {\n\tif form := processChatResponse(data); form != nil {\n\t\treturn &globals.Chunk{\n\t\t\tContent: form.Delta.Text,\n\t\t}, nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\treturn &globals.Chunk{Content: \"\"}, fmt.Errorf(\"anthropic error: %s (type: %s)\", form.Error.Message, form.Error.Type)\n\t}\n\n\treturn &globals.Chunk{Content: \"\"}, nil\n}\n\nfunc processChatErrorResponse(data string) *ChatErrorResponse {\n\tif form := utils.UnmarshalForm[ChatErrorResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\tif form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\n// CreateStreamChatRequest is the stream request for anthropic claude\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetChatHeaders(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tpartial, err := c.ProcessLine(data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn hook(partial)\n\t\t},\n\t},\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tif form.Error.Type == \"\" && form.Error.Message == \"\" {\n\t\t\t\treturn errors.New(utils.ToMarkdownCode(\"json\", err.Body))\n\t\t\t}\n\n\t\t\treturn errors.New(fmt.Sprintf(\"%s (type: %s)\", form.Error.Message, form.Error.Type))\n\t\t}\n\t\treturn fmt.Errorf(\"%s\\n%s\", err.Error, errors.New(utils.ToMarkdownCode(\"json\", err.Body)))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/claude/struct.go",
    "content": "package claude\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n"
  },
  {
    "path": "adapter/claude/types.go",
    "content": "package claude\n\n// ChatBody is the request body for anthropic claude\n\ntype Message struct {\n\tRole    string      `json:\"role\"`\n\tContent interface{} `json:\"content\"`\n}\n\ntype MessageImage struct {\n\tType      string      `json:\"type\"`\n\tMediaType interface{} `json:\"media_type\"`\n\tData      interface{} `json:\"data\"`\n}\n\ntype MessageContent struct {\n\tType   string        `json:\"type\"`\n\tText   *string       `json:\"text,omitempty\"`\n\tSource *MessageImage `json:\"source,omitempty\"`\n}\n\ntype ChatBody struct {\n\tMessages    []Message `json:\"messages\"`\n\tMaxTokens   int       `json:\"max_tokens\"`\n\tModel       string    `json:\"model\"`\n\tSystem      string    `json:\"system\"`\n\tStream      bool      `json:\"stream\"`\n\tTemperature *float32  `json:\"temperature,omitempty\"`\n\tTopP        *float32  `json:\"top_p,omitempty\"`\n\tTopK        *int      `json:\"top_k,omitempty\"`\n}\n\ntype ChatStreamResponse struct {\n\tType  string `json:\"type\"`\n\tIndex int    `json:\"index\"`\n\tDelta struct {\n\t\tType string `json:\"type\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"delta\"`\n}\n\ntype ChatErrorResponse struct {\n\tError struct {\n\t\tType    string `json:\"type\" binding:\"required\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n"
  },
  {
    "path": "adapter/common/interface.go",
    "content": "package adaptercommon\n\nimport (\n\t\"chat/globals\"\n)\n\ntype Factory interface {\n\tCreateStreamChatRequest(props *ChatProps, hook globals.Hook) error\n}\n\ntype VideoFactory interface {\n\tCreateVideoRequest(props *VideoProps, hook globals.Hook) error\n}\n\ntype FactoryCreator func(globals.ChannelConfig) Factory\n"
  },
  {
    "path": "adapter/common/types.go",
    "content": "package adaptercommon\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n)\n\ntype RequestProps struct {\n\tMaxRetries *int                `json:\"-\"`\n\tCurrent    int                 `json:\"-\"`\n\tGroup      string              `json:\"-\"`\n\tProxy      globals.ProxyConfig `json:\"-\"`\n}\n\ntype VideoProps struct {\n\tRequestProps\n\n\tModel         string `json:\"model,omitempty\"`\n\tOriginalModel string `json:\"-\"`\n\n\tPrompt         string  `json:\"prompt\"`\n\tSeconds        *string `json:\"seconds,omitempty\"`\n\tSize           *string `json:\"size,omitempty\"`\n\tInputReference *string `json:\"input_reference,omitempty\"`\n\n\tUser string `json:\"-\"`\n}\n\ntype ChatProps struct {\n\tRequestProps\n\n\tModel         string `json:\"model,omitempty\"`\n\tOriginalModel string `json:\"-\"`\n\n\tMessage           []globals.Message      `json:\"messages,omitempty\"`\n\tMaxTokens         *int                   `json:\"max_tokens,omitempty\"`\n\tPresencePenalty   *float32               `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty  *float32               `json:\"frequency_penalty,omitempty\"`\n\tRepetitionPenalty *float32               `json:\"repetition_penalty,omitempty\"`\n\tTemperature       *float32               `json:\"temperature,omitempty\"`\n\tTopP              *float32               `json:\"top_p,omitempty\"`\n\tTopK              *int                   `json:\"top_k,omitempty\"`\n\tTools             *globals.FunctionTools `json:\"tools,omitempty\"`\n\tToolChoice        *interface{}           `json:\"tool_choice,omitempty\"`\n\tBuffer            *utils.Buffer          `json:\"-\"`\n}\n\nfunc (c *ChatProps) SetupBuffer(buf *utils.Buffer) {\n\tbuf.SetPrompts(c)\n\tc.Buffer = buf\n}\n\nfunc CreateChatProps(props *ChatProps, buffer *utils.Buffer) *ChatProps {\n\tprops.SetupBuffer(buffer)\n\treturn props\n}\n\nfunc CreateVideoProps(props *VideoProps) *VideoProps {\n\treturn props\n}\n"
  },
  {
    "path": "adapter/coze/chat.go",
    "content": "package coze\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype ChatInstance struct {\n\tEndpoint         string\n\tApiKey           string\n\tAutoSaveHistory  bool\n\tresponseComplete bool\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint:        endpoint,\n\t\tApiKey:          apiKey,\n\t\tAutoSaveHistory: false,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v3/chat\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\tadditionalMessages := []EnterMessage{}\n\n\tfor _, msg := range props.Message {\n\t\tenterMsg := EnterMessage{\n\t\t\tRole:        msg.Role,\n\t\t\tContent:     msg.Content,\n\t\t\tContentType: \"text\",\n\t\t}\n\n\t\tif msg.Role == \"user\" {\n\t\t\tenterMsg.Type = \"question\"\n\t\t} else if msg.Role == \"assistant\" {\n\t\t\tenterMsg.Type = \"answer\"\n\t\t}\n\n\t\tadditionalMessages = append(additionalMessages, enterMsg)\n\t}\n\n\t// `user_id` is required in coze\n\ttimestamp := time.Now().UnixNano()\n\tuserID := fmt.Sprintf(\"user_%d\", timestamp)\n\n\treturn ChatRequest{\n\t\tBotID:              props.Model,\n\t\tUserID:             userID,\n\t\tAdditionalMessages: additionalMessages,\n\t\tStream:             stream,\n\t\tAutoSaveHistory:    c.AutoSaveHistory,\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string) (string, error) {\n\tif c.responseComplete {\n\t\treturn \"\", nil\n\t}\n\n\tif data == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tchunk, complete, err := processStreamResponse(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif complete {\n\t\tc.responseComplete = true\n\t}\n\n\treturn chunk.Content, nil\n}\n\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\t// TODO: use standard non-stream request\n\tc.AutoSaveHistory = true\n\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"coze error: %s\", err.Error())\n\t}\n\n\tresponseBody := utils.Marshal(res)\n\tresponse := processChatResponse(responseBody)\n\tif response == nil {\n\t\treturn \"\", fmt.Errorf(\"coze error: cannot parse response\")\n\t}\n\n\tif response.Code != 0 {\n\t\treturn \"\", fmt.Errorf(\"coze error: %s (code: %d)\", response.Msg, response.Code)\n\t}\n\n\tvar responseContent string\n\tvar responseMutex sync.Mutex\n\n\terr = c.CreateStreamChatRequest(props, func(chunk *globals.Chunk) error {\n\t\tresponseMutex.Lock()\n\t\tdefer responseMutex.Unlock()\n\t\tresponseContent += chunk.Content\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif responseContent == \"\" {\n\t\treturn \"\", fmt.Errorf(\"coze error: empty response from API\")\n\t}\n\n\treturn responseContent, nil\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tc.responseComplete = false\n\tc.AutoSaveHistory = false\n\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tFullSSE: true,\n\t\tCallback: func(data string) error {\n\t\t\tpartial, err := c.ProcessLine(data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif partial != \"\" {\n\t\t\t\terr = callback(&globals.Chunk{Content: partial})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}, props.Proxy)\n\n\tc.responseComplete = true\n\n\tif err != nil {\n\t\tif strings.Contains(err.Body, \"\\\"code\\\":\") {\n\t\t\terrorResp := processChatErrorResponse(err.Body)\n\t\t\tif errorResp != nil && errorResp.Data.Code != 0 {\n\t\t\t\treturn errors.New(fmt.Sprintf(\"coze error: %s (code: %d)\", errorResp.Data.Msg, errorResp.Data.Code))\n\t\t\t}\n\n\t\t\tvar genericResp map[string]interface{}\n\t\t\tif jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil {\n\t\t\t\terrMsg, _ := json.Marshal(genericResp)\n\t\t\t\treturn errors.New(fmt.Sprintf(\"coze error: %s\", string(errMsg)))\n\t\t\t}\n\t\t}\n\n\t\tif err.Error != nil {\n\t\t\treturn err.Error\n\t\t}\n\t\treturn errors.New(fmt.Sprintf(\"coze error: unexpected error in stream request\"))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/coze/processor.go",
    "content": "package coze\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc processChatResponse(data string) *ChatResponse {\n\tif form := utils.UnmarshalForm[ChatResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatStreamResponse(data string) *ChatStreamResponse {\n\tif form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatStreamData(data string) *ChatStreamData {\n\tif form := utils.UnmarshalForm[ChatStreamData](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\tif form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processSSEData(data string) (event string, eventData string, err error) {\n\tif data == \"\" {\n\t\treturn \"\", \"\", nil\n\t}\n\n\tsseLines := strings.Split(data, \"\\n\")\n\tfor _, line := range sseLines {\n\t\tline = strings.TrimSpace(line)\n\t\tif strings.HasPrefix(line, \"event:\") {\n\t\t\tevent = strings.TrimSpace(strings.TrimPrefix(line, \"event:\"))\n\t\t} else if strings.HasPrefix(line, \"data:\") {\n\t\t\teventData = strings.TrimSpace(strings.TrimPrefix(line, \"data:\"))\n\t\t}\n\t}\n\n\tif eventData == \"\" {\n\t\treturn \"\", \"\", nil\n\t}\n\n\tif strings.HasPrefix(eventData, \"\\\"\") && strings.HasSuffix(eventData, \"\\\"\") && len(eventData) > 2 {\n\t\tunquoted, err := strconv.Unquote(eventData)\n\t\tif err == nil {\n\t\t\teventData = unquoted\n\t\t}\n\t}\n\n\treturn event, eventData, nil\n}\n\nfunc processEventContent(event string, eventData string) (content string, complete bool, err error) {\n\tswitch event {\n\tcase \"conversation.message.delta\":\n\t\tcontent, _ := parseEventContent(event, eventData)\n\t\tif content != \"\" {\n\t\t\treturn content, false, nil\n\t\t}\n\n\t\tstreamData := processChatStreamData(eventData)\n\t\tif streamData != nil && streamData.Type == \"answer\" && streamData.Role == \"assistant\" && streamData.Content != \"\" {\n\t\t\treturn streamData.Content, false, nil\n\t\t}\n\tcase \"conversation.message.completed\":\n\t\treturn \"\", false, nil\n\tcase \"conversation.chat.completed\":\n\t\treturn \"\", true, nil\n\tcase \"conversation.chat.failed\":\n\t\tstreamData := processChatStreamData(eventData)\n\t\tif streamData != nil {\n\t\t\tif streamData.Code != 0 && streamData.Msg != \"\" {\n\t\t\t\treturn \"\", false, errors.New(fmt.Sprintf(\"coze error: %s (code: %d)\", streamData.Msg, streamData.Code))\n\t\t\t}\n\t\t}\n\t\treturn \"\", false, errors.New(\"coze error: conversation failed\")\n\tcase \"done\":\n\t\treturn \"\", true, nil\n\t}\n\n\terrorResp := processChatErrorResponse(eventData)\n\tif errorResp != nil && errorResp.Data.Code != 0 {\n\t\treturn \"\", false, errors.New(fmt.Sprintf(\"coze error: %s (code: %d)\", errorResp.Data.Msg, errorResp.Data.Code))\n\t}\n\n\tstreamData := processChatStreamData(eventData)\n\tif streamData != nil {\n\t\tif streamData.Code != 0 && streamData.Msg != \"\" {\n\t\t\treturn \"\", false, errors.New(fmt.Sprintf(\"coze error: %s (code: %d)\", streamData.Msg, streamData.Code))\n\t\t}\n\n\t\tif streamData.LastError.Code != 0 && streamData.LastError.Msg != \"\" {\n\t\t\treturn \"\", false, errors.New(fmt.Sprintf(\"coze error: %s (code: %d)\", streamData.LastError.Msg, streamData.LastError.Code))\n\t\t}\n\t}\n\n\treturn \"\", false, nil\n}\n\nfunc parseEventContent(eventType string, eventData string) (string, error) {\n\tif eventType == \"conversation.message.delta\" {\n\t\tstreamResp := processChatStreamResponse(fmt.Sprintf(`{\"event\":\"%s\",\"data\":%s}`, eventType, eventData))\n\t\tif streamResp != nil {\n\t\t\tstreamData := processChatStreamData(streamResp.Data)\n\t\t\tif streamData != nil && streamData.Type == \"answer\" && streamData.Role == \"assistant\" && streamData.Content != \"\" {\n\t\t\t\treturn streamData.Content, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\nfunc processStreamResponse(data string) (*globals.Chunk, bool, error) {\n\tevent, eventData, err := processSSEData(data)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tif event == \"\" || eventData == \"\" {\n\t\treturn &globals.Chunk{Content: \"\"}, false, nil\n\t}\n\n\tcontent, complete, err := processEventContent(event, eventData)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\treturn &globals.Chunk{\n\t\tContent: content,\n\t}, complete, nil\n}\n"
  },
  {
    "path": "adapter/coze/struct.go",
    "content": "package coze\n\ntype ChatRequest struct {\n\tBotID              string            `json:\"bot_id\"`\n\tUserID             string            `json:\"user_id\"`\n\tAdditionalMessages []EnterMessage    `json:\"additional_messages,omitempty\"`\n\tStream             bool              `json:\"stream\"`\n\tCustomVariables    map[string]string `json:\"custom_variables,omitempty\"`\n\tAutoSaveHistory    bool              `json:\"auto_save_history\"`\n\tMetaData           map[string]string `json:\"meta_data,omitempty\"`\n\tExtraParams        map[string]string `json:\"extra_params,omitempty\"`\n\tShortcutCommand    *ShortcutCommand  `json:\"shortcut_command,omitempty\"`\n}\n\ntype EnterMessage struct {\n\tRole        string            `json:\"role\"`\n\tType        string            `json:\"type,omitempty\"`\n\tContent     string            `json:\"content,omitempty\"`\n\tContentType string            `json:\"content_type,omitempty\"`\n\tMetaData    map[string]string `json:\"meta_data,omitempty\"`\n}\n\ntype ShortcutCommand struct {\n\t// TODO: support for adding this on demand\n}\n\ntype ObjectString struct {\n\tType    string `json:\"type\"`\n\tText    string `json:\"text,omitempty\"`\n\tFileID  string `json:\"file_id,omitempty\"`\n\tFileURL string `json:\"file_url,omitempty\"`\n}\n\ntype ChatResponse struct {\n\tData struct {\n\t\tID             string            `json:\"id\"`\n\t\tConversationID string            `json:\"conversation_id\"`\n\t\tBotID          string            `json:\"bot_id\"`\n\t\tCreatedAt      int64             `json:\"created_at\"`\n\t\tCompletedAt    int64             `json:\"completed_at\"`\n\t\tLastError      interface{}       `json:\"last_error\"`\n\t\tMetaData       map[string]string `json:\"meta_data\"`\n\t\tStatus         string            `json:\"status\"`\n\t\tUsage          *Usage            `json:\"usage\"`\n\t} `json:\"data\"`\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n}\n\ntype Usage struct {\n\tTokenCount   int `json:\"token_count\"`\n\tOutputTokens int `json:\"output_tokens\"`\n\tInputTokens  int `json:\"input_tokens\"`\n}\n\ntype ChatStreamResponse struct {\n\tEvent string `json:\"event\"`\n\tData  string `json:\"data\"`\n}\n\ntype ChatStreamData struct {\n\tID          string `json:\"id,omitempty\"`\n\tRole        string `json:\"role,omitempty\"`\n\tType        string `json:\"type,omitempty\"`\n\tContent     string `json:\"content,omitempty\"`\n\tContentType string `json:\"content_type,omitempty\"`\n\n\tChatID         string `json:\"chat_id,omitempty\"`\n\tConversationID string `json:\"conversation_id,omitempty\"`\n\tBotID          string `json:\"bot_id,omitempty\"`\n\tSectionID      string `json:\"section_id,omitempty\"`\n\n\tCreatedAt   int64 `json:\"created_at,omitempty\"`\n\tCompletedAt int64 `json:\"completed_at,omitempty\"`\n\tUpdatedAt   int64 `json:\"updated_at,omitempty\"`\n\n\tStatus    string `json:\"status,omitempty\"`\n\tLastError struct {\n\t\tCode int    `json:\"code\"`\n\t\tMsg  string `json:\"msg\"`\n\t} `json:\"last_error,omitempty\"`\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\n\tUsage *Usage `json:\"usage,omitempty\"`\n\n\tMetaData   map[string]string `json:\"meta_data,omitempty\"`\n\tFromModule interface{}       `json:\"from_module,omitempty\"`\n\tFromUnit   interface{}       `json:\"from_unit,omitempty\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tEvent string `json:\"event\"`\n\tData  struct {\n\t\tCode int    `json:\"code\"`\n\t\tMsg  string `json:\"msg\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "adapter/dashscope/chat.go",
    "content": "package dashscope\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst defaultMaxTokens = 1500\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":    \"application/json\",\n\t\t\"Authorization\":   fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t\t\"X-DashScope-SSE\": \"enable\",\n\t}\n}\n\nfunc (c *ChatInstance) FormatMessages(message []globals.Message) []Message {\n\tvar messages []Message\n\tvar start bool\n\tfor _, v := range message {\n\t\tif v.Role == globals.Tool {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !start {\n\t\t\tstart = true\n\n\t\t\t// dashscope first message should be [`user`, `system`] role, convert other roles to `user`\n\t\t\tif v.Role != globals.User && v.Role != globals.System {\n\t\t\t\tv.Role = globals.User\n\t\t\t}\n\t\t}\n\n\t\tmessages = append(messages, Message{\n\t\t\tRole:    v.Role,\n\t\t\tContent: v.Content,\n\t\t})\n\t}\n\n\treturn messages\n}\n\nfunc (c *ChatInstance) GetMaxTokens(props *adaptercommon.ChatProps) int {\n\t// dashscope has a restriction of 1500 tokens in completion\n\tif props.MaxTokens == nil || *props.MaxTokens <= 0 || *props.MaxTokens > 1500 {\n\t\treturn defaultMaxTokens\n\t}\n\n\treturn *props.MaxTokens\n}\n\nfunc (c *ChatInstance) GetTopP(props *adaptercommon.ChatProps) *float32 {\n\t// range of top_p should be (0.0, 1.0)\n\tif props.TopP == nil {\n\t\treturn nil\n\t}\n\n\tif *props.TopP <= 0.0 {\n\t\treturn utils.ToPtr[float32](0.1)\n\t} else if *props.TopP >= 1.0 {\n\t\treturn utils.ToPtr[float32](0.9)\n\t}\n\n\treturn props.TopP\n}\n\nfunc (c *ChatInstance) GetRepeatPenalty(props *adaptercommon.ChatProps) *float32 {\n\t// range of repetition_penalty should greater than 0.0\n\tif props.RepetitionPenalty == nil {\n\t\treturn nil\n\t}\n\n\tif *props.RepetitionPenalty <= 0.0 {\n\t\treturn utils.ToPtr[float32](0.1)\n\t}\n\n\treturn props.RepetitionPenalty\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps) ChatRequest {\n\treturn ChatRequest{\n\t\tModel: strings.TrimSuffix(props.Model, \"-net\"),\n\t\tInput: ChatInput{\n\t\t\tMessages: c.FormatMessages(props.Message),\n\t\t},\n\t\tParameters: ChatParam{\n\t\t\tMaxTokens:         c.GetMaxTokens(props),\n\t\t\tTemperature:       props.Temperature,\n\t\t\tTopP:              c.GetTopP(props),\n\t\t\tTopK:              props.TopK,\n\t\t\tRepetitionPenalty: c.GetRepeatPenalty(props),\n\t\t\tEnableSearch:      utils.ToPtr(strings.HasSuffix(props.Model, \"-net\")),\n\t\t\tIncrementalOutput: true,\n\t\t},\n\t}\n}\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/api/v1/services/aigc/text-generation/generation\", c.Endpoint)\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\treturn utils.EventSource(\n\t\t\"POST\",\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props),\n\t\tfunc(data string) error {\n\t\t\t// example:\n\t\t\t// id:1\n\t\t\t// event:result\n\t\t\t// :HTTP_STATUS/200\n\t\t\t// data:{\"output\":{\"finish_reason\":\"null\",\"text\":\"hi\"},\"usage\":{\"total_tokens\":15,\"input_tokens\":14,\"output_tokens\":1},\"request_id\":\"08da1369-e009-9f8f-8363-54b966f80daf\"}\n\n\t\t\tdata = strings.TrimSpace(data)\n\t\t\tif !strings.HasPrefix(data, \"data:\") {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tslice := strings.TrimSpace(strings.TrimPrefix(data, \"data:\"))\n\t\t\tif form := utils.UnmarshalForm[ChatResponse](slice); form != nil {\n\t\t\t\tif form.Output.Text == \"\" && form.Message != \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"dashscope error: %s\", form.Message)\n\t\t\t\t}\n\n\t\t\t\tif err := callback(&globals.Chunk{Content: form.Output.Text}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tglobals.Debug(fmt.Sprintf(\"dashscope error: cannot unmarshal data %s\", slice))\n\n\t\t\treturn nil\n\t\t},\n\t\tprops.Proxy,\n\t)\n}\n"
  },
  {
    "path": "adapter/dashscope/struct.go",
    "content": "package dashscope\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc NewChatInstance(endpoint string, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/dashscope/types.go",
    "content": "package dashscope\n\n// ChatRequest is the request body for dashscope\ntype ChatRequest struct {\n\tModel      string    `json:\"model\"`\n\tInput      ChatInput `json:\"input\"`\n\tParameters ChatParam `json:\"parameters\"`\n}\n\ntype Message struct {\n\tRole    string `json:\"role\"`\n\tContent string `json:\"content\"`\n}\n\ntype ChatInput struct {\n\tMessages []Message `json:\"messages\"`\n}\n\ntype ChatParam struct {\n\tIncrementalOutput bool     `json:\"incremental_output\"`\n\tEnableSearch      *bool    `json:\"enable_search,omitempty\"`\n\tMaxTokens         int      `json:\"max_tokens\"`\n\tTemperature       *float32 `json:\"temperature,omitempty\"`\n\tTopP              *float32 `json:\"top_p,omitempty\"`\n\tTopK              *int     `json:\"top_k,omitempty\"`\n\tRepetitionPenalty *float32 `json:\"repetition_penalty,omitempty\"`\n}\n\n// ChatResponse is the response body for dashscope\ntype ChatResponse struct {\n\tOutput struct {\n\t\tFinishReason string `json:\"finish_reason\"`\n\t\tText         string `json:\"text\"`\n\t} `json:\"output\"`\n\tRequestId string `json:\"request_id\"`\n\tUsage     struct {\n\t\tInputTokens  int `json:\"input_tokens\"`\n\t\tOutputTokens int `json:\"output_tokens\"`\n\t} `json:\"usage\"`\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "adapter/deepseek/chat.go",
    "content": "package deepseek\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n)\n\ntype ChatInstance struct {\n\tEndpoint         string\n\tApiKey           string\n\tisFirstReasoning bool\n\tisReasonOver     bool\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint:         endpoint,\n\t\tApiKey:           apiKey,\n\t\tisFirstReasoning: true,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/chat/completions\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\tmessages := props.Message\n\t// because of deepseek first message must be user role\n\t// convert assistant message to user message\n\tif len(messages) > 0 && messages[0].Role == globals.Assistant {\n\t\tmessages = make([]globals.Message, len(props.Message))\n\t\tcopy(messages, props.Message)\n\t\tmessages[0].Role = globals.User\n\t}\n\n\treturn ChatRequest{\n\t\tModel:            props.Model,\n\t\tMessages:         messages,\n\t\tMaxTokens:        props.MaxTokens,\n\t\tStream:           stream,\n\t\tTemperature:      props.Temperature,\n\t\tTopP:             props.TopP,\n\t\tPresencePenalty:  props.PresencePenalty,\n\t\tFrequencyPenalty: props.FrequencyPenalty,\n\t}\n}\n\nfunc processChatResponse(data string) *ChatResponse {\n\tif form := utils.UnmarshalForm[ChatResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatStreamResponse(data string) *ChatStreamResponse {\n\tif form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\tif form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc (c *ChatInstance) ProcessLine(data string) (string, error) {\n\tif form := processChatStreamResponse(data); form != nil {\n\t\tif len(form.Choices) == 0 {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tdelta := form.Choices[0].Delta\n\n\t\tif c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil {\n\t\t\tc.isReasonOver = true\n\t\t\tif delta.Content != \"\" {\n\t\t\t\treturn fmt.Sprintf(\"\\n</think>\\n\\n%s\", delta.Content), nil\n\t\t\t}\n\t\t\treturn \"\\n</think>\\n\\n\", nil\n\t\t}\n\n\t\tif delta.ReasoningContent != nil {\n\t\t\tcontent := *delta.ReasoningContent\n\t\t\tif c.isFirstReasoning {\n\t\t\t\tc.isFirstReasoning = false\n\t\t\t\treturn fmt.Sprintf(\"<think>\\n%s\", content), nil\n\t\t\t}\n\t\t\treturn content, nil\n\t\t}\n\n\t\treturn delta.Content, nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\tif form.Error.Message != \"\" {\n\t\t\treturn \"\", errors.New(fmt.Sprintf(\"deepseek error: %s\", form.Error.Message))\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"deepseek error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"deepseek error: cannot parse response\")\n\t}\n\n\tif len(data.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"deepseek error: no choices\")\n\t}\n\n\tmessage := data.Choices[0].Message\n\tcontent := message.Content\n\tif message.ReasoningContent != nil {\n\t\tcontent = fmt.Sprintf(\"<think>\\n%s\\n</think>\\n\\n%s\", *message.ReasoningContent, content)\n\t}\n\n\treturn content, nil\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tc.isFirstReasoning = true\n\tc.isReasonOver = false\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tpartial, err := c.ProcessLine(data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(&globals.Chunk{Content: partial})\n\t\t},\n\t}, props.Proxy)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tif form.Error.Type == \"\" && form.Error.Message == \"\" {\n\t\t\t\treturn errors.New(utils.ToMarkdownCode(\"json\", err.Body))\n\t\t\t}\n\t\t\treturn errors.New(fmt.Sprintf(\"deepseek error: %s (type: %s)\", form.Error.Message, form.Error.Type))\n\t\t}\n\t\treturn err.Error\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/deepseek/struct.go",
    "content": "package deepseek\n\nimport (\n\t\"chat/globals\"\n)\n\n// DeepSeek API is similar to OpenAI API with additional reasoning content\n\ntype ChatRequest struct {\n\tModel            string            `json:\"model\"`\n\tMessages         []globals.Message `json:\"messages\"`\n\tMaxTokens        *int              `json:\"max_tokens,omitempty\"`\n\tStream           bool              `json:\"stream\"`\n\tTemperature      *float32          `json:\"temperature,omitempty\"`\n\tTopP             *float32          `json:\"top_p,omitempty\"`\n\tPresencePenalty  *float32          `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty *float32          `json:\"frequency_penalty,omitempty\"`\n}\n\n// ChatResponse is the native http request body for deepseek\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tIndex        int             `json:\"index\"`\n\t\tMessage      globals.Message `json:\"message\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tUsage struct {\n\t\tPromptTokens     int `json:\"prompt_tokens\"`\n\t\tCompletionTokens int `json:\"completion_tokens\"`\n\t\tTotalTokens      int `json:\"total_tokens\"`\n\t} `json:\"usage\"`\n}\n\n// ChatStreamResponse is the stream response body for deepseek\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tDelta        globals.Message `json:\"delta\"`\n\t\tIndex        int             `json:\"index\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t\tType    string `json:\"type\"`\n\t} `json:\"error\"`\n}\n"
  },
  {
    "path": "adapter/dify/chat.go",
    "content": "package dify\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype ChatInstance struct {\n\tEndpoint         string\n\tApiKey           string\n\tresponseComplete bool\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/chat-messages\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\ttimestamp := time.Now().UnixNano()\n\tuserID := fmt.Sprintf(\"user_%d\", timestamp)\n\n\tquery := \"\"\n\tfor _, msg := range props.Message {\n\t\tif msg.Role == \"user\" {\n\t\t\tquery = msg.Content\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn ChatRequest{\n\t\tInputs:           map[string]interface{}{},\n\t\tQuery:            query,\n\t\tResponseMode:     \"streaming\",\n\t\tUser:             userID,\n\t\tAutoGenerateName: true,\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string) (string, error) {\n\tif c.responseComplete {\n\t\treturn \"\", nil\n\t}\n\n\tif data == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tchunk, complete, err := processStreamResponse(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif complete {\n\t\tc.responseComplete = true\n\t}\n\n\treturn chunk.Content, nil\n}\n\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"dify error: %s\", err.Error())\n\t}\n\n\tresponseBody := utils.Marshal(res)\n\tresponse := processChatResponse(responseBody)\n\tif response == nil {\n\t\treturn \"\", fmt.Errorf(\"dify error: cannot parse response\")\n\t}\n\n\treturn response.Answer, nil\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tc.responseComplete = false\n\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tpartial, err := c.ProcessLine(data)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif partial != \"\" {\n\t\t\t\terr = callback(&globals.Chunk{Content: partial})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}, props.Proxy)\n\n\tc.responseComplete = true\n\n\tif err != nil {\n\t\tif strings.Contains(err.Body, \"\\\"code\\\":\") {\n\t\t\terrorResp := processChatErrorResponse(err.Body)\n\t\t\tif errorResp != nil {\n\t\t\t\treturn errors.New(fmt.Sprintf(\"dify error: %s (code: %s)\", errorResp.Message, errorResp.Code))\n\t\t\t}\n\n\t\t\tvar genericResp map[string]interface{}\n\t\t\tif jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil {\n\t\t\t\terrMsg, _ := json.Marshal(genericResp)\n\t\t\t\treturn errors.New(fmt.Sprintf(\"dify error: %s\", string(errMsg)))\n\t\t\t}\n\t\t}\n\n\t\tif err.Error != nil {\n\t\t\treturn err.Error\n\t\t}\n\t\treturn errors.New(fmt.Sprintf(\"dify error: unexpected error in stream request\"))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/dify/processor.go",
    "content": "package dify\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nfunc processChatResponse(data string) *ChatResponse {\n\tif form := utils.UnmarshalForm[ChatResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatStreamResponse(data string) *ChatStreamResponse {\n\tif form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\tif form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {\n\t\treturn form\n\t}\n\treturn nil\n}\n\nfunc processStreamResponse(data string) (*globals.Chunk, bool, error) {\n\tif data == \"\" {\n\t\treturn &globals.Chunk{Content: \"\"}, false, nil\n\t}\n\n\tstreamData := processChatStreamResponse(data)\n\tif streamData == nil {\n\t\treturn &globals.Chunk{Content: \"\"}, false, nil\n\t}\n\n\tswitch streamData.Event {\n\tcase \"message\":\n\t\tif streamData.Answer != \"\" {\n\t\t\treturn &globals.Chunk{\n\t\t\t\tContent: streamData.Answer,\n\t\t\t}, false, nil\n\t\t}\n\tcase \"message_end\":\n\t\treturn &globals.Chunk{\n\t\t\tContent: \"\",\n\t\t}, true, nil\n\tcase \"error\":\n\t\tif streamData.Code != \"\" && streamData.Message != \"\" {\n\t\t\treturn nil, false, errors.New(fmt.Sprintf(\"dify error: %s (code: %s)\", streamData.Message, streamData.Code))\n\t\t}\n\t\treturn nil, false, errors.New(\"dify error: conversation failed\")\n\tcase \"workflow_started\", \"node_started\", \"node_finished\", \"workflow_finished\", \"iteration_started\", \"iteration_next\", \"iteration_finished\", \"iteration_completed\", \"parallel_branch_started\", \"parallel_branch_finished\", \"ping\":\n\t\treturn &globals.Chunk{Content: \"\"}, false, nil\n\t}\n\n\terrorResp := processChatErrorResponse(data)\n\tif errorResp != nil {\n\t\treturn nil, false, errors.New(fmt.Sprintf(\"dify error: %s (code: %s)\", errorResp.Message, errorResp.Code))\n\t}\n\n\treturn &globals.Chunk{Content: \"\"}, false, nil\n}\n"
  },
  {
    "path": "adapter/dify/struct.go",
    "content": "package dify\n\ntype ChatRequest struct {\n\tInputs           map[string]interface{} `json:\"inputs\"`\n\tQuery            string                 `json:\"query\"`\n\tResponseMode     string                 `json:\"response_mode\"`\n\tConversationID   string                 `json:\"conversation_id,omitempty\"`\n\tUser             string                 `json:\"user\"`\n\tFiles            []File                 `json:\"files,omitempty\"`\n\tAutoGenerateName bool                   `json:\"auto_generate_name,omitempty\"`\n}\n\ntype File struct {\n\tType           string `json:\"type\"`\n\tTransferMethod string `json:\"transfer_method\"`\n\tURL            string `json:\"url,omitempty\"`\n\tUploadFileID   string `json:\"upload_file_id,omitempty\"`\n}\n\ntype ChatResponse struct {\n\tMessageID          string                 `json:\"message_id\"`\n\tConversationID     string                 `json:\"conversation_id\"`\n\tMode               string                 `json:\"mode\"`\n\tAnswer             string                 `json:\"answer\"`\n\tMetadata           map[string]interface{} `json:\"metadata\"`\n\tUsage              Usage                  `json:\"usage\"`\n\tRetrieverResources []RetrieverResource    `json:\"retriever_resources\"`\n\tCreatedAt          int64                  `json:\"created_at\"`\n}\n\ntype Usage struct {\n\tTokenCount   int `json:\"token_count\"`\n\tOutputTokens int `json:\"output_tokens\"`\n\tInputTokens  int `json:\"input_tokens\"`\n}\n\ntype RetrieverResource struct {\n\tSegmentID string `json:\"segment_id\"`\n\tContent   string `json:\"content\"`\n\tSource    string `json:\"source\"`\n}\n\ntype ChatStreamResponse struct {\n\tEvent              string                 `json:\"event\"`\n\tTaskID             string                 `json:\"task_id\"`\n\tMessageID          string                 `json:\"message_id,omitempty\"`\n\tConversationID     string                 `json:\"conversation_id,omitempty\"`\n\tAnswer             string                 `json:\"answer,omitempty\"`\n\tCreatedAt          int64                  `json:\"created_at,omitempty\"`\n\tMetadata           map[string]interface{} `json:\"metadata,omitempty\"`\n\tUsage              *Usage                 `json:\"usage,omitempty\"`\n\tRetrieverResources []RetrieverResource    `json:\"retriever_resources,omitempty\"`\n\tAudio              string                 `json:\"audio,omitempty\"`\n\tStatus             int                    `json:\"status,omitempty\"`\n\tCode               string                 `json:\"code,omitempty\"`\n\tMessage            string                 `json:\"message,omitempty\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tEvent     string `json:\"event\"`\n\tTaskID    string `json:\"task_id\"`\n\tMessageID string `json:\"message_id\"`\n\tStatus    int    `json:\"status\"`\n\tCode      string `json:\"code\"`\n\tMessage   string `json:\"message\"`\n}\n"
  },
  {
    "path": "adapter/hunyuan/chat.go",
    "content": "package hunyuan\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"context\"\n\t\"fmt\"\n)\n\nfunc (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message {\n\tvar result []globals.Message\n\tfor _, message := range messages {\n\t\tswitch message.Role {\n\t\tcase globals.System:\n\t\t\tresult = append(result, globals.Message{Role: globals.User, Content: message.Content})\n\t\tcase globals.Assistant, globals.User:\n\t\t\tbound := len(result) > 0 && result[len(result)-1].Role == message.Role\n\t\t\tif bound {\n\t\t\t\tresult[len(result)-1].Content += message.Content\n\t\t\t} else {\n\t\t\t\tresult = append(result, message)\n\t\t\t}\n\t\tcase globals.Tool:\n\t\t\tcontinue\n\t\tdefault:\n\t\t\tresult = append(result, message)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tcredential := NewCredential(c.GetSecretId(), c.GetSecretKey())\n\tclient := NewInstance(c.GetAppId(), c.GetEndpoint(), credential)\n\tchannel, err := client.Chat(context.Background(), NewRequest(Stream, c.FormatMessages(props.Message), props.Temperature, props.TopP))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"tencent hunyuan error: %+v\", err)\n\t}\n\n\tfor chunk := range channel {\n\t\tif chunk.Error.Code != 0 {\n\t\t\tfmt.Printf(\"tencent hunyuan error: %+v\\n\", chunk.Error)\n\t\t\tbreak\n\t\t}\n\n\t\tif len(chunk.Choices) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tchoice := chunk.Choices[0].Delta\n\t\tif err := callback(&globals.Chunk{Content: choice.Content}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/hunyuan/sdk.go",
    "content": "package hunyuan\n\n/*\n * Copyright (c) 2017-2018 THL A29 Limited, a Tencent company. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"chat/globals\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/google/uuid\"\n\t\"io\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tdefaultProtocol = \"https\"\n\tdefaultHost     = \"hunyuan.cloud.tencent.com\"\n\tpath            = \"/hyllm/v1/chat/completions?\"\n)\n\nconst (\n\tSynchronize = iota\n\tStream\n)\n\nfunc getUrl(endpoint string) string {\n\treturn fmt.Sprintf(\"%s://%s%s\", getProtocol(endpoint), getHost(endpoint), path)\n}\n\nfunc getProtocol(endpoint string) string {\n\tseg := strings.Split(endpoint, \"://\")\n\tif len(seg) > 0 && seg[0] != \"\" {\n\t\treturn seg[0]\n\t}\n\n\treturn defaultProtocol\n}\n\nfunc getHost(endpoint string) string {\n\tseg := strings.Split(endpoint, \"://\")\n\tif len(seg) > 1 && seg[1] != \"\" {\n\t\treturn seg[1]\n\t}\n\n\treturn defaultHost\n}\n\nfunc getFullPath(endpoint string) string {\n\treturn getHost(endpoint) + path\n}\n\ntype ResponseChoices struct {\n\tFinishReason string            `json:\"finish_reason,omitempty\"`\n\tMessages     []globals.Message `json:\"messages,omitempty\"`\n\tDelta        globals.Message   `json:\"delta,omitempty\"`\n}\n\ntype ResponseUsage struct {\n\tPromptTokens     int64 `json:\"prompt_tokens,omitempty\"`\n\tTotalTokens      int64 `json:\"total_tokens,omitempty\"`\n\tCompletionTokens int64 `json:\"completion_tokens,omitempty\"`\n}\n\ntype ResponseError struct {\n\tMessage string `json:\"message,omitempty\"`\n\tCode    int    `json:\"code,omitempty\"`\n}\n\ntype StreamDelta struct {\n\tContent string `json:\"content\"`\n}\n\ntype ChatRequest struct {\n\tAppID       int64             `json:\"app_id\"`\n\tSecretID    string            `json:\"secret_id\"`\n\tTimestamp   int               `json:\"timestamp\"`\n\tExpired     int               `json:\"expired\"`\n\tQueryID     string            `json:\"query_id\"`\n\tTemperature float64           `json:\"temperature\"`\n\tTopP        float64           `json:\"top_p\"`\n\tStream      int               `json:\"stream\"`\n\tMessages    []globals.Message `json:\"messages\"`\n}\n\ntype ChatResponse struct {\n\tChoices []ResponseChoices `json:\"choices,omitempty\"`\n\tCreated string            `json:\"created,omitempty\"`\n\tID      string            `json:\"id,omitempty\"`\n\tUsage   ResponseUsage     `json:\"usage,omitempty\"`\n\tError   ResponseError     `json:\"error,omitempty\"`\n\tNote    string            `json:\"note,omitempty\"`\n\tReqID   string            `json:\"req_id,omitempty\"`\n}\n\ntype Credential struct {\n\tSecretID  string\n\tSecretKey string\n}\n\nfunc NewCredential(secretID, secretKey string) *Credential {\n\treturn &Credential{SecretID: secretID, SecretKey: secretKey}\n}\n\ntype Client struct {\n\tCredential *Credential\n\tAppID      int64\n\tEndPoint   string\n}\n\nfunc NewInstance(appId int64, endpoint string, credential *Credential) *Client {\n\treturn &Client{\n\t\tCredential: credential,\n\t\tAppID:      appId,\n\t\tEndPoint:   endpoint,\n\t}\n}\n\nfunc NewRequest(mod int, messages []globals.Message, temperature *float32, topP *float32) ChatRequest {\n\tqueryID := uuid.NewString()\n\treturn ChatRequest{\n\t\tTimestamp:   int(time.Now().Unix()),\n\t\tExpired:     int(time.Now().Unix()) + 24*60*60,\n\t\tTemperature: 0,\n\t\tTopP:        0.8,\n\t\tMessages:    messages,\n\t\tQueryID:     queryID,\n\t\tStream:      mod,\n\t}\n}\n\nfunc (t *Client) getHttpReq(ctx context.Context, req ChatRequest) (*http.Request, error) {\n\treq.AppID = t.AppID\n\treq.SecretID = t.Credential.SecretID\n\tsignatureUrl := t.buildURL(req)\n\tsignature := t.genSignature(signatureUrl)\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"json marshal err: %+v\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, \"POST\", getUrl(t.EndPoint), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new http request err: %+v\", err)\n\t}\n\thttpReq.Header.Set(\"Authorization\", signature)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tif req.Stream == Stream {\n\t\thttpReq.Header.Set(\"Cache-Control\", \"no-cache\")\n\t\thttpReq.Header.Set(\"Connection\", \"keep-alive\")\n\t\thttpReq.Header.Set(\"Accept\", \"text/event-Stream\")\n\t}\n\n\treturn httpReq, nil\n}\n\nfunc (t *Client) Chat(ctx context.Context, req ChatRequest) (<-chan ChatResponse, error) {\n\tres := make(chan ChatResponse, 1)\n\thttpReq, err := t.getHttpReq(ctx, req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"do general http request err: %+v\", err)\n\t}\n\thttpResp, err := http.DefaultClient.Do(httpReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"do chat request err: %+v\", err)\n\t}\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"do chat request failed status code :%d\", httpResp.StatusCode)\n\t}\n\n\tif req.Stream == Synchronize {\n\t\terr = t.synchronize(httpResp, res)\n\t\treturn res, err\n\t}\n\tgo t.stream(httpResp, res)\n\treturn res, nil\n}\n\nfunc (t *Client) synchronize(httpResp *http.Response, res chan ChatResponse) (err error) {\n\tdefer func() {\n\t\thttpResp.Body.Close()\n\t\tclose(res)\n\t}()\n\tvar chatResp ChatResponse\n\trespBody, err := io.ReadAll(httpResp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read response body err: %+v\", err)\n\t}\n\n\tif err = json.Unmarshal(respBody, &chatResp); err != nil {\n\t\treturn fmt.Errorf(\"json unmarshal err: %+v\", err)\n\t}\n\tres <- chatResp\n\treturn\n}\n\nfunc (t *Client) stream(httpResp *http.Response, res chan ChatResponse) {\n\tdefer func() {\n\t\thttpResp.Body.Close()\n\t\tclose(res)\n\t}()\n\treader := bufio.NewReader(httpResp.Body)\n\tfor {\n\t\traw, err := reader.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf(\"tencent error: read stream data failed: %+v\", err), Code: 500}}\n\t\t\treturn\n\t\t}\n\n\t\tdata := strings.TrimSpace(string(raw))\n\t\tif data == \"\" || !strings.HasPrefix(data, \"data: \") {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar chatResponse ChatResponse\n\t\tif err := json.Unmarshal([]byte(data[6:]), &chatResponse); err != nil {\n\t\t\tres <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf(\"json unmarshal err: %+v\", err), Code: 500}}\n\t\t\treturn\n\t\t}\n\n\t\tres <- chatResponse\n\t\tif chatResponse.Choices[0].FinishReason == \"stop\" {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (t *Client) genSignature(url string) string {\n\tmac := hmac.New(sha1.New, []byte(t.Credential.SecretKey))\n\tsignURL := url\n\tmac.Write([]byte(signURL))\n\tsign := mac.Sum([]byte(nil))\n\treturn base64.StdEncoding.EncodeToString(sign)\n}\n\nfunc (t *Client) getMessages(messages []globals.Message) string {\n\tvar message string\n\tfor _, msg := range messages {\n\t\tmessage += fmt.Sprintf(`{\"role\":\"%s\",\"content\":\"%s\"},`, msg.Role, msg.Content)\n\t}\n\tmessage = strings.TrimSuffix(message, \",\")\n\n\treturn message\n}\n\nfunc (t *Client) buildURL(req ChatRequest) string {\n\tparams := make([]string, 0)\n\tparams = append(params, \"app_id=\"+strconv.FormatInt(req.AppID, 10))\n\tparams = append(params, \"secret_id=\"+req.SecretID)\n\tparams = append(params, \"timestamp=\"+strconv.Itoa(req.Timestamp))\n\tparams = append(params, \"query_id=\"+req.QueryID)\n\tparams = append(params, \"temperature=\"+strconv.FormatFloat(req.Temperature, 'f', -1, 64))\n\tparams = append(params, \"top_p=\"+strconv.FormatFloat(req.TopP, 'f', -1, 64))\n\tparams = append(params, \"stream=\"+strconv.Itoa(req.Stream))\n\tparams = append(params, \"expired=\"+strconv.Itoa(req.Expired))\n\tparams = append(params, fmt.Sprintf(\"messages=[%s]\", t.getMessages(req.Messages)))\n\n\tsort.Sort(sort.StringSlice(params))\n\treturn getFullPath(t.EndPoint) + strings.Join(params, \"&\")\n}\n"
  },
  {
    "path": "adapter/hunyuan/struct.go",
    "content": "package hunyuan\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n)\n\ntype ChatInstance struct {\n\tEndpoint  string\n\tAppId     int64\n\tSecretId  string\n\tSecretKey string\n}\n\nfunc (c *ChatInstance) GetAppId() int64 {\n\treturn c.AppId\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetSecretId() string {\n\treturn c.SecretId\n}\n\nfunc (c *ChatInstance) GetSecretKey() string {\n\treturn c.SecretKey\n}\n\nfunc NewChatInstance(endpoint, appId, secretId, secretKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint:  endpoint,\n\t\tAppId:     utils.ParseInt64(appId),\n\t\tSecretId:  secretId,\n\t\tSecretKey: secretKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparams := conf.SplitRandomSecret(3)\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tparams[0], params[1], params[2],\n\t)\n}\n"
  },
  {
    "path": "adapter/midjourney/api.go",
    "content": "package midjourney\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n)\n\nfunc (c *ChatInstance) GetImagineEndpoint() string {\n\treturn fmt.Sprintf(\"%s/mj/submit/imagine\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetChangeEndpoint() string {\n\treturn fmt.Sprintf(\"%s/mj/submit/change\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetImagineRequest(prompt string) *ImagineRequest {\n\treturn &ImagineRequest{\n\t\tNotifyHook: c.GetNotifyEndpoint(),\n\t\tPrompt:     prompt,\n\t}\n}\n\nfunc (c *ChatInstance) GetChangeRequest(action string, task string, index *int) *ChangeRequest {\n\treturn &ChangeRequest{\n\t\tNotifyHook: c.GetNotifyEndpoint(),\n\t\tAction:     action,\n\t\tIndex:      index,\n\t\tTaskId:     task,\n\t}\n}\n\nfunc (c *ChatInstance) CreateImagineRequest(proxy globals.ProxyConfig, prompt string) (*CommonResponse, error) {\n\tcontent, err := utils.PostRaw(\n\t\tc.GetImagineEndpoint(),\n\t\tc.GetMidjourneyHeaders(),\n\t\tc.GetImagineRequest(prompt),\n\t\tproxy,\n\t)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif data, err := utils.UnmarshalString[CommonResponse](content); err == nil {\n\t\treturn &data, nil\n\t} else {\n\t\treturn nil, utils.ToMarkdownError(err, content)\n\t}\n}\n\nfunc (c *ChatInstance) CreateChangeRequest(proxy globals.ProxyConfig, action string, task string, index *int) (*CommonResponse, error) {\n\tres, err := utils.Post(\n\t\tc.GetChangeEndpoint(),\n\t\tc.GetMidjourneyHeaders(),\n\t\tc.GetChangeRequest(action, task, index),\n\t\tproxy,\n\t)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.MapToStruct[CommonResponse](res), nil\n}\n"
  },
  {
    "path": "adapter/midjourney/chat.go",
    "content": "package midjourney\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst maxActions = 4\nconst (\n\tImagineAction   = \"IMAGINE\"\n\tUpscaleAction   = \"UPSCALE\"\n\tVariationAction = \"VARIATION\"\n\tRerollAction    = \"REROLL\"\n)\n\nconst (\n\tImagineCommand   = \"/IMAGINE\"\n\tUpscaleCommand   = \"/UPSCALE\"\n\tVariationCommand = \"/VARIATION\"\n\tRerollCommand    = \"/REROLL\"\n)\n\ntype ChatProps struct {\n\tMessages []globals.Message\n\tModel    string\n}\n\nfunc getMode(model string) string {\n\tswitch model {\n\tcase globals.Midjourney: // relax\n\t\treturn RelaxMode\n\tcase globals.MidjourneyFast: // fast\n\t\treturn FastMode\n\tcase globals.MidjourneyTurbo: // turbo\n\t\treturn TurboMode\n\tdefault:\n\t\treturn RelaxMode\n\t}\n}\n\nfunc (c *ChatInstance) IsIgnoreMode() bool {\n\treturn strings.HasSuffix(c.Endpoint, \"/mj-relax\") ||\n\t\tstrings.HasSuffix(c.Endpoint, \"/mj-fast\") ||\n\t\tstrings.HasSuffix(c.Endpoint, \"/mj-turbo\")\n}\n\nfunc (c *ChatInstance) GetCleanPrompt(model string, prompt string) string {\n\tif c.IsIgnoreMode() {\n\t\treturn prompt\n\t}\n\n\tarr := strings.Split(strings.TrimSpace(prompt), \" \")\n\tvar res []string\n\n\tfor _, word := range arr {\n\t\tif utils.Contains[string](word, RendererMode) {\n\t\t\tcontinue\n\t\t}\n\t\tres = append(res, word)\n\t}\n\n\tres = append(res, getMode(model))\n\ttarget := strings.Join(res, \" \")\n\treturn target\n}\n\nfunc (c *ChatInstance) GetPrompt(props *adaptercommon.ChatProps) string {\n\tif len(props.Message) == 0 {\n\t\treturn \"\"\n\t}\n\n\tcontent := props.Message[len(props.Message)-1].Content\n\treturn c.GetCleanPrompt(props.Model, content)\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\t// partial response like:\n\t// ```progress\n\t// 0\n\t// ...\n\t// 100\n\t// ```\n\t// ![image](...)\n\n\tif len(globals.NotifyUrl) == 0 {\n\t\treturn fmt.Errorf(\"format error: please provide available notify url\")\n\t}\n\taction, prompt := c.ExtractPrompt(c.GetPrompt(props))\n\tif len(prompt) == 0 {\n\t\treturn fmt.Errorf(\"format error: please provide available prompt\")\n\t}\n\n\tvar begin bool\n\n\tform, err := c.CreateStreamTask(props, action, prompt, func(form *StorageForm, progress int) error {\n\t\tif progress == -1 {\n\t\t\t// ping event\n\t\t\treturn callback(&globals.Chunk{Content: \"\"})\n\t\t}\n\n\t\tif !begin {\n\t\t\tbegin = true\n\t\t\tif err := callback(&globals.Chunk{Content: \"```progress\\n\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if progress == 100 && !begin {\n\t\t\tif err := callback(&globals.Chunk{Content: \"```progress\\n\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif err := callback(&globals.Chunk{Content: fmt.Sprintf(\"%d\\n\", progress)}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif progress == 100 {\n\t\t\tif err := callback(&globals.Chunk{Content: \"```\\n\"}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error from midjourney: %s\", err.Error())\n\t}\n\n\tif err := callback(&globals.Chunk{Content: utils.GetImageMarkdown(form.Url)}); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.CallbackActions(props, form, callback)\n}\n\nfunc toVirtualMessage(message string, model string) string {\n\tprompt := strings.Replace(message, \" \", \"-\", -1)\n\treturn fmt.Sprintf(\"https://chatnio.virtual%s::%s\", prompt, model)\n}\n\nfunc (c *ChatInstance) CallbackActions(props *adaptercommon.ChatProps, form *StorageForm, callback globals.Hook) error {\n\tif form.Action == UpscaleAction {\n\t\treturn nil\n\t}\n\n\tactions := utils.Range(1, maxActions+1)\n\n\tupscale := strings.Join(utils.Each(actions, func(index int) string {\n\t\treturn fmt.Sprintf(\"[U%d](%s)\", index, toVirtualMessage(fmt.Sprintf(\"/UPSCALE %s %d\", form.Task, index), props.OriginalModel))\n\t}), \" \")\n\n\tvariation := strings.Join(utils.Each(actions, func(index int) string {\n\t\treturn fmt.Sprintf(\"[V%d](%s)\", index, toVirtualMessage(fmt.Sprintf(\"/VARIATION %s %d\", form.Task, index), props.OriginalModel))\n\t}), \" \")\n\n\treroll := fmt.Sprintf(\"[REROLL](%s)\", toVirtualMessage(fmt.Sprintf(\"/REROLL %s\", form.Task), props.OriginalModel))\n\n\treturn callback(&globals.Chunk{\n\t\tContent: fmt.Sprintf(\"\\n\\n%s\\n\\n%s\\n\\n%s\\n\", upscale, variation, reroll),\n\t})\n}\n"
  },
  {
    "path": "adapter/midjourney/expose.go",
    "content": "package midjourney\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nvar whiteList []string\n\nfunc SaveWhiteList(raw string) {\n\tarr := utils.Filter(strings.Split(raw, \",\"), func(s string) bool {\n\t\treturn len(strings.TrimSpace(s)) > 0\n\t})\n\n\tfor _, ip := range arr {\n\t\tif !utils.Contains(ip, whiteList) {\n\t\t\twhiteList = append(whiteList, ip)\n\t\t}\n\t}\n}\n\nfunc InWhiteList(ip string) bool {\n\tif len(whiteList) == 0 {\n\t\treturn true\n\t}\n\treturn utils.Contains(ip, whiteList)\n}\n\nfunc NotifyAPI(c *gin.Context) {\n\tif !InWhiteList(c.ClientIP()) {\n\t\tglobals.Info(fmt.Sprintf(\"[midjourney] notify api: banned request from %s\", c.ClientIP()))\n\t\tc.AbortWithStatus(http.StatusForbidden)\n\t\treturn\n\t}\n\n\tvar form NotifyForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.AbortWithStatus(http.StatusBadRequest)\n\t\treturn\n\t}\n\tglobals.Debug(fmt.Sprintf(\"[midjourney] notify api: get notify: %s (from: %s)\", utils.Marshal(form), c.ClientIP()))\n\n\tif !utils.Contains(form.Status, []string{InProgress, Success, Failure}) {\n\t\t// ignore\n\t\treturn\n\t}\n\n\treason, ok := form.FailReason.(string)\n\tif !ok {\n\t\treason = \"unknown\"\n\t}\n\n\terr := setStorage(form.Id, StorageForm{\n\t\tTask:       form.Id,\n\t\tAction:     form.Action,\n\t\tUrl:        form.ImageUrl,\n\t\tFailReason: reason,\n\t\tProgress:   form.Progress,\n\t\tStatus:     form.Status,\n\t})\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": err == nil,\n\t})\n}\n"
  },
  {
    "path": "adapter/midjourney/handler.go",
    "content": "package midjourney\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst maxTimeout = 30 * time.Minute // 30 min timeout\n\nfunc getStatusCode(action string, response *CommonResponse) error {\n\tcode := response.Code\n\tswitch code {\n\tcase SuccessCode, QueueCode:\n\t\treturn nil\n\tcase ExistedCode:\n\t\tif action != ImagineCommand {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"task is existed, please try again later with another prompt\")\n\tcase MaxQueueCode:\n\t\treturn fmt.Errorf(\"task queue is full, please try again later\")\n\tcase NudeCode:\n\t\treturn fmt.Errorf(\"prompt violates the content policy of midjourney, the request is rejected\")\n\tdefault:\n\t\treturn fmt.Errorf(fmt.Sprintf(\"unknown error from midjourney (code: %d, description: %s)\", code, response.Description))\n\t}\n}\n\nfunc getProgress(value string) int {\n\tprogress := strings.TrimSuffix(value, \"%\")\n\treturn utils.ParseInt(progress)\n}\n\nfunc (c *ChatInstance) GetAction(command string) string {\n\treturn strings.TrimLeft(command, \"/\")\n}\n\nfunc (c *ChatInstance) ExtractPrompt(input string) (action string, prompt string) {\n\tsegment := utils.SafeSplit(input, \" \", 2)\n\n\taction = strings.TrimSpace(segment[0])\n\tprompt = strings.TrimSpace(segment[1])\n\n\tswitch action {\n\tcase ImagineCommand, VariationCommand, UpscaleCommand, RerollCommand:\n\t\treturn\n\tdefault:\n\t\treturn ImagineCommand, strings.TrimSpace(input)\n\t}\n}\n\nfunc (c *ChatInstance) ExtractCommand(input string) (task string, index *int) {\n\tsegment := utils.SafeSplit(input, \" \", 2)\n\n\ttask = strings.TrimSpace(segment[0])\n\n\tif segment[1] != \"\" {\n\t\tdata := segment[1]\n\t\tslice := strings.Split(segment[1], \" \")\n\t\tif len(slice) > 1 {\n\t\t\tdata = slice[0]\n\t\t}\n\n\t\tindex = utils.ToPtr(utils.ParseInt(strings.TrimSpace(data)))\n\t}\n\n\treturn\n}\n\nfunc (c *ChatInstance) CreateRequest(proxy globals.ProxyConfig, action string, prompt string) (*CommonResponse, error) {\n\tswitch action {\n\tcase ImagineCommand:\n\t\treturn c.CreateImagineRequest(proxy, prompt)\n\tcase VariationCommand, UpscaleCommand, RerollCommand:\n\t\ttask, index := c.ExtractCommand(prompt)\n\n\t\treturn c.CreateChangeRequest(proxy, c.GetAction(action), task, index)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown action: %s\", action)\n\t}\n}\n\nfunc (c *ChatInstance) CreateStreamTask(props *adaptercommon.ChatProps, action string, prompt string, hook func(form *StorageForm, progress int) error) (*StorageForm, error) {\n\tres, err := c.CreateRequest(props.Proxy, action, prompt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := getStatusCode(action, res); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttask := res.Result\n\tprogress := -1\n\n\tticker := time.NewTicker(50 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tform := getNotifyStorage(task)\n\t\t\tif form == nil {\n\t\t\t\t// hook for ping (in order to catch the stop signal)\n\t\t\t\tif err := hook(nil, -1); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch form.Status {\n\t\t\tcase Success:\n\t\t\t\tif err := hook(form, 100); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn form, nil\n\t\t\tcase Failure:\n\t\t\t\treturn nil, fmt.Errorf(\"task failed: %s\", form.FailReason)\n\t\t\tcase InProgress:\n\t\t\t\tcurrent := getProgress(form.Progress)\n\t\t\t\tif progress != current {\n\t\t\t\t\tif err := hook(form, current); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tprogress = current\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// ping\n\t\t\t\tif err := hook(form, -1); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-time.After(maxTimeout):\n\t\t\treturn nil, fmt.Errorf(\"task timeout\")\n\t\t}\n\t}\n}"
  },
  {
    "path": "adapter/midjourney/storage.go",
    "content": "package midjourney\n\nimport (\n\t\"chat/connection\"\n\t\"chat/utils\"\n\t\"fmt\"\n)\n\nfunc getTaskName(task string) string {\n\treturn fmt.Sprintf(\"nio:mj-task:%s\", task)\n}\n\nfunc setStorage(task string, form StorageForm) error {\n\treturn utils.SetJson(connection.Cache, getTaskName(task), form, 60*60)\n}\n\nfunc getNotifyStorage(task string) *StorageForm {\n\treturn utils.GetCacheStore[StorageForm](connection.Cache, getTaskName(task))\n}\n"
  },
  {
    "path": "adapter/midjourney/struct.go",
    "content": "package midjourney\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\nvar midjourneyEmptySecret = \"null\"\n\ntype ChatInstance struct {\n\tEndpoint  string\n\tApiSecret string\n}\n\nfunc (c *ChatInstance) GetApiSecret() string {\n\treturn c.ApiSecret\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetMidjourneyHeaders() map[string]string {\n\tsecret := c.GetApiSecret()\n\tif secret == \"\" || secret == midjourneyEmptySecret {\n\t\treturn map[string]string{\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t}\n\t}\n\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"mj-api-secret\": secret,\n\t}\n}\n\nfunc (c *ChatInstance) GetNotifyEndpoint() string {\n\treturn fmt.Sprintf(\"%s/mj/notify\", globals.NotifyUrl)\n}\n\nfunc NewChatInstance(endpoint, apiSecret, whiteList string) *ChatInstance {\n\tSaveWhiteList(whiteList)\n\n\treturn &ChatInstance{\n\t\tEndpoint:  endpoint,\n\t\tApiSecret: apiSecret,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparams := conf.SplitRandomSecret(2)\n\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tparams[0], params[1],\n\t)\n}\n"
  },
  {
    "path": "adapter/midjourney/types.go",
    "content": "package midjourney\n\nconst (\n\tSuccessCode  = 1\n\tExistedCode  = 21\n\tQueueCode    = 22\n\tMaxQueueCode = 23\n\tNudeCode     = 24\n)\n\nconst (\n\tNotStartStatus = \"NOT_START\"\n\tSubmitted      = \"SUBMITTED\"\n\tInProgress     = \"IN_PROGRESS\"\n\tFailure        = \"FAILURE\"\n\tSuccess        = \"SUCCESS\"\n)\n\nconst (\n\tTurboMode = \"--turbo\"\n\tFastMode  = \"--fast\"\n\tRelaxMode = \"--relax\"\n)\n\nvar RendererMode = []string{TurboMode, FastMode, RelaxMode}\n\ntype CommonHeader struct {\n\tContentType string `json:\"Content-Type\"`\n\tMjApiSecret string `json:\"mj-api-secret,omitempty\"`\n}\n\ntype CommonResponse struct {\n\tCode        int    `json:\"code\"`\n\tDescription string `json:\"description\"`\n\tResult      string `json:\"result\"`\n}\n\ntype ImagineRequest struct {\n\tNotifyHook string `json:\"notifyHook\"`\n\tPrompt     string `json:\"prompt\"`\n}\n\ntype ChangeRequest struct {\n\tNotifyHook string `json:\"notifyHook\"`\n\tAction     string `json:\"action\"`\n\tIndex      *int   `json:\"index,omitempty\"`\n\tTaskId     string `json:\"taskId\"`\n}\n\ntype NotifyForm struct {\n\tId          string      `json:\"id\"`\n\tAction      string      `json:\"action\"`\n\tStatus      string      `json:\"status\"`\n\tPrompt      string      `json:\"prompt\"`\n\tPromptEn    string      `json:\"promptEn\"`\n\tDescription string      `json:\"description\"`\n\tSubmitTime  int64       `json:\"submitTime\"`\n\tStartTime   int64       `json:\"startTime\"`\n\tFinishTime  int64       `json:\"finishTime\"`\n\tProgress    string      `json:\"progress\"`\n\tImageUrl    string      `json:\"imageUrl\"`\n\tFailReason  interface{} `json:\"failReason\"`\n}\n\ntype StorageForm struct {\n\tTask       string `json:\"task\"`\n\tAction     string `json:\"action\"`\n\tUrl        string `json:\"url\"`\n\tFailReason string `json:\"failReason\"`\n\tProgress   string `json:\"progress\"`\n\tStatus     string `json:\"status\"`\n}\n"
  },
  {
    "path": "adapter/openai/chat.go",
    "content": "package openai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string {\n\tif props.Model == globals.GPT3TurboInstruct {\n\t\treturn fmt.Sprintf(\"%s/v1/completions\", c.GetEndpoint())\n\t}\n\treturn fmt.Sprintf(\"%s/v1/chat/completions\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {\n\tresult := \"\"\n\tfor _, message := range messages {\n\t\tresult += fmt.Sprintf(\"%s: %s\\n\", message.Role, message.Content)\n\t}\n\treturn result\n}\n\nfunc (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {\n\tif len(props.Message) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn props.Message[len(props.Message)-1].Content\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\tif props.Model == globals.GPT3TurboInstruct {\n\t\t// for completions\n\t\treturn CompletionRequest{\n\t\t\tModel:    props.Model,\n\t\t\tPrompt:   c.GetCompletionPrompt(props.Message),\n\t\t\tMaxToken: props.MaxTokens,\n\t\t\tStream:   stream,\n\t\t}\n\t}\n\n\tmessages := formatMessages(props)\n\n\t// o1, o3, gpt-5 compatibility\n\tisNewModel := len(props.Model) >= 2 && (props.Model[:2] == \"o1\" || props.Model[:2] == \"o3\") || strings.HasPrefix(props.Model, \"gpt-5\")\n\n\tvar temperature *float32\n\tif isNewModel {\n\t\ttemp := float32(1.0)\n\t\ttemperature = &temp\n\t} else {\n\t\ttemperature = props.Temperature\n\t}\n\n\trequest := ChatRequest{\n\t\tModel:            props.Model,\n\t\tMessages:         messages,\n\t\tStream:           stream,\n\t\tPresencePenalty:  props.PresencePenalty,\n\t\tFrequencyPenalty: props.FrequencyPenalty,\n\t\tTemperature:      temperature,\n\t\tTopP:             props.TopP,\n\t\tTools:            props.Tools,\n\t\tToolChoice:       props.ToolChoice,\n\t}\n\n\tif isNewModel {\n\t\trequest.MaxCompletionTokens = props.MaxTokens\n\t} else {\n\t\trequest.MaxToken = props.MaxTokens\n\t}\n\treturn request\n}\n\n// CreateChatRequest is the native http request body for openai\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tif globals.IsOpenAIDalleModel(props.Model) {\n\t\treturn c.CreateImage(props)\n\t}\n\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(props),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"openai error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"openai error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", fmt.Errorf(\"openai error: %s\", data.Error.Message)\n\t}\n\treturn data.Choices[0].Message.Content, nil\n}\n\nfunc hideRequestId(message string) string {\n\t// xxx (request id: 2024020311120561344953f0xfh0TX)\n\n\texp := regexp.MustCompile(`\\(request id: [a-zA-Z0-9]+\\)`)\n\treturn exp.ReplaceAllString(message, \"\")\n}\n\n// CreateStreamChatRequest is the stream response body for openai\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tif globals.IsOpenAIDalleModel(props.Model) {\n\t\tif url, err := c.CreateImage(props); err != nil {\n\t\t\treturn err\n\t\t} else {\n\t\t\treturn callback(&globals.Chunk{\n\t\t\t\tContent: url,\n\t\t\t})\n\t\t}\n\t}\n\n\tisCompletionType := props.Model == globals.GPT3TurboInstruct\n\n\tticks := 0\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(props),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tticks += 1\n\n\t\t\tpartial, err := c.ProcessLine(data, isCompletionType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(partial)\n\t\t},\n\t}, props.Proxy)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tif form.Error.Type == \"\" && form.Error.Message == \"\" {\n\t\t\t\treturn errors.New(utils.ToMarkdownCode(\"json\", err.Body))\n\t\t\t}\n\n\t\t\tmsg := fmt.Sprintf(\"%s (type: %s)\", form.Error.Message, form.Error.Type)\n\t\t\treturn errors.New(hideRequestId(msg))\n\t\t}\n\t\treturn err.Error\n\t}\n\n\tif ticks == 0 {\n\t\treturn errors.New(\"no response\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/openai/image.go",
    "content": "package openai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype ImageProps struct {\n\tModel  string\n\tPrompt string\n\tSize   ImageSize\n\tProxy  globals.ProxyConfig\n}\n\nfunc (c *ChatInstance) GetImageEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v1/images/generations\", c.GetEndpoint())\n}\n\n// CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error\nfunc (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) {\n\tres, err := utils.Post(\n\t\tc.GetImageEndpoint(),\n\t\tc.GetHeader(), ImageRequest{\n\t\t\tModel:  props.Model,\n\t\t\tPrompt: props.Prompt,\n\t\t\tSize: utils.Multi[ImageSize](\n\t\t\t\tprops.Model == globals.Dalle3 || props.Model == globals.GPTImage1,\n\t\t\t\tImageSize1024,\n\t\t\t\tImageSize512,\n\t\t\t),\n\t\t\tN: 1,\n\t\t}, props.Proxy)\n\tif err != nil || res == nil {\n\t\treturn \"\", \"\", fmt.Errorf(err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ImageResponse](res)\n\tif data == nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"openai error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(data.Error.Message)\n\t}\n\n\t// for gpt-image-1, return base64 data if available\n\tif props.Model == globals.GPTImage1 && data.Data[0].B64Json != \"\" {\n\t\treturn \"\", data.Data[0].B64Json, nil\n\t}\n\n\treturn data.Data[0].Url, \"\", nil\n}\n\n// CreateImage will create a dalle image from prompt, return markdown of image\nfunc (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {\n\turl, b64Json, err := c.CreateImageRequest(ImageProps{\n\t\tModel:  props.Model,\n\t\tPrompt: c.GetLatestPrompt(props),\n\t\tProxy:  props.Proxy,\n\t})\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"safety\") {\n\t\t\treturn err.Error(), nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\tif b64Json != \"\" {\n\t\treturn utils.GetBase64ImageMarkdown(b64Json), nil\n\t}\n\n\tstoredUrl := utils.StoreImage(url)\n\treturn utils.GetImageMarkdown(storedUrl), nil\n}\n"
  },
  {
    "path": "adapter/openai/processor.go",
    "content": "package openai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\nfunc formatMessages(props *adaptercommon.ChatProps) interface{} {\n\tif globals.IsVisionModel(props.Model) {\n\t\treturn utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {\n\t\t\tif message.Role == globals.User {\n\t\t\t\tcontent, urls := utils.ExtractImages(message.Content, true)\n\t\t\t\timages := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {\n\t\t\t\t\tobj, err := utils.NewImage(url)\n\t\t\t\t\tprops.Buffer.AddImage(obj)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tglobals.Info(fmt.Sprintf(\"cannot process image: %s (source: %s)\", err.Error(), utils.Extract(url, 24, \"...\")))\n\t\t\t\t\t}\n\n\t\t\t\t\treturn &MessageContent{\n\t\t\t\t\t\tType: \"image_url\",\n\t\t\t\t\t\tImageUrl: &ImageUrl{\n\t\t\t\t\t\t\tUrl: url,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\treturn Message{\n\t\t\t\t\tRole: message.Role,\n\t\t\t\t\tContent: utils.Prepend(images, MessageContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: &content,\n\t\t\t\t\t}),\n\t\t\t\t\tName:         message.Name,\n\t\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Message{\n\t\t\t\tRole: message.Role,\n\t\t\t\tContent: MessageContents{\n\t\t\t\t\tMessageContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: &message.Content,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tName:         message.Name,\n\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t}\n\t\t})\n\t}\n\n\treturn props.Message\n}\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\treturn utils.UnmarshalForm[ChatStreamResponse](data)\n}\n\nfunc processCompletionResponse(data string) *CompletionResponse {\n\treturn utils.UnmarshalForm[CompletionResponse](data)\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\treturn utils.UnmarshalForm[ChatStreamErrorResponse](data)\n}\n\nfunc getChoices(form *ChatStreamResponse) *globals.Chunk {\n\tif len(form.Choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoice := form.Choices[0].Delta\n\n\treturn &globals.Chunk{\n\t\tContent:      choice.Content,\n\t\tToolCall:     choice.ToolCalls,\n\t\tFunctionCall: choice.FunctionCall,\n\t}\n}\n\nfunc getCompletionChoices(form *CompletionResponse) string {\n\tif len(form.Choices) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn form.Choices[0].Text\n}\n\nfunc getRobustnessResult(chunk string) string {\n\texp := `\\\"content\\\":\\\"(.*?)\\\"`\n\tcompile, err := regexp.Compile(exp)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmatches := compile.FindStringSubmatch(chunk)\n\tif len(matches) > 1 {\n\t\treturn utils.ProcessRobustnessChar(matches[1])\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {\n\tif isCompletionType {\n\t\t// openai legacy support\n\t\tif completion := processCompletionResponse(data); completion != nil {\n\t\t\treturn &globals.Chunk{\n\t\t\t\tContent: getCompletionChoices(completion),\n\t\t\t}, nil\n\t\t}\n\n\t\tglobals.Warn(fmt.Sprintf(\"openai error: cannot parse completion response: %s\", data))\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse completion response\")\n\t}\n\n\tif form := processChatResponse(data); form != nil {\n\t\treturn getChoices(form), nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(fmt.Sprintf(\"openai error: %s (type: %s)\", form.Error.Message, form.Error.Type))\n\t}\n\n\tglobals.Warn(fmt.Sprintf(\"openai error: cannot parse chat completion response: %s\", data))\n\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse chat completion response\")\n}\n"
  },
  {
    "path": "adapter/openai/struct.go",
    "content": "package openai\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/openai/types.go",
    "content": "package openai\n\nimport \"chat/globals\"\n\ntype ImageUrl struct {\n\tUrl    string  `json:\"url\"`\n\tDetail *string `json:\"detail,omitempty\"`\n}\n\ntype MessageContent struct {\n\tType     string    `json:\"type\"`\n\tText     *string   `json:\"text,omitempty\"`\n\tImageUrl *ImageUrl `json:\"image_url,omitempty\"`\n}\n\ntype MessageContents []MessageContent\n\ntype Message struct {\n\tRole             string                `json:\"role\"`\n\tContent          MessageContents       `json:\"content\"`\n\tName             *string               `json:\"name,omitempty\"`\n\tFunctionCall     *globals.FunctionCall `json:\"function_call,omitempty\"` // only `function` role\n\tToolCallId       *string               `json:\"tool_call_id,omitempty\"`  // only `tool` role\n\tToolCalls        *globals.ToolCalls    `json:\"tool_calls,omitempty\"`    // only `assistant` role\n\tReasoningContent *string               `json:\"reasoning,omitempty\"`     // only for claude reasoning models\n}\n\n// ChatRequest is the request body for openai\ntype ChatRequest struct {\n\tModel               string                 `json:\"model\"`\n\tMessages            interface{}            `json:\"messages\"`\n\tMaxToken            *int                   `json:\"max_tokens,omitempty\"`\n\tMaxCompletionTokens *int                   `json:\"max_completion_tokens,omitempty\"`\n\tStream              bool                   `json:\"stream\"`\n\tPresencePenalty     *float32               `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty    *float32               `json:\"frequency_penalty,omitempty\"`\n\tTemperature         *float32               `json:\"temperature,omitempty\"`\n\tTopP                *float32               `json:\"top_p,omitempty\"`\n\tTools               *globals.FunctionTools `json:\"tools,omitempty\"`\n\tToolChoice          *interface{}           `json:\"tool_choice,omitempty\"` // string or object\n}\n\n// CompletionRequest is the request body for openai completion\ntype CompletionRequest struct {\n\tModel    string `json:\"model\"`\n\tPrompt   string `json:\"prompt\"`\n\tMaxToken *int   `json:\"max_tokens,omitempty\"`\n\tStream   bool   `json:\"stream\"`\n}\n\n// ChatResponse is the native http request body for openai\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tIndex        int             `json:\"index\"`\n\t\tMessage      globals.Message `json:\"message\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\n// ChatStreamResponse is the stream response body for openai\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tDelta        globals.Message `json:\"delta\"`\n\t\tIndex        int             `json:\"index\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n}\n\n// CompletionResponse is the native http request body / stream response body for openai completion\ntype CompletionResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tText  string `json:\"text\"`\n\t\tIndex int    `json:\"index\"`\n\t} `json:\"choices\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t\tType    string `json:\"type\"`\n\t} `json:\"error\"`\n}\n\ntype ImageSize string\n\n// ImageRequest is the request body for openai dalle image generation\ntype ImageRequest struct {\n\tModel  string    `json:\"model\"`\n\tPrompt string    `json:\"prompt\"`\n\tSize   ImageSize `json:\"size\"`\n\tN      int       `json:\"n\"`\n}\n\ntype ImageResponse struct {\n\tData []struct {\n\t\tUrl     string `json:\"url,omitempty\"`\n\t\tB64Json string `json:\"b64_json,omitempty\"`\n\t} `json:\"data\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\nvar (\n\tImageSize256  ImageSize = \"256x256\"\n\tImageSize512  ImageSize = \"512x512\"\n\tImageSize1024 ImageSize = \"1024x1024\"\n)\n"
  },
  {
    "path": "adapter/openai/videos.go",
    "content": "package openai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"time\"\n)\n\ntype VideoRequest struct {\n\tPrompt         string  `json:\"prompt\"`\n\tModel          string  `json:\"model,omitempty\"`\n\tSeconds        *string `json:\"seconds,omitempty\"`\n\tSize           *string `json:\"size,omitempty\"`\n\tInputReference *string `json:\"input_reference,omitempty\"`\n}\n\ntype VideoJob struct {\n\tCompletedAt        *int64  `json:\"completed_at,omitempty\"`\n\tCreatedAt          int64   `json:\"created_at\"`\n\tExpiresAt          *int64  `json:\"expires_at,omitempty\"`\n\tId                 string  `json:\"id\"`\n\tModel              string  `json:\"model\"`\n\tObject             string  `json:\"object\"`\n\tProgress           *int    `json:\"progress,omitempty\"`\n\tPrompt             string  `json:\"prompt\"`\n\tRemixedFromVideoId *string `json:\"remixed_from_video_id,omitempty\"`\n\tSeconds            string  `json:\"seconds\"`\n\tSize               string  `json:\"size\"`\n\tStatus             string  `json:\"status\"`\n\tError              *struct {\n\t\tCode    string `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error,omitempty\"`\n}\n\nfunc (c *ChatInstance) getVideoCreateEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v1/videos\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) getVideoQueryEndpoint(id string) string {\n\treturn fmt.Sprintf(\"%s/v1/videos/%s\", c.GetEndpoint(), id)\n}\n\nfunc (c *ChatInstance) CreateVideoRequest(props *adaptercommon.VideoProps, hook globals.Hook) error {\n\tbody := VideoRequest{\n\t\tPrompt:         props.Prompt,\n\t\tModel:          props.Model,\n\t\tSeconds:        props.Seconds,\n\t\tSize:           props.Size,\n\t\tInputReference: props.InputReference,\n\t}\n\n\tres, err := utils.Post(c.getVideoCreateEndpoint(), c.GetHeader(), body, props.Proxy)\n\tif err != nil || res == nil {\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"openai video error: %s\", err.Error())\n\t\t}\n\t\treturn fmt.Errorf(\"openai video error: empty response\")\n\t}\n\n\tjob := utils.MapToStruct[VideoJob](res)\n\tif job == nil {\n\t\treturn fmt.Errorf(\"openai video error: cannot parse response\")\n\t}\n\tif job.Error != nil && (job.Error.Message != \"\") {\n\t\treturn fmt.Errorf(\"openai video error: %s\", job.Error.Message)\n\t}\n\n\tconst maxTimeout = 30 * time.Minute\n\tticker := time.NewTicker(2 * time.Second)\n\tdefer ticker.Stop()\n\n\tdeadline := time.After(maxTimeout)\n\n\tvar begin bool\n\tvar lastProgress int = -1\n\n\tfor {\n\t\tif job.Status == \"completed\" {\n\t\t\tif begin {\n\t\t\t\tif err := hook(&globals.Chunk{Content: \"```\\n\"}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn hook(&globals.Chunk{Content: utils.Marshal(job)})\n\t\t}\n\t\tif job.Status == \"failed\" {\n\t\t\tif begin {\n\t\t\t\tif err := hook(&globals.Chunk{Content: \"```\\n\"}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif job.Error != nil && job.Error.Message != \"\" {\n\t\t\t\treturn fmt.Errorf(\"openai video job failed: %s\", job.Error.Message)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"openai video job failed\")\n\t\t}\n\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tif job.Id == \"\" {\n\t\t\t\treturn hook(&globals.Chunk{Content: utils.Marshal(job)})\n\t\t\t}\n\t\t\tdata, gErr := utils.Get(c.getVideoQueryEndpoint(job.Id), c.GetHeader(), props.Proxy)\n\t\t\tif gErr != nil || data == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif j := utils.MapToStruct[VideoJob](data); j != nil {\n\t\t\t\tjob = j\n\t\t\t}\n\n\t\t\tprogress := 0\n\t\t\tif job.Progress != nil {\n\t\t\t\tprogress = *job.Progress\n\t\t\t}\n\n\t\t\tif !begin {\n\t\t\t\tbegin = true\n\t\t\t\tif err := hook(&globals.Chunk{Content: \"```progress\\n\"}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif progress != lastProgress {\n\t\t\t\tif err := hook(&globals.Chunk{Content: fmt.Sprintf(\"%d\\n\", progress)}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tlastProgress = progress\n\t\t\t}\n\t\tcase <-deadline:\n\t\t\tif begin {\n\t\t\t\tif err := hook(&globals.Chunk{Content: \"```\\n\"}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"openai video job timeout\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "adapter/palm2/chat.go",
    "content": "package palm2\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar geminiMaxImages = 16\n\nfunc (c *ChatInstance) GetChatEndpoint(model string, stream bool) string {\n\tif model == globals.ChatBison001 {\n\t\treturn fmt.Sprintf(\"%s/v1beta2/models/%s:generateMessage?key=%s\", c.Endpoint, model, c.ApiKey)\n\t}\n\n\tif stream {\n\t\treturn fmt.Sprintf(\"%s/v1beta/models/%s:streamGenerateContent?alt=sse&key=%s\", c.Endpoint, model, c.ApiKey)\n\t}\n\n\treturn fmt.Sprintf(\"%s/v1beta/models/%s:generateContent?key=%s\", c.Endpoint, model, c.ApiKey)\n}\n\nfunc (c *ChatInstance) ConvertMessage(message []globals.Message) []PalmMessage {\n\tvar result []PalmMessage\n\tfor i, item := range message {\n\t\tif len(item.Content) == 0 {\n\t\t\t// palm model: message must include non empty content\n\t\t\tcontinue\n\t\t}\n\n\t\tif item.Role == globals.Tool {\n\t\t\tcontinue\n\t\t}\n\n\t\tif i > 0 && item.Role == result[len(result)-1].Author {\n\t\t\t// palm model: messages must alternate between authors\n\t\t\tresult[len(result)-1].Content += \" \" + item.Content\n\t\t\tcontinue\n\t\t}\n\n\t\tresult = append(result, PalmMessage{\n\t\t\tAuthor:  item.Role,\n\t\t\tContent: item.Content,\n\t\t})\n\t}\n\treturn result\n}\n\nfunc (c *ChatInstance) GetPalm2ChatBody(props *adaptercommon.ChatProps) *PalmChatBody {\n\treturn &PalmChatBody{\n\t\tPrompt: PalmPrompt{\n\t\t\tMessages: c.ConvertMessage(props.Message),\n\t\t},\n\t}\n}\n\nfunc (c *ChatInstance) GetGeminiChatBody(props *adaptercommon.ChatProps) *GeminiChatBody {\n\treturn &GeminiChatBody{\n\t\tContents: c.GetGeminiContents(props.Model, props.Message),\n\t\tGenerationConfig: GeminiConfig{\n\t\t\tTemperature:     props.Temperature,\n\t\t\tMaxOutputTokens: props.MaxTokens,\n\t\t\tTopP:            props.TopP,\n\t\t\tTopK:            props.TopK,\n\t\t},\n\t}\n}\n\nfunc (c *ChatInstance) GetPalm2ChatResponse(data interface{}) (string, error) {\n\tif form := utils.MapToStruct[PalmChatResponse](data); form != nil {\n\t\tif len(form.Candidates) == 0 {\n\t\t\treturn \"\", fmt.Errorf(\"palm2 error: the content violates content policy\")\n\t\t}\n\t\treturn form.Candidates[0].Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"palm2 error: cannot parse response\")\n}\n\nfunc (c *ChatInstance) GetGeminiChatResponse(data interface{}) (string, error) {\n\tif form := utils.MapToStruct[GeminiChatResponse](data); form != nil {\n\t\tif len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 {\n\t\t\treturn form.Candidates[0].Content.Parts[0].Text, nil\n\t\t}\n\t}\n\n\tif form := utils.MapToStruct[GeminiChatErrorResponse](data); form != nil {\n\t\treturn \"\", fmt.Errorf(\"gemini error: %s (code: %d, status: %s)\", form.Error.Message, form.Error.Code, form.Error.Status)\n\t}\n\n\treturn \"\", fmt.Errorf(\"gemini: cannot parse response\")\n}\n\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\turi := c.GetChatEndpoint(props.Model, false)\n\n\tif props.Model == globals.ChatBison001 {\n\t\tdata, err := utils.Post(uri, map[string]string{\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t}, c.GetPalm2ChatBody(props), props.Proxy)\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"palm2 error: %s\", err.Error())\n\t\t}\n\t\treturn c.GetPalm2ChatResponse(data)\n\t}\n\n\tdata, err := utils.Post(uri, map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}, c.GetGeminiChatBody(props), props.Proxy)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"gemini error: %s\", err.Error())\n\t}\n\n\treturn c.GetGeminiChatResponse(data)\n}\n\n// CreateStreamChatRequest is the stream request for palm2\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\t// Handle imagen models\n\tif globals.IsGoogleImagenModel(props.Model) {\n\t\tresponse, err := c.CreateImage(props)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn callback(&globals.Chunk{Content: response})\n\t}\n\n\t// Handle chat models\n\tif props.Model == globals.ChatBison001 {\n\t\tresponse, err := c.CreateChatRequest(props)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, item := range utils.SplitItem(response, \" \") {\n\t\t\tif err := callback(&globals.Chunk{Content: item}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tticks := 0\n\tscanErr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod: \"POST\",\n\t\tUri:    c.GetChatEndpoint(props.Model, true),\n\t\tHeaders: map[string]string{\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tBody: c.GetGeminiChatBody(props),\n\t\tCallback: func(data string) error {\n\t\t\tticks += 1\n\n\t\t\tif form := utils.UnmarshalForm[GeminiStreamResponse](data); form != nil {\n\t\t\t\tif len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 {\n\t\t\t\t\treturn callback(&globals.Chunk{\n\t\t\t\t\t\tContent: form.Candidates[0].Content.Parts[0].Text,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif form := utils.UnmarshalForm[GeminiChatErrorResponse](data); form != nil {\n\t\t\t\treturn fmt.Errorf(\"gemini error: %s (code: %d, status: %s)\", form.Error.Message, form.Error.Code, form.Error.Status)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t}, props.Proxy)\n\n\tif scanErr != nil {\n\t\tif scanErr.Error != nil && strings.Contains(scanErr.Error.Error(), \"status code: 404\") {\n\t\t\t// downgrade to non-stream request\n\t\t\tresponse, err := c.CreateChatRequest(props)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(&globals.Chunk{Content: response})\n\t\t}\n\n\t\tif scanErr.Body != \"\" {\n\t\t\tif form := utils.UnmarshalForm[GeminiChatErrorResponse](scanErr.Body); form != nil {\n\t\t\t\treturn fmt.Errorf(\"gemini error: %s (code: %d, status: %s)\", form.Error.Message, form.Error.Code, form.Error.Status)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"gemini error: %s\", scanErr.Body)\n\t\t}\n\t\treturn fmt.Errorf(\"gemini error: %v\", scanErr.Error)\n\t}\n\n\tif ticks == 0 {\n\t\treturn errors.New(\"no response\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {\n\tif len(props.Message) == 0 {\n\t\treturn \"\"\n\t}\n\treturn props.Message[len(props.Message)-1].Content\n}\n"
  },
  {
    "path": "adapter/palm2/formatter.go",
    "content": "package palm2\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"strings\"\n)\n\nfunc getGeminiRole(role string) string {\n\tswitch role {\n\tcase globals.User:\n\t\treturn GeminiUserType\n\tcase globals.Assistant, globals.Tool, globals.System:\n\t\treturn GeminiModelType\n\tdefault:\n\t\treturn GeminiUserType\n\t}\n}\n\nfunc getMimeType(content string) string {\n\tsegment := strings.Split(content, \".\")\n\tif len(segment) == 0 || len(segment) == 1 {\n\t\treturn \"image/png\"\n\t}\n\n\tsuffix := strings.TrimSpace(strings.ToLower(segment[len(segment)-1]))\n\n\tswitch suffix {\n\tcase \"png\":\n\t\treturn \"image/png\"\n\tcase \"jpg\", \"jpeg\":\n\t\treturn \"image/jpeg\"\n\tcase \"gif\":\n\t\treturn \"image/gif\"\n\tcase \"webp\":\n\t\treturn \"image/webp\"\n\tcase \"heif\":\n\t\treturn \"image/heif\"\n\tcase \"heic\":\n\t\treturn \"image/heic\"\n\tdefault:\n\t\treturn \"image/png\"\n\t}\n}\n\nfunc getGeminiContent(parts []GeminiChatPart, content string, model string) []GeminiChatPart {\n\tif model == globals.GeminiPro {\n\t\treturn append(parts, GeminiChatPart{\n\t\t\tText: &content,\n\t\t})\n\t}\n\n\traw, urls := utils.ExtractImages(content, true)\n\tif len(urls) > geminiMaxImages {\n\t\turls = urls[:geminiMaxImages]\n\t}\n\n\tparts = append(parts, GeminiChatPart{\n\t\tText: &raw,\n\t})\n\n\tfor _, url := range urls {\n\t\tdata, err := utils.ConvertToBase64(url)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts = append(parts, GeminiChatPart{\n\t\t\tInlineData: &GeminiInlineData{\n\t\t\t\tMimeType: getMimeType(url),\n\t\t\t\tData:     data,\n\t\t\t},\n\t\t})\n\t}\n\n\treturn parts\n}\n\nfunc (c *ChatInstance) GetGeminiContents(model string, message []globals.Message) []GeminiContent {\n\t// gemini role should be user-model\n\n\tresult := make([]GeminiContent, 0)\n\tfor _, item := range message {\n\t\trole := getGeminiRole(item.Role)\n\t\tif len(item.Content) == 0 {\n\t\t\t// gemini model: message must include non empty content\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(result) == 0 && getGeminiRole(item.Role) == GeminiModelType {\n\t\t\t// gemini model: first message must be user\n\n\t\t\tresult = append(result, GeminiContent{\n\t\t\t\tRole:  GeminiUserType,\n\t\t\t\tParts: getGeminiContent(make([]GeminiChatPart, 0), \"\", model),\n\t\t\t})\n\t\t}\n\n\t\tif len(result) > 0 && role == result[len(result)-1].Role {\n\t\t\t// gemini model: messages must alternate between authors\n\t\t\tresult[len(result)-1].Parts = getGeminiContent(result[len(result)-1].Parts, item.Content, model)\n\t\t\tcontinue\n\t\t}\n\n\t\tresult = append(result, GeminiContent{\n\t\t\tRole:  getGeminiRole(item.Role),\n\t\t\tParts: getGeminiContent(make([]GeminiChatPart, 0), item.Content, model),\n\t\t})\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "adapter/palm2/image.go",
    "content": "package palm2\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype ImageProps struct {\n\tModel  string\n\tPrompt string\n\tProxy  globals.ProxyConfig\n}\n\nfunc (c *ChatInstance) GetImageEndpoint(model string) string {\n\treturn fmt.Sprintf(\"%s/v1beta/models/%s:predict?key=%s\", c.Endpoint, model, c.ApiKey)\n}\n\n// CreateImageRequest will create a gemini imagen from prompt, return base64 of image and error\nfunc (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetImageEndpoint(props.Model),\n\t\tmap[string]string{\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tImageRequest{\n\t\t\tInstances: []ImageInstance{\n\t\t\t\t{\n\t\t\t\t\tPrompt: props.Prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t\tParameters: ImageParameters{\n\t\t\t\tSampleCount:      1,\n\t\t\t\tAspectRatio:      \"1:1\",\n\t\t\t\tPersonGeneration: \"allow_adult\",\n\t\t\t},\n\t\t},\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ImageResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"gemini error: cannot parse response\")\n\t}\n\n\tif len(data.Predictions) == 0 {\n\t\treturn \"\", fmt.Errorf(\"gemini error: no image generated\")\n\t}\n\n\treturn data.Predictions[0].BytesBase64Encoded, nil\n}\n\n// CreateImage will create a gemini imagen from prompt, return markdown of image\nfunc (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {\n\tif !globals.IsGoogleImagenModel(props.Model) {\n\t\treturn \"\", nil\n\t}\n\n\tbase64Data, err := c.CreateImageRequest(ImageProps{\n\t\tModel:  props.Model,\n\t\tPrompt: c.GetLatestPrompt(props),\n\t\tProxy:  props.Proxy,\n\t})\n\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"safety\") {\n\t\t\treturn err.Error(), nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\t// Convert base64 to data URL format\n\tdataUrl := fmt.Sprintf(\"data:image/png;base64,%s\", base64Data)\n\turl := utils.StoreImage(dataUrl)\n\treturn utils.GetImageMarkdown(url), nil\n}\n"
  },
  {
    "path": "adapter/palm2/struct.go",
    "content": "package palm2\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc NewChatInstance(endpoint string, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/palm2/types.go",
    "content": "package palm2\n\nconst (\n\tGeminiUserType  = \"user\"\n\tGeminiModelType = \"model\"\n)\n\ntype PalmMessage struct {\n\tAuthor  string `json:\"author\"`\n\tContent string `json:\"content\"`\n}\n\n// PalmChatBody is the native http request body for palm2\ntype PalmChatBody struct {\n\tPrompt PalmPrompt `json:\"prompt\"`\n}\n\ntype PalmPrompt struct {\n\tMessages []PalmMessage `json:\"messages\"`\n}\n\n// PalmChatResponse is the native http response body for palm2\ntype PalmChatResponse struct {\n\tCandidates []PalmMessage `json:\"candidates\"`\n}\n\n// GeminiChatBody is the native http request body for gemini\ntype GeminiChatBody struct {\n\tContents         []GeminiContent `json:\"contents\"`\n\tGenerationConfig GeminiConfig    `json:\"generationConfig\"`\n}\n\ntype GeminiConfig struct {\n\tTemperature     *float32 `json:\"temperature,omitempty\"`\n\tMaxOutputTokens *int     `json:\"maxOutputTokens,omitempty\"`\n\tTopP            *float32 `json:\"topP,omitempty\"`\n\tTopK            *int     `json:\"topK,omitempty\"`\n}\n\ntype GeminiContent struct {\n\tRole  string           `json:\"role\"`\n\tParts []GeminiChatPart `json:\"parts\"`\n}\n\ntype GeminiChatPart struct {\n\tText       *string           `json:\"text,omitempty\"`\n\tInlineData *GeminiInlineData `json:\"inline_data,omitempty\"`\n}\n\ntype GeminiInlineData struct {\n\tMimeType string `json:\"mime_type\"`\n\tData     string `json:\"data\"`\n}\n\ntype GeminiChatResponse struct {\n\tCandidates []struct {\n\t\tContent struct {\n\t\t\tParts []struct {\n\t\t\t\tText string `json:\"text\"`\n\t\t\t} `json:\"parts\"`\n\t\t\tRole string `json:\"role\"`\n\t\t} `json:\"content\"`\n\t} `json:\"candidates\"`\n}\n\ntype GeminiChatErrorResponse struct {\n\tError struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tStatus  string `json:\"status\"`\n\t} `json:\"error\"`\n}\n\ntype GeminiStreamResponse struct {\n\tCandidates []struct {\n\t\tContent struct {\n\t\t\tParts []struct {\n\t\t\t\tText string `json:\"text\"`\n\t\t\t} `json:\"parts\"`\n\t\t\tRole string `json:\"role\"`\n\t\t} `json:\"content\"`\n\t} `json:\"candidates\"`\n}\n\n// ImageRequest is the native http request body for imagen\ntype ImageRequest struct {\n\tInstances  []ImageInstance `json:\"instances\"`\n\tParameters ImageParameters `json:\"parameters\"`\n}\n\ntype ImageInstance struct {\n\tPrompt string `json:\"prompt\"`\n}\n\ntype ImageParameters struct {\n\tSampleCount      int    `json:\"sampleCount,omitempty\"`\n\tAspectRatio      string `json:\"aspectRatio,omitempty\"`\n\tPersonGeneration string `json:\"personGeneration,omitempty\"`\n}\n\n// ImageResponse is the native http response body for imagen\ntype ImageResponse struct {\n\tPredictions []ImagePrediction `json:\"predictions\"`\n}\n\ntype ImagePrediction struct {\n\tMimeType           string `json:\"mimeType\"`\n\tBytesBase64Encoded string `json:\"bytesBase64Encoded\"`\n}\n"
  },
  {
    "path": "adapter/request.go",
    "content": "package adapter\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc IsAvailableError(err error) bool {\n\treturn err != nil && (err.Error() != \"signal\" && !strings.Contains(err.Error(), \"signal\"))\n}\n\nfunc IsSkipError(err error) bool {\n\treturn err == nil || (err.Error() == \"signal\" || strings.Contains(err.Error(), \"signal\"))\n}\n\nfunc isQPSOverLimit(model string, err error) bool {\n\tif strings.Contains(model, \"spark-desk\") {\n\t\treturn strings.Contains(err.Error(), \"AppIdQpsOverFlowError\")\n\t}\n\treturn false\n}\n\nfunc NewChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error {\n\terr := createChatRequest(conf, props, hook)\n\n\tretries := conf.GetRetry()\n\tprops.Current++\n\n\tif IsAvailableError(err) {\n\t\tif isQPSOverLimit(props.OriginalModel, err) {\n\t\t\t// sleep for 0.5s to avoid qps limit\n\n\t\t\tglobals.Info(fmt.Sprintf(\"qps limit for %s, sleep and retry (times: %d)\", props.OriginalModel, props.Current))\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\treturn NewChatRequest(conf, props, hook)\n\t\t}\n\n\t\tif props.Current < retries {\n\t\t\tcontent := strings.Replace(err.Error(), \"\\n\", \"\", -1)\n\t\t\tglobals.Warn(fmt.Sprintf(\"retrying chat request for %s (attempt %d/%d, error: %s)\", props.OriginalModel, props.Current+1, retries, content))\n\t\t\treturn NewChatRequest(conf, props, hook)\n\t\t}\n\t}\n\n\treturn conf.ProcessError(err)\n}\n\nfunc NewVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error {\n\terr := createVideoRequest(conf, props, hook)\n\n\tretries := conf.GetRetry()\n\tprops.Current++\n\n\tif IsAvailableError(err) {\n\t\tif isQPSOverLimit(props.OriginalModel, err) {\n\t\t\t// sleep for 0.5s to avoid qps limit\n\n\t\t\tglobals.Info(fmt.Sprintf(\"qps limit for %s, sleep and retry (times: %d)\", props.OriginalModel, props.Current))\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\treturn NewVideoRequest(conf, props, hook)\n\t\t}\n\n\t\tif props.Current < retries {\n\t\t\tcontent := strings.Replace(err.Error(), \"\\n\", \"\", -1)\n\t\t\tglobals.Info(fmt.Sprintf(\"retrying error request for %s (attempt %d/%d, error: %s)\", props.OriginalModel, props.Current+1, retries, content))\n\t\t\treturn NewVideoRequest(conf, props, hook)\n\t\t}\n\t}\n\n\treturn conf.ProcessError(err)\n}\n\nfunc ClearMessages(model string, messages []globals.Message) []globals.Message {\n\tif globals.IsVisionModel(model) {\n\t\treturn messages\n\t}\n\n\treturn utils.Each[globals.Message](messages, func(message globals.Message) globals.Message {\n\t\tif message.Role != globals.User {\n\t\t\treturn message\n\t\t}\n\n\t\timages := utils.ExtractBase64Images(message.Content)\n\t\tfor _, image := range images {\n\t\t\tif len(image) <= 46 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmessage.Content = strings.Replace(message.Content, image, utils.Extract(image, 46, \" ...\"), -1)\n\t\t}\n\t\treturn message\n\t})\n}\n"
  },
  {
    "path": "adapter/router.go",
    "content": "package adapter\n\nimport (\n\t\"chat/adapter/midjourney\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Register(app *gin.RouterGroup) {\n\tapp.POST(\"/mj/notify\", midjourney.NotifyAPI)\n}\n"
  },
  {
    "path": "adapter/skylark/chat.go",
    "content": "package skylark\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model\"\n\t\"github.com/volcengine/volcengine-go-sdk/volcengine\"\n)\n\nconst defaultMaxTokens int = 4096\n\nfunc getMessages(messages []globals.Message) []*model.ChatCompletionMessage {\n\tresult := make([]*model.ChatCompletionMessage, 0)\n\n\tfor _, message := range messages {\n\t\tif message.Role == globals.Tool {\n\t\t\tmessage.Role = model.ChatMessageRoleTool\n\t\t}\n\n\t\tmsg := &model.ChatCompletionMessage{\n\t\t\tRole:             message.Role,\n\t\t\tContent:          &model.ChatCompletionMessageContent{StringValue: volcengine.String(message.Content)},\n\t\t\tFunctionCall:     getFunctionCall(message.ToolCalls),\n\t\t\tReasoningContent: message.ReasoningContent,\n\t\t}\n\n\t\thasPrevious := len(result) > 0\n\n\t\t//  a message should not followed by the same role message, merge them\n\t\tif hasPrevious && result[len(result)-1].Role == message.Role {\n\t\t\tprev := result[len(result)-1]\n\t\t\tprev.Content.StringValue = volcengine.String(*prev.Content.StringValue + *msg.Content.StringValue)\n\t\t\tif message.ToolCalls != nil {\n\t\t\t\tprev.FunctionCall = msg.FunctionCall\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// `assistant` message should follow a user or function message, if not has previous message, change the role to `user`\n\t\tif !hasPrevious && message.Role == model.ChatMessageRoleAssistant {\n\t\t\tmsg.Role = model.ChatMessageRoleUser\n\t\t}\n\n\t\tresult = append(result, msg)\n\t}\n\n\treturn result\n}\n\nfunc (c *ChatInstance) GetMaxTokens(token *int) int {\n\tif token == nil || *token < 0 {\n\t\treturn defaultMaxTokens\n\t}\n\n\treturn *token\n}\n\nfunc (c *ChatInstance) CreateRequest(props *adaptercommon.ChatProps) *model.ChatCompletionRequest {\n\treturn &model.ChatCompletionRequest{\n\t\tModel:       props.Model,\n\t\tMessages:    getMessages(props.Message),\n\t\tTemperature: utils.GetPtrVal(props.Temperature, 0.),\n\t\tTopP:        utils.GetPtrVal(props.TopP, 0.),\n\t\t// skylark v3 not support TopK\n\t\tPresencePenalty:   utils.GetPtrVal(props.PresencePenalty, 0.),\n\t\tFrequencyPenalty:  utils.GetPtrVal(props.FrequencyPenalty, 0.),\n\t\tRepetitionPenalty: utils.GetPtrVal(props.RepetitionPenalty, 0.),\n\t\tMaxTokens:         c.GetMaxTokens(props.MaxTokens),\n\t\tFunctionCall:      getFunctions(props.Tools),\n\t}\n}\n\nfunc getToolCalls(id string, choiceDelta model.ChatCompletionStreamChoiceDelta) *globals.ToolCalls {\n\tcalls := choiceDelta.FunctionCall\n\tif calls == nil {\n\t\treturn nil\n\t}\n\n\treturn &globals.ToolCalls{\n\t\tglobals.ToolCall{\n\t\t\tType: \"function\",\n\t\t\tId:   fmt.Sprintf(\"%s-%s\", calls.Name, id),\n\t\t\tFunction: globals.ToolCallFunction{\n\t\t\t\tName:      calls.Name,\n\t\t\t\tArguments: calls.Arguments,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc getChoice(choice model.ChatCompletionStreamResponse) *globals.Chunk {\n\tif len(choice.Choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tmessage := choice.Choices[0].Delta\n\treturn &globals.Chunk{\n\t\tContent:  message.Content,\n\t\tToolCall: getToolCalls(choice.ID, message),\n\t}\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\treq := c.CreateRequest(props)\n\tc.isFirstReasoning = true\n\tc.isReasonOver = false\n\n\tif globals.DebugMode {\n\t\tglobals.Debug(fmt.Sprintf(\"[skylark] request: %v\", utils.Marshal(req)))\n\t}\n\n\tstream, err := c.Instance.CreateChatCompletionStream(context.Background(), req)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[skylark] stream error: %v\", err))\n\t\t}\n\t\treturn err\n\t}\n\tdefer stream.Close()\n\n\tfor {\n\t\trecv, err2 := stream.Recv()\n\t\tif err2 == io.EOF {\n\t\t\treturn nil\n\t\t}\n\t\tif err2 != nil {\n\t\t\tif globals.DebugMode {\n\t\t\t\tglobals.Debug(fmt.Sprintf(\"[skylark] receive error: %v\", err2))\n\t\t\t}\n\t\t\treturn err2\n\t\t}\n\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[skylark] response: %v\", utils.Marshal(recv)))\n\t\t}\n\n\t\tchoice := getChoice(recv)\n\t\tif len(recv.Choices) > 0 {\n\t\t\tdelta := recv.Choices[0].Delta\n\n\t\t\t// Handle reasoning content\n\t\t\tif c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil {\n\t\t\t\tc.isReasonOver = true\n\t\t\t\tif delta.Content != \"\" {\n\t\t\t\t\tchoice.Content = fmt.Sprintf(\"\\n</think>\\n\\n%s\", delta.Content)\n\t\t\t\t} else {\n\t\t\t\t\tchoice.Content = \"\\n</think>\\n\\n\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif delta.ReasoningContent != nil {\n\t\t\t\tif c.isFirstReasoning {\n\t\t\t\t\tc.isFirstReasoning = false\n\t\t\t\t\tchoice.Content = fmt.Sprintf(\"<think>\\n%s\", *delta.ReasoningContent)\n\t\t\t\t} else {\n\t\t\t\t\tchoice.Content = *delta.ReasoningContent\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif err = callback(choice); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/skylark/formatter.go",
    "content": "package skylark\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model\"\n\n\tstructpb \"github.com/golang/protobuf/ptypes/struct\"\n\t\"github.com/volcengine/volc-sdk-golang/service/maas/models/api\"\n)\n\nfunc getFunctionCall(calls *globals.ToolCalls) *model.FunctionCall {\n\tif calls == nil || len(*calls) == 0 {\n\t\treturn nil\n\t}\n\n\tcall := (*calls)[0]\n\treturn &model.FunctionCall{\n\t\tName:      call.Function.Name,\n\t\tArguments: call.Function.Arguments,\n\t}\n}\n\nfunc getType(p globals.ToolProperty) string {\n\tt, ok := p[\"type\"]\n\tif !ok {\n\t\treturn \"string\"\n\t}\n\n\treturn t.(string)\n}\n\nfunc getDescription(p globals.ToolProperty) string {\n\tdesc, ok := p[\"description\"]\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn desc.(string)\n}\n\nfunc getValue(p globals.ToolProperty) *structpb.Value {\n\tswitch getType(p) {\n\tcase \"string\", \"enum\":\n\t\treturn &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: getDescription(p)}}\n\tcase \"number\":\n\t\treturn &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 0}}\n\tcase \"boolean\":\n\t\treturn &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: false}}\n\tcase \"object\":\n\t\treturn &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: map[string]*structpb.Value{}}}}\n\tcase \"array\":\n\t\treturn &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{}}}}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc getFunctions(tools *globals.FunctionTools) []*api.Function {\n\tif tools == nil || len(*tools) == 0 {\n\t\treturn nil\n\t}\n\n\treturn utils.Each[globals.ToolObject, *api.Function](*tools, func(tool globals.ToolObject) *api.Function {\n\t\tparam := &structpb.Struct{\n\t\t\tFields: map[string]*structpb.Value{},\n\t\t}\n\t\tfor k, v := range tool.Function.Parameters.Properties {\n\t\t\tparam.Fields[k] = getValue(v)\n\t\t}\n\n\t\treturn &api.Function{\n\t\t\tName:        tool.Function.Name,\n\t\t\tDescription: tool.Function.Description,\n\t\t\tParameters:  param,\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "adapter/skylark/struct.go",
    "content": "package skylark\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\n\t\"github.com/volcengine/volcengine-go-sdk/service/arkruntime\"\n)\n\ntype ChatInstance struct {\n\tInstance         *arkruntime.Client\n\tisFirstReasoning bool\n\tisReasonOver     bool\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\t//https://ark.cn-beijing.volces.com/api/v3\n\tinstance := arkruntime.NewClientWithApiKey(apiKey, arkruntime.WithBaseUrl(endpoint))\n\treturn &ChatInstance{\n\t\tInstance:         instance,\n\t\tisFirstReasoning: true,\n\t\tisReasonOver:     false,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparams := conf.SplitRandomSecret(1)\n\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tparams[0],\n\t)\n}\n"
  },
  {
    "path": "adapter/slack/chat.go",
    "content": "package slack\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"context\"\n)\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {\n\tif err := c.Instance.NewChannel(c.GetChannel()); err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.Instance.Reply(context.Background(), c.FormatMessage(props.Message), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.ProcessPartialResponse(resp, hook)\n}\n"
  },
  {
    "path": "adapter/slack/struct.go",
    "content": "package slack\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"github.com/bincooo/claude-api\"\n\t\"github.com/bincooo/claude-api/types\"\n\t\"github.com/bincooo/claude-api/vars\"\n\t\"strings\"\n)\n\ntype ChatInstance struct {\n\tBotId    string\n\tToken    string\n\tChannel  string\n\tInstance types.Chat\n}\n\nfunc (c *ChatInstance) GetBotId() string {\n\treturn c.BotId\n}\n\nfunc (c *ChatInstance) GetToken() string {\n\treturn c.Token\n}\n\nfunc (c *ChatInstance) GetChannel() string {\n\treturn c.Channel\n}\n\nfunc (c *ChatInstance) GetInstance() types.Chat {\n\treturn c.Instance\n}\n\nfunc NewChatInstance(botId, token, channel string) *ChatInstance {\n\toptions := claude.NewDefaultOptions(token, botId, vars.Model4Slack)\n\tif instance, err := claude.New(options); err != nil {\n\t\treturn nil\n\t} else {\n\t\treturn &ChatInstance{\n\t\t\tBotId:    botId,\n\t\t\tToken:    token,\n\t\t\tChannel:  channel,\n\t\t\tInstance: instance,\n\t\t}\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparams := conf.SplitRandomSecret(2)\n\treturn NewChatInstance(\n\t\tparams[0], params[1],\n\t\tconf.GetEndpoint(),\n\t)\n}\n\nfunc (c *ChatInstance) FormatMessage(message []globals.Message) string {\n\tresult := make([]string, len(message))\n\tfor i, item := range message {\n\t\tif item.Role == globals.Tool {\n\t\t\tcontinue\n\t\t}\n\t\tresult[i] = fmt.Sprintf(\"%s: %s\", item.Role, item.Content)\n\t}\n\n\treturn strings.Join(result, \"\\n\\n\")\n}\n\nfunc (c *ChatInstance) ProcessPartialResponse(res chan types.PartialResponse, hook globals.Hook) error {\n\tfor {\n\t\tselect {\n\t\tcase data, ok := <-res:\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif data.Error != nil {\n\t\t\t\treturn data.Error\n\t\t\t} else if data.Text != \"\" {\n\t\t\t\tif err := hook(&globals.Chunk{Content: data.Text}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "adapter/sparkdesk/chat.go",
    "content": "package sparkdesk\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar FunctionCallingModels = []string{\n\tglobals.SparkDeskMax,\n\tglobals.SparkDeskV4Ultra,\n}\n\nfunc GetToken(props *adaptercommon.ChatProps) *int {\n\tif props.MaxTokens == nil {\n\t\treturn nil\n\t}\n\n\tswitch props.Model {\n\tcase globals.SparkDeskLite, globals.SparkDeskPro128K:\n\t\tif *props.MaxTokens > 4096 {\n\t\t\treturn utils.ToPtr(4096)\n\t\t}\n\tcase globals.SparkDeskPro, globals.SparkDeskMax, globals.SparkDeskMax32K, globals.SparkDeskV4Ultra:\n\t\tif *props.MaxTokens > 8192 {\n\t\t\treturn utils.ToPtr(8192)\n\t\t}\n\t}\n\n\treturn props.MaxTokens\n}\n\nfunc GetTopK(props *adaptercommon.ChatProps) *int {\n\tif props.TopK == nil {\n\t\treturn nil\n\t}\n\t// topk max value is 6\n\tif *props.TopK > 6 {\n\t\treturn utils.ToPtr(6)\n\t}\n\n\treturn props.TopK\n}\n\nfunc (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message {\n\tvar messages []Message\n\tfor _, message := range props.Message {\n\t\tif message.Role == globals.Tool {\n\t\t\tcontinue\n\t\t}\n\t\tif message.Role == globals.System {\n\t\t\tmessage.Role = globals.Assistant\n\t\t}\n\t\tmessages = append(messages, Message{\n\t\t\tRole:    message.Role,\n\t\t\tContent: message.Content,\n\t\t})\n\t}\n\n\treturn messages\n}\n\nfunc (c *ChatInstance) GetFunctionCalling(props *adaptercommon.ChatProps) *FunctionsPayload {\n\tif props.Tools == nil {\n\t\treturn nil\n\t}\n\n\tif !utils.Contains(props.Model, FunctionCallingModels) {\n\t\treturn nil\n\t}\n\n\treturn &FunctionsPayload{\n\t\tText: utils.Each[globals.ToolObject, globals.ToolFunction](*props.Tools,\n\t\t\tfunc(tool globals.ToolObject) globals.ToolFunction {\n\t\t\t\treturn tool.Function\n\t\t\t}),\n\t}\n}\n\nfunc getFunctionCall(call *FunctionCall) *globals.ToolCalls {\n\tif call == nil {\n\t\treturn nil\n\t}\n\n\treturn &globals.ToolCalls{\n\t\tglobals.ToolCall{\n\t\t\tType: \"function\",\n\t\t\tId:   fmt.Sprintf(\"%s-%s\", call.Name, call.Arguments),\n\t\t\tFunction: globals.ToolCallFunction{\n\t\t\t\tName:      call.Name,\n\t\t\t\tArguments: call.Arguments,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc getChoice(form *ChatResponse) *globals.Chunk {\n\tif form == nil || len(form.Payload.Choices.Text) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoices := form.Payload.Choices.Text\n\tif len(choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoice := choices[0]\n\n\treturn &globals.Chunk{\n\t\tContent:  choice.Content,\n\t\tToolCall: getFunctionCall(choice.FunctionCall),\n\t}\n}\n\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {\n\tvar endpoint string\n\tswitch props.Model {\n\tcase globals.SparkDeskPro128K, globals.SparkDeskMax32K:\n\t\tendpoint = fmt.Sprintf(\"%s/chat/%s\", c.Endpoint, TransformModel(props.Model))\n\tdefault:\n\t\tendpoint = fmt.Sprintf(\"%s/%s/chat\", c.Endpoint, TransformAddr(props.Model))\n\t}\n\tvar conn *utils.WebSocket\n\tif conn = utils.NewWebsocketClient(c.GenerateUrl(endpoint)); conn == nil {\n\t\treturn fmt.Errorf(\"sparkdesk error: websocket connection failed\")\n\t}\n\tdefer conn.DeferClose()\n\n\tif err := conn.SendJSON(&ChatRequest{\n\t\tHeader: RequestHeader{\n\t\t\tAppId: c.AppId,\n\t\t},\n\t\tPayload: RequestPayload{\n\t\t\tMessage: MessagePayload{\n\t\t\t\tText: c.GetMessages(props),\n\t\t\t},\n\t\t\tFunctions: c.GetFunctionCalling(props),\n\t\t},\n\n\t\tParameter: RequestParameter{\n\t\t\tChat: ChatParameter{\n\t\t\t\tDomain:      TransformModel(props.Model),\n\t\t\t\tMaxToken:    GetToken(props),\n\t\t\t\tTemperature: props.Temperature,\n\t\t\t\tTopK:        GetTopK(props),\n\t\t\t},\n\t\t},\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tform, err := utils.ReadForm[ChatResponse](conn)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"websocket: close 1000\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tglobals.Debug(fmt.Sprintf(\"sparkdesk error: %s\", err.Error()))\n\t\t\treturn nil\n\t\t}\n\n\t\tif form.Header.Code != 0 {\n\t\t\treturn fmt.Errorf(\"sparkdesk error: %s (sid: %s)\", form.Header.Message, form.Header.Sid)\n\t\t}\n\n\t\tif err := hook(getChoice(form)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "adapter/sparkdesk/struct.go",
    "content": "package sparkdesk\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype ChatInstance struct {\n\tAppId     string\n\tApiSecret string\n\tApiKey    string\n\tEndpoint  string\n}\n\nfunc TransformAddr(model string) string {\n\tswitch model {\n\tcase globals.SparkDeskLite:\n\t\treturn \"v1.1\"\n\tcase globals.SparkDeskPro:\n\t\treturn \"v3.1\"\n\tcase globals.SparkDeskMax:\n\t\treturn \"v3.5\"\n\tcase globals.SparkDeskV4Ultra:\n\t\treturn \"v4.0\"\n\tdefault:\n\t\treturn \"v1.1\"\n\t}\n}\n\nfunc TransformModel(model string) string {\n\tswitch model {\n\tcase globals.SparkDeskLite:\n\t\treturn \"general\"\n\tcase globals.SparkDeskPro:\n\t\treturn \"generalv3\"\n\tcase globals.SparkDeskPro128K:\n\t\treturn \"pro-128k\"\n\tcase globals.SparkDeskMax:\n\t\treturn \"generalv3.5\"\n\tcase globals.SparkDeskMax32K:\n\t\treturn \"max-32k\"\n\tcase globals.SparkDeskV4Ultra:\n\t\treturn \"4.0Ultra\"\n\tdefault:\n\t\treturn \"general\"\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\tparams := conf.SplitRandomSecret(3)\n\n\treturn &ChatInstance{\n\t\tAppId:     params[0],\n\t\tApiSecret: params[1],\n\t\tApiKey:    params[2],\n\t\tEndpoint:  conf.GetEndpoint(),\n\t}\n}\n\nfunc (c *ChatInstance) CreateUrl(endpoint, host, date, auth string) string {\n\tv := make(url.Values)\n\tv.Add(\"host\", host)\n\tv.Add(\"date\", date)\n\tv.Add(\"authorization\", auth)\n\treturn fmt.Sprintf(\"%s?%s\", endpoint, v.Encode())\n}\n\nfunc (c *ChatInstance) Sign(data, key string) string {\n\tmac := hmac.New(sha256.New, []byte(key))\n\tmac.Write([]byte(data))\n\treturn base64.StdEncoding.EncodeToString(mac.Sum(nil))\n}\n\n// GenerateUrl will generate the signed url for sparkdesk api\nfunc (c *ChatInstance) GenerateUrl(endpoint string) string {\n\turi, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tdate := time.Now().UTC().Format(time.RFC1123)\n\tdata := strings.Join([]string{\n\t\tfmt.Sprintf(\"host: %s\", uri.Host),\n\t\tfmt.Sprintf(\"date: %s\", date),\n\t\tfmt.Sprintf(\"GET %s HTTP/1.1\", uri.Path),\n\t}, \"\\n\")\n\n\tsignature := c.Sign(data, c.ApiSecret)\n\tauthorization := base64.StdEncoding.EncodeToString([]byte(\n\t\tfmt.Sprintf(\n\t\t\t\"hmac username=\\\"%s\\\", algorithm=\\\"%s\\\", headers=\\\"%s\\\", signature=\\\"%s\\\"\",\n\t\t\tc.ApiKey,\n\t\t\t\"hmac-sha256\",\n\t\t\t\"host date request-line\",\n\t\t\tsignature,\n\t\t),\n\t))\n\n\treturn c.CreateUrl(endpoint, uri.Host, date, authorization)\n}\n"
  },
  {
    "path": "adapter/sparkdesk/types.go",
    "content": "package sparkdesk\n\nimport \"chat/globals\"\n\n// ChatRequest is the request body for sparkdesk\ntype ChatRequest struct {\n\tHeader    RequestHeader    `json:\"header\"`\n\tPayload   RequestPayload   `json:\"payload\"`\n\tParameter RequestParameter `json:\"parameter\"`\n}\n\ntype RequestHeader struct {\n\tAppId string `json:\"app_id\"`\n}\n\ntype RequestPayload struct {\n\tMessage   MessagePayload    `json:\"message\"`\n\tFunctions *FunctionsPayload `json:\"functions,omitempty\"`\n}\n\ntype FunctionsPayload struct {\n\tText []globals.ToolFunction `json:\"text\"`\n}\n\ntype Message struct {\n\tRole         string        `json:\"role\"`\n\tContent      string        `json:\"content\"`\n\tFunctionCall *FunctionCall `json:\"function_call,omitempty\"`\n}\n\ntype FunctionCall struct {\n\tName      string `json:\"name\"`\n\tArguments string `json:\"arguments\"`\n}\n\ntype MessagePayload struct {\n\tText []Message `json:\"text\"`\n}\n\ntype RequestParameter struct {\n\tChat ChatParameter `json:\"chat\"`\n}\n\ntype ChatParameter struct {\n\tDomain      string   `json:\"domain\"`\n\tMaxToken    *int     `json:\"max_tokens,omitempty\"`\n\tTemperature *float32 `json:\"temperature,omitempty\"`\n\tTopK        *int     `json:\"top_k,omitempty\"`\n}\n\n// ChatResponse is the websocket partial response body for sparkdesk\ntype ChatResponse struct {\n\tHeader struct {\n\t\tCode    int    `json:\"code\" required:\"true\"`\n\t\tMessage string `json:\"message\"`\n\t\tSid     string `json:\"sid\"`\n\t\tStatus  int    `json:\"status\"`\n\t} `json:\"header\"`\n\tPayload struct {\n\t\tChoices struct {\n\t\t\tStatus int       `json:\"status\"`\n\t\t\tSeq    int       `json:\"seq\"`\n\t\t\tText   []Message `json:\"text\"`\n\t\t} `json:\"choices\"`\n\t\tUsage struct {\n\t\t\tText struct {\n\t\t\t\tQuestionTokens   int `json:\"question_tokens\"`\n\t\t\t\tPromptTokens     int `json:\"prompt_tokens\"`\n\t\t\t\tCompletionTokens int `json:\"completion_tokens\"`\n\t\t\t\tTotalTokens      int `json:\"total_tokens\"`\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "adapter/zhinao/chat.go",
    "content": "package zhinao\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/v1/chat/completions\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetModel(model string) string {\n\tswitch model {\n\tcase globals.GPT360V9:\n\t\treturn \"360GPT_S2_V9\"\n\tdefault:\n\t\treturn model\n\t}\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\t// 2048 is the max token for 360GPT\n\tif props.MaxTokens != nil && *props.MaxTokens > 2048 {\n\t\tprops.MaxTokens = utils.ToPtr(2048)\n\t}\n\n\treturn ChatRequest{\n\t\tModel: c.GetModel(props.Model),\n\t\tMessages: utils.EachNotNil(props.Message, func(message globals.Message) *globals.Message {\n\t\t\tif message.Role == globals.Tool {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn &message\n\t\t}),\n\t\tMaxToken:          props.MaxTokens,\n\t\tStream:            stream,\n\t\tTemperature:       props.Temperature,\n\t\tTopP:              props.TopP,\n\t\tTopK:              props.TopK,\n\t\tRepetitionPenalty: props.RepetitionPenalty,\n\t}\n}\n\n// CreateChatRequest is the native http request body for zhinao\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"zhinao error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"zhinao error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", fmt.Errorf(\"zhinao error: %s\", data.Error.Message)\n\t}\n\treturn data.Choices[0].Message.Content, nil\n}\n\n// CreateStreamChatRequest is the stream response body for zhinao\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tbuf := \"\"\n\tcursor := 0\n\tchunk := \"\"\n\n\terr := utils.EventSource(\n\t\t\"POST\",\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, true),\n\t\tfunc(data string) error {\n\t\t\tdata, err := c.ProcessLine(buf, data)\n\t\t\tchunk += data\n\n\t\t\tif err != nil {\n\t\t\t\tif strings.HasPrefix(err.Error(), \"zhinao error\") {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// error when break line\n\t\t\t\tbuf = buf + data\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tbuf = \"\"\n\t\t\tif data != \"\" {\n\t\t\t\tcursor += 1\n\t\t\t\tif err := callback(&globals.Chunk{Content: data}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil {\n\t\treturn err\n\t} else if len(chunk) == 0 {\n\t\treturn fmt.Errorf(\"empty response\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/zhinao/processor.go",
    "content": "package zhinao\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc processFormat(data string) string {\n\trep := strings.NewReplacer(\n\t\t\"data: {\",\n\t\t\"\\\"data\\\": {\",\n\t)\n\titem := rep.Replace(data)\n\tif !strings.HasPrefix(item, \"{\") {\n\t\titem = \"{\" + item\n\t}\n\tif !strings.HasSuffix(item, \"}}\") {\n\t\titem = item + \"}\"\n\t}\n\n\treturn item\n}\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\tif strings.HasPrefix(data, \"{\") {\n\t\tvar form *ChatStreamResponse\n\t\tif form = utils.UnmarshalForm[ChatStreamResponse](data); form != nil {\n\t\t\treturn form\n\t\t}\n\n\t\tif form = utils.UnmarshalForm[ChatStreamResponse](data[:len(data)-1]); form != nil {\n\t\t\treturn form\n\t\t}\n\n\t\tif form = utils.UnmarshalForm[ChatStreamResponse](data + \"}\"); form != nil {\n\t\t\treturn form\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\tif strings.HasPrefix(data, \"{\") {\n\t\tvar form *ChatStreamErrorResponse\n\t\tif form = utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {\n\t\t\treturn form\n\t\t}\n\t\tif form = utils.UnmarshalForm[ChatStreamErrorResponse](data + \"}\"); form != nil {\n\t\t\treturn form\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc isDone(data string) bool {\n\treturn utils.Contains[string](data, []string{\n\t\t\"{data: [DONE]}\", \"{data: [DONE]}}\", \"null}}\", \"{null}\",\n\t\t\"{[DONE]}\", \"{data:}\", \"{data:}}\", \"data: [DONE]}}\",\n\t})\n}\n\nfunc getChoices(form *ChatStreamResponse) string {\n\tif len(form.Data.Choices) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn form.Data.Choices[0].Delta.Content\n}\n\nfunc (c *ChatInstance) ProcessLine(buf, data string) (string, error) {\n\titem := processFormat(buf + data)\n\tif isDone(item) {\n\t\treturn \"\", nil\n\t}\n\n\tif form := processChatResponse(item); form == nil {\n\t\t// recursive call\n\t\tif len(buf) > 0 {\n\t\t\treturn c.ProcessLine(\"\", buf+item)\n\t\t}\n\n\t\tif err := processChatErrorResponse(item); err == nil || err.Data.Error.Message == \"\" {\n\t\t\tglobals.Warn(fmt.Sprintf(\"zhinao error: cannot parse response: %s\", item))\n\t\t\treturn data, errors.New(\"parser error: cannot parse response\")\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"zhinao error: %s (type: %s)\", err.Data.Error.Message, err.Data.Error.Type)\n\t\t}\n\n\t} else {\n\t\treturn getChoices(form), nil\n\t}\n}\n"
  },
  {
    "path": "adapter/zhinao/struct.go",
    "content": "package zhinao\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"fmt\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetApiKey()),\n\t}\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/zhinao/types.go",
    "content": "package zhinao\n\nimport \"chat/globals\"\n\n// 360 ZhiNao API is similar to OpenAI API\n\n// ChatRequest is the request body for zhinao\ntype ChatRequest struct {\n\tModel             string            `json:\"model\"`\n\tMessages          []globals.Message `json:\"messages\"`\n\tMaxToken          *int              `json:\"max_tokens,omitempty\"`\n\tTopP              *float32          `json:\"top_p,omitempty\"`\n\tTopK              *int              `json:\"top_k,omitempty\"`\n\tTemperature       *float32          `json:\"temperature,omitempty\"`\n\tRepetitionPenalty *float32          `json:\"repetition_penalty,omitempty\"`\n\tStream            bool              `json:\"stream\"`\n}\n\n// ChatResponse is the native http request body for zhinao\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tMessage struct {\n\t\t\tContent string `json:\"content\"`\n\t\t}\n\t} `json:\"choices\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\n// ChatStreamResponse is the stream response body for zhinao\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tData    struct {\n\t\tChoices []struct {\n\t\t\tDelta struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t}\n\t\t\tIndex int `json:\"index\"`\n\t\t} `json:\"choices\"`\n\t} `json:\"data\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tData struct {\n\t\tError struct {\n\t\t\tMessage string `json:\"message\"`\n\t\t\tType    string `json:\"type\"`\n\t\t} `json:\"error\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "adapter/zhipuai/chat.go",
    "content": "package zhipuai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\nfunc (c *ChatInstance) GetChatEndpoint() string {\n\treturn fmt.Sprintf(\"%s/api/paas/v4/chat/completions\", c.GetEndpoint())\n}\n\nfunc (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {\n\tresult := \"\"\n\tfor _, message := range messages {\n\t\tresult += fmt.Sprintf(\"%s: %s\\n\", message.Role, message.Content)\n\t}\n\treturn result\n}\n\nfunc (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {\n\tif len(props.Message) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn props.Message[len(props.Message)-1].Content\n}\n\nfunc (c *ChatInstance) ConvertModel(model string) string {\n\t// for v3 legacy adapter\n\tswitch model {\n\tcase globals.ZhiPuChatGLMTurbo:\n\t\treturn GLMTurbo\n\tcase globals.ZhiPuChatGLMPro:\n\t\treturn GLMPro\n\tcase globals.ZhiPuChatGLMStd:\n\t\treturn GLMStd\n\tcase globals.ZhiPuChatGLMLite:\n\t\treturn GLMLite\n\tdefault:\n\t\treturn GLMStd\n\t}\n}\n\nfunc (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {\n\tif props.Model == globals.GPT3TurboInstruct {\n\t\t// for completions\n\t\treturn CompletionRequest{\n\t\t\tModel:    c.ConvertModel(props.Model),\n\t\t\tPrompt:   c.GetCompletionPrompt(props.Message),\n\t\t\tMaxToken: props.MaxTokens,\n\t\t\tStream:   stream,\n\t\t}\n\t}\n\n\tmessages := formatMessages(props)\n\n\t// chatglm top_p should be (0.0, 1.0) and cannot be 0 or 1\n\tif props.TopP != nil && *props.TopP >= 1.0 {\n\t\tprops.TopP = utils.ToPtr[float32](0.99)\n\t} else if props.TopP != nil && *props.TopP <= 0.0 {\n\t\tprops.TopP = utils.ToPtr[float32](0.01)\n\t}\n\n\treturn ChatRequest{\n\t\tModel:            props.Model,\n\t\tMessages:         messages,\n\t\tMaxToken:         props.MaxTokens,\n\t\tStream:           stream,\n\t\tPresencePenalty:  props.PresencePenalty,\n\t\tFrequencyPenalty: props.FrequencyPenalty,\n\t\tTemperature:      props.Temperature,\n\t\tTopP:             props.TopP,\n\t\tTools:            props.Tools,\n\t\tToolChoice:       props.ToolChoice,\n\t}\n}\n\n// CreateChatRequest is the native http request body for chatglm\nfunc (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {\n\tres, err := utils.Post(\n\t\tc.GetChatEndpoint(),\n\t\tc.GetHeader(),\n\t\tc.GetChatBody(props, false),\n\t\tprops.Proxy,\n\t)\n\n\tif err != nil || res == nil {\n\t\treturn \"\", fmt.Errorf(\"chatglm error: %s\", err.Error())\n\t}\n\n\tdata := utils.MapToStruct[ChatResponse](res)\n\tif data == nil {\n\t\treturn \"\", fmt.Errorf(\"chatglm error: cannot parse response\")\n\t} else if data.Error.Message != \"\" {\n\t\treturn \"\", fmt.Errorf(\"chatglm error: %s\", data.Error.Message)\n\t}\n\treturn data.Choices[0].Message.Content, nil\n}\n\nfunc hideRequestId(message string) string {\n\t// xxx (request id: 2024020311120561344953f0xfh0TX)\n\n\texp := regexp.MustCompile(`\\(request id: [a-zA-Z0-9]+\\)`)\n\treturn exp.ReplaceAllString(message, \"\")\n}\n\n// CreateStreamChatRequest is the stream response body for chatglm\nfunc (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {\n\tticks := 0\n\terr := utils.EventScanner(&utils.EventScannerProps{\n\t\tMethod:  \"POST\",\n\t\tUri:     c.GetChatEndpoint(),\n\t\tHeaders: c.GetHeader(),\n\t\tBody:    c.GetChatBody(props, true),\n\t\tCallback: func(data string) error {\n\t\t\tticks += 1\n\n\t\t\tpartial, err := c.ProcessLine(data, false)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn callback(partial)\n\t\t},\n\t}, props.Proxy)\n\n\tif err != nil {\n\t\tif form := processChatErrorResponse(err.Body); form != nil {\n\t\t\tif form.Error.Type == \"\" && form.Error.Message == \"\" {\n\t\t\t\treturn errors.New(utils.ToMarkdownCode(\"json\", err.Body))\n\t\t\t}\n\n\t\t\tmsg := fmt.Sprintf(\"%s (code: %s)\", form.Error.Message, form.Error.Code)\n\t\t\treturn errors.New(hideRequestId(msg))\n\t\t}\n\t\treturn err.Error\n\t}\n\n\tif ticks == 0 {\n\t\treturn errors.New(\"no response\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "adapter/zhipuai/processor.go",
    "content": "package zhipuai\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc formatMessages(props *adaptercommon.ChatProps) interface{} {\n\tif globals.IsVisionModel(props.Model) {\n\t\treturn utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {\n\t\t\tif message.Role == globals.User {\n\t\t\t\tcontent, urls := utils.ExtractImages(message.Content, true)\n\t\t\t\timages := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {\n\t\t\t\t\tobj, err := utils.NewImage(url)\n\t\t\t\t\tprops.Buffer.AddImage(obj)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tglobals.Info(fmt.Sprintf(\"cannot process image: %s (source: %s)\", err.Error(), utils.Extract(url, 24, \"...\")))\n\t\t\t\t\t}\n\n\t\t\t\t\tif strings.HasPrefix(url, \"data:image/\") {\n\t\t\t\t\t\t// remove base64 image prefix\n\t\t\t\t\t\tif idx := strings.Index(url, \"base64,\"); idx != -1 {\n\t\t\t\t\t\t\turl = url[idx+7:]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn &MessageContent{\n\t\t\t\t\t\tType: \"image_url\",\n\t\t\t\t\t\tImageUrl: &ImageUrl{\n\t\t\t\t\t\t\tUrl: url,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\treturn Message{\n\t\t\t\t\tRole: message.Role,\n\t\t\t\t\tContent: utils.Prepend(images, MessageContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: &content,\n\t\t\t\t\t}),\n\t\t\t\t\tName:         message.Name,\n\t\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn Message{\n\t\t\t\tRole:         message.Role,\n\t\t\t\tContent:      message.Content,\n\t\t\t\tName:         message.Name,\n\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t}\n\t\t})\n\t}\n\n\treturn props.Message\n}\n\nfunc processChatResponse(data string) *ChatStreamResponse {\n\treturn utils.UnmarshalForm[ChatStreamResponse](data)\n}\n\nfunc processCompletionResponse(data string) *CompletionResponse {\n\treturn utils.UnmarshalForm[CompletionResponse](data)\n}\n\nfunc processChatErrorResponse(data string) *ChatStreamErrorResponse {\n\treturn utils.UnmarshalForm[ChatStreamErrorResponse](data)\n}\n\nfunc getChoices(form *ChatStreamResponse) *globals.Chunk {\n\tif len(form.Choices) == 0 {\n\t\treturn &globals.Chunk{Content: \"\"}\n\t}\n\n\tchoice := form.Choices[0].Delta\n\n\treturn &globals.Chunk{\n\t\tContent:      choice.Content,\n\t\tToolCall:     choice.ToolCalls,\n\t\tFunctionCall: choice.FunctionCall,\n\t}\n}\n\nfunc getCompletionChoices(form *CompletionResponse) string {\n\tif len(form.Choices) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn form.Choices[0].Text\n}\n\nfunc getRobustnessResult(chunk string) string {\n\texp := `\\\"content\\\":\\\"(.*?)\\\"`\n\tcompile, err := regexp.Compile(exp)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tmatches := compile.FindStringSubmatch(chunk)\n\tif len(matches) > 1 {\n\t\treturn utils.ProcessRobustnessChar(matches[1])\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {\n\tif isCompletionType {\n\t\t// chatglm legacy support\n\t\tif completion := processCompletionResponse(data); completion != nil {\n\t\t\treturn &globals.Chunk{\n\t\t\t\tContent: getCompletionChoices(completion),\n\t\t\t}, nil\n\t\t}\n\n\t\tglobals.Warn(fmt.Sprintf(\"chatglm error: cannot parse completion response: %s\", data))\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse completion response\")\n\t}\n\n\tif form := processChatResponse(data); form != nil {\n\t\treturn getChoices(form), nil\n\t}\n\n\tif form := processChatErrorResponse(data); form != nil {\n\t\treturn &globals.Chunk{Content: \"\"}, errors.New(fmt.Sprintf(\"chatglm error: %s (type: %s)\", form.Error.Message, form.Error.Type))\n\t}\n\n\tglobals.Warn(fmt.Sprintf(\"chatglm error: cannot parse chat completion response: %s\", data))\n\treturn &globals.Chunk{Content: \"\"}, errors.New(\"parser error: cannot parse chat completion response\")\n}\n"
  },
  {
    "path": "adapter/zhipuai/struct.go",
    "content": "package zhipuai\n\nimport (\n\tfactory \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dgrijalva/jwt-go\"\n)\n\ntype ChatInstance struct {\n\tEndpoint string\n\tApiKey   string\n}\n\ntype Payload struct {\n\tApiKey    string `json:\"api_key\"`\n\tExp       int64  `json:\"exp\"`\n\tTimeStamp int64  `json:\"timestamp\"`\n}\n\nfunc (c *ChatInstance) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *ChatInstance) GetApiKey() string {\n\treturn c.ApiKey\n}\n\nfunc (c *ChatInstance) GetHeader() map[string]string {\n\treturn map[string]string{\n\t\t\"Content-Type\":  \"application/json\",\n\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", c.GetToken()),\n\t}\n}\n\nfunc (c *ChatInstance) GetToken() string {\n\t// get jwt token for zhipuai api\n\tsegment := strings.Split(c.ApiKey, \".\")\n\tif len(segment) != 2 {\n\t\treturn \"\"\n\t}\n\tid, secret := segment[0], segment[1]\n\n\tpayload := utils.MapToStruct[jwt.MapClaims](Payload{\n\t\tApiKey:    id,\n\t\tExp:       time.Now().Add(time.Minute*5).Unix() * 1000,\n\t\tTimeStamp: time.Now().Unix() * 1000,\n\t})\n\n\tinstance := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)\n\tinstance.Header = map[string]interface{}{\n\t\t\"alg\":       \"HS256\",\n\t\t\"sign_type\": \"SIGN\",\n\t}\n\ttoken, _ := instance.SignedString([]byte(secret))\n\treturn token\n}\n\nfunc NewChatInstance(endpoint, apiKey string) *ChatInstance {\n\treturn &ChatInstance{\n\t\tEndpoint: endpoint,\n\t\tApiKey:   apiKey,\n\t}\n}\n\nfunc NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {\n\treturn NewChatInstance(\n\t\tconf.GetEndpoint(),\n\t\tconf.GetRandomSecret(),\n\t)\n}\n"
  },
  {
    "path": "adapter/zhipuai/types.go",
    "content": "package zhipuai\n\nimport \"chat/globals\"\n\nconst (\n\tGLM4       = \"glm-4\"\n\tGLM4Vision = \"glm-4v\"\n\tGLMTurbo   = \"glm-3-turbo\"  // GLM3 Turbo\n\tGLMPro     = \"chatglm_pro\"  // GLM3 Pro (deprecated)\n\tGLMStd     = \"chatglm_std\"  // GLM3 Standard (deprecated)\n\tGLMLite    = \"chatglm_lite\" // GLM3 Lite (deprecated)\n)\n\ntype ImageUrl struct {\n\tUrl    string  `json:\"url\"`\n\tDetail *string `json:\"detail,omitempty\"`\n}\n\ntype MessageContent struct {\n\tType     string    `json:\"type\"`\n\tText     *string   `json:\"text,omitempty\"`\n\tImageUrl *ImageUrl `json:\"image_url,omitempty\"`\n}\n\ntype MessageContents []MessageContent\n\ntype Message struct {\n\tRole         string                `json:\"role\"`\n\tContent      interface{}           `json:\"content\"`\n\tName         *string               `json:\"name,omitempty\"`\n\tFunctionCall *globals.FunctionCall `json:\"function_call,omitempty\"` // only `function` role\n\tToolCallId   *string               `json:\"tool_call_id,omitempty\"`  // only `tool` role\n\tToolCalls    *globals.ToolCalls    `json:\"tool_calls,omitempty\"`    // only `assistant` role\n}\n\n// ChatRequest is the request body for chatglm\ntype ChatRequest struct {\n\tModel            string                 `json:\"model\"`\n\tMessages         interface{}            `json:\"messages\"`\n\tMaxToken         *int                   `json:\"max_tokens,omitempty\"`\n\tStream           bool                   `json:\"stream\"`\n\tPresencePenalty  *float32               `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty *float32               `json:\"frequency_penalty,omitempty\"`\n\tTemperature      *float32               `json:\"temperature,omitempty\"`\n\tTopP             *float32               `json:\"top_p,omitempty\"`\n\tTools            *globals.FunctionTools `json:\"tools,omitempty\"`\n\tToolChoice       *interface{}           `json:\"tool_choice,omitempty\"` // string or object\n}\n\n// CompletionRequest is the request body for chatglm completion\ntype CompletionRequest struct {\n\tModel    string `json:\"model\"`\n\tPrompt   string `json:\"prompt\"`\n\tMaxToken *int   `json:\"max_tokens,omitempty\"`\n\tStream   bool   `json:\"stream\"`\n}\n\n// ChatResponse is the native http request body for chatglm\ntype ChatResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tIndex        int             `json:\"index\"`\n\t\tMessage      globals.Message `json:\"message\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\n// ChatStreamResponse is the stream response body for chatglm\ntype ChatStreamResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tDelta        globals.Message `json:\"delta\"`\n\t\tIndex        int             `json:\"index\"`\n\t\tFinishReason string          `json:\"finish_reason\"`\n\t} `json:\"choices\"`\n}\n\n// CompletionResponse is the native http request body / stream response body for chatglm completion\ntype CompletionResponse struct {\n\tID      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tModel   string `json:\"model\"`\n\tChoices []struct {\n\t\tText  string `json:\"text\"`\n\t\tIndex int    `json:\"index\"`\n\t} `json:\"choices\"`\n}\n\ntype ChatStreamErrorResponse struct {\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t\tType    string `json:\"type\"`\n\t\tCode    string `json:\"code\"`\n\t} `json:\"error\"`\n}\n\ntype ImageSize string\n\n// ImageRequest is the request body for chatglm dalle image generation\ntype ImageRequest struct {\n\tModel  string    `json:\"model\"`\n\tPrompt string    `json:\"prompt\"`\n\tSize   ImageSize `json:\"size\"`\n\tN      int       `json:\"n\"`\n}\n\ntype ImageResponse struct {\n\tData []struct {\n\t\tUrl string `json:\"url\"`\n\t} `json:\"data\"`\n\tError struct {\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\nvar (\n\tImageSize256  ImageSize = \"256x256\"\n\tImageSize512  ImageSize = \"512x512\"\n\tImageSize1024 ImageSize = \"1024x1024\"\n)\n"
  },
  {
    "path": "addition/article/api.go",
    "content": "package article\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"strings\"\n)\n\ntype WebsocketArticleForm struct {\n\tToken  string `json:\"token\" binding:\"required\"`\n\tModel  string `json:\"model\" binding:\"required\"`\n\tPrompt string `json:\"prompt\" binding:\"required\"`\n\tTitle  string `json:\"title\" binding:\"required\"`\n\tWeb    bool   `json:\"web\"`\n}\n\ntype WebsocketArticleResponse struct {\n\tHash string                 `json:\"hash\"`\n\tData StreamProgressResponse `json:\"data\"`\n}\n\nfunc ProjectTarDownloadAPI(c *gin.Context) {\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tc.Writer.Header().Add(\"Content-Disposition\", \"attachment; filename=article.tar.gz\")\n\tc.File(fmt.Sprintf(\"storage/article/%s.tar.gz\", hash))\n}\n\nfunc ProjectZipDownloadAPI(c *gin.Context) {\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tc.Writer.Header().Add(\"Content-Disposition\", \"attachment; filename=article.zip\")\n\tc.File(fmt.Sprintf(\"storage/article/%s.zip\", hash))\n}\n\nfunc GenerateAPI(c *gin.Context) {\n\tvar conn *utils.WebSocket\n\tif conn = utils.NewWebsocket(c, false); conn == nil {\n\t\treturn\n\t}\n\tdefer conn.DeferClose()\n\n\tform, err := utils.ReadForm[WebsocketArticleForm](conn)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tuser := auth.ParseToken(c, form.Token)\n\tdb := utils.GetDBFromContext(c)\n\n\tif !auth.HitGroups(db, user, globals.ArticlePermissionGroup) {\n\t\treturn\n\t}\n\n\tif len(form.Title) == 0 {\n\t\treturn\n\t}\n\n\thash := CreateWorker(c, user, form.Model, form.Prompt, form.Title, form.Web, func(resp StreamProgressResponse) {\n\t\tconn.Send(WebsocketArticleResponse{\n\t\t\tHash: \"\",\n\t\t\tData: resp,\n\t\t})\n\t})\n\tconn.Send(WebsocketArticleResponse{\n\t\tHash: hash,\n\t})\n}\n"
  },
  {
    "path": "addition/article/data/.gitkeep",
    "content": ""
  },
  {
    "path": "addition/article/generate.go",
    "content": "package article\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/manager\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"strings\"\n)\n\ntype StreamProgressResponse struct {\n\tCurrent int     `json:\"current\"`\n\tTotal   int     `json:\"total\"`\n\tQuota   float32 `json:\"quota\"`\n}\n\ntype Response struct {\n\tFile  string\n\tQuota float32\n}\n\nfunc GenerateArticle(c *gin.Context, user *auth.User, model string, hash string, title string, prompt string, enableWeb bool) Response {\n\tmessage, quota := manager.NativeChatHandler(c, user, model, []globals.Message{{\n\t\tRole:    globals.User,\n\t\tContent: fmt.Sprintf(\"%s\\n%s\", prompt, title),\n\t}}, enableWeb)\n\n\treturn Response{\n\t\tFile:  CreateArticleFile(hash, title, message),\n\t\tQuota: quota,\n\t}\n}\n\nfunc ParseTitle(titles string) []string {\n\tvar result []string\n\tfor _, title := range strings.Split(titles, \"\\n\") {\n\t\ttitle = strings.TrimSpace(title)\n\t\tif len(title) > 0 {\n\t\t\tresult = append(result, title)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc CreateGenerationWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hash string) (int, chan Response) {\n\ttitles := ParseTitle(title)\n\tresult := make(chan Response, len(titles))\n\n\tfor _, name := range titles {\n\t\tgo func(title string) {\n\t\t\tresult <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)\n\t\t}(name)\n\t}\n\n\treturn len(titles), result\n}\n\nfunc CreateWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hook func(resp StreamProgressResponse)) string {\n\thash := utils.Md5Encrypt(fmt.Sprintf(\"%s%s%s%v\", model, prompt, title, enableWeb))\n\ttotal, channel := CreateGenerationWorker(c, user, model, prompt, title, enableWeb, hash)\n\tcurrent := 0\n\n\thook(StreamProgressResponse{Current: current, Total: total, Quota: 0})\n\n\tfor resp := range channel {\n\t\tcurrent += 1\n\t\thook(StreamProgressResponse{Current: current, Total: total, Quota: resp.Quota})\n\n\t\tif current == total {\n\t\t\tbreak\n\t\t}\n\t}\n\n\thook(StreamProgressResponse{Current: current, Total: total, Quota: 0})\n\n\tpath := fmt.Sprintf(\"storage/article/data/%s\", hash)\n\tif _, _, err := utils.GenerateCompressTask(hash, \"storage/article\", path, path); err != nil {\n\t\tglobals.Debug(fmt.Sprintf(\"[article] error during generate compress task: %s\", err.Error()))\n\t\treturn \"\"\n\t}\n\n\treturn hash\n}\n"
  },
  {
    "path": "addition/article/utils.go",
    "content": "package article\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/lukasjarosch/go-docx\"\n)\n\nfunc GenerateDocxFile(target, title, content string) error {\n\tdata := docx.PlaceholderMap{\n\t\t\"title\":   title,\n\t\t\"content\": content,\n\t}\n\n\tdoc, err := docx.Open(\"addition/article/template.docx\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := doc.ReplaceAll(data); err != nil {\n\t\treturn err\n\t}\n\n\tif err := doc.WriteToFile(target); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc CreateArticleFile(hash, title, content string) string {\n\ttarget := fmt.Sprintf(\"storage/article/data/%s/%s.docx\", hash, title)\n\tutils.FileDirSafe(target)\n\tif err := GenerateDocxFile(target, title, content); err != nil {\n\t\tglobals.Debug(fmt.Sprintf(\"[article] error during generate article %s: %s\", title, err.Error()))\n\t}\n\n\treturn target\n}\n"
  },
  {
    "path": "addition/card/.gitignore",
    "content": ".idea\n.vscode\n"
  },
  {
    "path": "addition/card/card.go",
    "content": "package card\n\nimport (\n\t\"chat/globals\"\n\t\"chat/manager\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/russross/blackfriday/v2\"\n\t\"net/http\"\n\t\"strings\"\n)\n\ntype RequestForm struct {\n\tMessage string `json:\"message\" required:\"true\"`\n\tWeb     bool   `json:\"web\"`\n}\n\nconst maxColumnPerLine = 50\n\nfunc ProcessMarkdownLine(source []byte) string {\n\tsegment := strings.Split(string(source), \"\\n\")\n\tvar result []rune\n\tfor _, line := range segment {\n\t\tdata := []rune(line)\n\t\tlength := len([]rune(line))\n\t\tif length < maxColumnPerLine {\n\t\t\tresult = append(result, data...)\n\t\t\tresult = append(result, '\\n')\n\t\t} else {\n\t\t\tfor i := 0; i < length; i += maxColumnPerLine {\n\t\t\t\tif i+maxColumnPerLine < length {\n\t\t\t\t\tresult = append(result, data[i:i+maxColumnPerLine]...)\n\t\t\t\t\tresult = append(result, '\\n')\n\t\t\t\t} else {\n\t\t\t\t\tresult = append(result, data[i:]...)\n\t\t\t\t\tresult = append(result, '\\n')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn string(result)\n}\n\nfunc MarkdownConvert(text string) string {\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\n\tresult := blackfriday.Run([]byte(text))\n\treturn string(result)\n}\n\nfunc HandlerAPI(c *gin.Context) {\n\tvar body RequestForm\n\tif err := c.ShouldBindJSON(&body); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"error\": \"invalid request body\",\n\t\t})\n\t}\n\tmessage := strings.TrimSpace(body.Message)\n\tif len(message) == 0 {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"error\": \"message is empty\",\n\t\t})\n\t\treturn\n\t}\n\n\tresponse, quota := manager.NativeChatHandler(c, nil, globals.GPT3Turbo0613, []globals.Message{\n\t\t{Role: globals.User, Content: message},\n\t}, body.Web)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"message\": MarkdownConvert(response),\n\t\t\"keyword\": \"\",\n\t\t\"quota\":   quota,\n\t})\n}\n"
  },
  {
    "path": "addition/card/card.php",
    "content": "<?php\ninclude 'utils.php';\n\n$dark = get('theme', 'light') === 'dark';\n$message = get('message', 'hi');\n$web = get('web', 'false') === 'false';\n$sign = get('sign', 'false') === 'true';\n\n$resp = fetch($message, $web);\nif (!$resp) {\n    include 'error.php';\n    exit;\n}\n\n$msg = str_replace(\"&hellip;\", \"-\",\n    str_replace(\"<br>\", \"<br></br>\", $resp['message']));\n$msgHeight = substr_count($resp['message'], \"\\n\") * 20 + strlen($msg) * 0.15 + substr_count($resp['message'], \"<img src=\") * 320 + 20;\n$height = 90 + $msgHeight;\n\n$header = $dark ? \"#fff\" : \"#0a0a0a\";\n$background = $dark ? \"#000\" : \"#fffefe\";\n\nob_start('compress');\n?>\n    <svg width=\"580\" viewBox=\"0 0 420 <?php echo $height + 1 ?>\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" role=\"img\" aria-labelledby=\"descId\">\n        <title id=\"titleId\">ChatGPT</title>\n        <desc id=\"descId\">ChatGPT Card</desc>\n        <style>\n            .header {\n                font: 600 26px 'Segoe UI', Ubuntu, Sans-Serif !important;\n                animation: fadeInAnimation 0.8s ease-in-out forwards;\n            }\n            .openai {\n                fill: <?php echo $header ?>;\n            }\n            @supports (appearance: auto) {\n                .header {\n                    font-size: 16px;\n                }\n            }\n            .stat {\n                font: 600 14px 'Segoe UI', Ubuntu, \"Helvetica Neue\", Sans-Serif;\n                fill: <?php echo $header ?>;\n            }\n            @supports (appearance: auto) {\n                .stat {\n                    font-size: 12px;\n                }\n            }\n            .stagger {\n                opacity: 0;\n                animation: fadeInAnimation 0.3s ease-in-out forwards;\n            }\n            .object {\n                width: calc(100% - 64px);\n            }\n            @keyframes fadeInAnimation {\n                from {\n                    opacity: 0;\n                }\n                to {\n                    opacity: 1;\n                }\n            }\n        </style>\n        <rect data-testid=\"card-bg\" x=\"0.5\" y=\"0.5\" rx=\"4.5\" height=\"99%\" stroke=\"#e4e2e2\" width=\"99%\" fill=\"<?php echo $background ?>\" stroke-opacity=\"1\"/>\n        <g data-testid=\"card-title\" transform=\"translate(30, 25)\">\n            <?php if ($sign) { ?>\n            <g class=\"stagger\" transform=\"translate(0, 0)\">\n                <text class=\"stat\" x=\"330\" y=\"-2\">chatnio</text>\n            </g>\n            <?php } ?>\n            <g transform=\"translate(0, 0)\">\n                <svg class=\"openai\" width=\"20\" height=\"20\" y=\"6\" role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>OpenAI</title><path d=\"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z\"/></svg>\n                <text x=\"28\" y=\"24\" class=\"stat header\" data-testid=\"question\"><?php echo $message ?></text>\n            </g>\n        </g>\n        <g data-testid=\"main-card\" transform=\"translate(0, 48)\">\n            <svg x=\"0\" y=\"0\">\n                <g transform=\"translate(0, 0)\">\n                    <g class=\"stagger\" style=\"animation-delay: 600ms\">\n                        <foreignObject class=\"stat object\" x=\"30\" y=\"10\" height=\"<?php echo $msgHeight ?>\" data-testid=\"message\">\n                            <style>\n                                .data * {\n                                    font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif;\n                                    font-weight: normal;\n                                    color: <?php echo $header ?>;\n                                }\n                                .data h1, .data h2, .data h3 {\n                                    font-weight: bold;\n                                }\n\n                                .data img {\n                                    height: 180px;\n                                }\n                            </style>\n                            <body xmlns=\"http://www.w3.org/1999/xhtml\" class=\"data\">\n                            <?php echo $msg ?>\n                            </body>\n                        </foreignObject>\n                    </g>\n                </g>\n            </svg>\n        </g>\n    </svg>\n\n<?php\nob_end_flush();\n?>"
  },
  {
    "path": "addition/card/error.php",
    "content": "<?php\ninclude_once 'utils.php';\n$dark = isset($_GET['theme']) && $_GET['theme'] === 'dark';\n\n$header = $dark ? \"#fff\" : \"#434d58\";\n$background = $dark ? \"#000\" : \"#fffefe\";\n\nob_start('compress');\n?>\n\n    <svg width=\"540\" viewBox=\"0 0 660 216\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-labelledby=\"descId\">\n        <title id=\"titleId\">Error</title>\n        <desc id=\"descId\">Error Card</desc>\n        <style>\n            .header {\n                font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;\n                fill: <?php echo $header ?>;\n                animation: fadeInAnimation 0.8s ease-in-out forwards\n            }\n            @supports(appearance: auto) {\n                .header {\n                    font-size: 16px\n                }\n            }\n            #rect-mask rect {\n                animation: fadeInAnimation 1s ease-in-out forwards\n            }\n            @keyframes fadeInAnimation {\n                from {\n                    opacity: 0\n                }\n                to {\n                    opacity: 1\n                }\n            }\n        </style>\n        <rect data-testid=\"card-bg\" x=\"0.5\" y=\"0.5\" rx=\"4.5\" height=\"99%\" stroke=\"#e4e2e2\" width=\"659\" fill=\"<?php echo $background; ?>\" stroke-opacity=\"1\"/>\n        <g data-testid=\"card-title\" transform=\"translate(200, 108)\">\n            <g transform=\"translate(0, 0)\">\n                <text x=\"0\" y=\"0\" class=\"header\" data-testid=\"header\">Sorry, there is something wrong...</text>\n            </g>\n        </g>\n    </svg>\n\n<?php\nob_end_flush();\n?>"
  },
  {
    "path": "addition/card/utils.php",
    "content": "<?php\nheader('Content-Type: image/svg+xml');\nheader('Cache-Control: no-cache');\n\nfunction compress($buffer): array|string|null\n{\n    $search = array('/>[^\\S ]+/', '/[^\\S ]+</', '/(\\s)+/', '/> </', '/:\\s+/', '/\\{\\s+/', '/\\s+}/');\n    $replace = array('>', '<', '\\\\1', '><', ':', '{', '}');\n    return preg_replace($search, $replace, $buffer);\n}\n\nfunction fetch($message, $web): array|string|null\n{\n    $opts = array('http' =>\n        array(\n            'method'  => 'POST',\n            'header'  => 'Content-type: application/json',\n            'content' => json_encode(array('message' => $message, 'web' => $web))\n        )\n    );\n\n    $context  = stream_context_create($opts);\n    $response = @file_get_contents(\"http://localhost:8094/card\", false, $context);\n    $ok = $response !== false;\n    return $ok ? json_decode($response, true) : null;\n}\n\nfunction get($param, $default = null)\n{\n    return $_GET[$param] ?? $default;\n}\n"
  },
  {
    "path": "addition/generation/api.go",
    "content": "package generation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype WebsocketGenerationForm struct {\n\tToken  string `json:\"token\" binding:\"required\"`\n\tPrompt string `json:\"prompt\" binding:\"required\"`\n\tModel  string `json:\"model\" binding:\"required\"`\n}\n\nfunc ProjectTarDownloadAPI(c *gin.Context) {\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tc.Writer.Header().Add(\"Content-Disposition\", \"attachment; filename=code.tar.gz\")\n\tc.File(fmt.Sprintf(\"storage/generation/%s.tar.gz\", hash))\n}\n\nfunc ProjectZipDownloadAPI(c *gin.Context) {\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tc.Writer.Header().Add(\"Content-Disposition\", \"attachment; filename=code.zip\")\n\tc.File(fmt.Sprintf(\"storage/generation/%s.zip\", hash))\n}\n\nfunc GenerateAPI(c *gin.Context) {\n\tvar conn *utils.WebSocket\n\tif conn = utils.NewWebsocket(c, false); conn == nil {\n\t\treturn\n\t}\n\tdefer conn.DeferClose()\n\n\tform, err := utils.ReadForm[WebsocketGenerationForm](conn)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tuser := auth.ParseToken(c, form.Token)\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tif !auth.HitGroups(db, user, globals.GenerationPermissionGroup) {\n\t\tconn.Send(globals.GenerationSegmentResponse{\n\t\t\tMessage: \"permission denied\",\n\t\t\tQuota:   0,\n\t\t\tEnd:     true,\n\t\t})\n\t\treturn\n\t}\n\n\tcheck, plan := auth.CanEnableModelWithSubscription(db, cache, user, form.Model, []globals.Message{})\n\tif check != nil {\n\t\tconn.Send(globals.GenerationSegmentResponse{\n\t\t\tMessage: check.Error(),\n\t\t\tQuota:   0,\n\t\t\tEnd:     true,\n\t\t})\n\t\treturn\n\t}\n\n\tvar instance *utils.Buffer\n\thash, err := CreateGenerationWithCache(\n\t\tauth.GetGroup(db, user),\n\t\tform.Model,\n\t\tform.Prompt,\n\t\tfunc(buffer *utils.Buffer, data string) {\n\t\t\tinstance = buffer\n\t\t\tconn.Send(globals.GenerationSegmentResponse{\n\t\t\t\tEnd:     false,\n\t\t\t\tMessage: data,\n\t\t\t\tQuota:   buffer.GetQuota(),\n\t\t\t})\n\t\t},\n\t)\n\n\tif instance != nil && !plan && instance.GetQuota() > 0 && user != nil {\n\t\tuser.UseQuota(db, instance.GetQuota())\n\t}\n\n\tif err != nil {\n\t\tauth.RevertSubscriptionUsage(db, cache, user, form.Model)\n\t\tconn.Send(globals.GenerationSegmentResponse{\n\t\t\tEnd:   true,\n\t\t\tError: err.Error(),\n\t\t\tQuota: instance.GetQuota(),\n\t\t})\n\t\treturn\n\t}\n\n\tconn.Send(globals.GenerationSegmentResponse{\n\t\tEnd:   true,\n\t\tHash:  hash,\n\t\tQuota: instance.GetQuota(),\n\t})\n}\n"
  },
  {
    "path": "addition/generation/build.go",
    "content": "package generation\n\nimport (\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc GetFolder(hash string) string {\n\treturn fmt.Sprintf(\"storage/generation/data/%s\", hash)\n}\n\nfunc GetFolderByHash(model string, prompt string) (string, string) {\n\thash := utils.Sha2Encrypt(model + prompt + time.Now().Format(\"2006-01-02 15:04:05\"))\n\treturn hash, GetFolder(hash)\n}\n\nfunc GenerateProject(path string, instance ProjectResult) bool {\n\tfor name, data := range instance.Result {\n\t\tcurrent := fmt.Sprintf(\"%s/%s\", path, name)\n\t\tif content, ok := data.(string); ok {\n\t\t\tif utils.WriteFile(current, content, true) != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\tGenerateProject(current, ProjectResult{\n\t\t\t\tResult: data.(map[string]interface{}),\n\t\t\t})\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "addition/generation/data/.gitkeep",
    "content": ""
  },
  {
    "path": "addition/generation/generate.go",
    "content": "package generation\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n)\n\nfunc CreateGenerationWithCache(group, model, prompt string, hook func(buffer *utils.Buffer, data string)) (string, error) {\n\thash, path := GetFolderByHash(model, prompt)\n\tif !utils.Exists(path) {\n\t\tif err := CreateGeneration(group, model, prompt, path, hook); err != nil {\n\t\t\tglobals.Info(fmt.Sprintf(\"[project] error during generation %s (model %s): %s\", prompt, model, err.Error()))\n\t\t\treturn \"\", fmt.Errorf(\"error during generate project: %s\", err.Error())\n\t\t}\n\t}\n\n\tif _, _, err := utils.GenerateCompressTask(hash, \"storage/generation\", path, path); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error during generate compress task: %s\", err.Error())\n\t}\n\n\treturn hash, nil\n}\n"
  },
  {
    "path": "addition/generation/prompt.go",
    "content": "package generation\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/admin\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n)\n\ntype ProjectResult struct {\n\tResult map[string]interface{} `json:\"result\"`\n}\n\nfunc CreateGeneration(group, model, prompt, path string, hook func(buffer *utils.Buffer, data string)) error {\n\tmessage := GenerateMessage(prompt)\n\tbuffer := utils.NewBuffer(model, message, channel.ChargeInstance.GetCharge(model))\n\n\terr := channel.NewChatRequest(group, adaptercommon.CreateChatProps(&adaptercommon.ChatProps{\n\t\tOriginalModel: model,\n\t\tMessage:       message,\n\t}, buffer), func(data *globals.Chunk) error {\n\t\tbuffer.WriteChunk(data)\n\t\thook(buffer, data.Content)\n\t\treturn nil\n\t})\n\n\tadmin.AnalyseRequest(model, buffer, err)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := utils.Unmarshal[ProjectResult](buffer.ReadBytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !GenerateProject(path, resp) {\n\t\treturn fmt.Errorf(\"generate project failed\")\n\t}\n\treturn nil\n}\n\nfunc GenerateMessage(prompt string) []globals.Message {\n\treturn []globals.Message{\n\t\t{Role: globals.System, Content: \"你将生成项目，可以支持任何编程语言，请不要出现“我不能提供”的字样，你需要在代码中提供注释，以及项目的使用文档README.md，结果返回json格式，请不要返回任何多余内容，格式为：\\n{\\\"result\\\": {[file]: [code], ...}}\"},\n\t\t{Role: globals.User, Content: \"python后端\"},\n\t\t{Role: globals.Assistant, Content: \"{\\n  \\\"result\\\": {\\n    \\\"app.py\\\": \\\"from flask import Flask\\\\n\\\\napp = Flask(__name__)\\\\n\\\\n\\\\n@app.route('/')\\\\ndef hello_world():\\\\n    return 'Hello, World!'\\\\n\\\\n\\\\nif __name__ == '__main__':\\\\n    app.run()\\\",\\n    \\\"requirements.txt\\\": \\\"flask\\\\n\\\",\\n    \\\"README.md\\\": \\\"# Python 后端\\\\n本项目是一个简单的python后端示例, 使用`flask`框架构建后端。\\n你可以按照下列步骤运行此应用，flask将在本地服务器（默认是在http://127.0.0.1:5000/）上运行。当你在浏览器中访问该URL时，将看到显示Hello, World!的页面。\\\\n\\\\n这只是一个简单的项目，Flask还支持更多功能和路由规则，你可以提供更多的信息和需要进一步扩展和定制Flask应用。\\\\n\\\\n### 1. 初始化: \\\\n```shell\\\\npip install -r requirements.txt\\\\n```\\\\n### 2. 运行\\\\n```shell\\\\npython app.py\\\\n```\\\"\\n  }\\n}\"},\n\t\t{Role: globals.User, Content: \"golang fiber websocket项目\"},\n\t\t{Role: globals.Assistant, Content: \"{\\n  \\\"result\\\": {\\n    \\\"main.go\\\": \\\"package main\\\\n\\\\nimport (\\\\n\\\\t\\\"log\\\\\\\"\\\\n\\\\n\\\\t\\\"github.com/gofiber/fiber/v2\\\\\\\"\\\\n\\\\t\\\"github.com/gofiber/websocket/v2\\\\\\\"\\\\n)\\\\n\\\\nfunc main() {\\\\n\\\\tapp := fiber.New()\\\\n\\\\n\\\\tapp.Get(\\\\\\\"/\\\\\\\", func(c *fiber.Ctx) error {\\\\n\\\\t\\\\treturn c.SendString(\\\\\\\"Hello, World!\\\\\\\")\\\\n\\\\t})\\\\n\\\\n\\\\tapp.Get(\\\\\\\"/ws\\\\\\\", websocket.New(func(c *websocket.Conn) {\\\\n\\\\t\\\\tfor {\\\\n\\\\t\\\\t\\\\tmt, message, err := c.ReadMessage()\\\\n\\\\t\\\\t\\\\tif err != nil {\\\\n\\\\t\\\\t\\\\t\\\\tlog.Println(\\\\\\\"read error:\\\\\\\", err)\\\\n\\\\t\\\\t\\\\t\\\\tbreak\\\\n\\\\t\\\\t\\\\t}\\\\n\\\\t\\\\t\\\\tlog.Printf(\\\\\\\"received: %s\\\\\\\", message)\\\\n\\\\t\\\\t\\\\terr = c.WriteMessage(mt, message)\\\\n\\\\t\\\\t\\\\tif err != nil {\\\\n\\\\t\\\\t\\\\t\\\\tlog.Println(\\\\\\\"write error:\\\\\\\", err)\\\\n\\\\t\\\\t\\\\t\\\\tbreak\\\\n\\\\t\\\\t\\\\t}\\\\n\\\\t\\\\t}\\\\n\\\\t}))\\\\n\\\\n\\\\tlog.Fatal(app.Listen(\\\\\\\":3000\\\\\\\"))\\\\n}\\\",\\n    \\\"go.mod\\\": \\\"module fiber-websocket\\\\n\\\\ngo 1.16\\\\n\\\\nrequire (\\\\n\\\\tgithub.com/gofiber/fiber/v2 v2.12.1\\\\n\\\\tgithub.com/gofiber/websocket/v2 v2.10.2\\\\n)\\\",\\n    \\\"README.md\\\": \\\"# Golang Fiber WebSocket项目\\\\n\\\\n这个项目是一个使用Golang和Fiber框架构建的WebSocket服务器示例。\\\\n\\\\n### 1. 初始化：\\\\n```shell\\\\ngo mod init fiber-websocket\\\\n```\\\\n\\\\n### 2. 安装依赖：\\\\n```shell\\\\ngo get github.com/gofiber/fiber/v2\\\\n```   \\\\n```shell\\\\ngo get github.com/gofiber/websocket/v2\\\\n```\\\\n\\\\n### 3. 创建main.go文件，将以下代码复制粘贴：\\\\n\\\\n```go\\\\npackage main\\\\n\\\\nimport (\\\\n\\\\t\\\\\\\"log\\\\\\\"\\\\n\\\\n\\\\t\\\\\\\"github.com/gofiber/fiber/v2\\\\\\\"\\\\n\\\\t\\\\\\\"github.com/gofiber/websocket/v2\\\\\\\"\\\\n)\\\\n\\\\nfunc main() {\\\\n\\\\tapp := fiber.New()\\\\n\\\\n\\\\tapp.Get(\\\\\\\"/\\\\\\\", func(c *fiber.Ctx) error {\\\\n\\\\t\\\\treturn c.SendString(\\\\\\\"Hello, World!\\\\\\\")\\\\n\\\\t})\\\\n\\\\n\\\\tapp.Get(\\\\\\\"/ws\\\\\\\", websocket.New(func(c *websocket.Conn) {\\\\n\\\\t\\\\tfor {\\\\n\\\\t\\\\t\\\\tmt, message, err := c.ReadMessage()\\\\n\\\\t\\\\t\\\\tif err != nil {\\\\n\\\\t\\\\t\\\\t\\\\tlog.Println(\\\\\\\"read error:\\\\\\\", err)\\\\n\\\\t\\\\t\\\\t\\\\tbreak\\\\n\\\\t\\\\t\\\\t}\\\\n\\\\t\\\\t\\\\tlog.Printf(\\\\\\\"received: %s\\\\\\\", message)\\\\n\\\\t\\\\t\\\\terr = c.WriteMessage(mt, message)\\\\n\\\\t\\\\t\\\\tif err != nil {\\\\n\\\\t\\\\t\\\\t\\\\tlog.Println(\\\\\\\"write error:\\\\\\\", err)\\\\n\\\\t\\\\t\\\\t\\\\tbreak\\\\n\\\\t\\\\t\\\\t}\\\\n\\\\t\\\\t}\\\\n\\\\t}))\\\\n\\\\n\\\\tlog.Fatal(app.Listen(\\\\\\\":3000\\\\\\\"))\\\\n}\\\\n```\\\\n\\\\n### 4. 运行应用程序：\\\\n```shell\\\\ngo run main.go\\\\n```\\\\n\\\\n应用程序将在本地服务器（默认是在http://localhost:3000）上运行。当你在浏览器中访问`http://localhost:3000`时，将看到显示\\\"Hello, World!\\\"的页面。你还可以访问`http://localhost:3000/ws`来测试WebSocket功能。\\n\\n这只是一个简单的示例，Fiber框架提供了更多的功能和路由规则，你可以在此基础上进行进一步扩展和定制。\\n\\n注意：在运行应用程序之前，请确保已经安装了Go语言开发环境。\"},\n\t\t{Role: globals.User, Content: prompt},\n\t}\n}\n"
  },
  {
    "path": "addition/router.go",
    "content": "package addition\n\nimport (\n\t\"chat/addition/article\"\n\t\"chat/addition/card\"\n\t\"chat/addition/generation\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Register(app *gin.RouterGroup) {\n\t{\n\t\tapp.POST(\"/card\", card.HandlerAPI)\n\n\t\tapp.GET(\"/generation/create\", generation.GenerateAPI)\n\t\tapp.GET(\"/generation/download/tar\", generation.ProjectTarDownloadAPI)\n\t\tapp.GET(\"/generation/download/zip\", generation.ProjectZipDownloadAPI)\n\n\t\tapp.GET(\"/article/create\", article.GenerateAPI)\n\t\tapp.GET(\"/article/download/tar\", article.ProjectTarDownloadAPI)\n\t\tapp.GET(\"/article/download/zip\", article.ProjectZipDownloadAPI)\n\t}\n}\n"
  },
  {
    "path": "addition/web/call.go",
    "content": "package web\n\nimport (\n\t\"chat/globals\"\n\t\"chat/manager/conversation\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"time\"\n)\n\ntype Hook func(message []globals.Message, token int) (string, error)\n\nfunc toWebSearchingMessage(message []globals.Message) []globals.Message {\n\tdata, _ := GenerateSearchResult(message[len(message)-1].Content)\n\n\treturn utils.Insert(message, 0, globals.Message{\n\t\tRole: globals.System,\n\t\tContent: fmt.Sprintf(\"You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources.\"+\n\t\t\t\"Current time: %s, Real-time internet search results: %s\",\n\t\t\ttime.Now().Format(\"2006-01-02 15:04:05\"), data,\n\t\t),\n\t})\n}\n\nfunc ToChatSearched(instance *conversation.Conversation, restart bool) []globals.Message {\n\tsegment := conversation.CopyMessage(instance.GetChatMessage(restart))\n\n\tif instance.IsEnableWeb() {\n\t\tsegment = toWebSearchingMessage(segment)\n\t}\n\n\treturn segment\n}\n\nfunc ToSearched(enable bool, message []globals.Message) []globals.Message {\n\tif enable {\n\t\treturn toWebSearchingMessage(message)\n\t}\n\n\treturn message\n}\n"
  },
  {
    "path": "addition/web/search.go",
    "content": "package web\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype SearXNGResponse struct {\n\tQuery           string `json:\"query\"`\n\tNumberOfResults int    `json:\"number_of_results\"`\n\tResults         []struct {\n\t\tUrl           string   `json:\"url\"`\n\t\tTitle         string   `json:\"title\"`\n\t\tContent       string   `json:\"content\"`\n\t\tPublishedDate *string  `json:\"publishedDate,omitempty\"`\n\t\tThumbnail     *string  `json:\"thumbnail,omitempty\"`\n\t\tEngine        string   `json:\"engine\"`\n\t\tParsedUrl     []string `json:\"parsed_url\"`\n\t\tTemplate      string   `json:\"template\"`\n\t\tEngines       []string `json:\"engines\"`\n\t\tPositions     []int    `json:\"positions\"`\n\t\tScore         float64  `json:\"score\"`\n\t\tCategory      string   `json:\"category\"`\n\t\tIframeSrc     string   `json:\"iframe_src,omitempty\"`\n\t} `json:\"results\"`\n\tAnswers             []interface{} `json:\"answers\"`\n\tCorrections         []interface{} `json:\"corrections\"`\n\tInfoboxes           []interface{} `json:\"infoboxes\"`\n\tSuggestions         []interface{} `json:\"suggestions\"`\n\tUnresponsiveEngines [][]string    `json:\"unresponsive_engines\"`\n}\n\nfunc formatResponse(data *SearXNGResponse) string {\n\tres := make([]string, 0)\n\tfor _, item := range data.Results {\n\t\tif item.Content == \"\" || item.Url == \"\" || item.Title == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tres = append(res, fmt.Sprintf(\"%s (%s): %s\", item.Title, item.Url, item.Content))\n\t}\n\n\treturn strings.Join(res, \"\\n\")\n}\n\nfunc createURLParams(query string) string {\n\tparams := url.Values{}\n\n\tparams.Add(\"q\", query)\n\tparams.Add(\"format\", \"json\")\n\tparams.Add(\"safesearch\", strconv.Itoa(globals.SearchSafeSearch))\n\tif len(globals.SearchEngines) > 0 {\n\t\tparams.Add(\"engines\", globals.SearchEngines)\n\t}\n\tif len(globals.SearchImageProxy) > 0 {\n\t\tparams.Add(\"image_proxy\", globals.SearchImageProxy)\n\t}\n\n\treturn fmt.Sprintf(\"%s?%s\", globals.SearchEndpoint, params.Encode())\n}\n\nfunc createSearXNGRequest(query string) (*SearXNGResponse, error) {\n\tdata, err := utils.Get(createURLParams(query), nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn utils.MapToRawStruct[SearXNGResponse](data)\n}\n\nfunc GenerateSearchResult(q string) (string, error) {\n\tres, err := createSearXNGRequest(q)\n\tif err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[web] failed to get search result: %s (query: %s)\", err.Error(), utils.Extract(q, 20, \"...\")))\n\n\t\tcontent := fmt.Sprintf(\"search failed: %s\", err.Error())\n\t\treturn content, errors.New(content)\n\t}\n\n\tcontent := formatResponse(res)\n\tglobals.Debug(fmt.Sprintf(\"[web] search result: %s (query: %s)\", utils.Extract(content, 50, \"...\"), q))\n\n\tif globals.SearchCrop {\n\t\tglobals.Debug(fmt.Sprintf(\"[web] crop search result length %d to %d max\", len(content), globals.SearchCropLength))\n\t\treturn utils.Extract(content, globals.SearchCropLength, \"...\"), nil\n\t}\n\treturn content, nil\n}\n\nfunc TestSearch(c *gin.Context) {\n\t// get `query` param from query\n\tquery := c.Query(\"query\")\n\n\tfmt.Println(query)\n\n\tres, err := GenerateSearchResult(query)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t} else {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"result\": res,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "admin/analysis/analysis.go",
    "content": "package analysis\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\ntype UserTypeForm struct {\n\tNormal       int64 `json:\"normal\"`\n\tApiPaid      int64 `json:\"api_paid\"`\n\tBasicPlan    int64 `json:\"basic_plan\"`\n\tStandardPlan int64 `json:\"standard_plan\"`\n\tProPlan      int64 `json:\"pro_plan\"`\n\tTotal        int64 `json:\"total\"`\n}\n\nfunc getDates(t []time.Time) []string {\n\treturn utils.Each[time.Time, string](t, func(date time.Time) string {\n\t\treturn date.Format(\"1/2\")\n\t})\n}\n\nfunc getFormat(t time.Time) string {\n\treturn t.Format(\"2006-01-02\")\n}\n\nfunc getMinuteFormat(t time.Time) string {\n\treturn t.Format(\"2006-01-02 15:04\")\n}\n\nfunc GetSubscriptionUsers(db *sql.DB) int64 {\n\tvar count int64\n\terr := globals.QueryRowDb(db, `\n   \t\tSELECT COUNT(*) FROM subscription WHERE expired_at > NOW()\n   \t`).Scan(&count)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\treturn count\n}\n\nfunc GetBillingToday(cache *redis.Client) float32 {\n\treturn float32(utils.MustInt(cache, getBillingFormat(getDay()))) / 100\n}\n\nfunc GetBillingYesterday(cache *redis.Client) float32 {\n\treturn float32(utils.MustInt(cache, getBillingFormat(getLastDay()))) / 100\n}\n\nfunc GetBillingMonth(cache *redis.Client) float32 {\n\treturn float32(utils.MustInt(cache, getMonthBillingFormat(getMonth()))) / 100\n}\n\nfunc GetBillingLastMonth(cache *redis.Client) float32 {\n\treturn float32(utils.MustInt(cache, getMonthBillingFormat(getLastMonth()))) / 100\n}\n\nfunc GetTpmToday(cache *redis.Client, user string) int64 {\n\t// this minute and last minute\n\treturn utils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now()), user)) +\n\t\tutils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user))\n}\n\nfunc GetRpmToday(cache *redis.Client, user string) int64 {\n\t// this minute and last minute\n\treturn utils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now()), user)) +\n\t\tutils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user))\n}\n\nfunc GetModelData(cache *redis.Client) ModelChartForm {\n\tdates := getDays(7)\n\n\treturn ModelChartForm{\n\t\tDate: getDates(dates),\n\t\tValue: utils.EachNotNil[string, ModelData](globals.SupportModels, func(model string) *ModelData {\n\t\t\tdata := ModelData{\n\t\t\t\tModel: model,\n\t\t\t\tData: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {\n\t\t\t\t\treturn utils.MustInt(cache, getModelFormat(getFormat(date), model))\n\t\t\t\t}),\n\t\t\t}\n\t\t\tif utils.Sum(data.Data) == 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn &data\n\t\t}),\n\t}\n}\n\nfunc GetSortedModelData(cache *redis.Client) ModelChartForm {\n\tform := GetModelData(cache)\n\tdata := utils.Sort(form.Value, func(a ModelData, b ModelData) bool {\n\t\treturn utils.Sum(a.Data) > utils.Sum(b.Data)\n\t})\n\n\tform.Value = data\n\n\treturn form\n}\n\nfunc GetRequestData(cache *redis.Client) RequestChartForm {\n\tdates := getDays(7)\n\n\treturn RequestChartForm{\n\t\tDate: getDates(dates),\n\t\tValue: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {\n\t\t\treturn utils.MustInt(cache, getRequestFormat(getFormat(date)))\n\t\t}),\n\t}\n}\n\nfunc GetBillingData(cache *redis.Client) BillingChartForm {\n\tdates := getDays(30)\n\n\treturn BillingChartForm{\n\t\tDate: getDates(dates),\n\t\tValue: utils.Each[time.Time, float32](dates, func(date time.Time) float32 {\n\t\t\treturn float32(utils.MustInt(cache, getBillingFormat(getFormat(date)))) / 100.\n\t\t}),\n\t}\n}\n\nfunc GetErrorData(cache *redis.Client) ErrorChartForm {\n\tdates := getDays(7)\n\n\treturn ErrorChartForm{\n\t\tDate: getDates(dates),\n\t\tValue: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {\n\t\t\treturn utils.MustInt(cache, getErrorFormat(getFormat(date)))\n\t\t}),\n\t}\n}\n\nfunc GetUserTypeData(db *sql.DB) (UserTypeForm, error) {\n\tvar form UserTypeForm\n\n\t// get total users\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT COUNT(*) FROM auth\n\t`).Scan(&form.Total); err != nil {\n\t\treturn form, err\n\t}\n\n\t// get subscription users count (current subscription)\n\t// level 1: basic plan, level 2: standard plan, level 3: pro plan\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT\n\t\t\t(SELECT COUNT(*) FROM subscription WHERE level = 1 AND expired_at > NOW()),\n\t\t\t(SELECT COUNT(*) FROM subscription WHERE level = 2 AND expired_at > NOW()),\n\t\t\t(SELECT COUNT(*) FROM subscription WHERE level = 3 AND expired_at > NOW())\n\t`).Scan(&form.BasicPlan, &form.StandardPlan, &form.ProPlan); err != nil {\n\t\treturn form, err\n\t}\n\n\t// get normal users count (no subscription in `subscription` table and `quota` + `used` < initial quota in `quota` table)\n\tinitialQuota := channel.SystemInstance.GetInitialQuota()\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT COUNT(*) FROM auth \n\t\tWHERE id NOT IN (SELECT user_id FROM subscription WHERE total_month > 0)\n\t\tAND id IN (SELECT user_id FROM quota WHERE quota + used <= ?)\n\t`, initialQuota).Scan(&form.Normal); err != nil {\n\t\treturn form, err\n\t}\n\n\tform.ApiPaid = form.Total - form.Normal - form.BasicPlan - form.StandardPlan - form.ProPlan\n\n\treturn form, nil\n}\n"
  },
  {
    "path": "admin/analysis/format.go",
    "content": "package analysis\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc getMonth() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01\")\n}\n\nfunc getLastMonth() string {\n\tdate := time.Now().AddDate(0, -1, 0)\n\treturn date.Format(\"2006-01\")\n}\n\nfunc getDay() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01-02\")\n}\n\nfunc getLastDay() string {\n\tdate := time.Now().AddDate(0, 0, -1)\n\treturn date.Format(\"2006-01-02\")\n}\n\nfunc getDays(n int) []time.Time {\n\tcurrent := time.Now()\n\tvar days []time.Time\n\tfor i := n; i > 0; i-- {\n\t\tdays = append(days, current.AddDate(0, 0, -i+1))\n\t}\n\n\treturn days\n}\n\nfunc getErrorFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:err-analysis-%s\", t)\n}\n\nfunc getBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc getMonthBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc getRequestFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:request-analysis-%s\", t)\n}\n\nfunc getModelFormat(t string, model string) string {\n\treturn fmt.Sprintf(\"nio:model-analysis-%s-%s\", model, t)\n}\n\nfunc getTpmFormat(t string, user string) string {\n\treturn fmt.Sprintf(\"nio:tpm-analysis-%s-%s\", user, t)\n}\n\nfunc getRpmFormat(t string, user string) string {\n\treturn fmt.Sprintf(\"nio:rpm-analysis-%s-%s\", user, t)\n}\n"
  },
  {
    "path": "admin/analysis/reflect.go",
    "content": "package analysis\n\nimport \"reflect\"\n\nvar _ = reflect.TypeOf(UserTypeForm{})\nvar _ = reflect.TypeOf(ModelData{})\nvar _ = reflect.TypeOf(ModelChartForm{})\nvar _ = reflect.TypeOf(RequestChartForm{})\nvar _ = reflect.TypeOf(BillingChartForm{})\nvar _ = reflect.TypeOf(ErrorChartForm{})\n"
  },
  {
    "path": "admin/analysis/statistic.go",
    "content": "package analysis\n\nimport (\n\t\"chat/adapter\"\n\t\"chat/connection\"\n\t\"chat/utils\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc IncrErrorRequest(cache *redis.Client) {\n\tutils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2)\n}\n\nfunc IncrBillingRequest(cache *redis.Client, amount int64, isAdmin bool) {\n\tif isAdmin {\n\t\t// do not count billing for admin user\n\t\treturn\n\t}\n\n\tutils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*90)        // 90 days\n\tutils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*90) // 90 days\n}\n\nfunc IncrRequest(cache *redis.Client) {\n\tutils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2)\n}\n\nfunc IncrModelRequest(cache *redis.Client, model string, tokens int64) {\n\tutils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2)\n}\n\nfunc AnalyseRequest(model string, user string, buffer *utils.Buffer, err error) {\n\tinstance := connection.Cache\n\n\tif adapter.IsAvailableError(err) {\n\t\tIncrErrorRequest(instance)\n\t\treturn\n\t}\n\n\ttoken := int64(buffer.CountInputToken() + buffer.CountOutputToken(false))\n\n\tIncrRequest(instance)\n\tIncrModelRequest(instance, model, token)\n\n\tif buffer != nil {\n\t\tIncrRpm(instance, user, 1)\n\t\tIncrTpm(instance, user, token)\n\n\t\t// add rpm/tpm to root\n\t\tIncrRpm(instance, \"root\", 1)\n\t\tIncrTpm(instance, \"root\", token)\n\t}\n}\n\nfunc IncrTpm(cache *redis.Client, user string, n int64) {\n\tutils.IncrWithExpire(cache, getTpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5)\n}\n\nfunc IncrRpm(cache *redis.Client, user string, n int64) {\n\tutils.IncrWithExpire(cache, getRpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5)\n}\n"
  },
  {
    "path": "admin/analysis/types.go",
    "content": "package analysis\n\ntype ModelData struct {\n\tModel string  `json:\"model\"`\n\tData  []int64 `json:\"data\"`\n}\n\ntype ModelChartForm struct {\n\tDate  []string    `json:\"date\"`\n\tValue []ModelData `json:\"value\"`\n}\n\ntype RequestChartForm struct {\n\tDate  []string `json:\"date\"`\n\tValue []int64  `json:\"value\"`\n}\n\ntype BillingChartForm struct {\n\tDate  []string  `json:\"date\"`\n\tValue []float32 `json:\"value\"`\n}\n\ntype ErrorChartForm struct {\n\tDate  []string `json:\"date\"`\n\tValue []int64  `json:\"value\"`\n}\n"
  },
  {
    "path": "admin/controller.go",
    "content": "package admin\n\nimport (\n\t\"chat/admin/analysis\"\n\t\"chat/utils\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype GenerateInvitationForm struct {\n\tType   string  `json:\"type\"`\n\tQuota  float32 `json:\"quota\"`\n\tNumber int     `json:\"number\"`\n}\n\ntype DeleteInvitationForm struct {\n\tCode string `json:\"code\"`\n}\n\ntype GenerateRedeemForm struct {\n\tQuota  float32 `json:\"quota\"`\n\tNumber int     `json:\"number\"`\n}\n\ntype PasswordMigrationForm struct {\n\tId       int64  `json:\"id\"`\n\tPassword string `json:\"password\"`\n}\n\ntype EmailMigrationForm struct {\n\tId    int64  `json:\"id\"`\n\tEmail string `json:\"email\"`\n}\n\ntype SetAdminForm struct {\n\tId    int64 `json:\"id\"`\n\tAdmin bool  `json:\"admin\"`\n}\n\ntype BanForm struct {\n\tId  int64 `json:\"id\"`\n\tBan bool  `json:\"ban\"`\n}\n\ntype QuotaOperationForm struct {\n\tId       int64    `json:\"id\" binding:\"required\"`\n\tQuota    *float32 `json:\"quota\" binding:\"required\"`\n\tOverride bool     `json:\"override\"`\n}\n\ntype SubscriptionOperationForm struct {\n\tId      int64  `json:\"id\" binding:\"required\"`\n\tExpired string `json:\"expired\" binding:\"required\"`\n}\n\ntype SubscriptionLevelForm struct {\n\tId    int64  `json:\"id\" binding:\"required\"`\n\tLevel *int64 `json:\"level\" binding:\"required\"`\n}\n\ntype ReleaseUsageForm struct {\n\tId int64 `json:\"id\" binding:\"required\"`\n}\n\ntype UpdateRootPasswordForm struct {\n\tPassword string `json:\"password\" binding:\"required\"`\n}\n\nfunc UpdateMarketAPI(c *gin.Context) {\n\tvar form MarketModelList\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := MarketInstance.SetModels(form)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc InfoAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tc.JSON(http.StatusOK, InfoForm{\n\t\tOnlineChats:       utils.GetConns(),\n\t\tSubscriptionCount: analysis.GetSubscriptionUsers(db),\n\t\tBillingToday:      analysis.GetBillingToday(cache),\n\t\tBillingMonth:      analysis.GetBillingMonth(cache),\n\t\tBillingYesterday:  analysis.GetBillingYesterday(cache),\n\t\tBillingLastMonth:  analysis.GetBillingLastMonth(cache),\n\t})\n}\n\nfunc ModelAnalysisAPI(c *gin.Context) {\n\tcache := utils.GetCacheFromContext(c)\n\tc.JSON(http.StatusOK, analysis.GetSortedModelData(cache))\n}\n\nfunc RequestAnalysisAPI(c *gin.Context) {\n\tcache := utils.GetCacheFromContext(c)\n\tc.JSON(http.StatusOK, analysis.GetRequestData(cache))\n}\n\nfunc BillingAnalysisAPI(c *gin.Context) {\n\tcache := utils.GetCacheFromContext(c)\n\tc.JSON(http.StatusOK, analysis.GetBillingData(cache))\n}\n\nfunc ErrorAnalysisAPI(c *gin.Context) {\n\tcache := utils.GetCacheFromContext(c)\n\tc.JSON(http.StatusOK, analysis.GetErrorData(cache))\n}\n\nfunc UserTypeAnalysisAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tif form, err := analysis.GetUserTypeData(db); err != nil {\n\t\tc.JSON(http.StatusOK, &analysis.UserTypeForm{})\n\t} else {\n\t\tc.JSON(http.StatusOK, form)\n\t}\n}\n\nfunc RedeemListAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tpage, _ := strconv.Atoi(c.Query(\"page\"))\n\tc.JSON(http.StatusOK, GetRedeemData(db, int64(page)))\n}\n\nfunc DeleteRedeemAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form DeleteInvitationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := DeleteRedeemCode(db, form.Code)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": err == nil,\n\t\t\"error\":  err,\n\t})\n}\n\nfunc InvitationPaginationAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tpage, _ := strconv.Atoi(c.Query(\"page\"))\n\tc.JSON(http.StatusOK, GetInvitationPagination(db, int64(page)))\n}\n\nfunc DeleteInvitationAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form DeleteInvitationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := DeleteInvitationCode(db, form.Code)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": err == nil,\n\t\t\"error\":  err,\n\t})\n}\nfunc GenerateInvitationAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form GenerateInvitationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, GenerateInvitations(db, form.Number, form.Quota, form.Type))\n}\n\nfunc GenerateRedeemAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form GenerateRedeemForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, GenerateRedeemCodes(db, form.Number, form.Quota))\n}\n\nfunc UserPaginationAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tpage, _ := strconv.Atoi(c.Query(\"page\"))\n\tsearch := strings.TrimSpace(c.Query(\"search\"))\n\tc.JSON(http.StatusOK, getUsersForm(db, int64(page), search))\n}\n\nfunc UpdatePasswordAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tvar form PasswordMigrationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := passwordMigration(db, cache, form.Id, form.Password)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc UpdateEmailAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form EmailMigrationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := emailMigration(db, form.Id, form.Email)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc SetAdminAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form SetAdminForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := setAdmin(db, form.Id, form.Admin)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc BanAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form BanForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := banUser(db, form.Id, form.Ban)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc UserQuotaAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form QuotaOperationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := quotaMigration(db, form.Id, *form.Quota, form.Override)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc UserSubscriptionAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form SubscriptionOperationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\t// convert to time\n\tif _, err := time.Parse(\"2006-01-02 15:04:05\", form.Expired); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tif err := subscriptionMigration(db, form.Id, form.Expired); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc SubscriptionLevelAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar form SubscriptionLevelForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := subscriptionLevelMigration(db, form.Id, *form.Level)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc ReleaseUsageAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tvar form ReleaseUsageForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\terr := releaseUsage(db, cache, form.Id)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc UpdateRootPasswordAPI(c *gin.Context) {\n\tvar form UpdateRootPasswordForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\terr := UpdateRootPassword(db, cache, form.Password)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc ListLoggerAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, ListLogs())\n}\n\nfunc DownloadLoggerAPI(c *gin.Context) {\n\tpath := c.Query(\"path\")\n\tgetBlobFile(c, path)\n}\n\nfunc DeleteLoggerAPI(c *gin.Context) {\n\tpath := c.Query(\"path\")\n\tif err := deleteLogFile(path); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc ConsoleLoggerAPI(c *gin.Context) {\n\tn := utils.ParseInt(c.Query(\"n\"))\n\n\tcontent := getLatestLogs(n)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"content\": content,\n\t})\n}\n"
  },
  {
    "path": "admin/format.go",
    "content": "package admin\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc getMonth() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01\")\n}\n\nfunc getDay() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01-02\")\n}\n\nfunc getDays(n int) []time.Time {\n\tcurrent := time.Now()\n\tvar days []time.Time\n\tfor i := n; i > 0; i-- {\n\t\tdays = append(days, current.AddDate(0, 0, -i+1))\n\t}\n\n\treturn days\n}\n\nfunc getErrorFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:err-analysis-%s\", t)\n}\n\nfunc getBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc getMonthBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc getRequestFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:request-analysis-%s\", t)\n}\n\nfunc getModelFormat(t string, model string) string {\n\treturn fmt.Sprintf(\"nio:model-analysis-%s-%s\", model, t)\n}\n"
  },
  {
    "path": "admin/instance.go",
    "content": "package admin\n\nvar MarketInstance *Market\n\nfunc InitInstance() {\n\tMarketInstance = NewMarket()\n}\n"
  },
  {
    "path": "admin/invitation.go",
    "content": "package admin\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n)\n\nfunc GetInvitationPagination(db *sql.DB, page int64) PaginationForm {\n\tvar invitations []interface{}\n\tvar total int64\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT COUNT(*) FROM invitation\n\t`).Scan(&total); err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\t// get used_user from auth table by `user_id`\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT invitation.code, invitation.quota, invitation.type, invitation.used, \n\t\t       invitation.created_at, invitation.updated_at, \n\t\t       COALESCE(auth.username, '-') as username\n\t\tFROM invitation\n\t\tLEFT JOIN auth ON auth.id = invitation.used_id\n\t\tORDER BY invitation.id DESC LIMIT ? OFFSET ?\n\t`, pagination, page*pagination)\n\tif err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\tfor rows.Next() {\n\t\tvar invitation InvitationData\n\t\tvar createdAt []uint8\n\t\tvar updatedAt []uint8\n\t\tif err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &createdAt, &updatedAt, &invitation.Username); err != nil {\n\t\t\treturn PaginationForm{\n\t\t\t\tStatus:  false,\n\t\t\t\tMessage: err.Error(),\n\t\t\t}\n\t\t}\n\t\tinvitation.CreatedAt = utils.ConvertTime(createdAt).Format(\"2006-01-02 15:04:05\")\n\t\tinvitation.UpdatedAt = utils.ConvertTime(updatedAt).Format(\"2006-01-02 15:04:05\")\n\t\tinvitations = append(invitations, invitation)\n\t}\n\n\treturn PaginationForm{\n\t\tStatus: true,\n\t\tTotal:  int(math.Ceil(float64(total) / float64(pagination))),\n\t\tData:   invitations,\n\t}\n}\n\nfunc DeleteInvitationCode(db *sql.DB, code string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tDELETE FROM invitation WHERE code = ?\n\t`, code)\n\treturn err\n}\n\nfunc NewInvitationCode(db *sql.DB, code string, quota float32, t string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO invitation (code, quota, type)\n\t\tVALUES (?, ?, ?)\n\t`, code, quota, t)\n\treturn err\n}\n\nfunc GenerateInvitations(db *sql.DB, num int, quota float32, t string) InvitationGenerateResponse {\n\tarr := make([]string, 0)\n\tidx := 0\n\tretry := 0\n\tfor idx < num {\n\t\tcode := fmt.Sprintf(\"%s-%s\", t, utils.GenerateChar(24))\n\t\tif err := NewInvitationCode(db, code, quota, t); err != nil {\n\t\t\t// ignore duplicate code\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif retry < 100 && strings.Contains(err.Error(), \"Duplicate entry\") {\n\t\t\t\tretry++\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tretry = 0\n\t\t\treturn InvitationGenerateResponse{\n\t\t\t\tStatus:  false,\n\t\t\t\tMessage: err.Error(),\n\t\t\t}\n\t\t}\n\t\tarr = append(arr, code)\n\t\tidx++\n\t}\n\n\treturn InvitationGenerateResponse{\n\t\tStatus: true,\n\t\tData:   arr,\n\t}\n}\n"
  },
  {
    "path": "admin/logger.go",
    "content": "package admin\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"strings\"\n)\n\ntype LogFile struct {\n\tPath string `json:\"path\"`\n\tSize int64  `json:\"size\"`\n}\n\nfunc ListLogs() []LogFile {\n\treturn utils.Each(utils.Walk(\"logs\"), func(path string) LogFile {\n\t\treturn LogFile{\n\t\t\tPath: strings.TrimLeft(path, \"logs/\"),\n\t\t\tSize: utils.GetFileSize(path),\n\t\t}\n\t})\n}\n\nfunc getLogPath(path string) string {\n\treturn fmt.Sprintf(\"logs/%s\", path)\n}\n\nfunc getBlobFile(c *gin.Context, path string) {\n\tc.File(getLogPath(path))\n}\n\nfunc deleteLogFile(path string) error {\n\treturn utils.DeleteFile(getLogPath(path))\n}\n\nfunc getLatestLogs(n int) string {\n\tif n <= 0 {\n\t\tn = 100\n\t}\n\n\tcontent, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n)\n\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"read error: %s\", err.Error())\n\t}\n\n\treturn content\n}\n"
  },
  {
    "path": "admin/market.go",
    "content": "package admin\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\n\t\"github.com/spf13/viper\"\n)\n\ntype ModelTag []string\ntype MarketModel struct {\n\tId          string   `json:\"id\" mapstructure:\"id\" required:\"true\"`\n\tName        string   `json:\"name\" mapstructure:\"name\" required:\"true\"`\n\tDescription string   `json:\"description\" mapstructure:\"description\"`\n\tDefault     bool     `json:\"default\" mapstructure:\"default\"`\n\tHighContext bool     `json:\"high_context\" mapstructure:\"highcontext\"`\n\tAvatar      string   `json:\"avatar\" mapstructure:\"avatar\"`\n\tTag         ModelTag `json:\"tag\" mapstructure:\"tag\"`\n}\ntype MarketModelList []MarketModel\n\ntype Market struct {\n\tModels MarketModelList `json:\"models\" mapstructure:\"models\"`\n}\n\nfunc NewMarket() *Market {\n\tvar models MarketModelList\n\tif err := viper.UnmarshalKey(\"market\", &models); err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[market] read config error: %s, use default config\", err.Error()))\n\t\tmodels = MarketModelList{}\n\t}\n\n\treturn &Market{\n\t\tModels: models,\n\t}\n}\n\nfunc (m *Market) GetModels() MarketModelList {\n\treturn m.Models\n}\n\nfunc (m *Market) GetModel(id string) *MarketModel {\n\tfor _, model := range m.Models {\n\t\tif model.Id == id {\n\t\t\treturn &model\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *Market) SaveConfig() error {\n\treturn utils.SaveConfig(\"market\", m.Models)\n}\n\nfunc (m *Market) SetModels(models MarketModelList) error {\n\tm.Models = models\n\treturn m.SaveConfig()\n}\n"
  },
  {
    "path": "admin/redeem.go",
    "content": "package admin\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n)\n\nfunc GetRedeemData(db *sql.DB, page int64) PaginationForm {\n\tvar data []interface{}\n\tvar total int64\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT COUNT(*) FROM redeem\n\t`).Scan(&total); err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT code, quota, used, created_at, updated_at\n\t\tFROM redeem\n\t\tORDER BY id DESC LIMIT ? OFFSET ?\n\t`, pagination, page*pagination)\n\n\tif err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\tfor rows.Next() {\n\t\tvar redeem RedeemData\n\t\tvar createdAt []uint8\n\t\tvar updatedAt []uint8\n\t\tif err := rows.Scan(&redeem.Code, &redeem.Quota, &redeem.Used, &createdAt, &updatedAt); err != nil {\n\t\t\treturn PaginationForm{\n\t\t\t\tStatus:  false,\n\t\t\t\tMessage: err.Error(),\n\t\t\t}\n\t\t}\n\n\t\tredeem.CreatedAt = utils.ConvertTime(createdAt).Format(\"2006-01-02 15:04:05\")\n\t\tredeem.UpdatedAt = utils.ConvertTime(updatedAt).Format(\"2006-01-02 15:04:05\")\n\t\tdata = append(data, redeem)\n\t}\n\n\treturn PaginationForm{\n\t\tStatus: true,\n\t\tTotal:  int(math.Ceil(float64(total) / float64(pagination))),\n\t\tData:   data,\n\t}\n}\n\nfunc DeleteRedeemCode(db *sql.DB, code string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tDELETE FROM redeem WHERE code = ?\n\t`, code)\n\n\treturn err\n}\n\nfunc GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {\n\tarr := make([]string, 0)\n\tidx := 0\n\tfor idx < num {\n\t\tcode, err := CreateRedeemCode(db, quota)\n\n\t\tif err != nil {\n\t\t\treturn RedeemGenerateResponse{\n\t\t\t\tStatus:  false,\n\t\t\t\tMessage: err.Error(),\n\t\t\t}\n\t\t}\n\t\tarr = append(arr, code)\n\t\tidx++\n\t}\n\n\treturn RedeemGenerateResponse{\n\t\tStatus: true,\n\t\tData:   arr,\n\t}\n}\n\nfunc CreateRedeemCode(db *sql.DB, quota float32) (string, error) {\n\tcode := fmt.Sprintf(\"nio-%s\", utils.GenerateChar(32))\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO redeem (code, quota) VALUES (?, ?)\n\t`, code, quota)\n\n\tif err != nil && strings.Contains(err.Error(), \"Duplicate entry\") {\n\t\t// code name is duplicate\n\t\treturn CreateRedeemCode(db, quota)\n\t}\n\n\treturn code, err\n}\n"
  },
  {
    "path": "admin/router.go",
    "content": "package admin\n\nimport (\n\t\"chat/addition/web\"\n\t\"chat/channel\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Register(app *gin.RouterGroup) {\n\tchannel.Register(app)\n\n\tapp.GET(\"/admin/config/test/search\", web.TestSearch)\n\n\tapp.GET(\"/admin/analytics/info\", InfoAPI)\n\tapp.GET(\"/admin/analytics/model\", ModelAnalysisAPI)\n\tapp.GET(\"/admin/analytics/request\", RequestAnalysisAPI)\n\tapp.GET(\"/admin/analytics/billing\", BillingAnalysisAPI)\n\tapp.GET(\"/admin/analytics/error\", ErrorAnalysisAPI)\n\tapp.GET(\"/admin/analytics/user\", UserTypeAnalysisAPI)\n\n\tapp.GET(\"/admin/invitation/list\", InvitationPaginationAPI)\n\tapp.POST(\"/admin/invitation/generate\", GenerateInvitationAPI)\n\tapp.POST(\"/admin/invitation/delete\", DeleteInvitationAPI)\n\n\tapp.GET(\"/admin/redeem/list\", RedeemListAPI)\n\tapp.POST(\"/admin/redeem/generate\", GenerateRedeemAPI)\n\tapp.POST(\"/admin/redeem/delete\", DeleteRedeemAPI)\n\n\tapp.GET(\"/admin/user/list\", UserPaginationAPI)\n\tapp.POST(\"/admin/user/quota\", UserQuotaAPI)\n\tapp.POST(\"/admin/user/subscription\", UserSubscriptionAPI)\n\tapp.POST(\"/admin/user/level\", SubscriptionLevelAPI)\n\tapp.POST(\"/admin/user/release\", ReleaseUsageAPI)\n\tapp.POST(\"/admin/user/password\", UpdatePasswordAPI)\n\tapp.POST(\"/admin/user/email\", UpdateEmailAPI)\n\tapp.POST(\"/admin/user/ban\", BanAPI)\n\tapp.POST(\"/admin/user/admin\", SetAdminAPI)\n\tapp.POST(\"/admin/user/root\", UpdateRootPasswordAPI)\n\n\tapp.POST(\"/admin/market/update\", UpdateMarketAPI)\n\n\tapp.GET(\"/admin/logger/list\", ListLoggerAPI)\n\tapp.GET(\"/admin/logger/download\", DownloadLoggerAPI)\n\tapp.GET(\"/admin/logger/console\", ConsoleLoggerAPI)\n\tapp.POST(\"/admin/logger/delete\", DeleteLoggerAPI)\n}\n"
  },
  {
    "path": "admin/statistic.go",
    "content": "package admin\n\nimport (\n\t\"chat/adapter\"\n\t\"chat/connection\"\n\t\"chat/utils\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc IncrErrorRequest(cache *redis.Client) {\n\tutils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2)\n}\n\nfunc IncrBillingRequest(cache *redis.Client, amount int64) {\n\tutils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2)\n\tutils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2)\n}\n\nfunc IncrRequest(cache *redis.Client) {\n\tutils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2)\n}\n\nfunc IncrModelRequest(cache *redis.Client, model string, tokens int64) {\n\tutils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2)\n}\n\nfunc AnalyseRequest(model string, buffer *utils.Buffer, err error) {\n\tinstance := connection.Cache\n\n\tif adapter.IsAvailableError(err) {\n\t\tIncrErrorRequest(instance)\n\t\treturn\n\t}\n\n\tIncrRequest(instance)\n\tIncrModelRequest(instance, model, int64(buffer.CountToken()))\n}\n"
  },
  {
    "path": "admin/types.go",
    "content": "package admin\n\nvar pagination int64 = 10\n\ntype InfoForm struct {\n\tBillingToday      float32 `json:\"billing_today\"`\n\tBillingMonth      float32 `json:\"billing_month\"`\n\tBillingYesterday  float32 `json:\"billing_yesterday\"`\n\tBillingLastMonth  float32 `json:\"billing_last_month\"`\n\tSubscriptionCount int64   `json:\"subscription_count\"`\n\tOnlineChats       int64   `json:\"online_chats\"`\n}\n\ntype ModelData struct {\n\tModel string  `json:\"model\"`\n\tData  []int64 `json:\"data\"`\n}\n\ntype ModelChartForm struct {\n\tDate  []string    `json:\"date\"`\n\tValue []ModelData `json:\"value\"`\n}\n\ntype RequestChartForm struct {\n\tDate  []string `json:\"date\"`\n\tValue []int64  `json:\"value\"`\n}\n\ntype BillingChartForm struct {\n\tDate  []string  `json:\"date\"`\n\tValue []float32 `json:\"value\"`\n}\n\ntype ErrorChartForm struct {\n\tDate  []string `json:\"date\"`\n\tValue []int64  `json:\"value\"`\n}\n\ntype PaginationForm struct {\n\tStatus  bool          `json:\"status\"`\n\tTotal   int           `json:\"total\"`\n\tData    []interface{} `json:\"data\"`\n\tMessage string        `json:\"message\"`\n}\n\ntype InvitationData struct {\n\tCode      string  `json:\"code\"`\n\tQuota     float32 `json:\"quota\"`\n\tType      string  `json:\"type\"`\n\tUsed      bool    `json:\"used\"`\n\tCreatedAt string  `json:\"created_at\"`\n\tUpdatedAt string  `json:\"updated_at\"`\n\tUsername  string  `json:\"username\"`\n}\n\ntype RedeemData struct {\n\tCode      string  `json:\"code\"`\n\tQuota     float32 `json:\"quota\"`\n\tUsed      bool    `json:\"used\"`\n\tCreatedAt string  `json:\"created_at\"`\n\tUpdatedAt string  `json:\"updated_at\"`\n}\n\ntype InvitationGenerateResponse struct {\n\tStatus  bool     `json:\"status\"`\n\tMessage string   `json:\"message\"`\n\tData    []string `json:\"data\"`\n}\n\ntype RedeemGenerateResponse struct {\n\tStatus  bool     `json:\"status\"`\n\tMessage string   `json:\"message\"`\n\tData    []string `json:\"data\"`\n}\n\ntype UserData struct {\n\tId           int64   `json:\"id\"`\n\tUsername     string  `json:\"username\"`\n\tEmail        string  `json:\"email\"`\n\tIsAdmin      bool    `json:\"is_admin\"`\n\tQuota        float32 `json:\"quota\"`\n\tUsedQuota    float32 `json:\"used_quota\"`\n\tExpiredAt    string  `json:\"expired_at\"`\n\tIsSubscribed bool    `json:\"is_subscribed\"`\n\tTotalMonth   int64   `json:\"total_month\"`\n\tEnterprise   bool    `json:\"enterprise\"`\n\tLevel        int     `json:\"level\"`\n\tIsBanned     bool    `json:\"is_banned\"`\n}\n"
  },
  {
    "path": "admin/user.go",
    "content": "package admin\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\n// AuthLike is to solve the problem of import cycle\ntype AuthLike struct {\n\tID int64 `json:\"id\"`\n}\n\nfunc (a *AuthLike) GetID(_ *sql.DB) int64 {\n\treturn a.ID\n}\n\nfunc (a *AuthLike) HitID() int64 {\n\treturn a.ID\n}\n\nfunc getUsersForm(db *sql.DB, page int64, search string) PaginationForm {\n\t// if search is empty, then search all users\n\n\tvar users []interface{}\n\tvar total int64\n\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT COUNT(*) FROM auth\n\t\tWHERE username LIKE ?\n\t`, \"%\"+search+\"%\").Scan(&total); err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT \n\t\t    auth.id, auth.username, auth.email, auth.is_admin,\n\t\t    quota.quota, quota.used,\n\t\t    subscription.expired_at, subscription.total_month, subscription.enterprise, subscription.level,\n\t\t    auth.is_banned\n\t\tFROM auth\n\t\tLEFT JOIN quota ON quota.user_id = auth.id\n\t\tLEFT JOIN subscription ON subscription.user_id = auth.id\n\t\tWHERE auth.username LIKE ?\n\t\tORDER BY auth.id LIMIT ? OFFSET ?\n\t`, \"%\"+search+\"%\", pagination, page*pagination)\n\tif err != nil {\n\t\treturn PaginationForm{\n\t\t\tStatus:  false,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\tfor rows.Next() {\n\t\tvar user UserData\n\t\tvar (\n\t\t\temail             sql.NullString\n\t\t\texpired           []uint8\n\t\t\tquota             sql.NullFloat64\n\t\t\tusedQuota         sql.NullFloat64\n\t\t\ttotalMonth        sql.NullInt64\n\t\t\tisEnterprise      sql.NullBool\n\t\t\tsubscriptionLevel sql.NullInt64\n\t\t\tisBanned          sql.NullBool\n\t\t)\n\t\tif err := rows.Scan(&user.Id, &user.Username, &email, &user.IsAdmin, &quota, &usedQuota, &expired, &totalMonth, &isEnterprise, &subscriptionLevel, &isBanned); err != nil {\n\t\t\treturn PaginationForm{\n\t\t\t\tStatus:  false,\n\t\t\t\tMessage: err.Error(),\n\t\t\t}\n\t\t}\n\t\tif email.Valid {\n\t\t\tuser.Email = email.String\n\t\t}\n\t\tif quota.Valid {\n\t\t\tuser.Quota = float32(quota.Float64)\n\t\t}\n\t\tif usedQuota.Valid {\n\t\t\tuser.UsedQuota = float32(usedQuota.Float64)\n\t\t}\n\t\tif totalMonth.Valid {\n\t\t\tuser.TotalMonth = totalMonth.Int64\n\t\t}\n\t\tif subscriptionLevel.Valid {\n\t\t\tuser.Level = int(subscriptionLevel.Int64)\n\t\t}\n\t\tstamp := utils.ConvertTime(expired)\n\t\tif stamp != nil {\n\t\t\tuser.IsSubscribed = stamp.After(time.Now())\n\t\t\tuser.ExpiredAt = stamp.Format(\"2006-01-02 15:04:05\")\n\t\t}\n\t\tuser.Enterprise = isEnterprise.Valid && isEnterprise.Bool\n\t\tuser.IsBanned = isBanned.Valid && isBanned.Bool\n\n\t\tusers = append(users, user)\n\t}\n\n\treturn PaginationForm{\n\t\tStatus: true,\n\t\tTotal:  int(math.Ceil(float64(total) / float64(pagination))),\n\t\tData:   users,\n\t}\n}\n\n// clearUserCache clears all cache keys starting with nio:user:\nfunc clearUserCache(cache *redis.Client) error {\n\tctx := context.Background()\n\titer := cache.Scan(ctx, 0, \"nio:user:*\", 100).Iterator()\n\tfor iter.Next(ctx) {\n\t\tif err := cache.Del(ctx, iter.Val()).Err(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete cache key %s: %v\", iter.Val(), err)\n\t\t}\n\t}\n\treturn iter.Err()\n}\n\nfunc passwordMigration(db *sql.DB, cache *redis.Client, id int64, password string) error {\n\tpassword = strings.TrimSpace(password)\n\tif len(password) < 6 || len(password) > 36 {\n\t\treturn fmt.Errorf(\"password length must be between 6 and 36\")\n\t}\n\thash_passwd := utils.Sha2Encrypt(password)\n\n\t// Update password in database\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE auth SET password = ? WHERE id = ?\n\t`, hash_passwd, id)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Clear all user related cache\n\tif err := clearUserCache(cache); err != nil {\n\t\treturn fmt.Errorf(\"failed to clear user cache: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc emailMigration(db *sql.DB, id int64, email string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE auth SET email = ? WHERE id = ?\n\t`, email, id)\n\n\treturn err\n}\n\nfunc setAdmin(db *sql.DB, id int64, isAdmin bool) error {\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE auth SET is_admin = ? WHERE id = ?\n\t`, isAdmin, id)\n\n\treturn err\n}\n\nfunc banUser(db *sql.DB, id int64, isBanned bool) error {\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE auth SET is_banned = ? WHERE id = ?\n\t`, isBanned, id)\n\n\treturn err\n}\n\nfunc quotaMigration(db *sql.DB, id int64, quota float32, override bool) error {\n\t// if quota is negative, then decrease quota\n\t// if quota is positive, then increase quota\n\n\tif override {\n\t\t_, err := globals.ExecDb(db, `\n\t\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)\n\t\t\tON DUPLICATE KEY UPDATE quota = ?\n\t\t`, id, quota, 0., quota)\n\n\t\treturn err\n\t}\n\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) \n\t\tON DUPLICATE KEY UPDATE quota = quota + ?\n\t`, id, quota, 0., quota)\n\n\treturn err\n}\n\nfunc subscriptionMigration(db *sql.DB, id int64, expired string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO subscription (user_id, expired_at) VALUES (?, ?)\n\t\tON DUPLICATE KEY UPDATE expired_at = ?\n\t`, id, expired, expired)\n\treturn err\n}\n\nfunc subscriptionLevelMigration(db *sql.DB, id int64, level int64) error {\n\tif level < 0 || level > 3 {\n\t\treturn fmt.Errorf(\"invalid subscription level\")\n\t}\n\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO subscription (user_id, level) VALUES (?, ?)\n\t\tON DUPLICATE KEY UPDATE level = ?\n\t`, id, level, level)\n\n\treturn err\n}\n\nfunc releaseUsage(db *sql.DB, cache *redis.Client, id int64) error {\n\tvar level sql.NullInt64\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT level FROM subscription WHERE user_id = ?\n\t`, id).Scan(&level); err != nil {\n\t\treturn err\n\t}\n\n\tif !level.Valid || level.Int64 == 0 {\n\t\treturn fmt.Errorf(\"user is not subscribed\")\n\t}\n\n\tu := &AuthLike{ID: id}\n\n\tplan := channel.PlanInstance.GetPlan(int(level.Int64))\n\tif !plan.ReleaseAll(u, cache) {\n\t\treturn fmt.Errorf(\"cannot release usage\")\n\t}\n\n\treturn nil\n}\n\nfunc UpdateRootPassword(db *sql.DB, cache *redis.Client, password string) error {\n\tpassword = strings.TrimSpace(password)\n\tif len(password) < 6 || len(password) > 36 {\n\t\treturn fmt.Errorf(\"password length must be between 6 and 36\")\n\t}\n\n\tif _, err := globals.ExecDb(db, `\n\t\tUPDATE auth SET password = ? WHERE username = 'root'\n\t`, utils.Sha2Encrypt(password)); err != nil {\n\t\treturn err\n\t}\n\n\t// Clear all user related cache\n\tif err := clearUserCache(cache); err != nil {\n\t\treturn fmt.Errorf(\"failed to clear user cache: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    'eslint:recommended',\n    'plugin:@typescript-eslint/recommended',\n    'plugin:react-hooks/recommended',\n  ],\n  ignorePatterns: ['dist', '.eslintrc.cjs'],\n  parser: '@typescript-eslint/parser',\n  plugins: ['react-refresh'],\n  rules: {\n    'react-refresh/only-export-components': [\n      'warn',\n      { allowConstantExport: true },\n    ],\n  },\n}\n"
  },
  {
    "path": "app/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\ndev-dist\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# Libre\ndb\n"
  },
  {
    "path": "app/.prettierrc.json",
    "content": "{}\n"
  },
  {
    "path": "app/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"src/components\",\n    \"utils\": \"@/components/ui/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "app/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n    <title>CoAI.Dev</title>\n    <meta name=\"keywords\" content=\"CoAI, coai, ChatNio, chatnio\">\n    <meta name=\"description\" content=\"CoAI.Dev - A powerful AI platform supporting 70+ models, knowledge base management and API distribution. Featuring popular models like ChatGPT, Claude and more for seamless AI interactions.\">\n    <meta name=\"theme-color\" content=\"#000000\">\n    <meta itemprop=\"image\" content=\"/favicon.ico\">\n    <link rel=\"manifest\" href=\"/site.webmanifest\">\n    <script src=\"/workbox.js\" defer></script>\n    <style>\n        html, body, #root {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n        }\n\n        #index-loading {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            background-color: #fff;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            gap: 20px;\n        }\n\n        @media (prefers-color-scheme: dark) {\n            html[data-theme=\"system\"] #index-loading {\n                background-color: #000;\n            }\n        }\n\n        html[data-theme=\"dark\"] #index-loading {\n            background-color: #000;\n        }\n\n        .loading-icon {\n            width: 48px;\n            height: 48px;\n            background-image: url('/favicon.ico');\n            background-size: contain;\n            background-repeat: no-repeat;\n        }\n\n        @media (min-width: 768px) {\n            .loading-icon {\n                width: 80px;\n                height: 80px;\n            }\n        }\n\n        .loading-bar {\n            width: 200px;\n            height: 4px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 2px;\n            overflow: hidden;\n            position: relative;\n        }\n\n        .loading-bar::after {\n            content: '';\n            position: absolute;\n            left: 0;\n            top: 0;\n            height: 100%;\n            width: 40%;\n            background: #eee;\n            animation: loading 1.5s infinite ease-in-out;\n            border-radius: 2px;\n        }\n\n        @keyframes loading {\n            0% {\n                left: -40%;\n            }\n            100% {\n                left: 100%;\n            }\n        }\n    </style>\n    <script>\n        // Set theme based on localStorage and system preference\n        const theme = localStorage.getItem('theme') || 'system';\n        document.documentElement.setAttribute('data-theme', theme);\n    </script>\n  </head>\n  <body>\n    <div id=\"root\">\n      <div id=\"index-loading\">\n        <div class=\"loading-icon\"></div>\n        <div class=\"loading-bar\"></div>\n      </div>\n    </div>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "app/package.json",
    "content": "{\n  \"name\": \"coai\",\n  \"private\": false,\n  \"version\": \"2.6.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build --mode production\",\n    \"fast-build\": \"cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\",\n    \"prettier\": \"prettier --write \\\"src/**/*.tsx\\\" \\\"src/**/*.ts\\\"\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@fontsource-variable/inter\": \"^5.1.0\",\n    \"@fontsource-variable/jetbrains-mono\": \"^5.1.1\",\n    \"@headlessui/react\": \"^1.7.18\",\n    \"@headlessui/tailwindcss\": \"^0.2.0\",\n    \"@lobehub/icons\": \"1.49.0\",\n    \"@radix-ui/react-accordion\": \"^1.1.2\",\n    \"@radix-ui/react-alert-dialog\": \"^1.0.4\",\n    \"@radix-ui/react-checkbox\": \"^1.0.4\",\n    \"@radix-ui/react-context-menu\": \"^2.1.4\",\n    \"@radix-ui/react-dialog\": \"^1.0.5\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.5\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-popover\": \"^1.0.7\",\n    \"@radix-ui/react-progress\": \"^1.0.3\",\n    \"@radix-ui/react-radio-group\": \"^1.1.3\",\n    \"@radix-ui/react-scroll-area\": \"^1.0.5\",\n    \"@radix-ui/react-select\": \"^2.0.0\",\n    \"@radix-ui/react-separator\": \"^1.0.3\",\n    \"@radix-ui/react-slider\": \"^1.1.2\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-switch\": \"^1.0.3\",\n    \"@radix-ui/react-tabs\": \"^1.0.4\",\n    \"@radix-ui/react-toast\": \"^1.1.4\",\n    \"@radix-ui/react-toggle\": \"^1.0.3\",\n    \"@radix-ui/react-toggle-group\": \"^1.0.4\",\n    \"@radix-ui/react-tooltip\": \"^1.0.6\",\n    \"@reduxjs/toolkit\": \"^1.9.5\",\n    \"@tanem/react-nprogress\": \"^5.0.51\",\n    \"@tremor/react\": \"^3.14.0\",\n    \"@types/crypto-js\": \"^4.2.2\",\n    \"axios\": \"^1.5.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.0.0\",\n    \"cmdk\": \"^0.2.0\",\n    \"crypto-js\": \"^4.2.0\",\n    \"date-fns\": \"^3.6.0\",\n    \"emoji-picker-react\": \"^4.7.18\",\n    \"framer-motion\": \"^11.3.28\",\n    \"html-to-image\": \"^1.11.11\",\n    \"i18next\": \"^23.4.6\",\n    \"localforage\": \"^1.10.0\",\n    \"lucide-react\": \"^0.483.0\",\n    \"match-sorter\": \"^6.3.1\",\n    \"mermaid\": \"^10.9.0\",\n    \"next-themes\": \"^0.2.1\",\n    \"normalize.css\": \"^8.0.1\",\n    \"qrcode.react\": \"^4.2.0\",\n    \"react\": \"^18.2.0\",\n    \"react-beautiful-dnd\": \"^13.1.1\",\n    \"react-day-picker\": \"^8.10.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-ga4\": \"^2.1.0\",\n    \"react-i18next\": \"^13.2.2\",\n    \"react-intersection-observer\": \"^9.13.1\",\n    \"react-markdown\": \"^8.0.7\",\n    \"react-redux\": \"^8.1.2\",\n    \"react-router-dom\": \"^6.17.0\",\n    \"react-syntax-highlighter\": \"^15.5.0\",\n    \"react-virtuoso\": \"^4.7.10\",\n    \"react-window\": \"^1.8.10\",\n    \"rehype-katex\": \"^6.0.3\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"remark-breaks\": \"^4.0.0\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-math\": \"^5.1.1\",\n    \"sonner\": \"^1.5.0\",\n    \"sort-by\": \"^1.2.0\",\n    \"tailwind-merge\": \"^1.14.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"use-debounce\": \"^10.0.0\",\n    \"vaul\": \"^0.9.0\",\n    \"workbox-window\": \"^7.0.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/forms\": \"^0.5.7\",\n    \"@tauri-apps/cli\": \"^1.5.6\",\n    \"@types/node\": \"^20.5.9\",\n    \"@types/react\": \"^18.2.15\",\n    \"@types/react-beautiful-dnd\": \"^13.1.8\",\n    \"@types/react-dom\": \"^18.2.7\",\n    \"@types/react-syntax-highlighter\": \"^15.5.7\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.0.0\",\n    \"@typescript-eslint/parser\": \"^6.0.0\",\n    \"@vitejs/plugin-react-swc\": \"^3.3.2\",\n    \"autoprefixer\": \"^10.4.15\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^8.45.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.3\",\n    \"less\": \"^4.2.0\",\n    \"less-loader\": \"^11.1.3\",\n    \"postcss\": \"^8.4.29\",\n    \"prettier\": \"^3.0.3\",\n    \"tailwindcss\": \"^3.3.3\",\n    \"typescript\": \"^5.0.2\",\n    \"vite\": \"^4.4.5\",\n    \"vite-plugin-html\": \"^3.2.0\"\n  }\n}\n"
  },
  {
    "path": "app/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "app/public/manifest.json",
    "content": "{\n  \"_1c.7075dba8.js\": {\n    \"file\": \"assets/1c.7075dba8.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_OperationAction.5b1f951a.js\": {\n    \"file\": \"assets/OperationAction.5b1f951a.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_Paragraph.4ca577cf.js\": {\n    \"file\": \"assets/Paragraph.4ca577cf.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_Tableau10.1b767f5e.js\": {\n    \"file\": \"assets/Tableau10.1b767f5e.js\"\n  },\n  \"_Tracker.20c1771e.js\": {\n    \"file\": \"assets/Tracker.20c1771e.js\",\n    \"imports\": [\n      \"index.html\",\n      \"__isIndex.00b9330c.js\",\n      \"_tiny-invariant.dd7d57d2.js\",\n      \"_path.53f90ab3.js\",\n      \"_linear.1519afab.js\",\n      \"_time.f5c88166.js\",\n      \"_init.a5b10ee5.js\",\n      \"_band.6ffec690.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_line.c9b6b09c.js\"\n    ]\n  },\n  \"__isIndex.00b9330c.js\": {\n    \"file\": \"assets/_isIndex.00b9330c.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_abnf.9bf3a7c1.js\": {\n    \"file\": \"assets/abnf.9bf3a7c1.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_accesslog.68f2b66c.js\": {\n    \"file\": \"assets/accesslog.68f2b66c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_actionscript.67a1e568.js\": {\n    \"file\": \"assets/actionscript.67a1e568.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_activity.612642ea.js\": {\n    \"file\": \"assets/activity.612642ea.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_ada.c0e23f60.js\": {\n    \"file\": \"assets/ada.c0e23f60.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_angelscript.410ffc68.js\": {\n    \"file\": \"assets/angelscript.410ffc68.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_apache.006764de.js\": {\n    \"file\": \"assets/apache.006764de.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_applescript.39299b7d.js\": {\n    \"file\": \"assets/applescript.39299b7d.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_arc.a247e62c.js\": {\n    \"file\": \"assets/arc.a247e62c.js\",\n    \"imports\": [\n      \"_path.53f90ab3.js\",\n      \"index.html\"\n    ]\n  },\n  \"_arcade.f590a3cc.js\": {\n    \"file\": \"assets/arcade.f590a3cc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_arduino.c4c70db8.js\": {\n    \"file\": \"assets/arduino.c4c70db8.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_armasm.50cdbe9a.js\": {\n    \"file\": \"assets/armasm.50cdbe9a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_array.9f3ba611.js\": {\n    \"file\": \"assets/array.9f3ba611.js\"\n  },\n  \"_arrow-right-left.f4c82cc0.js\": {\n    \"file\": \"assets/arrow-right-left.f4c82cc0.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_asciidoc.e1e2cbee.js\": {\n    \"file\": \"assets/asciidoc.e1e2cbee.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_aspectj.a696b879.js\": {\n    \"file\": \"assets/aspectj.a696b879.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_asterisk-square.2615ee58.js\": {\n    \"file\": \"assets/asterisk-square.2615ee58.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_autohotkey.c54238fc.js\": {\n    \"file\": \"assets/autohotkey.c54238fc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_autoit.25b13806.js\": {\n    \"file\": \"assets/autoit.25b13806.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_avrasm.a69814df.js\": {\n    \"file\": \"assets/avrasm.a69814df.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_awk.9f65b5a4.js\": {\n    \"file\": \"assets/awk.9f65b5a4.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_axapta.fa0aef8d.js\": {\n    \"file\": \"assets/axapta.fa0aef8d.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_band.6ffec690.js\": {\n    \"file\": \"assets/band.6ffec690.js\",\n    \"imports\": [\n      \"_init.a5b10ee5.js\",\n      \"_ordinal.93cdc51b.js\"\n    ]\n  },\n  \"_bash.a8e19ccc.js\": {\n    \"file\": \"assets/bash.a8e19ccc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_basic.6acc6ac9.js\": {\n    \"file\": \"assets/basic.6acc6ac9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_bnf.bd11f475.js\": {\n    \"file\": \"assets/bnf.bd11f475.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_book-dashed.798b0c5c.js\": {\n    \"file\": \"assets/book-dashed.798b0c5c.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_box.6dd688fe.js\": {\n    \"file\": \"assets/box.6dd688fe.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_brainfuck.db01d938.js\": {\n    \"file\": \"assets/brainfuck.db01d938.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_c-like.af826c24.js\": {\n    \"file\": \"assets/c-like.af826c24.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_c.cc4c2414.js\": {\n    \"file\": \"assets/c.cc4c2414.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_cal.b721a23c.js\": {\n    \"file\": \"assets/cal.b721a23c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_calendar-clock.1432b482.js\": {\n    \"file\": \"assets/calendar-clock.1432b482.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_calendar.2f96549a.js\": {\n    \"file\": \"assets/calendar.2f96549a.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_capnproto.7f37471d.js\": {\n    \"file\": \"assets/capnproto.7f37471d.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ceylon.f41d8cb9.js\": {\n    \"file\": \"assets/ceylon.f41d8cb9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_channel.0e442477.js\": {\n    \"file\": \"assets/channel.0e442477.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_chart.b43e29db.js\": {\n    \"file\": \"assets/chart.b43e29db.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_clean.2230529c.js\": {\n    \"file\": \"assets/clean.2230529c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_clock.36b36b21.js\": {\n    \"file\": \"assets/clock.36b36b21.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_clojure-repl.24369284.js\": {\n    \"file\": \"assets/clojure-repl.24369284.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_clojure.98540e11.js\": {\n    \"file\": \"assets/clojure.98540e11.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_clone.2acf797d.js\": {\n    \"file\": \"assets/clone.2acf797d.js\",\n    \"imports\": [\n      \"_graph.0ef9c3b7.js\"\n    ]\n  },\n  \"_cmake.687d96e9.js\": {\n    \"file\": \"assets/cmake.687d96e9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_coffeescript.09ac2f3f.js\": {\n    \"file\": \"assets/coffeescript.09ac2f3f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_coq.18e8d32c.js\": {\n    \"file\": \"assets/coq.18e8d32c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_core.b8b80318.js\": {\n    \"file\": \"assets/core.b8b80318.js\",\n    \"isDynamicEntry\": true\n  },\n  \"_cos.cfa5f5a1.js\": {\n    \"file\": \"assets/cos.cfa5f5a1.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_cpp.a6b75908.js\": {\n    \"file\": \"assets/cpp.a6b75908.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_createText-6b48ae7d.01af0827.js\": {\n    \"file\": \"assets/createText-6b48ae7d.01af0827.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_crmsh.929b5790.js\": {\n    \"file\": \"assets/crmsh.929b5790.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_crystal.520aae80.js\": {\n    \"file\": \"assets/crystal.520aae80.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_csharp.676d187e.js\": {\n    \"file\": \"assets/csharp.676d187e.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_csp.27c6fdf5.js\": {\n    \"file\": \"assets/csp.27c6fdf5.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_css.aed2fec3.js\": {\n    \"file\": \"assets/css.aed2fec3.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_d.deee4072.js\": {\n    \"file\": \"assets/d.deee4072.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dart.86736099.js\": {\n    \"file\": \"assets/dart.86736099.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_delphi.b2e83fb0.js\": {\n    \"file\": \"assets/delphi.b2e83fb0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_diff.52ac5061.js\": {\n    \"file\": \"assets/diff.52ac5061.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_django.acf6f5d0.js\": {\n    \"file\": \"assets/django.acf6f5d0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dns.ae462891.js\": {\n    \"file\": \"assets/dns.ae462891.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dockerfile.a461ef01.js\": {\n    \"file\": \"assets/dockerfile.a461ef01.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dos.66ec8774.js\": {\n    \"file\": \"assets/dos.66ec8774.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_download-cloud.04333a4f.js\": {\n    \"file\": \"assets/download-cloud.04333a4f.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_dsconfig.948d33c9.js\": {\n    \"file\": \"assets/dsconfig.948d33c9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dts.ace8285a.js\": {\n    \"file\": \"assets/dts.ace8285a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_dust.61c6165d.js\": {\n    \"file\": \"assets/dust.61c6165d.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ebnf.9f47f2bf.js\": {\n    \"file\": \"assets/ebnf.9f47f2bf.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_edges-d32062c0.bb379725.js\": {\n    \"file\": \"assets/edges-d32062c0.bb379725.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_line.c9b6b09c.js\"\n    ]\n  },\n  \"_elixir.87b77a7b.js\": {\n    \"file\": \"assets/elixir.87b77a7b.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_elm.62fc5561.js\": {\n    \"file\": \"assets/elm.62fc5561.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_erb.55b07d6c.js\": {\n    \"file\": \"assets/erb.55b07d6c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_erlang-repl.323f7160.js\": {\n    \"file\": \"assets/erlang-repl.323f7160.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_erlang.c7e57743.js\": {\n    \"file\": \"assets/erlang.c7e57743.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_excel.4522222f.js\": {\n    \"file\": \"assets/excel.4522222f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_filter.0ca1fe92.js\": {\n    \"file\": \"assets/filter.0ca1fe92.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_fix.51ea128a.js\": {\n    \"file\": \"assets/fix.51ea128a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_flix.c3fcc323.js\": {\n    \"file\": \"assets/flix.c3fcc323.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_flowDb-4b19a42f.0b3ed11f.js\": {\n    \"file\": \"assets/flowDb-4b19a42f.0b3ed11f.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_fortran.5261c416.js\": {\n    \"file\": \"assets/fortran.5261c416.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_fsharp.11981ee5.js\": {\n    \"file\": \"assets/fsharp.11981ee5.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gams.49652455.js\": {\n    \"file\": \"assets/gams.49652455.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gauss.012009e3.js\": {\n    \"file\": \"assets/gauss.012009e3.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gcode.0c400ff3.js\": {\n    \"file\": \"assets/gcode.0c400ff3.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gherkin.6750692c.js\": {\n    \"file\": \"assets/gherkin.6750692c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gift.b3e4cc82.js\": {\n    \"file\": \"assets/gift.b3e4cc82.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_glsl.3aa8f677.js\": {\n    \"file\": \"assets/glsl.3aa8f677.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gml.8a3d692a.js\": {\n    \"file\": \"assets/gml.8a3d692a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_go.9e6e090c.js\": {\n    \"file\": \"assets/go.9e6e090c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_golo.0c3cd41a.js\": {\n    \"file\": \"assets/golo.0c3cd41a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_gradle.9b2be066.js\": {\n    \"file\": \"assets/gradle.9b2be066.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_graph.0ef9c3b7.js\": {\n    \"file\": \"assets/graph.0ef9c3b7.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_groovy.e5a1e4e3.js\": {\n    \"file\": \"assets/groovy.e5a1e4e3.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_haml.0925c5ed.js\": {\n    \"file\": \"assets/haml.0925c5ed.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_handlebars.a705eec6.js\": {\n    \"file\": \"assets/handlebars.a705eec6.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_haskell.c1c10057.js\": {\n    \"file\": \"assets/haskell.c1c10057.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_haxe.bb1f4576.js\": {\n    \"file\": \"assets/haxe.bb1f4576.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_history.44d149e5.js\": {\n    \"file\": \"assets/history.44d149e5.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_hook.09e5815b.js\": {\n    \"file\": \"assets/hook.09e5815b.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_hsp.acbd24bb.js\": {\n    \"file\": \"assets/hsp.acbd24bb.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_htmlbars.ae56fbaf.js\": {\n    \"file\": \"assets/htmlbars.ae56fbaf.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_http.cadf2244.js\": {\n    \"file\": \"assets/http.cadf2244.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_hy.94f04bbe.js\": {\n    \"file\": \"assets/hy.94f04bbe.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_icons.7e9c9414.js\": {\n    \"file\": \"assets/icons.7e9c9414.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_index-fc10efb0.021331b3.js\": {\n    \"file\": \"assets/index-fc10efb0.021331b3.js\",\n    \"imports\": [\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"index.html\",\n      \"_createText-6b48ae7d.01af0827.js\"\n    ]\n  },\n  \"_inform7.573ba170.js\": {\n    \"file\": \"assets/inform7.573ba170.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ini.0ce80bb9.js\": {\n    \"file\": \"assets/ini.0ce80bb9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_init.a5b10ee5.js\": {\n    \"file\": \"assets/init.a5b10ee5.js\"\n  },\n  \"_irpf90.a8791531.js\": {\n    \"file\": \"assets/irpf90.a8791531.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_isbl.17bd57b9.js\": {\n    \"file\": \"assets/isbl.17bd57b9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_java.de21cb0c.js\": {\n    \"file\": \"assets/java.de21cb0c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_javascript.4b296926.js\": {\n    \"file\": \"assets/javascript.4b296926.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_jboss-cli.f0eb5633.js\": {\n    \"file\": \"assets/jboss-cli.f0eb5633.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_json.6e716f3a.js\": {\n    \"file\": \"assets/json.6e716f3a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_julia-repl.6f5d2fd3.js\": {\n    \"file\": \"assets/julia-repl.6f5d2fd3.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_julia.41a3881b.js\": {\n    \"file\": \"assets/julia.41a3881b.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_kanban-square-dashed.c88dceb2.js\": {\n    \"file\": \"assets/kanban-square-dashed.c88dceb2.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_kotlin.c59af0ba.js\": {\n    \"file\": \"assets/kotlin.c59af0ba.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_lasso.8a28f498.js\": {\n    \"file\": \"assets/lasso.8a28f498.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_latex.058112d0.js\": {\n    \"file\": \"assets/latex.058112d0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_layout.991ed0c6.js\": {\n    \"file\": \"assets/layout.991ed0c6.js\",\n    \"imports\": [\n      \"_graph.0ef9c3b7.js\",\n      \"index.html\"\n    ]\n  },\n  \"_ldif.e8f34ce7.js\": {\n    \"file\": \"assets/ldif.e8f34ce7.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_leaf.9ed3302f.js\": {\n    \"file\": \"assets/leaf.9ed3302f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_less.1e2f29fc.js\": {\n    \"file\": \"assets/less.1e2f29fc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_line.c9b6b09c.js\": {\n    \"file\": \"assets/line.c9b6b09c.js\",\n    \"imports\": [\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\",\n      \"index.html\"\n    ]\n  },\n  \"_linear.1519afab.js\": {\n    \"file\": \"assets/linear.1519afab.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_init.a5b10ee5.js\"\n    ]\n  },\n  \"_lisp.ab804084.js\": {\n    \"file\": \"assets/lisp.ab804084.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_livecodeserver.ada7c599.js\": {\n    \"file\": \"assets/livecodeserver.ada7c599.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_livescript.09fbd630.js\": {\n    \"file\": \"assets/livescript.09fbd630.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_llvm.c75c9c59.js\": {\n    \"file\": \"assets/llvm.c75c9c59.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_lsl.21a37c7a.js\": {\n    \"file\": \"assets/lsl.21a37c7a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_lua.c0a16d09.js\": {\n    \"file\": \"assets/lua.c0a16d09.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_makefile.bb0fcfbd.js\": {\n    \"file\": \"assets/makefile.bb0fcfbd.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_markdown.26dd2c01.js\": {\n    \"file\": \"assets/markdown.26dd2c01.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_market.93d110b3.js\": {\n    \"file\": \"assets/market.93d110b3.js\"\n  },\n  \"_mathematica.2dc23a04.js\": {\n    \"file\": \"assets/mathematica.2dc23a04.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_matlab.e39e79ce.js\": {\n    \"file\": \"assets/matlab.e39e79ce.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_maxima.b42deb33.js\": {\n    \"file\": \"assets/maxima.b42deb33.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_mel.30384c03.js\": {\n    \"file\": \"assets/mel.30384c03.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_mercury.4f44fcae.js\": {\n    \"file\": \"assets/mercury.4f44fcae.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_mipsasm.6d204ed1.js\": {\n    \"file\": \"assets/mipsasm.6d204ed1.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_mizar.6cf15cbd.js\": {\n    \"file\": \"assets/mizar.6cf15cbd.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_mojolicious.834a438a.js\": {\n    \"file\": \"assets/mojolicious.834a438a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_monkey.08e49847.js\": {\n    \"file\": \"assets/monkey.08e49847.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_moonscript.96b4ff81.js\": {\n    \"file\": \"assets/moonscript.96b4ff81.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_more-vertical.06df6aff.js\": {\n    \"file\": \"assets/more-vertical.06df6aff.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_multi-combobox.51bcb343.js\": {\n    \"file\": \"assets/multi-combobox.51bcb343.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_n1ql.bb73836f.js\": {\n    \"file\": \"assets/n1ql.bb73836f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_nginx.83dced37.js\": {\n    \"file\": \"assets/nginx.83dced37.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_nim.9b03230a.js\": {\n    \"file\": \"assets/nim.9b03230a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_nix.62253a60.js\": {\n    \"file\": \"assets/nix.62253a60.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_node-repl.e38345ae.js\": {\n    \"file\": \"assets/node-repl.e38345ae.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_nsis.aa91a0cd.js\": {\n    \"file\": \"assets/nsis.aa91a0cd.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_objectivec.636246aa.js\": {\n    \"file\": \"assets/objectivec.636246aa.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ocaml.6a0416aa.js\": {\n    \"file\": \"assets/ocaml.6a0416aa.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_openscad.fa2654cc.js\": {\n    \"file\": \"assets/openscad.fa2654cc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ordinal.93cdc51b.js\": {\n    \"file\": \"assets/ordinal.93cdc51b.js\",\n    \"imports\": [\n      \"_init.a5b10ee5.js\"\n    ]\n  },\n  \"_oxygene.8dc31ed9.js\": {\n    \"file\": \"assets/oxygene.8dc31ed9.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_pagination.524325c1.js\": {\n    \"file\": \"assets/pagination.524325c1.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_parser3.5d447324.js\": {\n    \"file\": \"assets/parser3.5d447324.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_path.53f90ab3.js\": {\n    \"file\": \"assets/path.53f90ab3.js\"\n  },\n  \"_perl.9060fb02.js\": {\n    \"file\": \"assets/perl.9060fb02.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_pf.a4464222.js\": {\n    \"file\": \"assets/pf.a4464222.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_pgsql.89d8e652.js\": {\n    \"file\": \"assets/pgsql.89d8e652.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_php-template.02d622f8.js\": {\n    \"file\": \"assets/php-template.02d622f8.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_php.df7925a7.js\": {\n    \"file\": \"assets/php.df7925a7.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_plaintext.bb4a0b1b.js\": {\n    \"file\": \"assets/plaintext.bb4a0b1b.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_pony.7125a88a.js\": {\n    \"file\": \"assets/pony.7125a88a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_powershell.bdb91f89.js\": {\n    \"file\": \"assets/powershell.bdb91f89.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_processing.160d59cc.js\": {\n    \"file\": \"assets/processing.160d59cc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_profile.94a3206f.js\": {\n    \"file\": \"assets/profile.94a3206f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_progress.b759e832.js\": {\n    \"file\": \"assets/progress.b759e832.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_prolog.e57149e1.js\": {\n    \"file\": \"assets/prolog.e57149e1.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_properties.a1b0cc3a.js\": {\n    \"file\": \"assets/properties.a1b0cc3a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_protobuf.6fd3c625.js\": {\n    \"file\": \"assets/protobuf.6fd3c625.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_puppet.e0eaa65e.js\": {\n    \"file\": \"assets/puppet.e0eaa65e.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_purebasic.d24275ac.js\": {\n    \"file\": \"assets/purebasic.d24275ac.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_python-repl.0f5b514e.js\": {\n    \"file\": \"assets/python-repl.0f5b514e.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_python.82adccf2.js\": {\n    \"file\": \"assets/python.82adccf2.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_q.9f7c28a1.js\": {\n    \"file\": \"assets/q.9f7c28a1.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_qml.116fd185.js\": {\n    \"file\": \"assets/qml.116fd185.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_r.42b21dcc.js\": {\n    \"file\": \"assets/r.42b21dcc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_radio-group.9fad45cf.js\": {\n    \"file\": \"assets/radio-group.9fad45cf.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_reasonml.6e89e640.js\": {\n    \"file\": \"assets/reasonml.6e89e640.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_request.9ae92294.js\": {\n    \"file\": \"assets/request.9ae92294.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_rib.44b3e5d6.js\": {\n    \"file\": \"assets/rib.44b3e5d6.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_roboconf.e09e943c.js\": {\n    \"file\": \"assets/roboconf.e09e943c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_routeros.1e8f226f.js\": {\n    \"file\": \"assets/routeros.1e8f226f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_rsl.1896018e.js\": {\n    \"file\": \"assets/rsl.1896018e.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ruby.f9cd1dd5.js\": {\n    \"file\": \"assets/ruby.f9cd1dd5.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_ruleslanguage.efb7e96b.js\": {\n    \"file\": \"assets/ruleslanguage.efb7e96b.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_rust.eaffbb35.js\": {\n    \"file\": \"assets/rust.eaffbb35.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_sas.791dadca.js\": {\n    \"file\": \"assets/sas.791dadca.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_save.cc03e881.js\": {\n    \"file\": \"assets/save.cc03e881.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_scala.0ec867bf.js\": {\n    \"file\": \"assets/scala.0ec867bf.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_scan-barcode.437edb2c.js\": {\n    \"file\": \"assets/scan-barcode.437edb2c.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_scheme.1acc1dfa.js\": {\n    \"file\": \"assets/scheme.1acc1dfa.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_scilab.25832050.js\": {\n    \"file\": \"assets/scilab.25832050.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_scss.e7f9a072.js\": {\n    \"file\": \"assets/scss.e7f9a072.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_settings-2.9ad8babc.js\": {\n    \"file\": \"assets/settings-2.9ad8babc.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_shell.1c309f31.js\": {\n    \"file\": \"assets/shell.1c309f31.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_smali.fddc422d.js\": {\n    \"file\": \"assets/smali.fddc422d.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_smalltalk.9a1542dc.js\": {\n    \"file\": \"assets/smalltalk.9a1542dc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_sml.871d736c.js\": {\n    \"file\": \"assets/sml.871d736c.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_sqf.d9ed268b.js\": {\n    \"file\": \"assets/sqf.d9ed268b.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_sql.c3b24f73.js\": {\n    \"file\": \"assets/sql.c3b24f73.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_sql_more.4bd7d0c7.js\": {\n    \"file\": \"assets/sql_more.4bd7d0c7.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_stan.7b1be381.js\": {\n    \"file\": \"assets/stan.7b1be381.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_stata.0a068841.js\": {\n    \"file\": \"assets/stata.0a068841.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_step21.4392b842.js\": {\n    \"file\": \"assets/step21.4392b842.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_styles-3ed67cfa.d41aab61.js\": {\n    \"file\": \"assets/styles-3ed67cfa.d41aab61.js\",\n    \"imports\": [\n      \"_graph.0ef9c3b7.js\",\n      \"index.html\",\n      \"_index-fc10efb0.021331b3.js\",\n      \"_channel.0e442477.js\"\n    ]\n  },\n  \"_styles-991ebdfc.bd043552.js\": {\n    \"file\": \"assets/styles-991ebdfc.bd043552.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_styles-d20c7d72.76540848.js\": {\n    \"file\": \"assets/styles-d20c7d72.76540848.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_stylus.7b7519f0.js\": {\n    \"file\": \"assets/stylus.7b7519f0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_subunit.fede25b7.js\": {\n    \"file\": \"assets/subunit.fede25b7.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_svgDrawCommon-5ccd53ef.362ba997.js\": {\n    \"file\": \"assets/svgDrawCommon-5ccd53ef.362ba997.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_swift.7b385dfc.js\": {\n    \"file\": \"assets/swift.7b385dfc.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_system.4d03cc26.js\": {\n    \"file\": \"assets/system.4d03cc26.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_table.b7adcb2f.js\": {\n    \"file\": \"assets/table.b7adcb2f.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_filter.0ca1fe92.js\"\n    ]\n  },\n  \"_tabs.7d7cdf4d.js\": {\n    \"file\": \"assets/tabs.7d7cdf4d.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_taggerscript.a5a3c164.js\": {\n    \"file\": \"assets/taggerscript.a5a3c164.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_tap.ec001561.js\": {\n    \"file\": \"assets/tap.ec001561.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_tcl.5faa7b55.js\": {\n    \"file\": \"assets/tcl.5faa7b55.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_thrift.501d09ba.js\": {\n    \"file\": \"assets/thrift.501d09ba.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_time.f5c88166.js\": {\n    \"file\": \"assets/time.f5c88166.js\",\n    \"imports\": [\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\"\n    ]\n  },\n  \"_tiny-invariant.dd7d57d2.js\": {\n    \"file\": \"assets/tiny-invariant.dd7d57d2.js\"\n  },\n  \"_tp.1545d2cf.js\": {\n    \"file\": \"assets/tp.1545d2cf.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_twig.0d7d2341.js\": {\n    \"file\": \"assets/twig.0d7d2341.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_typescript.d128319f.js\": {\n    \"file\": \"assets/typescript.d128319f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_upload-cloud.2f8ed3bd.js\": {\n    \"file\": \"assets/upload-cloud.2f8ed3bd.js\",\n    \"imports\": [\n      \"index.html\"\n    ]\n  },\n  \"_vala.c59c1f8e.js\": {\n    \"file\": \"assets/vala.c59c1f8e.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_vbnet.6c9093e0.js\": {\n    \"file\": \"assets/vbnet.6c9093e0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_vbscript-html.d290f5d5.js\": {\n    \"file\": \"assets/vbscript-html.d290f5d5.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_vbscript.3b9bcd96.js\": {\n    \"file\": \"assets/vbscript.3b9bcd96.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_verilog.9242d940.js\": {\n    \"file\": \"assets/verilog.9242d940.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_vhdl.aed608da.js\": {\n    \"file\": \"assets/vhdl.aed608da.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_vim.6dc8565f.js\": {\n    \"file\": \"assets/vim.6dc8565f.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_x86asm.ef055c19.js\": {\n    \"file\": \"assets/x86asm.ef055c19.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_xl.dce5d508.js\": {\n    \"file\": \"assets/xl.dce5d508.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_xml.1a48b5b5.js\": {\n    \"file\": \"assets/xml.1a48b5b5.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_xquery.5286f839.js\": {\n    \"file\": \"assets/xquery.5286f839.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_yaml.bff970e0.js\": {\n    \"file\": \"assets/yaml.bff970e0.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"_zephir.5f89667a.js\": {\n    \"file\": \"assets/zephir.5f89667a.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true\n  },\n  \"index.css\": {\n    \"file\": \"assets/index-21f57514.css\",\n    \"src\": \"index.css\"\n  },\n  \"index.html\": {\n    \"css\": [\n      \"assets/index-21f57514.css\"\n    ],\n    \"dynamicImports\": [\n      \"_1c.7075dba8.js\",\n      \"_abnf.9bf3a7c1.js\",\n      \"_accesslog.68f2b66c.js\",\n      \"_actionscript.67a1e568.js\",\n      \"_ada.c0e23f60.js\",\n      \"_angelscript.410ffc68.js\",\n      \"_apache.006764de.js\",\n      \"_applescript.39299b7d.js\",\n      \"_arcade.f590a3cc.js\",\n      \"_arduino.c4c70db8.js\",\n      \"_armasm.50cdbe9a.js\",\n      \"_asciidoc.e1e2cbee.js\",\n      \"_aspectj.a696b879.js\",\n      \"_autohotkey.c54238fc.js\",\n      \"_autoit.25b13806.js\",\n      \"_avrasm.a69814df.js\",\n      \"_awk.9f65b5a4.js\",\n      \"_axapta.fa0aef8d.js\",\n      \"_bash.a8e19ccc.js\",\n      \"_basic.6acc6ac9.js\",\n      \"_bnf.bd11f475.js\",\n      \"_brainfuck.db01d938.js\",\n      \"_c-like.af826c24.js\",\n      \"_c.cc4c2414.js\",\n      \"_cal.b721a23c.js\",\n      \"_capnproto.7f37471d.js\",\n      \"_ceylon.f41d8cb9.js\",\n      \"_clean.2230529c.js\",\n      \"_clojure-repl.24369284.js\",\n      \"_clojure.98540e11.js\",\n      \"_cmake.687d96e9.js\",\n      \"_coffeescript.09ac2f3f.js\",\n      \"_coq.18e8d32c.js\",\n      \"_cos.cfa5f5a1.js\",\n      \"_cpp.a6b75908.js\",\n      \"_crmsh.929b5790.js\",\n      \"_crystal.520aae80.js\",\n      \"_csharp.676d187e.js\",\n      \"_csp.27c6fdf5.js\",\n      \"_css.aed2fec3.js\",\n      \"_d.deee4072.js\",\n      \"_dart.86736099.js\",\n      \"_delphi.b2e83fb0.js\",\n      \"_diff.52ac5061.js\",\n      \"_django.acf6f5d0.js\",\n      \"_dns.ae462891.js\",\n      \"_dockerfile.a461ef01.js\",\n      \"_dos.66ec8774.js\",\n      \"_dsconfig.948d33c9.js\",\n      \"_dts.ace8285a.js\",\n      \"_dust.61c6165d.js\",\n      \"_ebnf.9f47f2bf.js\",\n      \"_elixir.87b77a7b.js\",\n      \"_elm.62fc5561.js\",\n      \"_erb.55b07d6c.js\",\n      \"_erlang-repl.323f7160.js\",\n      \"_erlang.c7e57743.js\",\n      \"_excel.4522222f.js\",\n      \"_fix.51ea128a.js\",\n      \"_flix.c3fcc323.js\",\n      \"_fortran.5261c416.js\",\n      \"_fsharp.11981ee5.js\",\n      \"_gams.49652455.js\",\n      \"_gauss.012009e3.js\",\n      \"_gcode.0c400ff3.js\",\n      \"_gherkin.6750692c.js\",\n      \"_glsl.3aa8f677.js\",\n      \"_gml.8a3d692a.js\",\n      \"_go.9e6e090c.js\",\n      \"_golo.0c3cd41a.js\",\n      \"_gradle.9b2be066.js\",\n      \"_groovy.e5a1e4e3.js\",\n      \"_haml.0925c5ed.js\",\n      \"_handlebars.a705eec6.js\",\n      \"_haskell.c1c10057.js\",\n      \"_haxe.bb1f4576.js\",\n      \"_hsp.acbd24bb.js\",\n      \"_htmlbars.ae56fbaf.js\",\n      \"_http.cadf2244.js\",\n      \"_hy.94f04bbe.js\",\n      \"_inform7.573ba170.js\",\n      \"_ini.0ce80bb9.js\",\n      \"_irpf90.a8791531.js\",\n      \"_isbl.17bd57b9.js\",\n      \"_java.de21cb0c.js\",\n      \"_javascript.4b296926.js\",\n      \"_jboss-cli.f0eb5633.js\",\n      \"_json.6e716f3a.js\",\n      \"_julia-repl.6f5d2fd3.js\",\n      \"_julia.41a3881b.js\",\n      \"_kotlin.c59af0ba.js\",\n      \"_lasso.8a28f498.js\",\n      \"_latex.058112d0.js\",\n      \"_ldif.e8f34ce7.js\",\n      \"_leaf.9ed3302f.js\",\n      \"_less.1e2f29fc.js\",\n      \"_lisp.ab804084.js\",\n      \"_livecodeserver.ada7c599.js\",\n      \"_livescript.09fbd630.js\",\n      \"_llvm.c75c9c59.js\",\n      \"_lsl.21a37c7a.js\",\n      \"_lua.c0a16d09.js\",\n      \"_makefile.bb0fcfbd.js\",\n      \"_markdown.26dd2c01.js\",\n      \"_mathematica.2dc23a04.js\",\n      \"_matlab.e39e79ce.js\",\n      \"_maxima.b42deb33.js\",\n      \"_mel.30384c03.js\",\n      \"_mercury.4f44fcae.js\",\n      \"_mipsasm.6d204ed1.js\",\n      \"_mizar.6cf15cbd.js\",\n      \"_mojolicious.834a438a.js\",\n      \"_monkey.08e49847.js\",\n      \"_moonscript.96b4ff81.js\",\n      \"_n1ql.bb73836f.js\",\n      \"_nginx.83dced37.js\",\n      \"_nim.9b03230a.js\",\n      \"_nix.62253a60.js\",\n      \"_node-repl.e38345ae.js\",\n      \"_nsis.aa91a0cd.js\",\n      \"_objectivec.636246aa.js\",\n      \"_ocaml.6a0416aa.js\",\n      \"_openscad.fa2654cc.js\",\n      \"_oxygene.8dc31ed9.js\",\n      \"_parser3.5d447324.js\",\n      \"_perl.9060fb02.js\",\n      \"_pf.a4464222.js\",\n      \"_pgsql.89d8e652.js\",\n      \"_php-template.02d622f8.js\",\n      \"_php.df7925a7.js\",\n      \"_plaintext.bb4a0b1b.js\",\n      \"_pony.7125a88a.js\",\n      \"_powershell.bdb91f89.js\",\n      \"_processing.160d59cc.js\",\n      \"_profile.94a3206f.js\",\n      \"_prolog.e57149e1.js\",\n      \"_properties.a1b0cc3a.js\",\n      \"_protobuf.6fd3c625.js\",\n      \"_puppet.e0eaa65e.js\",\n      \"_purebasic.d24275ac.js\",\n      \"_python-repl.0f5b514e.js\",\n      \"_python.82adccf2.js\",\n      \"_q.9f7c28a1.js\",\n      \"_qml.116fd185.js\",\n      \"_r.42b21dcc.js\",\n      \"_reasonml.6e89e640.js\",\n      \"_rib.44b3e5d6.js\",\n      \"_roboconf.e09e943c.js\",\n      \"_routeros.1e8f226f.js\",\n      \"_rsl.1896018e.js\",\n      \"_ruby.f9cd1dd5.js\",\n      \"_ruleslanguage.efb7e96b.js\",\n      \"_rust.eaffbb35.js\",\n      \"_sas.791dadca.js\",\n      \"_scala.0ec867bf.js\",\n      \"_scheme.1acc1dfa.js\",\n      \"_scilab.25832050.js\",\n      \"_scss.e7f9a072.js\",\n      \"_shell.1c309f31.js\",\n      \"_smali.fddc422d.js\",\n      \"_smalltalk.9a1542dc.js\",\n      \"_sml.871d736c.js\",\n      \"_sqf.d9ed268b.js\",\n      \"_sql.c3b24f73.js\",\n      \"_sql_more.4bd7d0c7.js\",\n      \"_stan.7b1be381.js\",\n      \"_stata.0a068841.js\",\n      \"_step21.4392b842.js\",\n      \"_stylus.7b7519f0.js\",\n      \"_subunit.fede25b7.js\",\n      \"_swift.7b385dfc.js\",\n      \"_taggerscript.a5a3c164.js\",\n      \"_tap.ec001561.js\",\n      \"_tcl.5faa7b55.js\",\n      \"_thrift.501d09ba.js\",\n      \"_tp.1545d2cf.js\",\n      \"_twig.0d7d2341.js\",\n      \"_typescript.d128319f.js\",\n      \"_vala.c59c1f8e.js\",\n      \"_vbnet.6c9093e0.js\",\n      \"_vbscript-html.d290f5d5.js\",\n      \"_vbscript.3b9bcd96.js\",\n      \"_verilog.9242d940.js\",\n      \"_vhdl.aed608da.js\",\n      \"_vim.6dc8565f.js\",\n      \"_x86asm.ef055c19.js\",\n      \"_xl.dce5d508.js\",\n      \"_xml.1a48b5b5.js\",\n      \"_xquery.5286f839.js\",\n      \"_yaml.bff970e0.js\",\n      \"_zephir.5f89667a.js\",\n      \"_core.b8b80318.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js\",\n      \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js\",\n      \"src/routes/Model.tsx\",\n      \"src/routes/Wallet.tsx\",\n      \"src/routes/Record.tsx\",\n      \"src/routes/Preset.tsx\",\n      \"src/routes/Account.tsx\",\n      \"src/routes/Generation.tsx\",\n      \"src/routes/Sharing.tsx\",\n      \"src/routes/Article.tsx\",\n      \"src/routes/Admin.tsx\",\n      \"src/routes/admin/DashBoard.tsx\",\n      \"src/routes/admin/Market.tsx\",\n      \"src/routes/admin/Channel.tsx\",\n      \"src/routes/admin/System.tsx\",\n      \"src/routes/admin/Charge.tsx\",\n      \"src/routes/admin/Users.tsx\",\n      \"src/routes/admin/Broadcast.tsx\",\n      \"src/routes/admin/Subscription.tsx\",\n      \"src/routes/admin/Record.tsx\",\n      \"src/routes/admin/Payment.tsx\",\n      \"src/routes/admin/Logger.tsx\",\n      \"src/routes/EpayNotify.tsx\"\n    ],\n    \"file\": \"assets/index.8d7e6a5a.js\",\n    \"isEntry\": true,\n    \"src\": \"index.html\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js\": {\n    \"file\": \"assets/blockDiagram-91b80b7a.047c225c.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_graph.0ef9c3b7.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_Tableau10.1b767f5e.js\",\n      \"_channel.0e442477.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\",\n      \"_init.a5b10ee5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js\": {\n    \"file\": \"assets/c4Diagram-b2a90758.e9bd027f.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_svgDrawCommon-5ccd53ef.362ba997.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js\": {\n    \"file\": \"assets/classDiagram-30eddba6.e8f5c5df.js\",\n    \"imports\": [\n      \"_styles-991ebdfc.bd043552.js\",\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js\": {\n    \"file\": \"assets/classDiagram-v2-f2df5561.ba89e794.js\",\n    \"imports\": [\n      \"_styles-991ebdfc.bd043552.js\",\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_index-fc10efb0.021331b3.js\",\n      \"_layout.991ed0c6.js\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js\": {\n    \"file\": \"assets/erDiagram-47591fe2.299b5ec2.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js\": {\n    \"file\": \"assets/flowDiagram-5540d9b9.675a4ea5.js\",\n    \"imports\": [\n      \"_flowDb-4b19a42f.0b3ed11f.js\",\n      \"_graph.0ef9c3b7.js\",\n      \"index.html\",\n      \"_layout.991ed0c6.js\",\n      \"_styles-3ed67cfa.d41aab61.js\",\n      \"_line.c9b6b09c.js\",\n      \"_index-fc10efb0.021331b3.js\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_channel.0e442477.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js\": {\n    \"file\": \"assets/flowDiagram-v2-3b53844e.cc42dd61.js\",\n    \"imports\": [\n      \"_flowDb-4b19a42f.0b3ed11f.js\",\n      \"_styles-3ed67cfa.d41aab61.js\",\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_index-fc10efb0.021331b3.js\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\",\n      \"_channel.0e442477.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js\": {\n    \"file\": \"assets/flowchart-elk-definition-5fe447d6.f3337586.js\",\n    \"imports\": [\n      \"_flowDb-4b19a42f.0b3ed11f.js\",\n      \"index.html\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_line.c9b6b09c.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js\": {\n    \"file\": \"assets/ganttDiagram-9a3bba1f.f22e0aee.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_time.f5c88166.js\",\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js\": {\n    \"file\": \"assets/gitGraphDiagram-96e6b4ee.d53591a6.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js\": {\n    \"file\": \"assets/infoDiagram-bcd20f53.23cab256.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js\": {\n    \"file\": \"assets/journeyDiagram-4fe6b3dc.6ea4bcd5.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_svgDrawCommon-5ccd53ef.362ba997.js\",\n      \"_arc.a247e62c.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js\": {\n    \"file\": \"assets/mindmap-definition-f354de21.8698e7a8.js\",\n    \"imports\": [\n      \"index.html\",\n      \"__isIndex.00b9330c.js\",\n      \"_createText-6b48ae7d.01af0827.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js\": {\n    \"file\": \"assets/pieDiagram-79897490.8acd8555.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_arc.a247e62c.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\",\n      \"_init.a5b10ee5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js\": {\n    \"file\": \"assets/quadrantDiagram-62f64e94.479e2729.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js\": {\n    \"file\": \"assets/requirementDiagram-05bf5f74.383dd160.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js\": {\n    \"file\": \"assets/sankeyDiagram-97764748.3efa2305.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_ordinal.93cdc51b.js\",\n      \"_Tableau10.1b767f5e.js\",\n      \"_init.a5b10ee5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js\": {\n    \"file\": \"assets/sequenceDiagram-acc0e65c.1f830b15.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_svgDrawCommon-5ccd53ef.362ba997.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js\": {\n    \"file\": \"assets/stateDiagram-0ff1cf1a.af40e4d4.js\",\n    \"imports\": [\n      \"_styles-d20c7d72.76540848.js\",\n      \"index.html\",\n      \"_graph.0ef9c3b7.js\",\n      \"_layout.991ed0c6.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js\": {\n    \"file\": \"assets/stateDiagram-v2-9a9d610d.5e37d388.js\",\n    \"imports\": [\n      \"_styles-d20c7d72.76540848.js\",\n      \"_graph.0ef9c3b7.js\",\n      \"index.html\",\n      \"_index-fc10efb0.021331b3.js\",\n      \"_layout.991ed0c6.js\",\n      \"_clone.2acf797d.js\",\n      \"_edges-d32062c0.bb379725.js\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_line.c9b6b09c.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js\": {\n    \"file\": \"assets/timeline-definition-fea2a41d.36bf6e4d.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_arc.a247e62c.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js\"\n  },\n  \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js\": {\n    \"file\": \"assets/xychartDiagram-ab372869.3444a59b.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_createText-6b48ae7d.01af0827.js\",\n      \"_band.6ffec690.js\",\n      \"_linear.1519afab.js\",\n      \"_line.c9b6b09c.js\",\n      \"_init.a5b10ee5.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_path.53f90ab3.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js\"\n  },\n  \"src/routes/Account.css\": {\n    \"file\": \"assets/Account-b93fa699.css\",\n    \"src\": \"src/routes/Account.css\"\n  },\n  \"src/routes/Account.tsx\": {\n    \"css\": [\n      \"assets/Account-b93fa699.css\"\n    ],\n    \"file\": \"assets/Account.9f79e18e.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_calendar-clock.1432b482.js\",\n      \"_gift.b3e4cc82.js\",\n      \"_clock.36b36b21.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Account.tsx\"\n  },\n  \"src/routes/Admin.css\": {\n    \"file\": \"assets/Admin-502861ce.css\",\n    \"src\": \"src/routes/Admin.css\"\n  },\n  \"src/routes/Admin.tsx\": {\n    \"css\": [\n      \"assets/Admin-502861ce.css\"\n    ],\n    \"file\": \"assets/Admin.f2a9d119.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_history.44d149e5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Admin.tsx\"\n  },\n  \"src/routes/Article.css\": {\n    \"file\": \"assets/Article-82d9a1fc.css\",\n    \"src\": \"src/routes/Article.css\"\n  },\n  \"src/routes/Article.tsx\": {\n    \"css\": [\n      \"assets/Article-82d9a1fc.css\"\n    ],\n    \"file\": \"assets/Article.58956150.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_progress.b759e832.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Article.tsx\"\n  },\n  \"src/routes/EpayNotify.css\": {\n    \"file\": \"assets/EpayNotify-b82ac077.css\",\n    \"src\": \"src/routes/EpayNotify.css\"\n  },\n  \"src/routes/EpayNotify.tsx\": {\n    \"css\": [\n      \"assets/EpayNotify-b82ac077.css\"\n    ],\n    \"file\": \"assets/EpayNotify.197de3d5.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_request.9ae92294.js\",\n      \"_box.6dd688fe.js\",\n      \"_scan-barcode.437edb2c.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/EpayNotify.tsx\"\n  },\n  \"src/routes/Generation.css\": {\n    \"file\": \"assets/Generation-7b5b93aa.css\",\n    \"src\": \"src/routes/Generation.css\"\n  },\n  \"src/routes/Generation.tsx\": {\n    \"css\": [\n      \"assets/Generation-7b5b93aa.css\"\n    ],\n    \"file\": \"assets/Generation.e6964a34.js\",\n    \"imports\": [\n      \"index.html\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Generation.tsx\"\n  },\n  \"src/routes/Model.tsx\": {\n    \"file\": \"assets/Model.beec7117.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_market.93d110b3.js\",\n      \"_arrow-right-left.f4c82cc0.js\",\n      \"_box.6dd688fe.js\",\n      \"_upload-cloud.2f8ed3bd.js\",\n      \"_download-cloud.04333a4f.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Model.tsx\"\n  },\n  \"src/routes/Preset.css\": {\n    \"file\": \"assets/Preset-8710ef85.css\",\n    \"src\": \"src/routes/Preset.css\"\n  },\n  \"src/routes/Preset.tsx\": {\n    \"css\": [\n      \"assets/Preset-8710ef85.css\"\n    ],\n    \"file\": \"assets/Preset.95a9b1e0.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_arrow-right-left.f4c82cc0.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Preset.tsx\"\n  },\n  \"src/routes/Record.css\": {\n    \"file\": \"assets/Record-dd76ad5a.css\",\n    \"src\": \"src/routes/Record.css\"\n  },\n  \"src/routes/Record.tsx\": {\n    \"css\": [\n      \"assets/Record-dd76ad5a.css\"\n    ],\n    \"file\": \"assets/Record.7203fb33.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_table.b7adcb2f.js\",\n      \"_Tracker.20c1771e.js\",\n      \"_pagination.524325c1.js\",\n      \"_calendar.2f96549a.js\",\n      \"_upload-cloud.2f8ed3bd.js\",\n      \"_clock.36b36b21.js\",\n      \"_activity.612642ea.js\",\n      \"_asterisk-square.2615ee58.js\",\n      \"_history.44d149e5.js\",\n      \"_filter.0ca1fe92.js\",\n      \"__isIndex.00b9330c.js\",\n      \"_tiny-invariant.dd7d57d2.js\",\n      \"_path.53f90ab3.js\",\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\",\n      \"_time.f5c88166.js\",\n      \"_band.6ffec690.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_line.c9b6b09c.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Record.tsx\"\n  },\n  \"src/routes/Sharing.css\": {\n    \"file\": \"assets/Sharing-07989aa3.css\",\n    \"src\": \"src/routes/Sharing.css\"\n  },\n  \"src/routes/Sharing.tsx\": {\n    \"css\": [\n      \"assets/Sharing-07989aa3.css\"\n    ],\n    \"file\": \"assets/Sharing.b857888c.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_clock.36b36b21.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Sharing.tsx\"\n  },\n  \"src/routes/Wallet.css\": {\n    \"file\": \"assets/Wallet-adc144da.css\",\n    \"src\": \"src/routes/Wallet.css\"\n  },\n  \"src/routes/Wallet.tsx\": {\n    \"css\": [\n      \"assets/Wallet-adc144da.css\"\n    ],\n    \"file\": \"assets/Wallet.23e502a3.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_request.9ae92294.js\",\n      \"_icons.7e9c9414.js\",\n      \"_progress.b759e832.js\",\n      \"_tabs.7d7cdf4d.js\",\n      \"_gift.b3e4cc82.js\",\n      \"_calendar.2f96549a.js\",\n      \"_calendar-clock.1432b482.js\",\n      \"_box.6dd688fe.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/Wallet.tsx\"\n  },\n  \"src/routes/admin/Broadcast.tsx\": {\n    \"file\": \"assets/Broadcast.10adc6a8.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_table.b7adcb2f.js\",\n      \"_more-vertical.06df6aff.js\",\n      \"_filter.0ca1fe92.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Broadcast.tsx\"\n  },\n  \"src/routes/admin/Channel.tsx\": {\n    \"file\": \"assets/Channel.22f78415.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_table.b7adcb2f.js\",\n      \"_OperationAction.5b1f951a.js\",\n      \"_hook.09e5815b.js\",\n      \"_activity.612642ea.js\",\n      \"_settings-2.9ad8babc.js\",\n      \"_asterisk-square.2615ee58.js\",\n      \"_Paragraph.4ca577cf.js\",\n      \"_multi-combobox.51bcb343.js\",\n      \"_kanban-square-dashed.c88dceb2.js\",\n      \"_book-dashed.798b0c5c.js\",\n      \"_filter.0ca1fe92.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Channel.tsx\"\n  },\n  \"src/routes/admin/Charge.tsx\": {\n    \"file\": \"assets/Charge.9dc9e031.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_radio-group.9fad45cf.js\",\n      \"_table.b7adcb2f.js\",\n      \"_OperationAction.5b1f951a.js\",\n      \"_hook.09e5815b.js\",\n      \"_kanban-square-dashed.c88dceb2.js\",\n      \"_activity.612642ea.js\",\n      \"_upload-cloud.2f8ed3bd.js\",\n      \"_download-cloud.04333a4f.js\",\n      \"_settings-2.9ad8babc.js\",\n      \"_filter.0ca1fe92.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Charge.tsx\"\n  },\n  \"src/routes/admin/DashBoard.tsx\": {\n    \"file\": \"assets/DashBoard.a5483326.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_chart.b43e29db.js\",\n      \"_Tracker.20c1771e.js\",\n      \"_multi-combobox.51bcb343.js\",\n      \"_filter.0ca1fe92.js\",\n      \"__isIndex.00b9330c.js\",\n      \"_tiny-invariant.dd7d57d2.js\",\n      \"_path.53f90ab3.js\",\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\",\n      \"_time.f5c88166.js\",\n      \"_band.6ffec690.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_line.c9b6b09c.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/DashBoard.tsx\"\n  },\n  \"src/routes/admin/Logger.tsx\": {\n    \"file\": \"assets/Logger.e813542f.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_Paragraph.4ca577cf.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Logger.tsx\"\n  },\n  \"src/routes/admin/Market.tsx\": {\n    \"file\": \"assets/Market.458a4f86.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_market.93d110b3.js\",\n      \"_hook.09e5815b.js\",\n      \"_tiny-invariant.dd7d57d2.js\",\n      \"_system.4d03cc26.js\",\n      \"_activity.612642ea.js\",\n      \"_save.cc03e881.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Market.tsx\"\n  },\n  \"src/routes/admin/Payment.tsx\": {\n    \"file\": \"assets/Payment.f83f77b2.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_request.9ae92294.js\",\n      \"_table.b7adcb2f.js\",\n      \"_pagination.524325c1.js\",\n      \"_icons.7e9c9414.js\",\n      \"_scan-barcode.437edb2c.js\",\n      \"_more-vertical.06df6aff.js\",\n      \"_filter.0ca1fe92.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Payment.tsx\"\n  },\n  \"src/routes/admin/Record.tsx\": {\n    \"file\": \"assets/Record.7a35bf1e.js\",\n    \"imports\": [\n      \"index.html\",\n      \"src/routes/Record.tsx\",\n      \"_table.b7adcb2f.js\",\n      \"_filter.0ca1fe92.js\",\n      \"_Tracker.20c1771e.js\",\n      \"__isIndex.00b9330c.js\",\n      \"_tiny-invariant.dd7d57d2.js\",\n      \"_path.53f90ab3.js\",\n      \"_linear.1519afab.js\",\n      \"_init.a5b10ee5.js\",\n      \"_time.f5c88166.js\",\n      \"_band.6ffec690.js\",\n      \"_ordinal.93cdc51b.js\",\n      \"_array.9f3ba611.js\",\n      \"_line.c9b6b09c.js\",\n      \"_pagination.524325c1.js\",\n      \"_calendar.2f96549a.js\",\n      \"_upload-cloud.2f8ed3bd.js\",\n      \"_clock.36b36b21.js\",\n      \"_activity.612642ea.js\",\n      \"_asterisk-square.2615ee58.js\",\n      \"_history.44d149e5.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Record.tsx\"\n  },\n  \"src/routes/admin/Subscription.tsx\": {\n    \"file\": \"assets/Subscription.facf4385.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_multi-combobox.51bcb343.js\",\n      \"_hook.09e5815b.js\",\n      \"_activity.612642ea.js\",\n      \"_save.cc03e881.js\",\n      \"_book-dashed.798b0c5c.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Subscription.tsx\"\n  },\n  \"src/routes/admin/System.tsx\": {\n    \"file\": \"assets/System.1954c6c5.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_Paragraph.4ca577cf.js\",\n      \"_system.4d03cc26.js\",\n      \"_multi-combobox.51bcb343.js\",\n      \"_hook.09e5815b.js\",\n      \"_tabs.7d7cdf4d.js\",\n      \"_icons.7e9c9414.js\",\n      \"_save.cc03e881.js\",\n      \"_settings-2.9ad8babc.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/System.tsx\"\n  },\n  \"src/routes/admin/Users.tsx\": {\n    \"file\": \"assets/Users.ee82f0be.js\",\n    \"imports\": [\n      \"index.html\",\n      \"_table.b7adcb2f.js\",\n      \"_chart.b43e29db.js\",\n      \"_pagination.524325c1.js\",\n      \"_OperationAction.5b1f951a.js\",\n      \"_radio-group.9fad45cf.js\",\n      \"_filter.0ca1fe92.js\",\n      \"_calendar-clock.1432b482.js\"\n    ],\n    \"isDynamicEntry\": true,\n    \"src\": \"src/routes/admin/Users.tsx\"\n  }\n}"
  },
  {
    "path": "app/public/robots.txt",
    "content": "User-Agent: *\nAllow: /\nDisallow: /admin/\n"
  },
  {
    "path": "app/public/service.js",
    "content": "\nconst SERVICE_NAME = \"coai\";\n\nself.addEventListener('activate', function (event) {\n  console.debug(\"[service] service worker activated\");\n});\n\nself.addEventListener('install', function (event) {\n  event.waitUntil(\n    caches.open(SERVICE_NAME)\n      .then(function (cache) {\n        return cache.addAll([]);\n      })\n  );\n});\n\nself.addEventListener('fetch', function (event) {\n  event.respondWith(\n    caches.match(event.request)\n      .then(function (response) {\n        return response || fetch(event.request);\n      })\n  );\n});\n"
  },
  {
    "path": "app/public/site.webmanifest",
    "content": "{\n    \"name\": \"CoAI\",\n    \"short_name\": \"CoAI\",\n    \"icons\": [\n      {\n        \"src\": \"/service/android-chrome-192x192.png\",\n        \"sizes\": \"192x192\",\n        \"type\": \"image/png\"\n      },\n      {\n        \"src\": \"/service/android-chrome-512x512.png\",\n        \"sizes\": \"512x512\",\n        \"type\": \"image/png\"\n      }\n    ],\n    \"start_url\": \"/\",\n    \"theme_color\": \"#000\",\n    \"background_color\": \"#000\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "app/public/workbox.js",
    "content": "\nif ('serviceWorker' in navigator) {\n  window.addEventListener('load', function () {\n    navigator.serviceWorker.register('/service.js').then(function (registration) {\n      console.debug(`[service] service worker registered with scope: ${registration.scope}`);\n    }, function (err) {\n      console.debug(`[service] service worker registration failed: ${err}`);\n    });\n  });\n}\n"
  },
  {
    "path": "app/qodana.yaml",
    "content": "#-------------------------------------------------------------------------------#\n#               Qodana analysis is configured by qodana.yaml file               #\n#             https://www.jetbrains.com/help/qodana/qodana-yaml.html            #\n#-------------------------------------------------------------------------------#\nversion: \"1.0\"\n\n#Specify inspection profile for code analysis\nprofile:\n  name: qodana.starter\n\n#Enable inspections\n#include:\n#  - name: <SomeEnabledInspectionId>\n\n#Disable inspections\n#exclude:\n#  - name: <SomeDisabledInspectionId>\n#    paths:\n#      - <path/where/not/run/inspection>\n\n#Execute shell command before Qodana execution (Applied in CI/CD pipeline)\n#bootstrap: sh ./prepare-qodana.sh\n\n#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)\n#plugins:\n#  - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)\n\n#Specify Qodana linter for analysis (Applied in CI/CD pipeline)\nlinter: jetbrains/qodana-js:latest\n"
  },
  {
    "path": "app/src/App.tsx",
    "content": "import { Provider } from \"react-redux\";\nimport store from \"./store/index.ts\";\nimport AppProvider from \"./components/app/AppProvider.tsx\";\nimport { AppRouter } from \"./router.tsx\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport Spinner from \"@/spinner.tsx\";\nimport ReloadPrompt from \"@/components/ReloadService.tsx\";\n\nfunction App() {\n  return (\n    <Provider store={store}>\n      <AppProvider>\n        <Toaster />\n        <Spinner />\n        <ReloadPrompt />\n        <AppRouter />\n      </AppProvider>\n    </Provider>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "app/src/admin/api/channel.ts",
    "content": "import { Channel } from \"@/admin/channel.ts\";\nimport axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { CommonResponse } from \"@/api/common.ts\";\n\nexport type ChannelListResponse = CommonResponse & {\n  data: Channel[];\n};\n\nexport type GetChannelResponse = CommonResponse & {\n  data?: Channel;\n};\n\nexport async function listChannel(): Promise<ChannelListResponse> {\n  try {\n    const response = await axios.get(\"/admin/channel/list\");\n    return response.data as ChannelListResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), data: [] };\n  }\n}\n\nexport async function getChannel(id: number): Promise<GetChannelResponse> {\n  try {\n    const response = await axios.get(`/admin/channel/get/${id}`);\n    return response.data as GetChannelResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function createChannel(channel: Channel): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/channel/create\", channel);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function updateChannel(\n  id: number,\n  channel: Channel,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/channel/update/${id}`, channel);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function deleteChannel(id: number): Promise<CommonResponse> {\n  try {\n    const response = await axios.get(`/admin/channel/delete/${id}`);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function activateChannel(id: number): Promise<CommonResponse> {\n  try {\n    const response = await axios.get(`/admin/channel/activate/${id}`);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function deactivateChannel(id: number): Promise<CommonResponse> {\n  try {\n    const response = await axios.get(`/admin/channel/deactivate/${id}`);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/charge.ts",
    "content": "import { CommonResponse } from \"@/api/common.ts\";\nimport { ChargeProps } from \"@/admin/charge.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport axios from \"axios\";\n\nexport type ChargeListResponse = CommonResponse & {\n  data: ChargeProps[];\n};\n\nexport type ChargeSyncRequest = {\n  overwrite: boolean;\n  data: ChargeProps[];\n};\n\nexport type ChargeFetchRequest = {\n  endpoint: string;\n  system?: string;\n};\n\nexport type ChargeFetchResponse = CommonResponse & {\n  data: ChargeProps[];\n};\n\nexport async function listCharge(): Promise<ChargeListResponse> {\n  try {\n    const response = await axios.get(\"/admin/charge/list\");\n    return response.data as ChargeListResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), data: [] };\n  }\n}\n\nexport async function setCharge(charge: ChargeProps): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/charge/set`, charge);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function deleteCharge(id: number): Promise<CommonResponse> {\n  try {\n    const response = await axios.get(`/admin/charge/delete/${id}`);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function syncCharge(\n  data: ChargeSyncRequest,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/charge/sync`, data);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function fetchUpstreamCharge(\n  req: ChargeFetchRequest,\n): Promise<ChargeFetchResponse> {\n  try {\n    const response = await axios.post(`/admin/charge/fetch`, req);\n    const data = response.data as ChargeFetchResponse;\n    return {\n      status: !!data.status,\n      error: data.error,\n      data: data.data || [],\n    };\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), data: [] };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/chart.ts",
    "content": "import {\n  BillingChartResponse,\n  CommonResponse,\n  ErrorChartResponse,\n  InfoResponse,\n  InvitationGenerateResponse,\n  InvitationResponse,\n  ModelChartResponse,\n  RedeemResponse,\n  RequestChartResponse,\n  UserResponse,\n  UserTypeChartResponse,\n} from \"@/admin/types.ts\";\nimport axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\nexport const initialAdminInfoState: InfoResponse = {\n  subscription_count: 0,\n  billing_today: 0,\n  billing_month: 0,\n  online_chats: 0,\n  billing_yesterday: 0,\n  billing_last_month: 0,\n};\n\nexport type UserFilterProps = {\n  plan: string;\n  admin: string;\n  ban: string;\n  sort: string;\n};\n\nexport const initialUserFilter: UserFilterProps = {\n  plan: \"all\", // all/no/yes\n  admin: \"all\", // all/no/yes\n  ban: \"all\", // all/no/yes\n  sort: \"id-asc\",\n  // id-asc/id-desc\n  // quota-asc/quota-desc\n  // used-quota-asc/used-quota-desc\n  // plan-desc/plan-asc\n};\n\nexport async function getAdminInfo(): Promise<InfoResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/info\");\n    return response.data as InfoResponse;\n  } catch (e) {\n    console.warn(e);\n    return {\n      ...initialAdminInfoState,\n    };\n  }\n}\n\nexport async function getModelChart(): Promise<ModelChartResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/model\");\n    const data = response.data as ModelChartResponse;\n\n    return {\n      date: data.date,\n      value: data.value || [],\n    };\n  } catch (e) {\n    console.warn(e);\n    return { date: [], value: [] };\n  }\n}\n\nexport async function getRequestChart(): Promise<RequestChartResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/request\");\n    return response.data as RequestChartResponse;\n  } catch (e) {\n    console.warn(e);\n    return { date: [], value: [] };\n  }\n}\n\nexport async function getBillingChart(): Promise<BillingChartResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/billing\");\n    return response.data as BillingChartResponse;\n  } catch (e) {\n    console.warn(e);\n    return { date: [], value: [] };\n  }\n}\n\nexport async function getErrorChart(): Promise<ErrorChartResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/error\");\n    return response.data as ErrorChartResponse;\n  } catch (e) {\n    console.warn(e);\n    return { date: [], value: [] };\n  }\n}\n\nexport async function getUserTypeChart(): Promise<UserTypeChartResponse> {\n  try {\n    const response = await axios.get(\"/admin/analytics/user\");\n    return response.data as UserTypeChartResponse;\n  } catch (e) {\n    console.warn(e);\n    return {\n      total: 0,\n      normal: 0,\n      api_paid: 0,\n      basic_plan: 0,\n      standard_plan: 0,\n      pro_plan: 0,\n    };\n  }\n}\n\nexport async function getInvitationList(\n  page: number,\n): Promise<InvitationResponse> {\n  try {\n    const response = await axios.get(`/admin/invitation/list?page=${page}`);\n    return response.data as InvitationResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: getErrorMessage(e),\n      data: [],\n      total: 0,\n    };\n  }\n}\n\nexport async function deleteInvitation(code: string): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/invitation/delete\", { code });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function generateInvitation(\n  type: string,\n  quota: number,\n  number: number,\n): Promise<InvitationGenerateResponse> {\n  try {\n    const response = await axios.post(\"/admin/invitation/generate\", {\n      type,\n      quota,\n      number,\n    });\n    return response.data as InvitationGenerateResponse;\n  } catch (e) {\n    return { status: false, data: [], message: getErrorMessage(e) };\n  }\n}\n\nexport async function getRedeemList(page: number): Promise<RedeemResponse> {\n  try {\n    const response = await axios.get(`/admin/redeem/list?page=${page}`);\n    return response.data as RedeemResponse;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, message: getErrorMessage(e), data: [], total: 0 };\n  }\n}\n\nexport async function deleteRedeem(code: string): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/redeem/delete\", { code });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function generateRedeem(\n  quota: number,\n  number: number,\n): Promise<InvitationGenerateResponse> {\n  try {\n    const response = await axios.post(\"/admin/redeem/generate\", {\n      quota,\n      number,\n    });\n    return response.data as InvitationGenerateResponse;\n  } catch (e) {\n    return { status: false, data: [], message: getErrorMessage(e) };\n  }\n}\n\nexport async function getUserList(\n  page: number,\n  search: string,\n  params: UserFilterProps,\n): Promise<UserResponse> {\n  try {\n    const response = await axios.get(`/admin/user/list`, {\n      params: {\n        page,\n        search,\n        params,\n      },\n    });\n    return response.data as UserResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: getErrorMessage(e),\n      data: [],\n      total: 0,\n    };\n  }\n}\n\nexport async function updatePassword(\n  id: number,\n  password: string,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/password\", {\n      id,\n      password,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function updateEmail(\n  id: number,\n  email: string,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/email\", {\n      id,\n      email,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function quotaOperation(\n  id: number,\n  quota: number,\n  override?: boolean,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/quota\", {\n      id,\n      quota,\n      override: override ?? false,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function subscriptionOperation(\n  id: number,\n  expired: string,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/subscription\", {\n      id,\n      expired,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function banUserOperation(\n  id: number,\n  ban: boolean,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/ban\", {\n      id,\n      ban,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function setAdminOperation(\n  id: number,\n  admin: boolean,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/admin\", {\n      id,\n      admin,\n    });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function subscriptionLevelOperation(\n  id: number,\n  level: number,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/level\", { id, level });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n\nexport async function releaseUsageOperation(\n  id: number,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(\"/admin/user/release\", { id });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, message: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/info.ts",
    "content": "import axios from \"axios\";\nimport {\n  setAppLogo,\n  setAppName,\n  setBlobEndpoint,\n  setBuyLink,\n  setDocsUrl,\n} from \"@/conf/env.ts\";\nimport { infoEvent } from \"@/events/info.ts\";\nimport { initGoogleAnalytics } from \"@/utils/analytics.ts\";\nimport { BroadcastEvent, getBroadcast } from \"@/api/broadcast\";\n\nexport type SiteInfo = {\n  title: string;\n  logo: string;\n  docs: string;\n  file: string;\n  backend?: string;\n  currency: string;\n  announcement: string;\n  buy_link: string;\n  mail: boolean;\n  contact: string;\n  footer: string;\n  auth_footer: boolean;\n  hide_key_docs?: boolean;\n  article: string[];\n  generation: string[];\n  relay_plan: boolean;\n  payment: string[];\n  payment_aggregation: boolean;\n  ga_tracking_id?: string;\n  broadcast?: BroadcastEvent;\n};\n\nexport async function getSiteInfo(): Promise<SiteInfo> {\n  try {\n    const response = await axios.get(\"/info\");\n    return response.data as SiteInfo;\n  } catch (e) {\n    console.warn(e);\n    return {\n      title: \"\",\n      logo: \"\",\n      docs: \"\",\n      file: \"\",\n      backend: undefined,\n      currency: \"cny\",\n      announcement: \"\",\n      buy_link: \"\",\n      contact: \"\",\n      footer: \"\",\n      auth_footer: false,\n      hide_key_docs: false,\n      mail: false,\n      article: [],\n      generation: [],\n      relay_plan: false,\n      payment: [],\n      payment_aggregation: false,\n\n      broadcast: {\n        message: \"\",\n        firstReceived: false,\n      },\n    };\n  }\n}\n\nexport function syncSiteInfo() {\n  setTimeout(async () => {\n    const info = await getSiteInfo();\n    info.broadcast = await getBroadcast();\n\n    setAppName(info.title);\n    setAppLogo(info.logo);\n    setDocsUrl(info.docs);\n    setBlobEndpoint(info.file);\n    setBuyLink(info.buy_link);\n    initGoogleAnalytics(info.ga_tracking_id);\n\n    infoEvent.emit(info);\n  }, 25);\n}\n"
  },
  {
    "path": "app/src/admin/api/logger.ts",
    "content": "import axios from \"axios\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\nexport type Logger = {\n  path: string;\n  size: number;\n};\n\nexport async function listLoggers(): Promise<Logger[]> {\n  try {\n    const response = await axios.get(\"/admin/logger/list\");\n    return (response.data || []) as Logger[];\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function getLoggerConsole(n?: number): Promise<string> {\n  try {\n    const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`);\n    return response.data.content as string;\n  } catch (e) {\n    console.warn(e);\n    return `failed to get info from server: ${getErrorMessage(e)}`;\n  }\n}\n\nexport async function downloadLogger(path: string): Promise<void> {\n  try {\n    const response = await axios.get(\"/admin/logger/download\", {\n      responseType: \"blob\",\n      params: { path },\n    });\n    const url = window.URL.createObjectURL(new Blob([response.data]));\n    const link = document.createElement(\"a\");\n    link.href = url;\n    link.setAttribute(\"download\", path);\n    document.body.appendChild(link);\n    link.click();\n  } catch (e) {\n    console.warn(e);\n  }\n}\n\nexport async function deleteLogger(path: string): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/logger/delete?path=${path}`);\n    return response.data as CommonResponse;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/market.ts",
    "content": "import { Model } from \"@/api/types.tsx\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\nexport async function updateMarket(data: Model[]): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/admin/market/update\", data);\n    return resp.data as CommonResponse;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/plan.ts",
    "content": "import { Plan } from \"@/api/types.tsx\";\nimport axios from \"axios\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { getApiPlans } from \"@/api/v1.ts\";\n\nexport type PlanConfig = {\n  enabled: boolean;\n  plans: Plan[];\n};\n\nexport async function getPlanConfig(): Promise<PlanConfig> {\n  try {\n    const response = await axios.get(\"/admin/plan/view\");\n    const conf = response.data as PlanConfig;\n    conf.plans = (conf.plans || []).filter((item) => item.level > 0);\n    if (conf.plans.length === 0)\n      conf.plans = [1, 2, 3].map(\n        (level) => ({ level, price: 0, items: [] }) as Plan,\n      );\n    return conf;\n  } catch (e) {\n    console.warn(e);\n    return { enabled: false, plans: [] };\n  }\n}\n\nexport async function getExternalPlanConfig(\n  endpoint: string,\n): Promise<PlanConfig> {\n  const response = await getApiPlans({ endpoint });\n  return { enabled: response.length > 0, plans: response };\n}\n\nexport async function setPlanConfig(\n  config: PlanConfig,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/plan/update`, config);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/admin/api/system.ts",
    "content": "import { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport axios from \"axios\";\nimport { backendEndpoint } from \"@/conf/env.ts\";\n\nexport type TestWebSearchResponse = CommonResponse & {\n  result: string;\n};\n\nexport type whiteList = {\n  enabled: boolean;\n  custom: string;\n  white_list: string[];\n};\n\nexport type GeneralState = {\n  title: string;\n  logo: string;\n  description: string;\n  backend: string;\n  docs: string;\n  file: string;\n  pwa_manifest: string;\n  gravatar: string;\n  debug_mode: boolean;\n  realtime?: {\n    ws?: {\n      buffer_size?: number;\n      aggregate?: boolean;\n      aggregate_window_ms?: number;\n    };\n  };\n};\n\nexport type MailState = {\n  host: string;\n  protocol: boolean;\n  port: number;\n  username: string;\n  password: string;\n  from: string;\n  white_list: whiteList;\n};\n\nexport type SearchState = {\n  endpoint: string;\n  crop: boolean;\n  crop_len: number;\n  engines: string[];\n  image_proxy: boolean;\n  safe_search: number;\n  llm_extract: boolean;\n  llm_model: string;\n};\n\nexport type SecurityState = {\n  check_type: string;\n  check_models?: string[];\n\n  text_database: string;\n  regex_database: string;\n\n  baidu_api_key: string;\n  baidu_secret_key: string;\n\n  custom_endpoint: string;\n  custom_audit_token: string;\n  \n  blacklist_ips: string[];\n  whitelist_ips: string[];\n};\n\nexport type PaymentState = {\n  stripe: {\n    enabled: boolean;\n    public_key: string;\n    secret_key: string;\n    webhook_secret: string;\n  };\n  epay: {\n    domain: string;\n    business_id: string;\n    business_key: string;\n    enabled: boolean;\n    methods: string[];\n    aggregation: boolean;\n  };\n  wechatpay?: {\n    enabled: boolean;\n    app_id: string;\n    mch_id: string;\n    serial_no: string;\n    apiv3_key: string;\n    wechatcertificate: string;\n  };\n  xunhupay?: {\n    wechat_enabled: boolean;\n    alipay_enabled: boolean;\n    wechat_app_id: string;\n    wechat_app_secret: string;\n    alipay_app_id: string;\n    alipay_app_secret: string;\n    endpoint: string;\n  };\n  affiliate?: {\n    enabled: boolean;\n    commission_rate: number;\n    min_withdraw: number;\n    allow_existing_bind: boolean;\n  };\n};\n\nexport type SiteState = {\n  close_register: boolean;\n  currency: string;\n  close_relay: boolean;\n  relay_plan: boolean;\n  quota: number;\n  buy_link: string;\n  announcement: string;\n  contact: string;\n  footer: string;\n  auth_footer: boolean;\n  pre_deduct_quota: boolean;\n  hide_key_docs: boolean;\n};\n\nexport type CustomState = {\n  custom_js: string;\n  custom_css: string;\n  custom_html: string;\n  ga_tracking_id: string;\n};\n\nexport type AutoTitleState = {\n  enabled: boolean;\n  model: string;\n  max_len: number;\n  min_msgs: number;\n  overwrite: boolean;\n  prompt: string;\n};\n\nexport type CommonState = {\n  cache: string[];\n  expire: number;\n  size: number;\n\n  article: string[];\n  generation: string[];\n\n  prompt_store: boolean;\n  image_store: boolean;\n};\n\nexport type SystemProps = {\n  general: GeneralState;\n  site: SiteState;\n  mail: MailState;\n  search: SearchState;\n  common: CommonState;\n  payment: PaymentState;\n  security: SecurityState;\n  custom: CustomState;\n  auto_title?: AutoTitleState;\n};\n\nexport type SystemResponse = CommonResponse & {\n  data?: SystemProps;\n};\n\nexport const initialSystemState: SystemProps = {\n  general: {\n    logo: \"\",\n    description: \"\",\n    title: \"\",\n    backend: \"\",\n    docs: \"\",\n    file: \"\",\n    pwa_manifest: \"\",\n    gravatar: \"\",\n    debug_mode: false,\n    realtime: {\n      ws: {\n        buffer_size: 24,\n        aggregate: true,\n        aggregate_window_ms: 20,\n      },\n    },\n  },\n  site: {\n    close_register: false,\n    currency: \"cny\",\n    close_relay: false,\n    relay_plan: false,\n    quota: 0,\n    buy_link: \"\",\n    announcement: \"\",\n    contact: \"\",\n    footer: \"\",\n    auth_footer: false,\n    pre_deduct_quota: true,\n    hide_key_docs: false,\n  },\n  mail: {\n    host: \"\",\n    protocol: false,\n    port: 465,\n    username: \"\",\n    password: \"\",\n    from: \"\",\n    white_list: {\n      enabled: false,\n      custom: \"\",\n      white_list: [],\n    },\n  },\n  search: {\n    endpoint: \"\",\n    crop: false,\n    crop_len: 1000,\n    engines: [],\n    image_proxy: false,\n    safe_search: 0,\n    llm_extract: false,\n    llm_model: \"\",\n  },\n  common: {\n    article: [],\n    generation: [],\n    cache: [],\n    expire: 3600,\n    size: 1,\n    prompt_store: false,\n    image_store: false,\n  },\n  payment: {\n    stripe: {\n      enabled: false,\n      public_key: \"\",\n      secret_key: \"\",\n      webhook_secret: \"\",\n    },\n    epay: {\n      domain: \"\",\n      business_id: \"\",\n      business_key: \"\",\n      enabled: false,\n      methods: [],\n      aggregation: false,\n    },\n    affiliate: {\n      enabled: false,\n      commission_rate: 0.1,\n      min_withdraw: 10,\n      allow_existing_bind: false,\n    },\n  },\n  security: {\n    check_type: \"\",\n    check_models: [],\n    text_database: \"\",\n    regex_database: \"\",\n    baidu_api_key: \"\",\n    baidu_secret_key: \"\",\n    custom_endpoint: \"\",\n    custom_audit_token: \"\",\n    blacklist_ips: [],\n    whitelist_ips: [],\n  },\n  custom: {\n    custom_js: \"\",\n    custom_css: \"\",\n    custom_html: \"\",\n    ga_tracking_id: \"\",\n  },\n  auto_title: {\n    enabled: false,\n    model: \"\",\n    max_len: 50,\n    min_msgs: 6,\n    overwrite: false,\n    prompt: \"\",\n  },\n};\n\nexport async function getConfig(): Promise<SystemResponse> {\n  try {\n    const response = await axios.get(\"/admin/config/view\");\n    const data = response.data as SystemResponse;\n    if (data.status && data.data) {\n      // init system data pre-format\n\n      data.data.mail.white_list.white_list =\n        data.data.mail.white_list.white_list || commonWhiteList;\n      data.data.search.engines = data.data.search.engines || [];\n      data.data.search.crop_len =\n        data.data.search.crop_len && data.data.search.crop_len > 0\n          ? data.data.search.crop_len\n          : 1000;\n\n      data.data.site.currency = data.data.site.currency || \"cny\";\n\n      if (\n        !data.data.common.group ||\n        Object.keys(data.data.common.group).length === 0\n      ) {\n        data.data.common.group = {\n          anonymous: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n          normal: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n          basic: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n          standard: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n          pro: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n          admin: {\n            buy_price: 1,\n            consume_price: 1,\n            description: \"\",\n          },\n        };\n      }\n\n      const rt = (data.data.general.realtime = data.data.general.realtime || {});\n      const ws = (rt.ws = rt.ws || {});\n      ws.buffer_size = typeof ws.buffer_size === \"number\" && ws.buffer_size > 0 ? ws.buffer_size : 1;\n      ws.aggregate = typeof ws.aggregate === \"boolean\" ? ws.aggregate : true;\n      ws.aggregate_window_ms = typeof ws.aggregate_window_ms === \"number\" && ws.aggregate_window_ms > 0 ? ws.aggregate_window_ms : 20;\n\n      const at = (data.data.auto_title = data.data.auto_title || {\n        enabled: false,\n        model: \"\",\n        max_len: 50,\n        min_msgs: 6,\n        overwrite: false,\n        prompt: \"\",\n      });\n      at.enabled = !!at.enabled;\n      at.model = at.model || \"\";\n      at.max_len = typeof at.max_len === \"number\" && at.max_len > 0 ? at.max_len : 50;\n      at.min_msgs = typeof at.min_msgs === \"number\" && at.min_msgs > 0 ? at.min_msgs : 6;\n      at.overwrite = !!at.overwrite;\n      at.prompt = at.prompt || \"\";\n    }\n\n    return data;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function setConfig(config: SystemProps): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/config/update`, config);\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\ntype UploadResponse = CommonResponse & {\n  url?: string;\n};\n\nexport async function uploadFavicon(file: File): Promise<UploadResponse> {\n  try {\n    const formData = new FormData();\n    formData.append(\"file\", file);\n\n    const response = await axios.post(`/admin/favicon/upload`, formData, {\n      headers: {\n        \"Content-Type\": \"multipart/form-data\",\n      },\n    });\n    return response.data as UploadResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function uploadResource(file: File): Promise<UploadResponse> {\n  try {\n    const formData = new FormData();\n    formData.append(\"file\", file);\n\n    const response = await axios.post(`/admin/resource/upload`, formData, {\n      headers: {\n        \"Content-Type\": \"multipart/form-data\",\n      },\n    });\n\n    const data = response.data as UploadResponse;\n    if (data.status) {\n      data.url = backendEndpoint + data.url;\n    }\n\n    return data;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function updateRootPassword(\n  password: string,\n): Promise<CommonResponse> {\n  try {\n    const response = await axios.post(`/admin/user/root`, { password });\n    return response.data as CommonResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function testWebSearching(\n  query: string,\n): Promise<TestWebSearchResponse> {\n  try {\n    const response = await axios.get(\n      `/admin/config/test/search?query=${encodeURIComponent(query)}`,\n    );\n    return response.data as TestWebSearchResponse;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), result: \"\" };\n  }\n}\n\nexport enum AuditTypes {\n  None = \"none\",\n  Dict = \"dict\",\n  Regex = \"regex\",\n  Baidu = \"baidu\",\n  Custom = \"custom\",\n}\n\nexport const auditTypes: string[] = [\n  AuditTypes.None,\n  AuditTypes.Dict,\n  AuditTypes.Regex,\n  AuditTypes.Baidu,\n  AuditTypes.Custom,\n];\n\nexport const commonWhiteList: string[] = [\n  \"gmail.com\",\n  \"outlook.com\",\n  \"yahoo.com\",\n  \"hotmail.com\",\n  \"foxmail.com\",\n  \"icloud.com\",\n  \"qq.com\",\n  \"163.com\",\n  \"126.com\",\n];\n"
  },
  {
    "path": "app/src/admin/channel.ts",
    "content": "import { getUniqueList } from \"@/utils/base.ts\";\nimport {\n  AnonymousType,\n  BasicType,\n  NormalType,\n  ProType,\n  StandardType,\n} from \"@/utils/groups.ts\";\n\nexport type Channel = {\n  id: number;\n  name: string;\n  type: string;\n  models: string[];\n  priority: number;\n  weight: number;\n  retry: number;\n  secret: string;\n  endpoint: string;\n  mapper: string;\n  state: boolean;\n  group?: string[];\n  proxy?: {\n    proxy: string;\n    proxy_type: number;\n    username: string;\n    password: string;\n  };\n  first_message_as_user?: boolean;\n  merge_consecutive_user_messages?: boolean;\n};\n\nexport enum proxyType {\n  NoneProxy = 0,\n  HttpProxy = 1,\n  HttpsProxy = 2,\n  Socks5Proxy = 3,\n}\n\nexport const ProxyTypes: Record<number, string> = {\n  [proxyType.NoneProxy]: \"None Proxy\",\n  [proxyType.HttpProxy]: \"HTTP Proxy\",\n  [proxyType.HttpsProxy]: \"HTTPS Proxy\",\n  [proxyType.Socks5Proxy]: \"SOCKS5 Proxy\",\n};\n\nexport type ChannelInfo = {\n  description?: string;\n  endpoint: string;\n  format: string;\n  models: string[];\n};\n\nexport const ChannelTypes: Record<string, string> = {\n  openai: \"OpenAI\",\n  azure: \"Azure OpenAI\",\n  claude: \"Anthropic Claude\",\n  palm: \"Google Gemini\",\n  midjourney: \"Midjourney Proxy\",\n  sparkdesk: \"讯飞星火 SparkDesk\",\n  chatglm: \"智谱清言 ChatGLM\",\n  moonshot: \"月之暗面 Moonshot\",\n  qwen: \"通义千问 TongYi\",\n  hunyuan: \"腾讯混元 Hunyuan\",\n  zhinao: \"360智脑 360GLM\",\n  baichuan: \"百川大模型 BaichuanAI\",\n  skylark: \"云雀大模型 SkylarkLLM\",\n  groq: \"Groq Cloud\",\n  bing: \"New Bing\",\n  slack: \"Slack Claude\",\n  deepseek: \"深度求索 DeepSeek\",\n  coze: \"扣子 Coze\",\n  dify: \"Dify\",\n};\n\nexport const ShortChannelTypes: Record<string, string> = {\n  openai: \"OpenAI\",\n  azure: \"Azure\",\n  claude: \"Claude\",\n  palm: \"Gemini\",\n  midjourney: \"Midjourney\",\n  sparkdesk: \"讯飞星火\",\n  chatglm: \"ChatGLM\",\n  moonshot: \"Moonshot\",\n  qwen: \"通义千问\",\n  hunyuan: \"腾讯混元\",\n  zhinao: \"360 智脑\",\n  baichuan: \"百川 AI\",\n  skylark: \"火山方舟\",\n  groq: \"Groq\",\n  bing: \"Bing\",\n  slack: \"Slack\",\n  deepseek: \"DeepSeek\",\n  coze: \"Coze\",\n  dify: \"Dify\",\n};\n\nexport const ChannelInfos: Record<string, ChannelInfo> = {\n  openai: {\n    endpoint: \"https://api.openai.com\",\n    format: \"<api-key>\",\n    models: [\n      \"gpt-3.5-turbo\",\n      \"gpt-3.5-turbo-instruct\",\n      \"gpt-3.5-turbo-0613\",\n      \"gpt-3.5-turbo-0301\",\n      \"gpt-3.5-turbo-1106\",\n      \"gpt-3.5-turbo-0125\",\n      \"gpt-3.5-turbo-16k\",\n      \"gpt-3.5-turbo-16k-0613\",\n      \"gpt-3.5-turbo-16k-0301\",\n      \"gpt-4\",\n      \"gpt-4-0314\",\n      \"gpt-4-0613\",\n      \"gpt-4-1106-preview\",\n      \"gpt-4-0125-preview\",\n      \"gpt-4-turbo-preview\",\n      \"gpt-4-vision-preview\",\n      \"gpt-4-1106-vision-preview\",\n      \"gpt-4-turbo\",\n      \"gpt-4-turbo-2024-04-09\",\n      \"gpt-4-32k\",\n      \"gpt-4-32k-0314\",\n      \"gpt-4-32k-0613\",\n      \"gpt-4o\",\n      \"gpt-4o-2024-05-13\",\n      \"gpt-4o-mini\",\n      \"gpt-4o-2024-08-06\",\n      \"gpt-4o-mini-2024-07-18\",\n      \"dalle\",\n      \"dall-e-2\",\n      \"dall-e-3\",\n    ],\n  },\n  azure: {\n    endpoint: \"2023-12-01-preview\",\n    format: \"<api-key>|<api-endpoint>\",\n    description:\n      \"> Azure 密钥 API Key 1 和 API Key 2 任填一个即可，密钥格式为 **api-key|api-endpoint**, api-endpoint 为 Azure 的 **API 端点**。\\n\" +\n      \"> 接入点填 **API Version**，如 2023-12-01-preview。\\n\" +\n      \"Azure 模型名称忽略点号等问题内部已经进行适配，无需额外任何设置。\",\n    models: [\n      \"gpt-3.5-turbo\",\n      \"gpt-3.5-turbo-instruct\",\n      \"gpt-3.5-turbo-0613\",\n      \"gpt-3.5-turbo-0301\",\n      \"gpt-3.5-turbo-1106\",\n      \"gpt-3.5-turbo-0125\",\n      \"gpt-3.5-turbo-16k\",\n      \"gpt-3.5-turbo-16k-0613\",\n      \"gpt-3.5-turbo-16k-0301\",\n      \"gpt-4\",\n      \"gpt-4-0314\",\n      \"gpt-4-0613\",\n      \"gpt-4-1106-preview\",\n      \"gpt-4-0125-preview\",\n      \"gpt-4-turbo-preview\",\n      \"gpt-4-vision-preview\",\n      \"gpt-4-1106-vision-preview\",\n      \"gpt-4-turbo\",\n      \"gpt-4-turbo-2024-04-09\",\n      \"gpt-4-32k\",\n      \"gpt-4-32k-0314\",\n      \"gpt-4-32k-0613\",\n      \"dalle\",\n      \"dall-e-2\",\n      \"dall-e-3\",\n    ],\n  },\n  claude: {\n    endpoint: \"https://api.anthropic.com\",\n    format: \"<x-api-key>\",\n    description:\n      \"> Anthropic Claude 密钥即为 **x-api-key**，Anthropic 对请求 IP 地域有限制，可能出现 **Request not allowed** 的错误，请尝试更换 IP 或者使用代理。\\n\",\n    models: [\n      \"claude-instant-1.2\",\n      \"claude-2\",\n      \"claude-2.1\",\n      \"claude-3-opus-20240229\",\n      \"claude-3-sonnet-20240229\",\n      \"claude-3-haiku-20240307\",\n    ],\n  },\n  slack: {\n    endpoint: \"your-channel\",\n    format: \"<bot-id>|<xoxp-token>\",\n    models: [\"claude-slack\"],\n    description:\n      \"> **注意！当前个人免费版 Slack 已不支持 Claude 调用。** \\n\" +\n      \"> 密钥请填写 bot-id|xoxp-token，其中 bot-id 为 Slack Bot 的 ID，xoxp-token 为 Slack Bot 的 xoxp-token \\n\" +\n      \"> 接入点填写你的 Slack Channel 名称，如 *chatnio* \\n\" +\n      \"> 详情参考 [claude-api](https://github.com/bincooo/claude-api) \\n\",\n  },\n  sparkdesk: {\n    endpoint: \"wss://spark-api.xf-yun.com\",\n    format: \"<app-id>|<api-secret>|<api-key>\",\n    models: [\n      \"spark-desk-v1.5\",\n      \"spark-desk-v2\",\n      \"spark-desk-v3\",\n      \"spark-desk-v3.5\",\n    ],\n  },\n  chatglm: {\n    endpoint: \"https://open.bigmodel.cn\",\n    format: \"<api-key>\",\n    models: [\"glm-4\", \"glm-4v\", \"glm-3-turbo\"],\n    description:\n      \"> 智谱 ChatGLM 密钥格式为 **api-key**，接入点填写 *https://open.bigmodel.cn* \\n\",\n  },\n  qwen: {\n    endpoint: \"https://dashscope.aliyuncs.com\",\n    format: \"<api-key>\",\n    models: [\"qwen-turbo\", \"qwen-plus\", \"qwen-turbo-net\", \"qwen-plus-net\"],\n  },\n  hunyuan: {\n    endpoint: \"https://hunyuan.cloud.tencent.com\",\n    format: \"<app-id>|<secret-id>|<secret-key>\",\n    models: [\"hunyuan\"],\n    // endpoint\n  },\n  zhinao: {\n    endpoint: \"https://api.360.cn\",\n    format: \"<api-key>\",\n    models: [\"360-gpt-v9\"],\n  },\n  baichuan: {\n    endpoint: \"https://api.baichuan-ai.com\",\n    format: \"<api-key>\",\n    models: [\"baichuan-53b\"],\n  },\n  skylark: {\n    endpoint: \"https://ark.cn-beijing.volces.com/api/v3\",\n    format: \"<access-key>|<secret-key>\",\n    models: [\n      \"skylark-lite-public\",\n      \"skylark-plus-public\",\n      \"skylark-pro-public\",\n      \"skylark-chat\",\n    ],\n    description:\n      \"> Skylark 格式密钥请填写获取到的 ak|sk 或 apikey \\n\" +\n      \"> 接入点填写生成的接入点，如 *https://ark.cn-beijing.volces.com/api/v3* \\n\" +\n      \"> Skylark API 的地域字段无需手动填写，系统会自动根据接入点获取 \\n\",\n  },\n  bing: {\n    endpoint: \"wss://your.bing.service\",\n    format: \"<secret>\",\n    models: [\"bing-creative\", \"bing-balanced\", \"bing-precise\"],\n    description:\n      \"> New Bing 服务搭建详情请参考 [chatnio-bing-service](https://github.com/coaidev/chatnio-bing-service) \\n \" +\n      \"> bing2api (如 [bingo](https://github.com/weaigc/bingo)) 可直接使用 **OpenAI** 格式而非 **New Bing** 格式 \\n \" +\n      \"> 接入点填写你部署的站点即可，如 *http://localhost:8765* \",\n  },\n  palm: {\n    endpoint: \"https://generativelanguage.googleapis.com\",\n    format: \"<api-key>\",\n    models: [\n      \"chat-bison-001\",\n      \"gemini-pro\",\n      \"gemini-pro-vision\",\n      \"gemini-1.5-pro-latest\",\n      \"gemini-1.5-flash-latest\",\n    ],\n    description:\n      \"> Google Gemini / PaLM2 密钥格式为 **api-key**，接入点填写 *https://generativelanguage.googleapis.com* 或其反代地址 \\n\" +\n      \"> Google 对请求 IP 地域有限制，可能出现 **User Location Is Not Supported** 的错误，可以看运气通过反代解决。 \\n\" +\n      \"> Gemini Pro 的返回结果一次性而非流式（即使 `streamGenerateContent` 接口也为假流式），系统内部做了平滑伪流式处理，但仍然无法从根本解决 Gemini Pro 自身假流式的特性。\\n\",\n  },\n  midjourney: {\n    endpoint: \"https://your.midjourney.proxy\",\n    format: \"<mj-api-secret>|<white-list>\",\n    models: [\"midjourney\", \"midjourney-fast\", \"midjourney-turbo\"],\n    description:\n      \"> 请参考 [midjourney-proxy](https://github.com/novicezk/midjourney-proxy) 项目填入参数，可设置白名单 *white-list* 以限制回调 IP \\n\" +\n      \"> 密钥举例： password|localhost,127.0.0.1,196.128.0.31\\n\" +\n      \"> 密钥即为 *mj-api-secret* （如果没有设置 secret 请填 `null` ） \\n\" +\n      \"> 白名单即为 *white-list*（如果没有回调 IP 白名单默认接收所有 IP 的回调，不需要加 | 以及后面的内容） \\n\" +\n      \"> 接入点填写你的 Midjourney Proxy 的部署地址，如 *http://localhost:8080*, *https://example.com* \\n\" +\n      \"> 注意：**请在系统设置中设置后端的公网 IP / 域名，否则无法接收回调报错 please provide available notify url** \\n\",\n  },\n  moonshot: {\n    endpoint: \"https://api.moonshot.cn\",\n    format: \"<api-key>\",\n    models: [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"],\n  },\n  groq: {\n    endpoint: \"https://api.groq.com/openai\",\n    format: \"<api-key>\",\n    models: [\"llama2-70b-4096\", \"mixtral-8x7b-32768\", \"gemma-7b-it\"],\n  },\n  deepseek: {\n    endpoint: \"https://api.deepseek.com\",\n    format: \"<api-key>\",\n    models: [\"deepseek-chat\", \"deepseek-reasoner\"],\n  },\n  coze: {\n    endpoint: \"https://api.coze.cn\",\n    format: \"<api-key>\",\n    models: [\"\"],\n    description:\n      \"> 扣子 Coze 的模型名称即为 Coze 平台的 **bot_id** \\n\" +\n      \"> 进入智能体的开发页面，开发页面 URL 中 bot 参数后的数字就是智能体 ID \\n\" +\n      \"> 例如 [https://www.coze.cn/space/341****/bot/73428668*****](https://www.coze.cn/space/341****/bot/73428668*****)，智能体 ID 为 73428668***** \\n\" +\n      \"> 确保当前使用的访问密钥已被授予智能体所属空间的 chat 权限 \\n\" +\n      \"> 如果需要让系统自动适配扣子 Coze 平台的图标，请在 **模型映射** 中将 **bot_id** 映射为 **coze** 开头的模型，如 coze-chat>73428668***** \\n\",\n  },\n  dify: {\n    endpoint: \"https://api.dify.ai/v1\",\n    format: \"<api-key>\",\n    models: [\"\"],\n    description:\n      \"> 由于 Dify 平台一个 Key 对应一个 CHATFLOW （模型），所以模型名称仅在用户调用本系统时用于标识用户调用的对象，不代表调用 Dify 平台 CHATFLOW 时被调用 CHATFLOW 的名称 \\n\" +\n      \"> 因此，您需要为每一个 Dify 平台的 CHATFLOW 分别创建渠道 \\n\" +\n      \"> 如果需要让系统自动适配 Dify 平台的图标，请将模型名称填写为 **dify** 开头的模型，如 **dify-chat** \\n\",\n  },\n};\n\nexport const defaultChannelModels: string[] = getUniqueList(\n  Object.values(ChannelInfos).flatMap((info) => info.models),\n);\n\nexport const channelGroups: string[] = [\n  AnonymousType,\n  NormalType,\n  BasicType,\n  StandardType,\n  ProType,\n];\n\nexport function getChannelInfo(type?: string): ChannelInfo {\n  if (type && type in ChannelInfos) return ChannelInfos[type];\n  return ChannelInfos.openai;\n}\n\nexport function getChannelType(type?: string): string {\n  if (type && type in ChannelTypes) return ChannelTypes[type];\n  return ChannelTypes.openai;\n}\n\nexport function getShortChannelType(type?: string): string {\n  if (type && type in ShortChannelTypes) return ShortChannelTypes[type];\n  return ShortChannelTypes.openai;\n}\n"
  },
  {
    "path": "app/src/admin/charge.ts",
    "content": "export const tokenBilling = \"token-billing\";\nexport const timesBilling = \"times-billing\";\nexport const nonBilling = \"non-billing\";\n\nexport const defaultChargeType = tokenBilling;\nexport const chargeTypes = [nonBilling, timesBilling, tokenBilling];\nexport type ChargeType = (typeof chargeTypes)[number];\n\nexport type ChargeBaseProps = {\n  type: string;\n  anonymous: boolean;\n  input: number;\n  output: number;\n};\n\nexport type ChargeProps = ChargeBaseProps & {\n  id: number;\n  models: string[];\n};\n"
  },
  {
    "path": "app/src/admin/colors.ts",
    "content": "export const modelColorMapper: Record<string, string> = {\n  // OpenAI & Azure OpenAI\n  \"gpt-3.5-turbo\": \"green-500\",\n  \"gpt-3.5-turbo-16k\": \"green-600\",\n  \"gpt-4\": \"purple-600\",\n\n  dalle: \"green-600\",\n  \"dall-e-2\": \"green-600\",\n  \"dall-e-3\": \"purple-700\",\n\n  whisper: \"gray-300\",\n  tts: \"gray-300\",\n  openai: \"gray-300\",\n  azure: \"gray-300\",\n\n  // Anthropic Claude\n  \"claude-3\": \"orange-500\",\n  claude: \"orange-400\",\n  anthropic: \"orange-400\",\n\n  // Spark Desk\n  \"spark-desk\": \"blue-400\",\n  sparkdesk: \"blue-400\",\n\n  // Moonshot\n  moonshot: \"black-500\",\n  kimi: \"black-500\",\n\n  // Midjourney\n  midjourney: \"indigo-600\",\n  \"mid-journey\": \"indigo-600\",\n  niji: \"indigo-600\",\n\n  // Stable Diffusion\n  \"stable-diffusion\": \"gray-400\",\n  stablediffusion: \"gray-400\",\n  stability: \"gray-400\",\n\n  // Groq Cloud\n  \"llama2-70b-4096\": \"red-500\",\n  \"mixtral-8x7b-32768\": \"red-500\",\n  \"gemma-7b-it\": \"red-500\",\n\n  // Google Gemini & Gemma\n  \"chat-bison-001\": \"red-500\",\n  palm: \"red-500\",\n  gemini: \"red-500\",\n  gemma: \"red-500\",\n\n  // DeepSeek\n  deepseek: \"blue-700\",\n\n  // New Bing\n  bing: \"blue-700\",\n\n  // ChatGLM\n  zhipu: \"lime-500\",\n  glm: \"lime-500\",\n\n  // Tongyi Qwen\n  qwen: \"indigo-600\",\n  tongyi: \"indigo-600\",\n\n  // Meta LLaMA\n  llama: \"sky-400\",\n\n  // Tencent Hunyuan\n  hunyuan: \"blue-500\",\n\n  // 360 GPT\n  \"360\": \"stone-500\",\n\n  // Baichuan AI\n  baichuan: \"orange-700\",\n\n  // ByteDance Skylark / Doubao / Coze\n  skylark: \"sky-300\",\n  doubao: \"sky-300\",\n  coze: \"sky-300\",\n\n  // Dify\n  dify: \"gray-300\",\n\n  // OpenRouter\n  openrouter: \"purple-600\",\n};\n\nconst unknownColors = [\n  \"gray-700\",\n  \"indigo-600\",\n  \"green-500\",\n  \"green-600\",\n  \"purple-600\",\n  \"purple-700\",\n  \"orange-400\",\n  \"blue-400\",\n  \"red-500\",\n  \"blue-700\",\n  \"lime-500\",\n  \"sky-400\",\n  \"stone-500\",\n  \"orange-700\",\n  \"sky-300\",\n];\n\nexport function getUnknownModelColor(model: string): string {\n  const char = model.length > 0 ? model[0] : \"A\";\n  const code = char.charCodeAt(0);\n\n  return unknownColors[code % unknownColors.length];\n}\n\nexport function getModelColor(model: string): string {\n  for (const key in modelColorMapper) {\n    if (model.toLowerCase().includes(key)) {\n      return modelColorMapper[key];\n    }\n  }\n\n  return getUnknownModelColor(model);\n}\n"
  },
  {
    "path": "app/src/admin/datasets/charge.ts",
    "content": "import {\n  ChargeProps,\n  ChargeType,\n  timesBilling,\n  tokenBilling,\n} from \"@/admin/charge.ts\";\n\nexport enum Currency {\n  CNY = \"CNY\",\n  USD = \"USD\",\n}\n\nexport type PricingItem = {\n  models: string[];\n  input?: number;\n  output: number;\n  currency?: Currency;\n  billing_type?: ChargeType;\n};\n\nexport type PricingDataset = PricingItem[];\n\nexport const pricing: PricingDataset = [\n  {\n    models: [\n      \"gpt-3.5-turbo\",\n      \"gpt-3.5-turbo-0301\",\n      \"gpt-3.5-turbo-0613\",\n      \"gpt-3.5-turbo-instruct\",\n    ],\n    input: 0.0015,\n    output: 0.002,\n  },\n  {\n    models: [\"gpt-3.5-turbo-1106\"],\n    input: 0.001,\n    output: 0.002,\n  },\n  {\n    models: [\"gpt-3.5-turbo-0125\"],\n    input: 0.0005,\n    output: 0.0015,\n  },\n  {\n    models: [\n      \"gpt-3.5-turbo-16k\",\n      \"gpt-3.5-turbo-16k-0301\",\n      \"gpt-3.5-turbo-16k-0613\",\n    ],\n    input: 0.003,\n    output: 0.004,\n  },\n  {\n    models: [\"gpt-4\", \"gpt-4-0314\", \"gpt-4-0613\"],\n    input: 0.03,\n    output: 0.06,\n  },\n  {\n    models: [\n      \"gpt-4-1106-preview\",\n      \"gpt-4-0125-preview\",\n      \"gpt-4-turbo-preview\",\n      \"gpt-4-1106-vision-preview\",\n      \"gpt-4-vision-preview\",\n      \"gpt-4-turbo\",\n      \"gpt-4-turbo-2024-04-09\",\n    ],\n    input: 0.01,\n    output: 0.03,\n  },\n  {\n    models: [\"gpt-4o\", \"gpt-4o-2024-05-13\"],\n    input: 0.005,\n    output: 0.015,\n  },\n  {\n    models: [\"gpt-4o-2024-08-06\"],\n    input: 0.0025,\n    output: 0.01,\n  },\n  {\n    models: [\"gpt-4o-mini\", \"gpt-4o-mini-2024-07-18\"],\n    input: 0.00015,\n    output: 0.0006,\n  },\n  {\n    models: [\"gpt-4-32k\", \"gpt-4-32k-0314\", \"gpt-4-32k-0613\"],\n    input: 0.06,\n    output: 0.12,\n  },\n  {\n    models: [\"dalle\", \"dall-e-2\"], // dall-e-2 512x512 size\n    output: 0.018,\n    billing_type: timesBilling,\n  },\n  {\n    models: [\"dall-e-3\"], // dall-e-3 HD 1024x1024 size\n    output: 0.08,\n    billing_type: timesBilling,\n  },\n  {\n    models: [\n      \"claude-1\",\n      \"claude-1-100k\",\n      \"claude-1.2\",\n      \"claude-1.3\",\n      \"claude-instant\",\n      \"claude-instant-1.2\",\n      \"claude-slack\",\n    ],\n    input: 0.0008,\n    output: 0.0024,\n    // input: $0.8/1m tokens, output: $2.4/1m tokens\n  },\n  {\n    models: [\"claude-2\", \"claude-2-100k\", \"claude-2.1\"],\n    input: 0.008,\n    output: 0.024,\n  },\n  // claude 3 haiku $0.25/1m tokens input & $1.25/1m tokens output\n  {\n    models: [\"claude-3-haiku-20240307\"],\n    input: 0.00025,\n    output: 0.00125,\n  },\n  // claude 3 sonnet $3/1m tokens input & $15/1m tokens output\n  {\n    models: [\"claude-3-sonnet-20240229\"],\n    input: 0.003,\n    output: 0.015,\n  },\n  // claude 3 sonnet $15/1m tokens input & $75/1m tokens output\n  {\n    models: [\"claude-3-opus-20240229\"],\n    input: 0.015,\n    output: 0.075,\n  },\n  {\n    models: [\"midjourney\"],\n    output: 0.1,\n    currency: Currency.CNY,\n    billing_type: timesBilling,\n  },\n  {\n    models: [\"midjourney-fast\"],\n    output: 0.2,\n    currency: Currency.CNY,\n    billing_type: timesBilling,\n  },\n  {\n    models: [\"midjourney-turbo\"],\n    output: 0.5,\n    currency: Currency.CNY,\n    billing_type: timesBilling,\n  },\n  {\n    models: [\"spark-desk-v1.5\"],\n    input: 0.015,\n    output: 0.015,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"spark-desk-v2\", \"spark-desk-v3\", \"spark-desk-v3.5\"],\n    input: 0.03,\n    output: 0.03,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"moonshot-v1-8k\"],\n    input: 0.012,\n    output: 0.012,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"moonshot-v1-32k\"],\n    input: 0.024,\n    output: 0.024,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"moonshot-v1-128k\"],\n    input: 0.06,\n    output: 0.06,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"glm-4\", \"glm-4v\"],\n    input: 0.1,\n    output: 0.1,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\n      \"zhipu-chatglm-lite\",\n      \"zhipu-chatglm-std\",\n      \"zhipu-chatglm-turbo\",\n      \"glm-3-turbo\",\n    ],\n    input: 0.005,\n    output: 0.005,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"zhipu-chatglm-pro\"],\n    input: 0.01,\n    output: 0.01,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"qwen-plus\", \"qwen-plus-net\"],\n    input: 0.02,\n    output: 0.02,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"qwen-turbo\", \"qwen-turbo-net\"],\n    input: 0.008,\n    output: 0.008,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"chat-bison-001\"], // free marked as $0.001\n    output: 0.001,\n  },\n  {\n    models: [\n      \"gemini-pro\",\n      \"gemini-pro-vision\",\n      \"gemini-1.5-pro-latest\",\n      \"gemini-1.5-flash-latest\",\n    ],\n    input: 0.000125,\n    output: 0.000375,\n  },\n  {\n    models: [\"hunyuan\"],\n    input: 0.1,\n    output: 0.1,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"deepseek-chat\", \"deepseek-coder\"],\n    input: 0.001,\n    output: 0.002,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"360-gpt-v9\"],\n    input: 0.12,\n    output: 0.12,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"baichuan-53b\"],\n    input: 0.02,\n    output: 0.02,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"skylark-lite-public\"],\n    input: 0.004,\n    output: 0.004,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"skylark-plus-public\"],\n    input: 0.008,\n    output: 0.008,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"skylark-pro-public\", \"skylark-chat\"],\n    input: 0.011,\n    output: 0.011,\n    currency: Currency.CNY,\n  },\n  {\n    models: [\"llama2-70b-4096\", \"mixtral-8x7b-32768\", \"gemma-7b-it\"],\n    output: 0.001, // free marked as $0.001\n    currency: Currency.USD,\n  },\n];\n\nconst countPricing = (\n  _price?: number,\n  _currency?: Currency,\n  usd?: number,\n): number => {\n  const price = _price ?? 0;\n  const currency = _currency ?? Currency.USD;\n\n  switch (currency) {\n    case Currency.CNY:\n      return price * 10; // 1 cny = 10 quota\n    case Currency.USD:\n      return price * 10 * (usd ?? 1);\n    default:\n      return countPricing(price, Currency.USD, usd);\n  }\n};\n\nexport const getPricing = (currency: number): ChargeProps[] =>\n  pricing.map(\n    (item, index): ChargeProps => ({\n      id: index,\n      models: item.models,\n      type: item.billing_type ?? tokenBilling,\n      anonymous: false,\n      input: countPricing(item.input, item.currency, currency),\n      output: countPricing(item.output, item.currency, currency),\n    }),\n  );\n"
  },
  {
    "path": "app/src/admin/hook.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { getUniqueList } from \"@/utils/base.ts\";\nimport { defaultChannelModels } from \"@/admin/channel.ts\";\nimport { getApiMarket, getApiModels } from \"@/api/v1.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { Model } from \"@/api/types.tsx\";\n\nexport type onStateChange<T> = (state: boolean, data?: T) => void;\n\nexport const useAllModels = (onStateChange?: onStateChange<string[]>) => {\n  const [allModels, setAllModels] = useState<string[]>([]);\n\n  const update = async () => {\n    onStateChange?.(false, allModels);\n    const models = await getApiModels();\n    onStateChange?.(true, models.data);\n\n    setAllModels(models.data);\n  };\n\n  useEffectAsync(update, []);\n\n  return {\n    allModels,\n    update,\n  };\n};\n\nexport const useChannelModels = (onStateChange?: onStateChange<string[]>) => {\n  const { allModels, update } = useAllModels(onStateChange);\n\n  const channelModels = useMemo(\n    () => getUniqueList([...allModels, ...defaultChannelModels]),\n    [allModels],\n  );\n\n  return {\n    channelModels,\n    allModels,\n    update,\n  };\n};\n\nexport const useSupportModels = (onStateChange?: onStateChange<Model[]>) => {\n  const [supportModels, setSupportModels] = useState<Model[]>([]);\n\n  const update = async () => {\n    onStateChange?.(false, supportModels);\n    const market = await getApiMarket();\n    onStateChange?.(true, market);\n\n    setSupportModels(market);\n  };\n\n  useEffectAsync(update, []);\n\n  return {\n    supportModels,\n    update,\n  };\n};\n"
  },
  {
    "path": "app/src/admin/market.ts",
    "content": "export const marketEditableTags = [\n  \"official\",\n  \"multi-modal\",\n  \"web\",\n  \"high-quality\",\n  \"high-price\",\n  \"open-source\",\n  \"image-generation\",\n  \"unstable\",\n];\n\nexport const deprecatedModelImages = [\n  \"gpt35turbo.png\",\n  \"gpt35turbo16k.webp\",\n  \"gpt4.png\",\n  \"gpt432k.webp\",\n  \"gpt4v.png\",\n  \"gpt4dalle.png\",\n  \"claude.png\",\n  \"claude100k.png\",\n  \"stablediffusion.jpeg\",\n  \"llama2.webp\",\n  \"llamacode.webp\",\n  \"dalle.jpeg\",\n  \"midjourney.jpg\",\n  \"newbing.jpg\",\n  \"palm2.webp\",\n  \"gemini.jpeg\",\n  \"chatglm.png\",\n  \"tongyi.png\",\n  \"sparkdesk.jpg\",\n  \"hunyuan.png\",\n  \"360gpt.png\",\n  \"baichuan.png\",\n  \"skylark.jpg\",\n];\n\nexport const marketTags = [\"high-context\", ...marketEditableTags, \"free\"];\n"
  },
  {
    "path": "app/src/admin/types.ts",
    "content": "export type CommonResponse = {\n  status: boolean;\n  message: string;\n  error?: string;\n};\n\nexport type InfoResponse = {\n  billing_today: number;\n  billing_yesterday: number;\n  billing_month: number;\n  billing_last_month: number;\n  subscription_count: number;\n  online_chats: number;\n};\n\nexport type ModelChartResponse = {\n  date: string[];\n  value: {\n    model: string;\n    data: number[];\n  }[];\n};\n\nexport type RequestChartResponse = {\n  date: string[];\n  value: number[];\n};\n\nexport type BillingChartResponse = {\n  date: string[];\n  value: number[];\n};\n\nexport type ErrorChartResponse = {\n  date: string[];\n  value: number[];\n};\n\nexport type UserTypeChartResponse = {\n  total: number;\n  normal: number;\n  api_paid: number;\n  basic_plan: number;\n  standard_plan: number;\n  pro_plan: number;\n};\n\nexport type InvitationData = {\n  code: string;\n  quota: number;\n  type: string;\n  used: boolean;\n  username: string;\n  created_at: string;\n  updated_at: string;\n};\n\nexport type InvitationForm = {\n  data: InvitationData[];\n  total: number;\n};\n\nexport type InvitationResponse = {\n  status: boolean;\n  message: string;\n  data: InvitationData[];\n  total: number;\n};\n\nexport type Redeem = {\n  code: string;\n  quota: number;\n  used: boolean;\n  created_at: string;\n  updated_at: string;\n};\n\nexport type RedeemForm = {\n  data: Redeem[];\n  total: number;\n};\n\nexport type RedeemResponse = CommonResponse & {\n  data: Redeem[];\n  total: number;\n};\n\nexport type InvitationGenerateResponse = {\n  status: boolean;\n  data: string[];\n  message: string;\n};\n\nexport type RedeemGenerateResponse = {\n  status: boolean;\n  data: string[];\n  message: string;\n};\n\nexport type UserData = {\n  id: number;\n  username: string;\n  email: string;\n  is_banned: boolean;\n  is_admin: boolean;\n  quota: number;\n  used_quota: number;\n  is_subscribed: boolean;\n  total_month: number;\n  expired_at: string;\n  level: number;\n  enterprise: boolean;\n};\n\nexport type UserForm = {\n  data: UserData[];\n  total: number;\n};\n\nexport type UserResponse = {\n  status: boolean;\n  message: string;\n  data: UserData[];\n  total: number;\n};\n"
  },
  {
    "path": "app/src/api/addition.ts",
    "content": "import axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\ntype QuotaResponse = {\n  status: boolean;\n  error: string;\n};\n\ntype PackageResponse = {\n  status: boolean;\n  cert: boolean;\n  teenager: boolean;\n};\n\ntype SubscriptionResponse = {\n  status: boolean;\n  is_subscribed: boolean;\n  expired: number;\n  enterprise?: boolean;\n  usage: Record<string, number>;\n  level: number;\n  expired_at?: string;\n  refresh?: number;\n  refresh_at?: string;\n};\n\ntype BuySubscriptionResponse = {\n  status: boolean;\n  error: string;\n};\n\ntype ApiKeyResponse = {\n  status: boolean;\n  key: string;\n};\n\ntype ResetApiKeyResponse = {\n  status: boolean;\n  key: string;\n  error: string;\n};\n\nexport async function buyQuota(quota: number): Promise<QuotaResponse> {\n  try {\n    const resp = await axios.post(`/buy`, { quota });\n    return resp.data as QuotaResponse;\n  } catch (e) {\n    console.debug(e);\n    return { status: false, error: \"network error\" };\n  }\n}\n\nexport async function getPackage(): Promise<PackageResponse> {\n  try {\n    const resp = await axios.get(`/package`);\n    if (resp.data.status === false) {\n      return { status: false, cert: false, teenager: false };\n    }\n    return {\n      status: resp.data.status,\n      cert: resp.data.data.cert,\n      teenager: resp.data.data.teenager,\n    };\n  } catch (e) {\n    console.debug(e);\n    return { status: false, cert: false, teenager: false };\n  }\n}\n\nexport async function getSubscription(): Promise<SubscriptionResponse> {\n  try {\n    const resp = await axios.get(`/subscription`);\n    if (resp.data.status === false) {\n      return {\n        status: false,\n        is_subscribed: false,\n        level: 0,\n        expired: 0,\n        usage: {},\n      };\n    }\n    return resp.data as SubscriptionResponse;\n  } catch (e) {\n    console.debug(e);\n    return {\n      status: false,\n      is_subscribed: false,\n      level: 0,\n      expired: 0,\n      usage: {},\n    };\n  }\n}\n\nexport async function buySubscription(\n  month: number,\n  level: number,\n): Promise<BuySubscriptionResponse> {\n  try {\n    const resp = await axios.post(`/subscribe`, { level, month });\n    return resp.data as BuySubscriptionResponse;\n  } catch (e) {\n    console.debug(e);\n    return { status: false, error: \"network error\" };\n  }\n}\n\nexport async function getKey(): Promise<ApiKeyResponse> {\n  try {\n    const resp = await axios.get(`/apikey`);\n    if (resp.data.status === false) {\n      return { status: false, key: \"\" };\n    }\n    return {\n      status: resp.data.status,\n      key: resp.data.key,\n    };\n  } catch (e) {\n    console.debug(e);\n    return { status: false, key: \"\" };\n  }\n}\n\nexport async function regenerateKey(): Promise<ResetApiKeyResponse> {\n  try {\n    const resp = await axios.post(`/resetkey`);\n    return resp.data as ResetApiKeyResponse;\n  } catch (e) {\n    console.debug(e);\n    return { status: false, key: \"\", error: getErrorMessage(e) };\n  }\n}\n"
  },
  {
    "path": "app/src/api/auth.ts",
    "content": "import axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { isEmailValid } from \"@/utils/form.ts\";\nimport { toast } from \"sonner\";\n\nexport type LoginForm = {\n  username: string;\n  password: string;\n};\n\nexport type DeepLoginForm = {\n  token: string;\n};\n\nexport type LoginResponse = {\n  status: boolean;\n  error: string;\n  token: string;\n};\n\nexport type StateResponse = {\n  status: boolean;\n  user: string;\n  admin: boolean;\n};\n\nexport type RegisterForm = {\n  username: string;\n  password: string;\n  repassword: string;\n  email: string;\n  code: string;\n};\n\nexport type RegisterResponse = {\n  status: boolean;\n  error: string;\n  token: string;\n};\n\nexport type VerifyForm = {\n  email: string;\n};\n\nexport type VerifyResponse = {\n  status: boolean;\n  error: string;\n};\n\nexport type ResetForm = {\n  email: string;\n  code: string;\n  password: string;\n  repassword: string;\n};\n\nexport type ResetResponse = {\n  status: boolean;\n  error: string;\n};\n\nexport type UserInfo = {\n  id: number;\n  register_days: number;\n  used_quota: number;\n  plan_total_month: number;\n  email: string;\n};\n\nexport type UserInfoResponse = {\n  status: boolean;\n  error: string;\n  data: UserInfo;\n};\n\nexport async function doLogin(\n  data: DeepLoginForm | LoginForm,\n): Promise<LoginResponse> {\n  const response = await axios.post(\"/login\", data);\n  return response.data as LoginResponse;\n}\n\nexport async function doState(): Promise<StateResponse> {\n  const response = await axios.post(\"/state\");\n  return response.data as StateResponse;\n}\n\nexport async function doRegister(\n  data: RegisterForm,\n): Promise<RegisterResponse> {\n  try {\n    const response = await axios.post(\"/register\", data);\n    return response.data as RegisterResponse;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n      token: \"\",\n    };\n  }\n}\n\nexport async function doVerify(\n  email: string,\n  checkout?: boolean,\n): Promise<VerifyResponse> {\n  try {\n    const response = await axios.post(\"/verify\", {\n      email,\n      checkout,\n    } as VerifyForm);\n    return response.data as VerifyResponse;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function doReset(data: ResetForm): Promise<ResetResponse> {\n  try {\n    const response = await axios.post(\"/reset\", data);\n    return response.data as ResetResponse;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function sendCode(\n  t: any,\n  email: string,\n  checkout?: boolean,\n): Promise<boolean> {\n  if (email.trim().length === 0 || !isEmailValid(email)) return false;\n\n  const res = await doVerify(email, checkout);\n  if (!res.status)\n    toast.error(t(\"auth.send-code-failed\"), {\n      description: t(\"auth.send-code-failed-prompt\", { reason: res.error }),\n    });\n  else\n    toast.info(t(\"auth.send-code-success\"), {\n      description: t(\"auth.send-code-success-prompt\"),\n    });\n\n  return res.status;\n}\n\nexport const initialUserInfo: UserInfo = {\n  id: 0,\n  register_days: 0,\n  used_quota: 0,\n  plan_total_month: 0,\n  email: \"\",\n};\n\nexport async function getUserInfo(): Promise<UserInfoResponse> {\n  try {\n    const response = await axios.get(\"/userinfo\");\n    return response.data as UserInfoResponse;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n      data: { ...initialUserInfo },\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/api/broadcast.ts",
    "content": "import axios from \"axios\";\nimport { getMemory, setMemory } from \"@/utils/memory.ts\";\n\nexport type Broadcast = {\n  content: string;\n  index: number;\n};\n\nexport type BroadcastInfo = Broadcast & {\n  poster: string;\n  created_at: string;\n};\n\nexport type BroadcastListResponse = {\n  data: BroadcastInfo[];\n};\n\nexport type CommonBroadcastResponse = {\n  status: boolean;\n  error: string;\n};\n\nexport async function getRawBroadcast(): Promise<Broadcast> {\n  try {\n    const data = await axios.get(\"/broadcast/view\");\n    if (data.data) return data.data as Broadcast;\n  } catch (e) {\n    console.warn(e);\n  }\n\n  return {\n    content: \"\",\n    index: 0,\n  };\n}\n\nexport type BroadcastEvent = {\n  message: string;\n  firstReceived: boolean;\n};\n\nexport async function getBroadcast(): Promise<BroadcastEvent> {\n  const data = await getRawBroadcast();\n  const content = data.content.trim();\n\n  if (content.length === 0)\n    return {\n      message: \"\",\n      firstReceived: false,\n    };\n\n  const memory = getMemory(\"broadcast\");\n  if (memory === content)\n    return {\n      message: content,\n      firstReceived: false,\n    };\n\n  setMemory(\"broadcast\", content);\n  return {\n    message: content,\n    firstReceived: true,\n  };\n}\n\nexport async function getBroadcastList(): Promise<BroadcastInfo[]> {\n  try {\n    const resp = await axios.get(\"/broadcast/list\");\n    const data = resp.data as BroadcastListResponse;\n    return data.data || [];\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function createBroadcast(\n  content: string,\n  notify_all?: boolean,\n): Promise<CommonBroadcastResponse> {\n  try {\n    const resp = await axios.post(\"/broadcast/create\", {\n      content,\n      notify_all,\n    });\n    return resp.data as CommonBroadcastResponse;\n  } catch (e) {\n    console.warn(e);\n    return {\n      status: false,\n      error: (e as Error).message,\n    };\n  }\n}\n\nexport async function removeBroadcast(\n  index: number,\n): Promise<CommonBroadcastResponse> {\n  try {\n    const resp = await axios.post(`/broadcast/remove/${index}`);\n    return resp.data as CommonBroadcastResponse;\n  } catch (e) {\n    console.warn(e);\n    return {\n      status: false,\n      error: (e as Error).message,\n    };\n  }\n}\n\nexport async function updateBroadcast(\n  id: number,\n  content: string,\n): Promise<CommonBroadcastResponse> {\n  try {\n    const resp = await axios.post(\"/broadcast/update\", { id, content });\n    return resp.data as CommonBroadcastResponse;\n  } catch (e) {\n    console.warn(e);\n    return {\n      status: false,\n      error: (e as Error).message,\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/api/common.ts",
    "content": "import { toast } from \"sonner\";\n\nexport type CommonResponse = {\n  status: boolean;\n  error?: string;\n  reason?: string;\n  message?: string;\n  data?: any;\n};\n\nexport function withNotify(\n  t: any,\n  state: CommonResponse,\n  toastSuccess?: boolean,\n  toastSuccessMessage?: string,\n) {\n  if (state.status)\n    toastSuccess &&\n      toast.success(t(\"success\"), {\n        description: toastSuccessMessage || t(\"request-success\"),\n      });\n  else\n    toast.error(t(\"error\"), {\n      description:\n        state.error ?? state.reason ?? state.message ?? \"error occurred\",\n    });\n}\n"
  },
  {
    "path": "app/src/api/connection.ts",
    "content": "import { tokenField, websocketEndpoint } from \"@/conf/bootstrap.ts\";\nimport { getMemory } from \"@/utils/memory.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { Mask } from \"@/masks/types.ts\";\n\nexport const endpoint = `${websocketEndpoint}/chat`;\nexport const maxRetry = 60; // 30s max websocket retry\nexport const maxConnection = 5;\n\nexport type StreamMessage = {\n  conversation?: number;\n  keyword?: string;\n  quota?: number;\n  message: string;\n  end: boolean;\n  plan?: boolean;\n  title?: string;\n  search_query?: {\n    type: string;\n    search_queries: string[];\n  };\n  search_result?: {\n    type: string;\n    search_results: Array<{\n      url: string;\n      title: string;\n      snippet: string;\n      published_at?: number;\n      site_name?: string;\n      site_icon?: string;\n    }>;\n  };\n  search_index?: {\n    type: string;\n    search_indexes: Array<{\n      url: string;\n      cite_index: number;\n    }>;\n  };\n  tool_call?: {\n    name: string;\n    arguments?: unknown;\n    result?: string;\n    error?: string;\n    status: \"start\" | \"executing\" | \"success\" | \"error\";\n  };\n  response_type?: string;\n};\n\nexport type ChatProps = {\n  type?: string;\n  message: string;\n  model: string;\n  web?: boolean;\n  web_search_mode?: \"quick\" | \"detailed\";\n  web_page_summary?: boolean;\n  think?: boolean;\n  context?: number;\n  ignore_context?: boolean;\n\n  // mcp related fields\n  enable_mcp?: boolean;\n  mcp_plugin_id?: number;\n\n  max_tokens?: number;\n  temperature?: number;\n  top_p?: number;\n  top_k?: number;\n  presence_penalty?: number;\n  frequency_penalty?: number;\n  repetition_penalty?: number;\n};\n\ntype StreamCallback = (id: number, message: StreamMessage) => void;\n\nexport class Connection {\n  protected connection?: WebSocket;\n  protected callback?: StreamCallback;\n  protected stack?: Record<string, any>;\n  public id: number;\n  public state: boolean;\n\n  public constructor(id: number, callback?: StreamCallback) {\n    this.state = false;\n    this.id = id;\n\n    callback && this.setCallback(callback);\n  }\n\n  public init(): void {\n    this.connection = new WebSocket(endpoint);\n    this.state = false;\n    this.connection.onopen = () => {\n      this.state = true;\n      this.send({\n        token: getMemory(tokenField) || \"anonymous\",\n        id: this.id,\n      });\n    };\n    this.connection.onclose = (event) => {\n      this.state = false;\n\n      this.stack = {\n        error: \"websocket connection failed\",\n        code: event.code,\n        reason: event.reason,\n        endpoint: endpoint,\n      };\n\n      setTimeout(() => {\n        console.debug(`[connection] reconnecting... (id: ${this.id})`);\n        this.init();\n      }, 3000);\n    };\n    this.connection.onmessage = (event) => {\n      const message = JSON.parse(event.data);\n      this.triggerCallback(message as StreamMessage);\n    };\n  }\n\n  public reconnect(): void {\n    this.init();\n  }\n\n  public send(data: Record<string, string | boolean | number>): boolean {\n    if (!this.state || !this.connection) {\n      if (this.connection === undefined) this.init();\n      console.debug(\"[connection] connection not ready, retrying in 500ms...\");\n      return false;\n    }\n    this.connection.send(JSON.stringify(data));\n    return true;\n  }\n\n  public sendWithRetry(t: any, data: ChatProps, times?: number): void {\n    try {\n      if (!times || times < maxRetry) {\n        if (!this.send(data)) {\n          setTimeout(() => {\n            this.sendWithRetry(t, data, (times ?? 0) + 1);\n          }, 500);\n        }\n\n        return;\n      }\n    } catch (e) {\n      console.warn(\n        `[connection] failed to send message: ${getErrorMessage(e)}`,\n      );\n    }\n\n    const trace = JSON.stringify(\n      this.stack ?? {\n        message: data.message,\n        endpoint: endpoint,\n      },\n      null,\n      2,\n    );\n    this.stack = undefined;\n\n    t &&\n      this.triggerCallback({\n        message: `${t(\"request-failed\")}\\n\\`\\`\\`json\\n${trace}\\n\\`\\`\\`\\n`,\n        end: true,\n      });\n  }\n\n  public sendEvent(t: any, event: string, data?: string, props?: ChatProps) {\n    this.sendWithRetry(t, {\n      type: event,\n      message: data || \"\",\n      model: \"event\",\n      ...props,\n    });\n  }\n\n  public sendStopEvent(t: any) {\n    this.sendEvent(t, \"stop\");\n  }\n\n  public sendRestartEvent(t: any, data?: ChatProps) {\n    this.sendEvent(t, \"restart\", undefined, data);\n  }\n\n  public sendMaskEvent(t: any, mask: Mask) {\n    this.sendEvent(t, \"mask\", JSON.stringify(mask.context));\n  }\n\n  public sendEditEvent(t: any, id: number, message: string) {\n    this.sendEvent(t, \"edit\", `${id}:${message}`);\n  }\n\n  public sendRemoveEvent(t: any, id: number) {\n    this.sendEvent(t, \"remove\", id.toString());\n  }\n\n  public sendShareEvent(t: any, refer: string) {\n    this.sendEvent(t, \"share\", refer);\n  }\n\n  public close(): void {\n    if (!this.connection) return;\n    this.connection.close();\n  }\n\n  public setCallback(callback?: StreamCallback): void {\n    this.callback = callback;\n  }\n\n  protected triggerCallback(message: StreamMessage): void {\n    this.callback && this.callback(this.id, message);\n  }\n\n  public setId(id: number): void {\n    this.id = id;\n  }\n\n  public isReady(): boolean {\n    return this.state;\n  }\n\n  public isRunning(): boolean {\n    if (!this.connection || !this.state) return false;\n\n    return this.connection.readyState === WebSocket.OPEN;\n  }\n}\n\nexport class ConnectionStack {\n  protected connections: Connection[];\n  protected callback?: StreamCallback;\n\n  public constructor(callback?: StreamCallback) {\n    this.connections = [];\n    this.callback = callback;\n  }\n\n  public getConnection(id: number): Connection | undefined {\n    return this.connections.find((conn) => conn.id === id);\n  }\n\n  public createConnection(id: number): Connection {\n    const conn = new Connection(id, this.triggerCallback.bind(this));\n    this.connections.push(conn);\n\n    // max connection garbage collection\n    if (this.connections.length > maxConnection) {\n      const garbage = this.connections.shift();\n      garbage && garbage.close();\n    }\n    return conn;\n  }\n\n  public send(id: number, t: any, props: ChatProps) {\n    const conn = this.getConnection(id);\n    if (!conn) return false;\n\n    conn.sendWithRetry(t, props);\n    return true;\n  }\n\n  public hasConnection(id: number): boolean {\n    return this.connections.some((conn) => conn.id === id);\n  }\n\n  public setCallback(callback?: StreamCallback): void {\n    this.callback = callback;\n  }\n\n  public sendEvent(id: number, t: any, event: string, data?: string) {\n    const conn = this.getConnection(id);\n    conn && conn.sendEvent(t, event, data);\n  }\n\n  public sendStopEvent(id: number, t: any) {\n    const conn = this.getConnection(id);\n    conn && conn.sendStopEvent(t);\n  }\n\n  public sendRestartEvent(id: number, t: any, data?: ChatProps) {\n    const conn = this.getConnection(id);\n    conn && conn.sendRestartEvent(t, data);\n  }\n\n  public sendMaskEvent(id: number, t: any, mask: Mask) {\n    const conn = this.getConnection(id);\n    conn && conn.sendMaskEvent(t, mask);\n  }\n\n  public sendEditEvent(id: number, t: any, messageId: number, message: string) {\n    const conn = this.getConnection(id);\n    conn && conn.sendEditEvent(t, messageId, message);\n  }\n\n  public sendRemoveEvent(id: number, t: any, messageId: number) {\n    const conn = this.getConnection(id);\n    conn && conn.sendRemoveEvent(t, messageId);\n  }\n\n  public sendShareEvent(id: number, t: any, refer: string) {\n    const conn = this.getConnection(id);\n    conn && conn.sendShareEvent(t, refer);\n  }\n\n  public close(id: number): void {\n    const conn = this.getConnection(id);\n    conn && conn.close();\n  }\n\n  public closeAll(): void {\n    this.connections.forEach((conn) => conn.close());\n  }\n\n  public reconnect(id: number): void {\n    const conn = this.getConnection(id);\n    conn && conn.reconnect();\n  }\n\n  public reconnectAll(): void {\n    this.connections.forEach((conn) => conn.reconnect());\n  }\n\n  public raiseConnection(id: number): void {\n    const conn = this.getConnection(-1);\n    if (!conn) return;\n\n    conn.setId(id);\n  }\n\n  public triggerCallback(id: number, message: StreamMessage): void {\n    this.callback && this.callback(id, message);\n  }\n}\n"
  },
  {
    "path": "app/src/api/file.ts",
    "content": "import { blobEndpoint } from \"@/conf/env.ts\";\nimport { trimSuffixes } from \"@/utils/base.ts\";\n\nexport type BlobParserResponse = {\n  status: boolean;\n  content: string;\n  error?: string;\n};\n\nexport type FileObject = {\n  name: string;\n  content: string;\n  size?: number;\n};\n\ntype Model = {\n  id: string;\n  ocr_model?: boolean;\n  vision_model?: boolean;\n  reverse_model?: boolean;\n};\n\nexport type FileArray = FileObject[];\n\nexport async function fileToBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.readAsDataURL(file);\n    reader.onload = () => resolve(reader.result as string);\n    reader.onerror = () => reject(new Error(\"Failed to read file\"));\n  });\n}\n\nexport function checkFileSuffix(\n  filename: string,\n  suffixes: string | string[],\n): boolean {\n  filename = filename.toLowerCase();\n\n  if (typeof suffixes === \"string\") {\n    return filename.endsWith(suffixes);\n  }\n\n  return suffixes.some((suffix) => filename.endsWith(suffix));\n}\n\nexport async function quickBlobParser(\n  file: File,\n  model: Model,\n  onProgress?: (progress: number) => void,\n): Promise<string> {\n  // this function is used to parse the file quickly in local\n  // otherwise, it will be parsed as a file\n\n  if (file.size === 0 || file.name.length === 0) {\n    throw new Error(\"File is empty\");\n  }\n\n  if (!model.reverse_model) {\n    try {\n      // if the file is an image, it will be parsed as an image by local parser first\n      const couldLocalVision = model.vision_model;\n      if (couldLocalVision && file.type.startsWith(\"image/\")) {\n        console.log(\"[parser] hit image/* file, using local parser\");\n        // parse image as base64 (e.g. result: data:image/png;base64,xxx)\n        const base64 = await fileToBase64(file);\n        return base64;\n      }\n\n      // if the file is txt, parse it as txt\n      if (\n        file.type === \"text/plain\" ||\n        checkFileSuffix(file.name, [\n          \"txt\",\n          \"md\",\n          \"markdown\",\n          \"json\",\n          \"xml\",\n          \"csv\",\n          \"yaml\",\n          \"yml\",\n          \"toml\",\n          \"ini\",\n          \"cfg\",\n          \"conf\",\n        ])\n      ) {\n        console.log(\"[parser] hit text/plain file, using local parser\");\n        return await file.text();\n      }\n      console.log(file.type);\n    } catch (e) {\n      console.error(\n        \"[parser] local parser failed, switch to server parser: \",\n        e,\n      );\n    }\n  }\n\n  return blobParser(file, model, onProgress);\n}\n\nexport async function blobParser(\n  file: File,\n  model: Model,\n  onProgress?: (progress: number) => void,\n): Promise<string> {\n  const endpoint = trimSuffixes(blobEndpoint, [\"/upload\", \"/\"]);\n\n  return new Promise((resolve, reject) => {\n    const xhr = new XMLHttpRequest();\n    const formData = new FormData();\n    formData.append(\"file\", file);\n    formData.append(\"model\", model.id);\n    formData.append(\"enable_ocr\", (model.ocr_model ?? false).toString());\n    formData.append(\"enable_vision\", (model.vision_model ?? false).toString());\n    formData.append(\"save_all\", (model.reverse_model ?? false).toString());\n    xhr.open(\"POST\", `${endpoint}/upload`, true);\n    xhr.upload.onprogress = (progressEvent) => {\n      console.debug(progressEvent);\n      if (progressEvent.lengthComputable) {\n        const percentCompleted = Math.round(\n          (progressEvent.loaded * 100) / progressEvent.total,\n        );\n        console.debug(percentCompleted);\n        onProgress?.(percentCompleted);\n      }\n    };\n    xhr.onload = () => {\n      if (xhr.status >= 200 && xhr.status < 300) {\n        try {\n          const data = JSON.parse(xhr.responseText) as BlobParserResponse;\n          if (!data.status) {\n            reject(new Error(data.error));\n          } else if (data.content.length === 0) {\n            reject(new Error(\"Result is empty\"));\n          } else {\n            resolve(data.content);\n          }\n        } catch (e) {\n          reject(new Error(\"Invalid JSON response\"));\n        }\n      } else {\n        reject(new Error(`HTTP error! status: ${xhr.status}`));\n      }\n    };\n    xhr.onerror = () => {\n      reject(new Error(\"Network error\"));\n    };\n    xhr.send(formData);\n  });\n}\n"
  },
  {
    "path": "app/src/api/generation.ts",
    "content": "import { tokenField, websocketEndpoint } from \"@/conf/bootstrap.ts\";\nimport { getMemory } from \"@/utils/memory.ts\";\n\nexport const endpoint = `${websocketEndpoint}/generation/create`;\n\nexport type GenerationForm = {\n  token: string;\n  prompt: string;\n  model: string;\n};\n\nexport type GenerationSegmentResponse = {\n  message: string;\n  quota: number;\n  end: boolean;\n  error: string;\n  hash: string;\n  title?: string;\n};\n\nexport type MessageEvent = {\n  message: string;\n  quota: number;\n};\n\nexport class GenerationManager {\n  protected processing: boolean;\n  protected connection: WebSocket | null;\n  protected message: string;\n  protected onProcessingChange?: (processing: boolean) => void;\n  protected onMessage?: (message: MessageEvent) => void;\n  protected onError?: (error: string) => void;\n  protected onFinished?: (hash: string) => void;\n\n  constructor() {\n    this.processing = false;\n    this.connection = null;\n    this.message = \"\";\n  }\n\n  public setProcessingChangeHandler(\n    handler: (processing: boolean) => void,\n  ): void {\n    this.onProcessingChange = handler;\n  }\n\n  public setMessageHandler(handler: (message: MessageEvent) => void): void {\n    this.onMessage = handler;\n  }\n\n  public setErrorHandler(handler: (error: string) => void): void {\n    this.onError = handler;\n  }\n\n  public setFinishedHandler(handler: (hash: string) => void): void {\n    this.onFinished = handler;\n  }\n\n  public isProcessing(): boolean {\n    return this.processing;\n  }\n\n  protected setProcessing(processing: boolean): boolean {\n    this.processing = processing;\n    if (!processing) {\n      this.connection = null;\n      this.message = \"\";\n    }\n    this.onProcessingChange?.(processing);\n    return processing;\n  }\n\n  public getConnection(): WebSocket | null {\n    return this.connection;\n  }\n\n  protected handleMessage(message: GenerationSegmentResponse): void {\n    if (message.error && message.end) {\n      this.onError?.(message.error);\n      this.setProcessing(false);\n      return;\n    }\n\n    this.message += message.message;\n    this.onMessage?.({\n      message: this.message,\n      quota: message.quota,\n    });\n\n    if (message.end) {\n      this.onFinished?.(message.hash);\n      this.setProcessing(false);\n    }\n  }\n\n  public generate(prompt: string, model: string) {\n    this.setProcessing(true);\n    const token = getMemory(tokenField) || \"anonymous\";\n    if (token) {\n      this.connection = new WebSocket(endpoint);\n      this.connection.onopen = () => {\n        this.connection?.send(\n          JSON.stringify({ token, prompt, model } as GenerationForm),\n        );\n      };\n      this.connection.onmessage = (event) => {\n        this.handleMessage(JSON.parse(event.data) as GenerationSegmentResponse);\n      };\n      this.connection.onclose = () => {\n        this.setProcessing(false);\n      };\n    }\n  }\n\n  public generateWithBlock(prompt: string, model: string): boolean {\n    if (this.isProcessing()) {\n      return false;\n    }\n    this.generate(prompt, model);\n    return true;\n  }\n}\n\nexport const manager = new GenerationManager();\n"
  },
  {
    "path": "app/src/api/history.ts",
    "content": "import axios from \"axios\";\nimport type { ConversationInstance } from \"./types.tsx\";\nimport { setHistory } from \"@/store/chat.ts\";\nimport { AppDispatch } from \"@/store\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { VirtualWebSearchRole, VirtualRolePrefix, Message } from \"./types.tsx\";\nimport { formatToolCallResult } from \"@/api/plugin.ts\";\n\nexport async function getConversationList(): Promise<ConversationInstance[]> {\n  try {\n    const resp = await axios.get(\"/conversation/list\");\n    return (\n      resp.data.status ? resp.data.data || [] : []\n    ) as ConversationInstance[];\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function updateConversationList(\n  dispatch: AppDispatch,\n): Promise<void> {\n  const resp = await getConversationList();\n  dispatch(setHistory(resp));\n}\n\nexport async function loadConversation(\n  id: number,\n): Promise<ConversationInstance> {\n  try {\n    const resp = await axios.get(`/conversation/load?id=${id}`);\n    \n    if (resp.data.status) {\n      const conversation = resp.data.data as ConversationInstance;\n\n      if (conversation.message && conversation.message.length > 0) {\n        const processedMessages: Message[] = [];\n        \n        for (let i = 0; i < conversation.message.length; i++) {\n          const currentMsg = conversation.message[i];\n          \n\n          if (currentMsg.role === VirtualWebSearchRole) {\n\n            let nextMsgIndex = i + 1;\n            while (\n              nextMsgIndex < conversation.message.length && \n              conversation.message[nextMsgIndex].role.startsWith(VirtualRolePrefix)\n            ) {\n              nextMsgIndex++;\n            }\n            \n\n            if (nextMsgIndex < conversation.message.length) {\n\n              conversation.message[nextMsgIndex].search_query = currentMsg.search_query;\n              conversation.message[nextMsgIndex].search_result = currentMsg.search_result;\n              conversation.message[nextMsgIndex].search_index = currentMsg.search_index;\n            }\n            \n\n            continue;\n          }\n          \n          if (currentMsg.role === \"assistant\" && currentMsg.tool_calls) {\n            processedMessages.push(currentMsg);\n          } else if (currentMsg.role === \"tool\" && currentMsg.tool_call_id) {\n            const toolCallId = currentMsg.tool_call_id;\n            for (let j = processedMessages.length - 1; j >= 0; j--) {\n              const prevMsg = processedMessages[j];\n              if (prevMsg.role === \"assistant\" && prevMsg.tool_calls) {\n                const toolCall = prevMsg.tool_calls.find(tc => tc.id === toolCallId);\n                if (toolCall) {\n                  try {\n                    const result = JSON.parse(currentMsg.content);\n                    if (result.error) {\n                      toolCall.error = result.error;\n                      toolCall.status = \"error\";\n                    } else {\n                      const formattedResult = formatToolCallResult(currentMsg.content);\n                      toolCall.result = formattedResult;\n                      toolCall.status = \"success\";\n                    }\n                  } catch {\n                    const formattedResult = formatToolCallResult(currentMsg.content);\n                    toolCall.result = formattedResult;\n                    toolCall.status = \"success\";\n                  }\n                }\n                break;\n              }\n            }\n            processedMessages.push(currentMsg);\n          } else {\n            processedMessages.push(currentMsg);\n          }\n        }\n        \n\n        conversation.message = processedMessages;\n      }\n      \n      return conversation;\n    }\n    return { id, name: \"\", message: [] };\n  } catch (e) {\n    console.warn(e);\n    return { id, name: \"\", message: [] };\n  }\n}\n\nexport async function deleteConversation(id: number): Promise<boolean> {\n  try {\n    const resp = await axios.get(`/conversation/delete?id=${id}`);\n    return resp.data.status;\n  } catch (e) {\n    console.warn(e);\n    return false;\n  }\n}\n\nexport async function renameConversation(\n  id: number,\n  name: string,\n): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/conversation/rename\", { id, name });\n    return resp.data as CommonResponse;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function deleteAllConversations(): Promise<boolean> {\n  try {\n    const resp = await axios.get(\"/conversation/clean\");\n    return resp.data.status;\n  } catch (e) {\n    console.warn(e);\n    return false;\n  }\n}\n"
  },
  {
    "path": "app/src/api/mask.ts",
    "content": "import { CustomMask } from \"@/masks/types.ts\";\nimport axios from \"axios\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\ntype ListMaskResponse = CommonResponse & {\n  data: CustomMask[];\n};\n\nexport async function listMasks(): Promise<ListMaskResponse> {\n  try {\n    const resp = await axios.get(\"/conversation/mask/view\");\n    return (\n      resp.data ?? {\n        status: true,\n        data: [],\n      }\n    );\n  } catch (e) {\n    return {\n      status: false,\n      data: [],\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function saveMask(mask: CustomMask): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/conversation/mask/save\", mask);\n    return resp.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function deleteMask(id: number): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/conversation/mask/delete\", { id });\n    return resp.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/api/plugin.ts",
    "content": "import axios from \"axios\";\nimport { CommonResponse } from \"@/api/common.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\ntype ListPluginResponse = CommonResponse & {\n  data: Plugin[];\n};\n\ntype TestPluginResponse = CommonResponse & {\n  data?: {\n    tools?: Array<{\n      name: string;\n      description: string;\n      inputSchema: Record<string, unknown>;\n    }>;\n  };\n};\n\nexport async function listPlugins(): Promise<ListPluginResponse> {\n  try {\n    const resp = await axios.get(\"/conversation/plugin/view\");\n    return (\n      resp.data ?? {\n        status: true,\n        data: [],\n      }\n    );\n  } catch (e) {\n    return {\n      status: false,\n      data: [],\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function savePlugin(plugin: Partial<Plugin>): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/conversation/plugin/save\", plugin);\n    return resp.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function deletePlugin(id: number): Promise<CommonResponse> {\n  try {\n    const resp = await axios.post(\"/conversation/plugin/delete\", { id });\n    return resp.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport async function testPlugin(serverUrl: string): Promise<TestPluginResponse> {\n  try {\n    const resp = await axios.get(\"/conversation/plugin/test\", {\n      params: { server_url: serverUrl }\n    });\n    return resp.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n\nexport function formatToolCallResult(result: string): string {\n  try {\n    const parsed = JSON.parse(result);\n    let textContent = '';\n    \n    if (parsed.text) {\n      textContent = parsed.text;\n    } else if (typeof parsed === 'string') {\n      textContent = parsed;\n    } else {\n      textContent = result;\n    }\n\n    try {\n      const secondParsed = JSON.parse(textContent);\n      if (secondParsed.text) {\n        return secondParsed.text;\n      } else if (typeof secondParsed === 'string') {\n        return secondParsed;\n      } else {\n        return JSON.stringify(secondParsed, null, 2);\n      }\n    } catch {\n      return textContent;\n    }\n  } catch {\n    return result;\n  }\n}\n\nexport function parseMCPInput(input: string): {\n  status: 'success' | 'error' | 'noop';\n  identifier?: string;\n  mcpConfig?: {\n    name: string;\n    description: string;\n    server_url: string;\n  };\n  errorCode?: string;\n} {\n  try {\n    const parsed = JSON.parse(input);\n\n    if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {\n      return { status: 'error', errorCode: 'plugin.import-error.invalid-format' };\n    }\n\n    const serverKeys = Object.keys(parsed.mcpServers);\n    if (serverKeys.length === 0) {\n      return { status: 'error', errorCode: 'plugin.import-error.no-servers' };\n    }\n\n    const identifier = serverKeys[0];\n    const serverConfig = parsed.mcpServers[identifier];\n\n    let server_url = '';\n    let description = '';\n\n    if (serverConfig.url) {\n      server_url = serverConfig.url;\n      description = `HTTP MCP Server: ${server_url}`;\n    } else if (serverConfig.command) {\n      return { status: 'error', errorCode: 'plugin.import-error.stdio-not-supported' };\n    } else {\n      return { status: 'error', errorCode: 'plugin.import-error.unsupported-config' };\n    }\n\n    return {\n      status: 'success',\n      identifier,\n      mcpConfig: {\n        name: identifier,\n        description,\n        server_url,\n      }\n    };\n  } catch (error) {\n    return { status: 'error', errorCode: 'plugin.import-error.invalid-json' };\n  }\n}\n"
  },
  {
    "path": "app/src/api/quota.ts",
    "content": "import axios from \"axios\";\n\nexport async function getQuota(): Promise<number> {\n  try {\n    const response = await axios.get(\"/quota\");\n    if (response.data.status) {\n      return response.data.quota as number;\n    }\n  } catch (e) {\n    console.debug(e);\n  }\n\n  return NaN;\n}\n"
  },
  {
    "path": "app/src/api/record.ts",
    "content": "import { CommonResponse } from \"@/api/common.ts\";\nimport axios from \"axios\";\n\nexport type Record = {\n  username: string;\n  type: string;\n  token_name: string;\n  model: string;\n  input_tokens: number;\n  output_tokens: number;\n  quota: number;\n  duration: number;\n  detail: string;\n  prompts: string;\n  response_prompts: string;\n  channel?: number;\n  channel_name?: string;\n  created_at: string;\n};\nexport type RecordData = {\n  total: number;\n  records: Record[];\n};\n\nexport type RecordStats = {\n  billing_today: number;\n  billing_month: number;\n  request_today: number;\n  request_month: number;\n  rpm: number;\n  tpm: number;\n};\n\nexport type RecordQuery = {\n  user_id?: number;\n  start_time?: string;\n  end_time?: string;\n  token_name?: string;\n  model?: string;\n  type?: RecordType;\n  show_channel?: boolean;\n};\n\ntype ListRecordsResponse = CommonResponse & {\n  data?: RecordData;\n};\n\ntype RecordStatsResponse = CommonResponse & {\n  data?: RecordStats;\n};\n\nexport enum RecordType {\n  All = \"all\",\n  Topup = \"topup\",\n  Consume = \"consume\",\n  System = \"system\",\n}\n\nexport const RecordTypes = [\n  RecordType.All,\n  RecordType.Topup,\n  RecordType.Consume,\n  RecordType.System,\n];\n\nexport async function listRecords(\n  page: number,\n  options?: RecordQuery,\n): Promise<ListRecordsResponse> {\n  try {\n    const payload: Partial<RecordQuery> = { ...options };\n    if (options && options.show_channel === undefined) {\n      delete payload.show_channel;\n    }\n    const resp = await axios.post(`/record/view?page=${page}`, payload);\n    return resp.data as ListRecordsResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: (e as Error).message,\n    };\n  }\n}\n\nexport async function getRecordStats(): Promise<RecordStatsResponse> {\n  try {\n    const resp = await axios.post(`/record/stats`);\n    return resp.data as RecordStatsResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: (e as Error).message,\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/api/redeem.ts",
    "content": "import axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\nexport type RedeemResponse = {\n  status: boolean;\n  error: string;\n  quota: number;\n};\n\nexport async function useRedeem(code: string): Promise<RedeemResponse> {\n  try {\n    const resp = await axios.get(`/redeem?code=${code}`);\n    return resp.data as RedeemResponse;\n  } catch (e) {\n    console.debug(e);\n    return {\n      status: false,\n      error: `network error: ${getErrorMessage(e)}`,\n      quota: 0,\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/api/sharing.ts",
    "content": "import axios from \"axios\";\nimport { Message } from \"./types.tsx\";\n\nexport type SharingForm = {\n  status: boolean;\n  message: string;\n  data: string;\n};\n\nexport type SharingPreviewForm = {\n  name: string;\n  conversation_id: number;\n  hash: string;\n  time: string;\n};\n\nexport type ViewData = {\n  name: string;\n  username: string;\n  time: string;\n  model?: string;\n  messages: Message[];\n};\n\nexport type ViewForm = {\n  status: boolean;\n  message: string;\n  data: ViewData | null;\n};\n\nexport type ListSharingResponse = {\n  status: boolean;\n  message: string;\n  data?: SharingPreviewForm[];\n};\n\nexport type DeleteSharingResponse = {\n  status: boolean;\n  message: string;\n};\n\nexport async function shareConversation(\n  id: number,\n  refs: number[] = [-1],\n): Promise<SharingForm> {\n  try {\n    const resp = await axios.post(\"/conversation/share\", { id, refs });\n    return resp.data;\n  } catch (e) {\n    return { status: false, message: (e as Error).message, data: \"\" };\n  }\n}\n\nexport async function viewConversation(hash: string): Promise<ViewForm> {\n  try {\n    const resp = await axios.get(`/conversation/view?hash=${hash}`);\n    return resp.data as ViewForm;\n  } catch (e) {\n    return {\n      status: false,\n      message: (e as Error).message,\n      data: null,\n    };\n  }\n}\n\nexport async function listSharing(): Promise<ListSharingResponse> {\n  try {\n    const resp = await axios.get(\"/conversation/share/list\");\n    return resp.data as ListSharingResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: (e as Error).message,\n    };\n  }\n}\n\nexport async function deleteSharing(\n  hash: string,\n): Promise<DeleteSharingResponse> {\n  try {\n    const resp = await axios.get(`/conversation/share/delete?hash=${hash}`);\n    return resp.data as DeleteSharingResponse;\n  } catch (e) {\n    return {\n      status: false,\n      message: (e as Error).message,\n    };\n  }\n}\n\nexport function getSharedLink(hash: string): string {\n  return `${location.origin}/share/${hash}`;\n}\n"
  },
  {
    "path": "app/src/api/types.tsx",
    "content": "import { ChargeBaseProps } from \"@/admin/charge.ts\";\nimport { useMemo } from \"react\";\nimport { BotIcon, ServerIcon, UserIcon } from \"lucide-react\";\n\nexport const UserRole = \"user\";\nexport const AssistantRole = \"assistant\";\nexport const SystemRole = \"system\";\nexport const VirtualRolePrefix = \"virtualRole::\";\nexport const VirtualWebSearchRole = \"virtualRole::websearch\";\nexport type Role = typeof UserRole | typeof AssistantRole | typeof SystemRole;\nexport const Roles = [UserRole, AssistantRole, SystemRole];\n\nexport const getRoleIcon = (role: string) => {\n  return useMemo(() => {\n    switch (role) {\n      case UserRole:\n        return <UserIcon />;\n      case AssistantRole:\n        return <BotIcon />;\n      case SystemRole:\n        return <ServerIcon />;\n      default:\n        return <UserIcon />;\n    }\n  }, [role]);\n};\n\nexport type Message = {\n  role: string;\n  content: string;\n  keyword?: string;\n  quota?: number;\n  end?: boolean;\n  plan?: boolean;\n  search_query?: {\n    type: string;\n    search_queries: string[];\n  };\n  search_result?: {\n    type: string;\n    search_results: Array<{\n      url: string;\n      title: string;\n      snippet: string;\n      published_at?: number;\n      site_name?: string;\n      site_icon?: string;\n    }>;\n  };\n  search_index?: {\n    type: string;\n    search_indexes: Array<{\n      url: string;\n      cite_index: number;\n    }>;\n  };\n  tool_calls?: Array<{\n    index: number;\n    type: string;\n    id: string;\n    function: {\n      name: string;\n      arguments: string;\n    };\n    status?: \"start\" | \"executing\" | \"success\" | \"error\";\n    result?: string;\n    error?: string;\n  }>;\n  tool_call_id?: string;\n  name?: string;\n  response_type?: string;\n};\n\nexport type Model = {\n  id: string;\n  name: string;\n  description?: string;\n  free: boolean;\n  auth: boolean;\n  default: boolean;\n  high_context: boolean;\n  function_calling?: boolean;\n  vision_model?: boolean;\n  ocr_model?: boolean;\n  reverse_model?: boolean;\n  thinking_model?: boolean;\n  avatar: string;\n  tag?: string[];\n\n  price?: ChargeBaseProps;\n};\n\nexport type Id = number;\n\nexport type ConversationInstance = {\n  id: number;\n  name: string;\n  message: Message[];\n  model?: string;\n  shared?: boolean;\n};\n\nexport type PlanItem = {\n  id: string;\n  name: string;\n  value: number;\n  icon: string;\n  models: string[];\n};\n\nexport type Plan = {\n  level: number;\n  price: number;\n  items: PlanItem[];\n  discounts?: Record<string, number>;\n};\n\nexport type Plans = Plan[];\n\nexport function newModel(id: string, name?: string, avatar?: string): Model {\n  return {\n    id,\n    name: name ?? id,\n    avatar: avatar ?? \"\",\n    free: false,\n    auth: false,\n    default: false,\n    high_context: false,\n  };\n}\n"
  },
  {
    "path": "app/src/api/v1.ts",
    "content": "import axios from \"axios\";\nimport { Model, Plan } from \"@/api/types.tsx\";\nimport { ChargeProps, nonBilling } from \"@/admin/charge.ts\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\n\ntype v1Options = {\n  endpoint?: string;\n};\n\ntype v1Models = {\n  object: string;\n  data: v1ModelItem[];\n};\n\ntype v1ModelItem =\n  | string\n  | {\n      id: string;\n      object: string;\n      created: number;\n      owned_by: string;\n    };\n\ntype v1Resp<T> = {\n  data: T;\n  status: boolean;\n  error?: string;\n};\n\nexport type v1ApiKey = {\n  id: number;\n  user_id?: number;\n  name: string;\n  expired_at: string;\n  quota: number;\n  used_quota: number;\n  infinite_quota: boolean;\n  ip_whitelist: string;\n  model_whitelist: string;\n  token_group?: string;\n  group_id?: string;\n  api_key?: string;\n  disabled: boolean;\n  created_at?: string;\n};\n\nexport const initialApiKey: v1ApiKey = {\n  id: -1,\n  name: \"\",\n  expired_at: \"1970-01-01 00:00:00\",\n  quota: 100.0,\n  used_quota: 0,\n  infinite_quota: false,\n  ip_whitelist: \"\",\n  model_whitelist: \"\",\n  token_group: \"default\",\n  disabled: false,\n};\n\nexport function getModelName(id: string): string {\n  // replace all `-` to ` ` except first `-` keep it\n  let begin = true;\n\n  return id\n    .replace(/-/g, (l) => {\n      if (begin) {\n        begin = false;\n        return l;\n      }\n      return \" \";\n    })\n    .replace(/\\b\\w/g, (l) => l.toUpperCase())\n    .replace(/Gpt/g, \"GPT\")\n    .replace(/Tts/g, \"TTS\")\n    .replace(/Dall-E/g, \"DALL-E\")\n    .replace(/Dalle/g, \"DALLE\")\n    .replace(/Glm/g, \"GLM\")\n    .trim();\n}\n\nexport function getV1Path(path: string, options?: v1Options): string {\n  let endpoint = options && options.endpoint ? options.endpoint : \"\";\n  if (endpoint.endsWith(\"/\")) endpoint = endpoint.slice(0, -1);\n\n  return endpoint + path;\n}\n\nexport async function getApiModels(\n  secret?: string,\n  options?: v1Options,\n): Promise<v1Resp<string[]>> {\n  try {\n    const res = await axios.get(\n      getV1Path(\"/v1/models\", options),\n      secret\n        ? {\n            headers: {\n              Authorization: `Bearer ${secret}`,\n            },\n          }\n        : undefined,\n    );\n\n    const data = res.data as v1Models;\n\n    // if data.data is an array of strings, we can just return it\n\n    const models = data.data\n      ? data.data.map((model) => (typeof model === \"string\" ? model : model.id))\n      : [];\n\n    return models.length > 0\n      ? { status: true, data: models }\n      : { status: false, data: [], error: \"No models found\" };\n  } catch (e) {\n    console.warn(e);\n    return { status: false, data: [], error: getErrorMessage(e) };\n  }\n}\n\nexport async function getApiPlans(options?: v1Options): Promise<Plan[]> {\n  try {\n    const res = await axios.get(getV1Path(\"/v1/plans\", options));\n    const plans = res.data as Plan[];\n    return plans.filter((plan: Plan) => plan.level !== 0);\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function getApiMarket(options?: v1Options): Promise<Model[]> {\n  try {\n    const res = await axios.get(getV1Path(\"/v1/market\", options));\n    return (res.data || []) as Model[];\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function getFilledApiMarket(\n  secret?: string,\n  options?: v1Options,\n): Promise<Model[]> {\n  const data = await getApiMarket(options);\n  if (data.length > 0) return data;\n\n  const resp = await getApiModels(secret, options);\n  if (!resp.status) return [];\n\n  return resp.data.map((id) => ({\n    id,\n    default: true,\n    name: getModelName(id),\n    tag: [],\n    avatar: \"\",\n    description: id,\n    free: false,\n    auth: true,\n    high_context: false,\n    price: {\n      type: nonBilling,\n      anonymous: false,\n      models: [id],\n      input: 0,\n      output: 0,\n    },\n  }));\n}\n\nexport async function bindMarket(options?: v1Options): Promise<Model[]> {\n  const market = await getFilledApiMarket(undefined, options);\n  const charge = await getApiCharge(options);\n\n  market.forEach((item: Model) => {\n    const instance = charge.find((i: ChargeProps) =>\n      i.models.includes(item.id),\n    );\n    if (!instance) return;\n\n    item.free = instance.type === nonBilling;\n    item.auth = !item.free || !instance.anonymous;\n    item.price = { ...instance };\n  });\n\n  return market;\n}\n\nexport async function getApiCharge(\n  options?: v1Options,\n): Promise<ChargeProps[]> {\n  try {\n    const res = await axios.get(getV1Path(\"/v1/charge\", options));\n    return res.data as ChargeProps[];\n  } catch (e) {\n    console.warn(e);\n    return [];\n  }\n}\n\nexport async function listApiKey(\n  options?: v1Options,\n): Promise<v1Resp<v1ApiKey[]>> {\n  try {\n    const res = await axios.get(getV1Path(\"/v1/list_keys\", options));\n    return res.data as v1Resp<v1ApiKey[]>;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, data: [], error: getErrorMessage(e) };\n  }\n}\n\nexport async function updateApiKey(\n  data: v1ApiKey,\n  options?: v1Options,\n): Promise<v1Resp<v1ApiKey>> {\n  try {\n    const res = await axios.post(getV1Path(\"/v1/update_key\", options), data);\n    return res.data as v1Resp<v1ApiKey>;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, data: initialApiKey, error: getErrorMessage(e) };\n  }\n}\n\nexport async function deleteApiKey(\n  id: number,\n  options?: v1Options,\n): Promise<v1Resp<v1ApiKey>> {\n  try {\n    const res = await axios.post(getV1Path(`/v1/delete_key?id=${id}`, options));\n    return res.data as v1Resp<v1ApiKey>;\n  } catch (e) {\n    console.warn(e);\n    return { status: false, data: initialApiKey, error: getErrorMessage(e) };\n  }\n}\n\ntype ManifestJson = {\n  data?: Record<\n    string,\n    {\n      file: string;\n      src: string;\n    }\n  >;\n  status: boolean;\n  error?: string;\n};\n\nexport async function getManifestJson(): Promise<ManifestJson> {\n  try {\n    const res = await axios.get(\"/manifest.json\", {\n      baseURL: \"/\",\n    });\n    return {\n      status: true,\n      data: res.data,\n    };\n  } catch (e) {\n    console.warn(e);\n    return {\n      status: false,\n      error: getErrorMessage(e),\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/all.less",
    "content": "@import \"menu\";\n@import \"dashboard\";\n@import \"market\";\n@import \"management\";\n@import \"broadcast\";\n@import \"channel\";\n@import \"charge\";\n@import \"system\";\n@import \"subscription\";\n@import \"logger\";\n\n\n.admin-page {\n  position: relative;\n  display: flex;\n  flex-direction: row;\n  width: 100%;\n  height: 100%;\n  max-width: 100%;\n}\n\n@media (orientation: landscape) {\n  .admin-page {\n    max-width: calc(100vw - 3.5rem);\n  }\n}\n\n.admin-container {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n}\n\n.admin-card {\n  position: relative;\n  border: 0 !important;\n\n  width: 100%;\n  height: 100%;\n  min-height: 20vh;\n}\n\n.record-card {\n  .record-wrapper > * {\n    width: 100%;\n  }\n}\n\n\n@media (max-width: 1268px) {\n  .admin-card {\n    border-radius: 0 !important;\n  }\n\n  .user-interface,\n  .market,\n  .broadcast,\n  .channel,\n  .charge,\n  .system,\n  .logger,\n  .admin-subscription,\n  .admin-container\n  {\n    padding: 0 !important;\n\n    & > * {\n      margin-bottom: 0 !important;\n      border-bottom: 1px solid hsl(var(--border)) !important;\n      border-radius: 0 !important;\n    }\n  }\n}\n\n.object-id {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-items: center;\n  border-radius: var(--radius);\n  border: 1px solid hsl(var(--border));\n  color: hsl(var(--text-secondary));\n  user-select: none;\n  font-size: 0.75rem;\n  height: 2.5rem;\n  padding: 0.5rem 1.25rem;\n  cursor: pointer;\n  transition: 0.25s;\n  flex-shrink: 0;\n\n  &:hover {\n    color: hsl(var(--text));\n    border-color: hsl(var(--border-hover));\n  }\n\n  svg {\n    transform: translateY(1px);\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/broadcast.less",
    "content": ".broadcast {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .broadcast-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n\n  .empty {\n    color: hsl(var(--text-secondary)) !important;\n    font-size: 14px;\n    margin: auto;\n    user-select: none;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/channel.less",
    "content": ".channel {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .channel-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n\n  .channel-table {\n    .channel-id {\n      color: hsl(var(--text-secondary));\n    }\n  }\n}\n\n.channel-editor {\n  position: relative;\n\n  .channel-loader {\n    position: absolute;\n    top: 0;\n    right: 0.25rem;\n  }\n}\n\n.channel-wrapper {\n  display: flex;\n  flex-direction: column;\n  margin-top: 0.5rem;\n  margin-bottom: 2rem;\n\n  & > * {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .channel-row {\n    display: flex;\n    flex-direction: column;\n    user-select: none;\n    white-space: nowrap;\n\n    &.column-layout {\n      flex-direction: row;\n      align-items: center;\n      white-space: nowrap;\n      width: 100%;\n\n      .channel-content {\n        margin-left: 0;\n        margin-bottom: 0;\n      }\n\n      & > * {\n        margin-right: 1rem;\n        margin-bottom: 0;\n\n        &:last-child {\n          margin-right: 0;\n        }\n      }\n    }\n\n    .channel-content {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      margin-left: 0.25rem;\n      margin-bottom: 0.5rem;\n    }\n  }\n}\n\n.channel-model-wrapper {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  gap: 0.5rem;\n  height: max-content;\n  width: 100%;\n  border-radius: var(--radius);\n  border: 1px solid hsl(var(--border));\n  background: hsl(var(--background));\n  padding: 1rem;\n  min-height: 5rem;\n\n  .channel-model-item {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    padding: 0.25rem 0.5rem;\n    border: 1px solid hsl(var(--border));\n    border-radius: var(--radius);\n    transition: .25s;\n    height: max-content;\n    white-space: break-spaces;\n\n    &:hover {\n      border-color: hsl(var(--border-hover));\n    }\n\n    .remove-action {\n      width: 0.75rem;\n      height: 0.75rem;\n      cursor: pointer;\n      margin-left: 0.5rem;\n      color: hsl(var(--text-secondary));\n      transition: .25s;\n      flex-shrink: 0;\n\n      &:hover {\n        color: hsl(var(--text-primary));\n      }\n    }\n  }\n}\n\n.channel-model-action {\n  display: flex;\n  flex-direction: row;\n  width: 100%;\n  flex-wrap: wrap;\n  gap: 0.5rem;\n\n  @media (max-width: 620px) {\n    & > * {\n      width: 100%;\n    }\n  }\n}\n\n.channel-description {\n  white-space: break-spaces;\n  line-height: 1.25em;\n}\n"
  },
  {
    "path": "app/src/assets/admin/charge.less",
    "content": ".charge {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .charge-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n}\n\n.charge-widget {\n  height: max-content;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n\n  & > * {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.charge-alert {\n  .model-list {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    gap: 0.5rem;\n    margin-top: 1rem;\n\n    .model {\n      padding: 0.5rem 0.75rem;\n      border-radius: var(--radius);\n      border: 1px solid hsl(var(--border));\n    }\n  }\n}\n\n.charge-editor {\n  padding: 1.5rem;\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n\n  .token {\n    color: hsl(var(--text-secondary));\n    user-select: none;\n  }\n}\n\n\n.charge-table {\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n  overflow-x: auto;\n  scrollbar-width: thin;\n\n  &::-webkit-scrollbar {\n    width: 0.5rem;\n  }\n\n  .table {\n    scrollbar-width: thin;\n  }\n\n  .charge-id {\n    color: hsl(var(--text-secondary));\n    user-select: none;\n\n    &:before {\n      content: '#';\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/dashboard.less",
    "content": ".dashboard {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n  padding: 0.5rem 0;\n\n  & > * {\n    margin-bottom: 0.5rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.info-boxes {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  grid-gap: 1rem;\n  width: 100%;\n  height: max-content;\n  padding: 1rem 2rem 0;\n\n  @media (max-width: 940px) {\n    grid-template-columns: 1fr;\n    padding: 1rem 1.5rem 0;\n  }\n\n  .info-box {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n    height: max-content;\n    padding: 0.75rem 1.5rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    box-shadow: 0.5rem 0.5rem 1rem 0 hsl(var(--shadow));\n    user-select: none;\n    width: 100%;\n\n    margin-right: 1.5rem;\n    margin-left: auto;\n\n    &:last-child {\n      margin-right: auto;\n    }\n\n    & > * {\n      flex-shrink: 0;\n    }\n\n    .box-wrapper {\n      flex-grow: 1;\n\n      .box-title {\n        font-size: 1rem;\n        margin-bottom: 0.5rem;\n      }\n\n      .box-value {\n        font-size: 1.5rem;\n        font-weight: normal;\n\n        &.money::after,\n        .box-subvalue {\n          font-size: 1rem;\n          font-weight: normal;\n          margin-left: 0.5rem;\n          content: 'CNY';\n        }\n      }\n    }\n\n    .box-icon {\n      width: max-content;\n      height: max-content;\n      transform: translate(0.25rem, 0.25rem);\n      border-radius: 0.25rem;\n\n      svg {\n        width: 2rem;\n        height: 2rem;\n        stroke-width: 1.25;\n      }\n    }\n  }\n}\n\n.chart-boxes {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  padding: 0 1.5rem 1rem;\n  width: 100%;\n\n  @media (max-width: 940px) {\n    flex-direction: column;\n    padding: 0 1rem 1rem;\n\n    .chart-box {\n      width: calc(100% - 1rem) !important;\n    }\n  }\n\n  .chart-box {\n    width: calc(50% - 1rem);\n    height: max-content;\n    min-height: 235px;\n\n    padding: 1rem 2rem;\n    margin: 0.5rem;\n    border-radius: var(--radius);\n    background: hsl(var(--background));\n    box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);\n    user-select: none;\n\n    border: 1px solid hsl(var(--border));\n\n    .chart {\n      #model-usage-chart,\n      #user-type-chart {\n        min-height: 8rem !important;\n        max-height: 8rem !important;\n        margin-top: 1.5rem;\n        margin-bottom: 1rem;\n        flex-shrink: 0;\n      }\n\n      .common-chart {\n        min-height: 10rem !important;\n        max-height: 10rem !important;\n        flex-shrink: 0;\n\n        font-size: 0.8rem !important;\n        font-family: var(--font-family) !important;\n      }\n\n      .chart-title {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        flex-wrap: nowrap;\n\n        .chart-title-info {\n          color: hsl(var(--text-secondary));\n        }\n\n        svg {\n          position: relative;\n          top: 1px;\n        }\n\n        & > * {\n          flex-shrink: 0;\n          margin-right: 0.25rem;\n\n          &:last-child {\n            margin-right: 0;\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/logger.less",
    "content": ".logger {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .logger-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n}\n\n.logger-container {\n  .paragraph-header {\n    margin-bottom: 0.5rem;\n  }\n}\n\n.logger-toolbar {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n\n  input {\n    max-width: 4.5rem;\n    text-align: center;\n  }\n\n  button {\n    flex-shrink: 0;\n  }\n\n  & > * {\n    margin-right: 0.5rem !important;\n    white-space: nowrap;\n\n    &:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n.logger-console {\n  position: relative;\n  border-radius: var(--radius);\n  font-size: 14px;\n  width: 100%;\n  height: max-content;\n  overflow: hidden;\n\n\n  pre {\n    width: 100%;\n    height: max-content;\n    min-height: 20vh;\n    max-height: 60vh;\n    overflow-x: hidden;\n    overflow-y: auto;\n    touch-action: pan-y;\n    padding: 0.5rem;\n    white-space: pre-wrap !important;\n  }\n\n  .console-icon {\n    position: absolute;\n    top: 0.75rem;\n    right: 0.75rem;\n    user-select: none;\n  }\n}\n\n.logger-list {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: max-content;\n\n  & > * {\n    margin-bottom: 0.5rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.logger-item {\n  display: flex;\n  flex-direction: row;\n  padding: 0.75rem 1rem;\n  flex-wrap: wrap;\n  border-radius: var(--radius);\n  border: 1px solid hsl(var(--border));\n  transition: all 0.2s ease-in-out;\n  align-items: center;\n\n  &:hover {\n    border-color: hsl(var(--border-hover));\n  }\n\n  & > * {\n    margin-right: 1rem;\n    flex-shrink: 0;\n    white-space: nowrap;\n\n    &:last-child {\n      margin-right: 0;\n    }\n  }\n\n  .logger-item-title {\n    font-size: 16px;\n    color: hsl(var(--text));\n  }\n\n  .logger-item-size {\n    font-size: 14px;\n    color: hsl(var(--text-secondary));\n  }\n\n  .logger-item-action {\n    cursor: pointer;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/management.less",
    "content": ".user-interface {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  max-width: 100%;\n  height: 100%;\n  padding: 2rem;\n\n  & > * {\n    margin-bottom: 2rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  &.mobile {\n    padding: 1rem;\n  }\n\n  .pagination {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin-top: 1rem;\n\n    & > * {\n      scale: 0.8;\n      margin: 0 0.5rem;\n    }\n  }\n\n  .empty {\n    user-select: none;\n    text-align: center;\n    font-size: 14px;\n    margin: 4rem 0 2rem;\n    color: hsl(var(--text-secondary));\n  }\n\n  .action {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    margin-top: 1rem;\n  }\n}\n\n.user-row,\n.redeem-row,\n.invitation-row {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  white-space: nowrap;\n  user-select: none;\n  color: hsl(var(--text));\n  margin: 1rem 0;\n}\n\n.user-action,\n.redeem-action,\n.invitation-action {\n  display: flex;\n  margin-top: 1rem;\n  flex-direction: row;\n  align-items: center;\n\n  & > * {\n    margin-right: 0.5rem;\n\n    &:last-child {\n      margin-right: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/market.less",
    "content": ".market {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .market-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n\n  & > * {\n    margin-bottom: 0.5rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .model-combobox {\n    width: 320px;\n    margin-left: auto;\n\n    @media (max-width: 768px) {\n      width: 100% !important;\n      margin-left: 0 !important;\n      max-width: 100vw !important;\n    }\n  }\n}\n\n.market-alert {\n  display: flex;\n  flex-direction: column;\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n  margin-bottom: 1rem;\n  padding: 0.75rem;\n\n  .market-alert-wrapper {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    gap: 0.5rem;\n  }\n}\n\n.market-list {\n  display: flex;\n  flex-direction: column;\n\n  & > * {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .empty {\n    display: flex;\n    text-align: center;\n    color: hsl(var(--text-secondary));\n    user-select: none;\n    margin: 1.5rem auto;\n  }\n\n  .market-item {\n    display: flex;\n    flex-direction: row;\n    padding: 1rem 1rem 1.5rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    user-select: none;\n    width: 100%;\n    height: max-content;\n    align-items: center;\n    background: hsl(var(--card));\n    transition: 0.25s;\n    transition-property: border, background;\n\n    &.error {\n      border-color: hsl(var(--error));\n    }\n\n    &.stacked {\n      border-color: transparent !important;\n      padding: 0.25rem 0.5rem;\n      margin-bottom: 0.25rem;\n\n      .market-row {\n        width: max-content;\n        flex-wrap: nowrap;\n        flex-shrink: 0;\n        gap: 0.5rem;\n      }\n    }\n\n    .market-tags {\n      display: flex;\n      flex-direction: row;\n      gap: 0.5rem;\n      flex-wrap: wrap;\n      width: 100%;\n\n      .market-tag {\n        white-space: nowrap;\n        padding: 0.25rem 0.75rem !important;\n      }\n    }\n\n    .market-image-wrapper {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      height: max-content;\n\n      & > * {\n        margin-bottom: 0.5rem;\n\n        &:last-child {\n          margin-bottom: 0;\n        }\n      }\n    }\n\n    .market-custom-image {\n      display: flex;\n      flex-direction: row;\n      flex-wrap: wrap;\n      gap: 0.25rem;\n\n      .market-checkbox {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        margin: 0.25rem 0;\n\n        button {\n          margin-right: 0.25rem;\n          transform: translateY(1px);\n        }\n      }\n    }\n\n    .market-images {\n      display: flex;\n      flex-direction: row;\n      gap: 0.5rem;\n      flex-wrap: wrap;\n      width: 100%;\n\n      .market-image {\n        width: 2.5rem;\n        height: 2.5rem;\n        padding: 0.25rem;\n        transition: 0.1s;\n\n        img {\n          width: 2rem;\n          height: 2rem;\n          opacity: 0.6;\n          border-radius: calc(var(--radius) - 2px);\n          transition: 0.1s;\n        }\n\n        &.active {\n          img {\n            opacity: 1;\n          }\n        }\n      }\n    }\n\n    svg {\n      flex-shrink: 0;\n    }\n\n    .drop-icon {\n      color: hsl(var(--text-secondary));\n      transition: color 0.25s ease;\n    }\n\n    &:hover {\n      .drop-icon {\n        color: hsl(var(--text));\n      }\n    }\n\n    .model-wrapper {\n      display: flex;\n      flex-direction: column;\n      flex-grow: 1;\n      flex-basis: 0;\n\n      & > * {\n        margin-bottom: 0.75rem;\n\n        &:last-child {\n          margin-bottom: 0;\n        }\n      }\n    }\n\n    .market-row {\n      display: flex;\n      flex-direction: row;\n      flex-wrap: wrap;\n      align-items: center;\n      width: 100%;\n      gap: 0.75rem;\n\n      & > span {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        font-size: 0.9rem;\n        white-space: nowrap;\n        min-width: 68px;\n        text-align: center;\n\n        svg {\n          transform: translateY(1px);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/menu.less",
    "content": ".admin-menu {\n  display: flex;\n  flex-shrink: 0;\n  flex-direction: column;\n  height: 100%;\n  padding: 0.75rem;\n  margin: 0;\n  background: hsl(var(--background));\n  transition: 0.225s ease-in-out;\n  min-height: calc(100% - var(--navbar-height));\n  transition-property: width, background, box-shadow, opacity;\n  border-right: 0;\n  overflow-x: hidden;\n\n  width: 4.25rem;\n  border-right: 1px solid hsl(var(--border));\n  opacity: 1;\n  pointer-events: all;\n\n  &.open {\n    width: 12rem;\n    .menu-item-title {\n      display: block !important;\n      opacity: 1 !important;\n    }\n\n    .menu-item-badge {\n      display: block !important;\n      opacity: 1 !important;\n    }\n  }\n\n  .menu-item {\n    display: flex;\n    flex-direction: row;\n    width: 100%;\n    height: fit-content;\n    padding: 0.5rem;\n    align-items: center;\n    user-select: none;\n    cursor: pointer;\n    transition: 0.2s ease-in-out;\n    border-radius: var(--radius);\n    font-size: 16px;\n    color: hsl(var(--text-secondary));\n\n    &:hover {\n      color: hsl(var(--text));\n    }\n\n    &.active {\n      color: hsl(var(--text));\n      background: hsl(var(--card-hover));\n    }\n\n    & > * {\n      flex-shrink: 0;\n    }\n\n    .menu-item-title {\n      font-size: 0.85rem;\n      margin-left: 0.25rem;\n      font-weight: normal;\n      display: none;\n      opacity: 0;\n    }\n\n    .menu-item-badge {\n      display: none;\n      opacity: 0;\n    }\n\n    .menu-item-icon {\n      width: fit-content;\n      height: fit-content;\n      padding: 0.25rem;\n      transform: translateY(1px);\n\n      svg {\n        width: 1.25rem;\n        height: 1.25rem;\n        stroke-width: 1.5;\n      }\n    }\n  }\n\n  @media (max-width: 668px) {\n    &.open {\n      width: 100%;\n      border-right: 0;\n    }\n  }\n}\n\n.admin-content {\n  flex-grow: 1;\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: auto;\n  touch-action: pan-y;\n  background: hsla(var(--background-container));\n\n  & > .scrollarea-viewport > div {\n    display: flex !important;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/subscription.less",
    "content": ".admin-subscription {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .sub-card {\n    width: 100%;\n    height: 100%;\n    min-height: 20vh;\n  }\n}\n\n.plan-config {\n  display: flex;\n  flex-direction: column;\n  margin-top: 0.25rem;\n\n  & > * {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .plan-config-row {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n  }\n\n  .plan-config-card {\n    display: flex;\n    flex-direction: column;\n    padding: 1rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n\n    .plan-config-title {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      white-space: nowrap;\n      user-select: none;\n      margin-bottom: 0.75rem;\n\n      &:before {\n        display: inline-block;\n        content: '';\n        margin-right: 0.5rem;\n        height: 1.25rem;\n        width: 2px;\n        border-radius: 1px;\n        background: hsl(var(--text-secondary));\n        transition: .25s;\n      }\n    }\n\n    .plan-items-action {\n      display: flex;\n      flex-direction: row;\n      gap: 1rem;\n      align-items: center;\n      flex-wrap: wrap;\n      margin-top: 1rem;\n    }\n\n    .plan-items-wrapper {\n      display: flex;\n      flex-direction: column;\n      width: 100%;\n      height: max-content;\n      margin-top: 1rem;\n\n      .plan-item {\n        padding: 1rem;\n        border-radius: var(--radius);\n        border: 1px solid hsl(var(--border));\n\n        &.stacked {\n          flex-direction: row;\n\n          .plan-editor-row {\n            margin-bottom: 0;\n            flex-grow: 1;\n            margin-right: 1rem;\n\n            .plan-editor-label {\n              min-width: 0;\n              margin-right: 0.75rem;\n              flex-shrink: 0;\n\n              @media (max-width: 768px) {\n                svg {\n                  display: none;\n                }\n              }\n\n              svg {\n                margin: 0 0.25rem;\n              }\n            }\n          }\n        }\n\n        .plan-editor-row > p {\n          min-width: 4.25rem;\n        }\n      }\n\n      & > * {\n        margin-bottom: 0.5rem;\n\n        &:last-child {\n          margin-bottom: 0;\n        }\n      }\n    }\n\n    .plan-editor-row {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n\n      .plan-editor-label {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        white-space: nowrap;\n        margin-right: 0.5rem;\n        user-select: none;\n\n        svg {\n          display: inline-block;\n          flex-shrink: 0;\n          transform: translateY(-2px);\n        }\n      }\n\n      & > p {\n        white-space: nowrap;\n      }\n    }\n\n    & > * {\n      margin-bottom: 0.25rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/admin/system.less",
    "content": ".system {\n  width: 100%;\n  height: max-content;\n  padding: 2rem;\n  display: flex;\n  flex-direction: column;\n\n  .system-card {\n    width: 100%;\n    height: 100%;\n    min-height: 50vh;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/common/404.less",
    "content": ".error-page {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  height: 100%;\n  font-size: 30px;\n  color: hsl(var(--tw-content));\n  gap: 12px;\n  width: 100%;\n  user-select: none;\n\n  .icon {\n    width: 58px;\n    height: 58px;\n    transform: translateY(-62px);\n  }\n\n  h1 {\n    font-size: 48px;\n    transform: translateY(-50px);\n  }\n\n  p {\n    font-size: 24px;\n    transform: translateY(-32px);\n  }\n\n  button {\n    transform: translateY(-4px);\n  }\n}\n"
  },
  {
    "path": "app/src/assets/common/editor.less",
    "content": ".editor-action {\n  position: absolute;\n  top: 50%;\n  right: 8px;\n  transform: translateY(-50%);\n  background: hsl(var(--input)) !important;\n  padding: 6px;\n  border-radius: 50%;\n  cursor: pointer;\n  transition: 0.1s;\n  outline: 0;\n  opacity: 0;\n\n  &.active {\n    opacity: 1;\n  }\n\n  &:hover {\n    background: hsl(var(--border-hover)) !important;\n  }\n}\n\n.editor-dialog {\n  max-width: min(90vw, 920px) !important;\n}\n\n.editor-container {\n  padding: 2px 4px 0;\n}\n\n.editor-wrapper {\n  padding: 4px 0;\n}\n\n.editor-object {\n  position: relative;\n  display: grid;\n  grid-gap: 12px;\n  height: 100%;\n\n  &.show-editor {\n    grid-template-columns: 1fr;\n    grid-template-rows: 1fr;\n    grid-template-areas: 'editor';\n  }\n\n  &.show-preview {\n    grid-template-columns: 1fr;\n    grid-template-rows: 1fr;\n    grid-template-areas: 'markdown';\n  }\n\n  &.show-editor.show-preview {\n    grid-template-columns: 1fr 1fr;\n    grid-template-rows: 1fr;\n    grid-template-areas: 'editor markdown';\n  }\n}\n\n.editor-input {\n  grid-area: editor;\n  scrollbar-width: thin;\n  height: max-content;\n  transition: .1s;\n  min-height: 50vh !important;\n  color: hsl(var(--text));\n  font-size: 16px !important;\n  padding: 14px 12px !important;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\";\n}\n\n.editor-preview {\n  height: 100%;\n  grid-area: markdown;\n  overflow: auto;\n  padding: 12px;\n  border-radius: 4px;\n  color: hsl(var(--text));\n  border: 1px solid hsl(var(--border));\n  scrollbar-width: thin;\n  transition: 0.1s;\n  min-height: 50vh !important;\n  font-size: 16px;\n  text-align: left;\n}\n\n.editor-toolbar {\n  display: flex;\n  width: 100%;\n  margin: 8px 0 4px;\n\n  & > * {\n    margin-right: 4px;\n\n    &:last-child {\n      margin-right: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/common/file.less",
    "content": ".file-action {\n  position: absolute;\n  top: 50%;\n  left: 8px;\n  transform: translateY(-50%);\n  background: hsl(var(--input)) !important;\n  padding: 6px;\n  border-radius: 50%;\n  cursor: pointer;\n  transition: 0.1s;\n  outline: 0;\n\n  &:hover {\n    background: hsl(var(--border-hover)) !important;\n  }\n}\n\n.file-dialog {\n  max-width: min(90vw, 720px) !important;\n}\n\n.file-wrapper {\n  max-width: calc(90vw - 3rem);\n}\n\n.drop-window {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  min-height: 200px;\n  height: 20vh;\n  border: 2px dashed hsl(var(--border));\n  border-radius: var(--radius);\n  transition: 0.25s;\n  margin-top: 1rem;\n  cursor: pointer;\n  color: hsl(var(--text-secondary));\n}\n\n.drop-window:hover {\n  border: 2px dashed hsl(var(--border-hover));\n  color: hsl(var(--text));\n}\n\n.file-object {\n  position: relative;\n  display: flex;\n  flex-direction: row;\n  gap: 4px;\n  align-items: center;\n  padding: 10px 12px;\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n  transition: 0.25s;\n  color: hsl(var(--text-secondary)) !important;\n  cursor: pointer;\n\n  &:hover {\n    border: 1px solid hsl(var(--border-hover));\n    color: hsl(var(--text)) !important;\n  }\n\n  .close {\n    color: hsl(var(--text-secondary)) !important;\n    transition: .1s;\n\n    &:hover {\n      color: hsl(var(--text)) !important;\n    }\n  }\n}\n\n.file-list {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: max-content;\n  user-select: none;\n  margin: 1rem 0;\n\n  .file-item {\n    width: 100%;\n    height: max-content;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    margin-bottom: 4px;\n    color: hsl(var(--foreground));\n    background: hsl(var(--background));\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    padding: 0.5rem;\n\n    .file-size {\n      color: hsl(var(--text-secondary));\n    }\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.file-name {\n  word-wrap: anywhere;\n  word-break: break-all;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  max-width: 450px;\n}\n"
  },
  {
    "path": "app/src/assets/common/loader.less",
    "content": ".loader-wrapper {\n  position: absolute;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  vertical-align: center;\n  text-align: center;\n  top: 50%;\n  left: 50%;\n  gap: 20px;\n  transform: translate(-50%, -50%);\n  margin-top: -28px;\n\n  p {\n    text-align: center;\n    user-select: none;\n\n    &:after {\n      content: '.';\n      color: hsl(var(--text-secondary));\n      animation: dots 1s steps(5, end) infinite;\n\n      @keyframes dots {\n        0%, 20% {\n          color: rgba(0, 0, 0, 0);\n          text-shadow:\n            .25em 0 0 rgba(0, 0, 0, 0),\n            .5em 0 0 rgba(0, 0, 0, 0);\n        }\n        40% {\n          color: hsl(var(--text-secondary));\n          text-shadow:\n            .25em 0 0 rgba(0, 0, 0, 0),\n            .5em 0 0 rgba(0, 0, 0, 0);\n        }\n        60% {\n          text-shadow:\n            .25em 0 0 hsl(var(--text-secondary)),\n            .5em 0 0 rgba(0, 0, 0, 0);\n        }\n        80%, 100% {\n          text-shadow:\n            .25em 0 0 hsl(var(--text-secondary)),\n            .5em 0 0 hsl(var(--text-secondary));\n        }\n      }\n    }\n  }\n\n  .loader {\n    position: relative;\n    top: 0 !important;\n    left: 0 !important;\n    border: 4px solid hsl(var(--text));\n    border-left-color: transparent;\n    border-radius: 50%;\n    width: 46px;\n    height: 46px;\n    animation: SpinAnimation 1s linear infinite;\n\n    @keyframes SpinAnimation {\n      0% {\n        transform: rotate(0deg);\n      }\n\n      100% {\n        transform: rotate(360deg);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/common/plugin.less",
    "content": ".plugin-dialog {\n  max-width: min(95vw, 1024px) !important;\n  width: 100%;\n  height: auto;\n  max-height: 90vh;\n}\n\n.plugin-wrapper {\n  max-width: calc(95vw - 3rem);\n  max-height: calc(90vh - 8rem);\n  overflow-y: auto;\n  padding: 0.5rem;\n  padding-top: 1rem;\n  display: flex;\n  flex-direction: column;\n}\n\n.plugin-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 1rem;\n  padding: 0 0.5rem;\n}\n\n.plugin-header-title {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n.plugin-header-actions {\n  display: flex;\n  gap: 0.5rem;\n}\n\n.plugin-list {\n  width: 100%;\n  flex: 1;\n  \n  & > div {\n    height: 100%;\n  }\n  \n  .space-y-3 {\n    width: 100%;\n    height: auto;\n    padding: 0 0.5rem;\n  }\n}\n\n.plugin-item {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1rem;\n  border: 1px solid var(--border);\n  border-radius: 0.5rem;\n  transition: background-color 0.2s;\n  cursor: pointer;\n  min-height: 80px;\n  width: 100%;\n  box-sizing: border-box;\n  \n  &:hover {\n    background-color: hsl(var(--muted-foreground) / 0.05);\n  }\n}\n\n.plugin-item-content {\n  display: flex;\n  align-items: center;\n  gap: 0.75rem;\n  flex: 1;\n  min-width: 0;\n}\n\n.plugin-avatar {\n  width: 2.5rem;\n  height: 2.5rem;\n  border-radius: 50%;\n  background: linear-gradient(135deg, #3b82f6, #8b5cf6);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: white;\n  font-weight: 600;\n  flex-shrink: 0;\n}\n\n.plugin-info {\n  flex: 1;\n  min-width: 0;\n}\n\n.plugin-name {\n  font-weight: 500;\n  margin: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.plugin-description {\n  font-size: 0.875rem;\n  color: var(--muted-foreground);\n  margin: 0.25rem 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.plugin-url {\n  font-size: 0.75rem;\n  color: var(--muted-foreground);\n  margin: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.plugin-actions {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  flex-shrink: 0;\n}\n\n.plugin-form {\n  padding: 1rem;\n  margin: 0 0.5rem;\n}\n\n.plugin-import-form {\n  padding: 1rem;\n  border: 1px solid var(--border);\n  border-radius: 0.5rem;\n  background-color: transparent;\n  margin: 0 0.5rem;\n  transition: background-color 0.2s, border-color 0.2s;\n  \n  &:hover {\n    background-color: hsl(var(--muted-foreground) / 0.05);\n    border-color: hsl(var(--border-hover));\n  }\n}\n\n.plugin-import-textarea {\n  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n  font-size: 0.875rem;\n  line-height: 1.5;\n  background-color: transparent;\n  border: 1px solid var(--border);\n  border-radius: 0.375rem;\n  transition: border-color 0.2s, background-color 0.2s;\n  \n  &:hover {\n    border-color: hsl(var(--border-hover));\n    background-color: hsl(var(--muted-foreground) / 0.02);\n  }\n  \n  &:focus {\n    border-color: hsl(var(--border-active));\n    background-color: hsl(var(--background));\n    box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2);\n  }\n  \n  &::placeholder {\n    color: var(--muted-foreground);\n    opacity: 0.7;\n  }\n}\n\n.plugin-empty-state {\n  text-align: center;\n  padding: 2rem 0;\n  color: var(--muted-foreground);\n  margin: 0 0.5rem;\n}\n\n.plugin-empty-icon {\n  width: 3rem;\n  height: 3rem;\n  margin: 0 auto 1rem;\n  opacity: 0.5;\n}\n\n.plugin-loading {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 2rem 0;\n  margin: 0 0.5rem;\n}\n\n/* 响应式设计 */\n@media (max-width: 768px) {\n  .plugin-dialog {\n    max-width: 95vw !important;\n    margin: 1rem;\n  }\n  \n  .plugin-wrapper {\n    max-width: calc(95vw - 2rem);\n    padding: 0.25rem;\n    padding-top: 1rem;\n  }\n  \n  .plugin-header {\n    flex-direction: column;\n    gap: 1rem;\n    align-items: stretch;\n    text-align: center;\n    padding: 0;\n  }\n  \n  .plugin-header-title {\n    justify-content: center;\n  }\n  \n  .plugin-header-actions {\n    justify-content: center;\n    width: 100%;\n  }\n  \n  .plugin-list {\n    height: 60vh !important;\n  }\n  \n  .plugin-item {\n    flex-direction: column;\n    gap: 1rem;\n    padding: 0.75rem;\n    align-items: stretch;\n  }\n  \n  .plugin-item-content {\n    flex-direction: column;\n    align-items: flex-start;\n    text-align: left;\n  }\n  \n  .plugin-actions {\n    justify-content: center;\n    width: 100%;\n  }\n  \n  .plugin-avatar {\n    align-self: center;\n  }\n  \n  .plugin-info {\n    width: 100%;\n    text-align: center;\n  }\n  \n  .plugin-name,\n  .plugin-description,\n  .plugin-url {\n    white-space: normal;\n    word-break: break-all;\n  }\n  \n  .plugin-form {\n    padding: 0.75rem;\n    margin: 0;\n  }\n  \n  .plugin-import-form {\n    padding: 0.75rem;\n    margin: 0;\n  }\n  \n  .plugin-import-textarea {\n    font-size: 0.75rem;\n  }\n  \n  .plugin-empty-state,\n  .plugin-loading {\n    margin: 0;\n  }\n  \n  .plugin-form-row {\n    flex-direction: column;\n    gap: 0.75rem;\n    align-items: stretch;\n  }\n  \n  .plugin-form-column {\n    width: 100%;\n  }\n  \n  .plugin-avatar-picker {\n    align-self: center;\n  }\n  \n  .plugin-test-scroll-area {\n    max-height: calc(60vh - 100px);\n  }\n}\n\n@media (max-width: 480px) {\n  .plugin-dialog {\n    max-width: 98vw !important;\n    margin: 0.5rem;\n  }\n  \n  .plugin-wrapper {\n    max-width: calc(98vw - 1rem);\n    padding-top: 0.75rem;\n  }\n  \n  .plugin-header {\n    margin-bottom: 0.75rem;\n  }\n  \n  .plugin-item {\n    padding: 0.5rem;\n  }\n  \n  .plugin-actions {\n    flex-wrap: wrap;\n    gap: 0.25rem;\n  }\n  \n  .plugin-form {\n    padding: 0.5rem;\n  }\n  \n  .plugin-import-form {\n    padding: 0.5rem;\n  }\n  \n  .plugin-import-textarea {\n    font-size: 0.75rem;\n  }\n}\n\n/* Plugin Editor styles */\n.plugin-drawer-viewport {\n  width: 100%;\n  max-width: 100%;\n}\n\n.plugin-form-row {\n  display: flex;\n  flex-direction: row;\n  gap: 1rem;\n  align-items: flex-end;\n}\n\n.plugin-form-column {\n  display: flex;\n  flex-direction: column;\n  gap: 0.5rem;\n  flex-shrink: 0;\n}\n\n.plugin-form-column-grow {\n  flex: 1;\n  min-width: 0;\n}\n\n.plugin-avatar-picker {\n  width: 2.5rem !important;\n  height: 2.5rem !important;\n  border-radius: 0.5rem;\n  border: 1px solid hsl(var(--border));\n  transition: 0.2s ease-in-out;\n  \n  &:hover {\n    border-color: hsl(var(--border-hover));\n    background-color: hsl(var(--background-hover));\n  }\n  \n  &:focus {\n    border-color: hsl(var(--border-active));\n    box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2);\n  }\n}\n\n.plugin-test-scroll-area {\n  max-height: calc(70vh - 120px);\n  \n  .scrollarea-viewport {\n    padding-right: 0.5rem;\n  }\n  \n  &[data-radix-scroll-area-viewport] {\n    max-height: 100%;\n  }\n}\n\n.plugin-picker-dialog {\n  width: max-content !important;\n  height: max-content !important;\n  padding: 0 !important;\n\n  .picker {\n    --epr-category-navigation-button-size: 28px;\n    --epr-search-input-bg-color: hsl(var(--background));\n    --epr-search-border-color: hsl(var(--border-hover));\n    --epr-bg-color: hsl(var(--background));\n    --epr-category-label-bg-color: hsl(var(--background));\n\n    img {\n      padding: 0.5rem;\n    }\n\n    .epr-icn-search {\n      width: 1rem;\n      height: 1rem;\n      transform: translateY(-0.55rem);\n    }\n\n    .epr-body {\n      scrollbar-width: thin;\n      -ms-overflow-style: none;\n\n      &::-webkit-scrollbar {\n        width: 6px;\n      }\n    }\n\n    .epr-search-container {\n      input {\n        border: 1px solid hsl(var(--border));\n      }\n    }\n\n    .epr-cat-btn {\n      &:focus:before {\n        border: none;\n      }\n    }\n\n    * {\n      font-family: var(--font-family) !important;\n      font-size: 0.85rem !important;\n    }\n  }\n}\n\n/* MCP Tool Call styles */\n.mcp-container {\n  margin: 0.5rem 0;\n  padding: 0.5rem;\n  border-radius: 0.5rem;\n  background-color: rgba(var(--primary) / 0.05);\n  border: 1px solid rgba(var(--primary) / 0.1);\n}\n\n.mcp-tool-call {\n  background-color: rgba(var(--background) / 0.5);\n  border-radius: 0.5rem;\n  padding: 1rem;\n  margin-top: 0.5rem;\n}\n\n.tool-arguments {\n  background-color: rgba(var(--muted) / 0.3);\n  border-radius: 0.375rem;\n  padding: 0.75rem;\n  margin-bottom: 0.75rem;\n  border: 1px solid rgba(var(--border) / 0.5);\n}\n\n.tool-result {\n  border-radius: 0.375rem;\n  overflow: hidden;\n}\n\n.tool-result .bg-blue-500\\/10 {\n  background-color: rgba(59, 130, 246, 0.1);\n  border: 1px solid rgba(59, 130, 246, 0.2);\n}\n\n.tool-result .bg-green-500\\/10 {\n  background-color: rgba(34, 197, 94, 0.1);\n  border: 1px solid rgba(34, 197, 94, 0.2);\n}\n\n.tool-result .bg-red-500\\/10 {\n  background-color: rgba(239, 68, 68, 0.1);\n  border: 1px solid rgba(239, 68, 68, 0.2);\n}\n\n.tool-result pre {\n  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n  font-size: 0.875rem;\n  line-height: 1.5;\n  max-height: 400px;\n  overflow-y: auto;\n}\n\n/* MCP Tool Call Button styles */\n.mcp-container button {\n  transition: all 0.2s ease-in-out;\n}\n\n.mcp-container button:hover {\n  transform: translateY(-1px);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\n/* Status indicators */\n.text-blue-500 {\n  color: rgb(59, 130, 246);\n}\n\n.text-green-500 {\n  color: rgb(34, 197, 94);\n}\n\n.text-red-500 {\n  color: rgb(239, 68, 68);\n}\n\n.text-blue-600 {\n  color: rgb(37, 99, 235);\n}\n\n.text-green-700 {\n  color: rgb(21, 128, 61);\n}\n\n.text-red-600 {\n  color: rgb(220, 38, 38);\n}\n\n/* Animation for spinner */\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.animate-spin {\n  animation: spin 1s linear infinite;\n}\n\n/* Responsive styles for MCP components */\n@media (max-width: 768px) {\n  .mcp-container {\n    margin: 0.25rem 0;\n    padding: 0.375rem;\n  }\n  \n  .mcp-tool-call {\n    padding: 0.75rem;\n  }\n  \n  .tool-arguments,\n  .tool-result {\n    padding: 0.5rem;\n  }\n  \n  .tool-result pre {\n    font-size: 0.75rem;\n    max-height: 300px;\n  }\n}\n\n/* MCP Debug Tabs styles */\n.mcp-debug-tabs {\n  grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));\n  gap: 2px;\n  background-color: hsl(var(--muted-foreground) / 0.05);\n  padding: 2px;\n  border-radius: 0.5rem;\n  border: 1px solid var(--border);\n}\n\n.mcp-debug-tabs [data-state=\"active\"] {\n  background-color: var(--background);\n  border: 1px solid var(--border);\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n  transition: background-color 0.2s;\n}\n\n.mcp-debug-tabs [data-state=\"inactive\"] {\n  background-color: transparent;\n  color: var(--muted-foreground);\n  transition: background-color 0.2s;\n}\n\n.mcp-debug-tabs [data-state=\"inactive\"]:hover {\n  background-color: hsl(var(--muted-foreground) / 0.05);\n  color: var(--foreground);\n}\n\n/* Debug panel tabs content */\n.debug-panel [data-orientation=\"horizontal\"][role=\"tablist\"] {\n  margin-bottom: 0.75rem;\n}\n\n.debug-panel [role=\"tabpanel\"] {\n  min-height: 60px;\n  max-height: 400px;\n}\n\n/* MCP Debug ScrollArea styles */\n.mcp-debug-scroll-area {\n  height: 300px;\n  max-height: 300px;\n  width: 100%;\n  border: 1px solid var(--border);\n  border-radius: 0.5rem;\n  background-color: hsl(var(--muted-foreground) / 0.02);\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-viewport] {\n  height: 100%;\n  width: 100%;\n  border-radius: inherit;\n}\n\n.mcp-debug-scroll-area pre {\n  border: none;\n  background-color: transparent;\n  margin: 0;\n  min-height: 100%;\n  width: 100%;\n}\n\n/* MCP Debug Content styles (for non-scrollable content) */\n.mcp-debug-content {\n  width: 100%;\n  border: 1px solid var(--border);\n  border-radius: 0.5rem;\n  background-color: hsl(var(--muted-foreground) / 0.02);\n}\n\n.mcp-debug-content pre {\n  border: none;\n  background-color: transparent;\n  margin: 0;\n}\n\n/* ScrollArea scrollbar styling to match plugin-item theme */\n.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar] {\n  background-color: hsl(var(--muted-foreground) / 0.05);\n  border-radius: 0.375rem;\n  transition: background-color 0.2s;\n  width: 8px;\n  height: 8px;\n  padding: 1px;\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar]:hover {\n  background-color: hsl(var(--muted-foreground) / 0.08);\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation=\"vertical\"] {\n  width: 8px;\n  border-left: 1px solid transparent;\n  border-right: 1px solid transparent;\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation=\"horizontal\"] {\n  height: 8px;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-thumb] {\n  background-color: hsl(var(--muted-foreground) / 0.3);\n  border-radius: 0.25rem;\n  transition: background-color 0.2s;\n  position: relative;\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-thumb]:hover {\n  background-color: hsl(var(--muted-foreground) / 0.5);\n}\n\n.mcp-debug-scroll-area [data-radix-scroll-area-corner] {\n  background-color: hsl(var(--muted-foreground) / 0.05);\n}\n\n/* Responsive styles for debug tabs */\n@media (max-width: 768px) {\n  .mcp-debug-tabs {\n    grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));\n    gap: 1px;\n    padding: 1px;\n  }\n  \n  .debug-panel [role=\"tabpanel\"] {\n    min-height: 40px;\n    max-height: 250px;\n  }\n  \n  .mcp-debug-scroll-area {\n    height: 180px;\n    max-height: 180px;\n  }\n  \n  .mcp-debug-scroll-area pre,\n  .mcp-debug-content pre {\n    font-size: 0.75rem;\n  }\n}\n\n@media (max-width: 480px) {\n  .mcp-debug-tabs {\n    grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));\n  }\n  \n  .debug-panel [role=\"tabpanel\"] {\n    min-height: 30px;\n    max-height: 180px;\n  }\n  \n  .mcp-debug-scroll-area {\n    height: 120px;\n    max-height: 120px;\n  }\n  \n  .mcp-debug-scroll-area pre,\n  .mcp-debug-content pre {\n    padding: 0.5rem;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/fonts/all.less",
    "content": "@import \"common\";\n@import \"katex\";"
  },
  {
    "path": "app/src/assets/fonts/common.less",
    "content": "\n@import '@fontsource-variable/inter';\n@import '@fontsource-variable/jetbrains-mono';"
  },
  {
    "path": "app/src/assets/fonts/katex.less",
    "content": "/* stylelint-disable font-family-no-missing-generic-family-keyword */\n@font-face {\n  font-family: 'KaTeX_AMS';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Caligraphic';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.ttf) format('truetype');\n  font-weight: bold;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Caligraphic';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Fraktur';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.ttf) format('truetype');\n  font-weight: bold;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Fraktur';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Main';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.ttf) format('truetype');\n  font-weight: bold;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Main';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.ttf) format('truetype');\n  font-weight: bold;\n  font-style: italic;\n}\n@font-face {\n  font-family: 'KaTeX_Main';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.ttf) format('truetype');\n  font-weight: normal;\n  font-style: italic;\n}\n@font-face {\n  font-family: 'KaTeX_Main';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Math';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.ttf) format('truetype');\n  font-weight: bold;\n  font-style: italic;\n}\n@font-face {\n  font-family: 'KaTeX_Math';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.ttf) format('truetype');\n  font-weight: normal;\n  font-style: italic;\n}\n@font-face {\n  font-family: 'KaTeX_SansSerif';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.ttf) format('truetype');\n  font-weight: bold;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_SansSerif';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.ttf) format('truetype');\n  font-weight: normal;\n  font-style: italic;\n}\n@font-face {\n  font-family: 'KaTeX_SansSerif';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Script';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Size1';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Size2';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Size3';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Size4';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: 'KaTeX_Typewriter';\n  src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n.katex {\n  font: normal 1.21em KaTeX_Main, Times New Roman, serif;\n  line-height: 1.2;\n  text-indent: 0;\n  text-rendering: auto;\n}\n.katex * {\n  -ms-high-contrast-adjust: none !important;\n  border-color: currentColor;\n}\n.katex .katex-version::after {\n  content: \"0.16.0\";\n}\n.katex .katex-mathml {\n  /* Accessibility hack to only show to screen readers\n         Found at: http://a11yproject.com/posts/how-to-hide-content/ */\n  position: absolute;\n  clip: rect(1px, 1px, 1px, 1px);\n  padding: 0;\n  border: 0;\n  height: 1px;\n  width: 1px;\n  overflow: hidden;\n}\n.katex .katex-html {\n  /* \\newline is an empty block at top level, between .base elements */\n\n  white-space: pre-wrap;\n}\n.katex .katex-html > .newline {\n  display: block;\n}\n.katex .base {\n  position: relative;\n  display: inline-block;\n}\n.katex .strut {\n  display: inline-block;\n}\n.katex .textbf {\n  font-weight: bold;\n}\n.katex .textit {\n  font-style: italic;\n}\n.katex .textrm {\n  font-family: KaTeX_Main;\n}\n.katex .textsf {\n  font-family: KaTeX_SansSerif;\n}\n.katex .texttt {\n  font-family: KaTeX_Typewriter;\n}\n.katex .mathnormal {\n  font-family: KaTeX_Math;\n  font-style: italic;\n}\n.katex .mathit {\n  font-family: KaTeX_Main;\n  font-style: italic;\n}\n.katex .mathrm {\n  font-style: normal;\n}\n.katex .mathbf {\n  font-family: KaTeX_Main;\n  font-weight: bold;\n}\n.katex .boldsymbol {\n  font-family: KaTeX_Math;\n  font-weight: bold;\n  font-style: italic;\n}\n.katex .amsrm {\n  font-family: KaTeX_AMS;\n}\n.katex .mathbb,\n.katex .textbb {\n  font-family: KaTeX_AMS;\n}\n.katex .mathcal {\n  font-family: KaTeX_Caligraphic;\n}\n.katex .mathfrak,\n.katex .textfrak {\n  font-family: KaTeX_Fraktur;\n}\n.katex .mathtt {\n  font-family: KaTeX_Typewriter;\n}\n.katex .mathscr,\n.katex .textscr {\n  font-family: KaTeX_Script;\n}\n.katex .mathsf,\n.katex .textsf {\n  font-family: KaTeX_SansSerif;\n}\n.katex .mathboldsf,\n.katex .textboldsf {\n  font-family: KaTeX_SansSerif;\n  font-weight: bold;\n}\n.katex .mathitsf,\n.katex .textitsf {\n  font-family: KaTeX_SansSerif;\n  font-style: italic;\n}\n.katex .mainrm {\n  font-family: KaTeX_Main;\n  font-style: normal;\n}\n.katex .vlist-t {\n  display: inline-table;\n  table-layout: fixed;\n  border-collapse: collapse;\n}\n.katex .vlist-r {\n  display: table-row;\n}\n.katex .vlist {\n  display: table-cell;\n  vertical-align: bottom;\n  position: relative;\n}\n.katex .vlist > span {\n  display: block;\n  height: 0;\n  position: relative;\n}\n.katex .vlist > span > span {\n  display: inline-block;\n}\n.katex .vlist > span > .pstrut {\n  overflow: hidden;\n  width: 0;\n}\n.katex .vlist-t2 {\n  margin-right: -2px;\n}\n.katex .vlist-s {\n  display: table-cell;\n  vertical-align: bottom;\n  font-size: 1px;\n  width: 2px;\n  min-width: 2px;\n}\n.katex .vbox {\n  display: inline-flex;\n  flex-direction: column;\n  align-items: baseline;\n}\n.katex .hbox {\n  display: inline-flex;\n  flex-direction: row;\n  width: 100%;\n}\n.katex .thinbox {\n  display: inline-flex;\n  flex-direction: row;\n  width: 0;\n  max-width: 0;\n}\n.katex .msupsub {\n  text-align: left;\n}\n.katex .mfrac > span > span {\n  text-align: center;\n}\n.katex .mfrac .frac-line {\n  display: inline-block;\n  width: 100%;\n  border-bottom-style: solid;\n}\n.katex .mfrac .frac-line,\n.katex .overline .overline-line,\n.katex .underline .underline-line,\n.katex .hline,\n.katex .hdashline,\n.katex .rule {\n  min-height: 1px;\n}\n.katex .mspace {\n  display: inline-block;\n}\n.katex .llap,\n.katex .rlap,\n.katex .clap {\n  width: 0;\n  position: relative;\n}\n.katex .llap > .inner,\n.katex .rlap > .inner,\n.katex .clap > .inner {\n  position: absolute;\n}\n.katex .llap > .fix,\n.katex .rlap > .fix,\n.katex .clap > .fix {\n  display: inline-block;\n}\n.katex .llap > .inner {\n  right: 0;\n}\n.katex .rlap > .inner,\n.katex .clap > .inner {\n  left: 0;\n}\n.katex .clap > .inner > span {\n  margin-left: -50%;\n  margin-right: 50%;\n}\n.katex .rule {\n  display: inline-block;\n  border: solid 0;\n  position: relative;\n}\n.katex .overline .overline-line,\n.katex .underline .underline-line,\n.katex .hline {\n  display: inline-block;\n  width: 100%;\n  border-bottom-style: solid;\n}\n.katex .hdashline {\n  display: inline-block;\n  width: 100%;\n  border-bottom-style: dashed;\n}\n.katex .sqrt > .root {\n  /* These values are taken from the definition of `\\r@@t`,\n             `\\mkern 5mu` and `\\mkern -10mu`. */\n  margin-left: 0.27777778em;\n  margin-right: -0.55555556em;\n}\n.katex .sizing.reset-size1.size1,\n.katex .fontsize-ensurer.reset-size1.size1 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size1.size2,\n.katex .fontsize-ensurer.reset-size1.size2 {\n  font-size: 1.2em;\n}\n.katex .sizing.reset-size1.size3,\n.katex .fontsize-ensurer.reset-size1.size3 {\n  font-size: 1.4em;\n}\n.katex .sizing.reset-size1.size4,\n.katex .fontsize-ensurer.reset-size1.size4 {\n  font-size: 1.6em;\n}\n.katex .sizing.reset-size1.size5,\n.katex .fontsize-ensurer.reset-size1.size5 {\n  font-size: 1.8em;\n}\n.katex .sizing.reset-size1.size6,\n.katex .fontsize-ensurer.reset-size1.size6 {\n  font-size: 2em;\n}\n.katex .sizing.reset-size1.size7,\n.katex .fontsize-ensurer.reset-size1.size7 {\n  font-size: 2.4em;\n}\n.katex .sizing.reset-size1.size8,\n.katex .fontsize-ensurer.reset-size1.size8 {\n  font-size: 2.88em;\n}\n.katex .sizing.reset-size1.size9,\n.katex .fontsize-ensurer.reset-size1.size9 {\n  font-size: 3.456em;\n}\n.katex .sizing.reset-size1.size10,\n.katex .fontsize-ensurer.reset-size1.size10 {\n  font-size: 4.148em;\n}\n.katex .sizing.reset-size1.size11,\n.katex .fontsize-ensurer.reset-size1.size11 {\n  font-size: 4.976em;\n}\n.katex .sizing.reset-size2.size1,\n.katex .fontsize-ensurer.reset-size2.size1 {\n  font-size: 0.83333333em;\n}\n.katex .sizing.reset-size2.size2,\n.katex .fontsize-ensurer.reset-size2.size2 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size2.size3,\n.katex .fontsize-ensurer.reset-size2.size3 {\n  font-size: 1.16666667em;\n}\n.katex .sizing.reset-size2.size4,\n.katex .fontsize-ensurer.reset-size2.size4 {\n  font-size: 1.33333333em;\n}\n.katex .sizing.reset-size2.size5,\n.katex .fontsize-ensurer.reset-size2.size5 {\n  font-size: 1.5em;\n}\n.katex .sizing.reset-size2.size6,\n.katex .fontsize-ensurer.reset-size2.size6 {\n  font-size: 1.66666667em;\n}\n.katex .sizing.reset-size2.size7,\n.katex .fontsize-ensurer.reset-size2.size7 {\n  font-size: 2em;\n}\n.katex .sizing.reset-size2.size8,\n.katex .fontsize-ensurer.reset-size2.size8 {\n  font-size: 2.4em;\n}\n.katex .sizing.reset-size2.size9,\n.katex .fontsize-ensurer.reset-size2.size9 {\n  font-size: 2.88em;\n}\n.katex .sizing.reset-size2.size10,\n.katex .fontsize-ensurer.reset-size2.size10 {\n  font-size: 3.45666667em;\n}\n.katex .sizing.reset-size2.size11,\n.katex .fontsize-ensurer.reset-size2.size11 {\n  font-size: 4.14666667em;\n}\n.katex .sizing.reset-size3.size1,\n.katex .fontsize-ensurer.reset-size3.size1 {\n  font-size: 0.71428571em;\n}\n.katex .sizing.reset-size3.size2,\n.katex .fontsize-ensurer.reset-size3.size2 {\n  font-size: 0.85714286em;\n}\n.katex .sizing.reset-size3.size3,\n.katex .fontsize-ensurer.reset-size3.size3 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size3.size4,\n.katex .fontsize-ensurer.reset-size3.size4 {\n  font-size: 1.14285714em;\n}\n.katex .sizing.reset-size3.size5,\n.katex .fontsize-ensurer.reset-size3.size5 {\n  font-size: 1.28571429em;\n}\n.katex .sizing.reset-size3.size6,\n.katex .fontsize-ensurer.reset-size3.size6 {\n  font-size: 1.42857143em;\n}\n.katex .sizing.reset-size3.size7,\n.katex .fontsize-ensurer.reset-size3.size7 {\n  font-size: 1.71428571em;\n}\n.katex .sizing.reset-size3.size8,\n.katex .fontsize-ensurer.reset-size3.size8 {\n  font-size: 2.05714286em;\n}\n.katex .sizing.reset-size3.size9,\n.katex .fontsize-ensurer.reset-size3.size9 {\n  font-size: 2.46857143em;\n}\n.katex .sizing.reset-size3.size10,\n.katex .fontsize-ensurer.reset-size3.size10 {\n  font-size: 2.96285714em;\n}\n.katex .sizing.reset-size3.size11,\n.katex .fontsize-ensurer.reset-size3.size11 {\n  font-size: 3.55428571em;\n}\n.katex .sizing.reset-size4.size1,\n.katex .fontsize-ensurer.reset-size4.size1 {\n  font-size: 0.625em;\n}\n.katex .sizing.reset-size4.size2,\n.katex .fontsize-ensurer.reset-size4.size2 {\n  font-size: 0.75em;\n}\n.katex .sizing.reset-size4.size3,\n.katex .fontsize-ensurer.reset-size4.size3 {\n  font-size: 0.875em;\n}\n.katex .sizing.reset-size4.size4,\n.katex .fontsize-ensurer.reset-size4.size4 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size4.size5,\n.katex .fontsize-ensurer.reset-size4.size5 {\n  font-size: 1.125em;\n}\n.katex .sizing.reset-size4.size6,\n.katex .fontsize-ensurer.reset-size4.size6 {\n  font-size: 1.25em;\n}\n.katex .sizing.reset-size4.size7,\n.katex .fontsize-ensurer.reset-size4.size7 {\n  font-size: 1.5em;\n}\n.katex .sizing.reset-size4.size8,\n.katex .fontsize-ensurer.reset-size4.size8 {\n  font-size: 1.8em;\n}\n.katex .sizing.reset-size4.size9,\n.katex .fontsize-ensurer.reset-size4.size9 {\n  font-size: 2.16em;\n}\n.katex .sizing.reset-size4.size10,\n.katex .fontsize-ensurer.reset-size4.size10 {\n  font-size: 2.5925em;\n}\n.katex .sizing.reset-size4.size11,\n.katex .fontsize-ensurer.reset-size4.size11 {\n  font-size: 3.11em;\n}\n.katex .sizing.reset-size5.size1,\n.katex .fontsize-ensurer.reset-size5.size1 {\n  font-size: 0.55555556em;\n}\n.katex .sizing.reset-size5.size2,\n.katex .fontsize-ensurer.reset-size5.size2 {\n  font-size: 0.66666667em;\n}\n.katex .sizing.reset-size5.size3,\n.katex .fontsize-ensurer.reset-size5.size3 {\n  font-size: 0.77777778em;\n}\n.katex .sizing.reset-size5.size4,\n.katex .fontsize-ensurer.reset-size5.size4 {\n  font-size: 0.88888889em;\n}\n.katex .sizing.reset-size5.size5,\n.katex .fontsize-ensurer.reset-size5.size5 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size5.size6,\n.katex .fontsize-ensurer.reset-size5.size6 {\n  font-size: 1.11111111em;\n}\n.katex .sizing.reset-size5.size7,\n.katex .fontsize-ensurer.reset-size5.size7 {\n  font-size: 1.33333333em;\n}\n.katex .sizing.reset-size5.size8,\n.katex .fontsize-ensurer.reset-size5.size8 {\n  font-size: 1.6em;\n}\n.katex .sizing.reset-size5.size9,\n.katex .fontsize-ensurer.reset-size5.size9 {\n  font-size: 1.92em;\n}\n.katex .sizing.reset-size5.size10,\n.katex .fontsize-ensurer.reset-size5.size10 {\n  font-size: 2.30444444em;\n}\n.katex .sizing.reset-size5.size11,\n.katex .fontsize-ensurer.reset-size5.size11 {\n  font-size: 2.76444444em;\n}\n.katex .sizing.reset-size6.size1,\n.katex .fontsize-ensurer.reset-size6.size1 {\n  font-size: 0.5em;\n}\n.katex .sizing.reset-size6.size2,\n.katex .fontsize-ensurer.reset-size6.size2 {\n  font-size: 0.6em;\n}\n.katex .sizing.reset-size6.size3,\n.katex .fontsize-ensurer.reset-size6.size3 {\n  font-size: 0.7em;\n}\n.katex .sizing.reset-size6.size4,\n.katex .fontsize-ensurer.reset-size6.size4 {\n  font-size: 0.8em;\n}\n.katex .sizing.reset-size6.size5,\n.katex .fontsize-ensurer.reset-size6.size5 {\n  font-size: 0.9em;\n}\n.katex .sizing.reset-size6.size6,\n.katex .fontsize-ensurer.reset-size6.size6 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size6.size7,\n.katex .fontsize-ensurer.reset-size6.size7 {\n  font-size: 1.2em;\n}\n.katex .sizing.reset-size6.size8,\n.katex .fontsize-ensurer.reset-size6.size8 {\n  font-size: 1.44em;\n}\n.katex .sizing.reset-size6.size9,\n.katex .fontsize-ensurer.reset-size6.size9 {\n  font-size: 1.728em;\n}\n.katex .sizing.reset-size6.size10,\n.katex .fontsize-ensurer.reset-size6.size10 {\n  font-size: 2.074em;\n}\n.katex .sizing.reset-size6.size11,\n.katex .fontsize-ensurer.reset-size6.size11 {\n  font-size: 2.488em;\n}\n.katex .sizing.reset-size7.size1,\n.katex .fontsize-ensurer.reset-size7.size1 {\n  font-size: 0.41666667em;\n}\n.katex .sizing.reset-size7.size2,\n.katex .fontsize-ensurer.reset-size7.size2 {\n  font-size: 0.5em;\n}\n.katex .sizing.reset-size7.size3,\n.katex .fontsize-ensurer.reset-size7.size3 {\n  font-size: 0.58333333em;\n}\n.katex .sizing.reset-size7.size4,\n.katex .fontsize-ensurer.reset-size7.size4 {\n  font-size: 0.66666667em;\n}\n.katex .sizing.reset-size7.size5,\n.katex .fontsize-ensurer.reset-size7.size5 {\n  font-size: 0.75em;\n}\n.katex .sizing.reset-size7.size6,\n.katex .fontsize-ensurer.reset-size7.size6 {\n  font-size: 0.83333333em;\n}\n.katex .sizing.reset-size7.size7,\n.katex .fontsize-ensurer.reset-size7.size7 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size7.size8,\n.katex .fontsize-ensurer.reset-size7.size8 {\n  font-size: 1.2em;\n}\n.katex .sizing.reset-size7.size9,\n.katex .fontsize-ensurer.reset-size7.size9 {\n  font-size: 1.44em;\n}\n.katex .sizing.reset-size7.size10,\n.katex .fontsize-ensurer.reset-size7.size10 {\n  font-size: 1.72833333em;\n}\n.katex .sizing.reset-size7.size11,\n.katex .fontsize-ensurer.reset-size7.size11 {\n  font-size: 2.07333333em;\n}\n.katex .sizing.reset-size8.size1,\n.katex .fontsize-ensurer.reset-size8.size1 {\n  font-size: 0.34722222em;\n}\n.katex .sizing.reset-size8.size2,\n.katex .fontsize-ensurer.reset-size8.size2 {\n  font-size: 0.41666667em;\n}\n.katex .sizing.reset-size8.size3,\n.katex .fontsize-ensurer.reset-size8.size3 {\n  font-size: 0.48611111em;\n}\n.katex .sizing.reset-size8.size4,\n.katex .fontsize-ensurer.reset-size8.size4 {\n  font-size: 0.55555556em;\n}\n.katex .sizing.reset-size8.size5,\n.katex .fontsize-ensurer.reset-size8.size5 {\n  font-size: 0.625em;\n}\n.katex .sizing.reset-size8.size6,\n.katex .fontsize-ensurer.reset-size8.size6 {\n  font-size: 0.69444444em;\n}\n.katex .sizing.reset-size8.size7,\n.katex .fontsize-ensurer.reset-size8.size7 {\n  font-size: 0.83333333em;\n}\n.katex .sizing.reset-size8.size8,\n.katex .fontsize-ensurer.reset-size8.size8 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size8.size9,\n.katex .fontsize-ensurer.reset-size8.size9 {\n  font-size: 1.2em;\n}\n.katex .sizing.reset-size8.size10,\n.katex .fontsize-ensurer.reset-size8.size10 {\n  font-size: 1.44027778em;\n}\n.katex .sizing.reset-size8.size11,\n.katex .fontsize-ensurer.reset-size8.size11 {\n  font-size: 1.72777778em;\n}\n.katex .sizing.reset-size9.size1,\n.katex .fontsize-ensurer.reset-size9.size1 {\n  font-size: 0.28935185em;\n}\n.katex .sizing.reset-size9.size2,\n.katex .fontsize-ensurer.reset-size9.size2 {\n  font-size: 0.34722222em;\n}\n.katex .sizing.reset-size9.size3,\n.katex .fontsize-ensurer.reset-size9.size3 {\n  font-size: 0.40509259em;\n}\n.katex .sizing.reset-size9.size4,\n.katex .fontsize-ensurer.reset-size9.size4 {\n  font-size: 0.46296296em;\n}\n.katex .sizing.reset-size9.size5,\n.katex .fontsize-ensurer.reset-size9.size5 {\n  font-size: 0.52083333em;\n}\n.katex .sizing.reset-size9.size6,\n.katex .fontsize-ensurer.reset-size9.size6 {\n  font-size: 0.5787037em;\n}\n.katex .sizing.reset-size9.size7,\n.katex .fontsize-ensurer.reset-size9.size7 {\n  font-size: 0.69444444em;\n}\n.katex .sizing.reset-size9.size8,\n.katex .fontsize-ensurer.reset-size9.size8 {\n  font-size: 0.83333333em;\n}\n.katex .sizing.reset-size9.size9,\n.katex .fontsize-ensurer.reset-size9.size9 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size9.size10,\n.katex .fontsize-ensurer.reset-size9.size10 {\n  font-size: 1.20023148em;\n}\n.katex .sizing.reset-size9.size11,\n.katex .fontsize-ensurer.reset-size9.size11 {\n  font-size: 1.43981481em;\n}\n.katex .sizing.reset-size10.size1,\n.katex .fontsize-ensurer.reset-size10.size1 {\n  font-size: 0.24108004em;\n}\n.katex .sizing.reset-size10.size2,\n.katex .fontsize-ensurer.reset-size10.size2 {\n  font-size: 0.28929605em;\n}\n.katex .sizing.reset-size10.size3,\n.katex .fontsize-ensurer.reset-size10.size3 {\n  font-size: 0.33751205em;\n}\n.katex .sizing.reset-size10.size4,\n.katex .fontsize-ensurer.reset-size10.size4 {\n  font-size: 0.38572806em;\n}\n.katex .sizing.reset-size10.size5,\n.katex .fontsize-ensurer.reset-size10.size5 {\n  font-size: 0.43394407em;\n}\n.katex .sizing.reset-size10.size6,\n.katex .fontsize-ensurer.reset-size10.size6 {\n  font-size: 0.48216008em;\n}\n.katex .sizing.reset-size10.size7,\n.katex .fontsize-ensurer.reset-size10.size7 {\n  font-size: 0.57859209em;\n}\n.katex .sizing.reset-size10.size8,\n.katex .fontsize-ensurer.reset-size10.size8 {\n  font-size: 0.69431051em;\n}\n.katex .sizing.reset-size10.size9,\n.katex .fontsize-ensurer.reset-size10.size9 {\n  font-size: 0.83317261em;\n}\n.katex .sizing.reset-size10.size10,\n.katex .fontsize-ensurer.reset-size10.size10 {\n  font-size: 1em;\n}\n.katex .sizing.reset-size10.size11,\n.katex .fontsize-ensurer.reset-size10.size11 {\n  font-size: 1.19961427em;\n}\n.katex .sizing.reset-size11.size1,\n.katex .fontsize-ensurer.reset-size11.size1 {\n  font-size: 0.20096463em;\n}\n.katex .sizing.reset-size11.size2,\n.katex .fontsize-ensurer.reset-size11.size2 {\n  font-size: 0.24115756em;\n}\n.katex .sizing.reset-size11.size3,\n.katex .fontsize-ensurer.reset-size11.size3 {\n  font-size: 0.28135048em;\n}\n.katex .sizing.reset-size11.size4,\n.katex .fontsize-ensurer.reset-size11.size4 {\n  font-size: 0.32154341em;\n}\n.katex .sizing.reset-size11.size5,\n.katex .fontsize-ensurer.reset-size11.size5 {\n  font-size: 0.36173633em;\n}\n.katex .sizing.reset-size11.size6,\n.katex .fontsize-ensurer.reset-size11.size6 {\n  font-size: 0.40192926em;\n}\n.katex .sizing.reset-size11.size7,\n.katex .fontsize-ensurer.reset-size11.size7 {\n  font-size: 0.48231511em;\n}\n.katex .sizing.reset-size11.size8,\n.katex .fontsize-ensurer.reset-size11.size8 {\n  font-size: 0.57877814em;\n}\n.katex .sizing.reset-size11.size9,\n.katex .fontsize-ensurer.reset-size11.size9 {\n  font-size: 0.69453376em;\n}\n.katex .sizing.reset-size11.size10,\n.katex .fontsize-ensurer.reset-size11.size10 {\n  font-size: 0.83360129em;\n}\n.katex .sizing.reset-size11.size11,\n.katex .fontsize-ensurer.reset-size11.size11 {\n  font-size: 1em;\n}\n.katex .delimsizing.size1 {\n  font-family: KaTeX_Size1;\n}\n.katex .delimsizing.size2 {\n  font-family: KaTeX_Size2;\n}\n.katex .delimsizing.size3 {\n  font-family: KaTeX_Size3;\n}\n.katex .delimsizing.size4 {\n  font-family: KaTeX_Size4;\n}\n.katex .delimsizing.mult .delim-size1 > span {\n  font-family: KaTeX_Size1;\n}\n.katex .delimsizing.mult .delim-size4 > span {\n  font-family: KaTeX_Size4;\n}\n.katex .nulldelimiter {\n  display: inline-block;\n  width: 0.12em;\n}\n.katex .delimcenter {\n  position: relative;\n}\n.katex .op-symbol {\n  position: relative;\n}\n.katex .op-symbol.small-op {\n  font-family: KaTeX_Size1;\n}\n.katex .op-symbol.large-op {\n  font-family: KaTeX_Size2;\n}\n.katex .op-limits > .vlist-t {\n  text-align: center;\n}\n.katex .accent > .vlist-t {\n  text-align: center;\n}\n.katex .accent .accent-body {\n  position: relative;\n}\n.katex .accent .accent-body:not(.accent-full) {\n  width: 0;\n}\n.katex .overlay {\n  display: block;\n}\n.katex .mtable .vertical-separator {\n  display: inline-block;\n  min-width: 1px;\n}\n.katex .mtable .arraycolsep {\n  display: inline-block;\n}\n.katex .mtable .col-align-c > .vlist-t {\n  text-align: center;\n}\n.katex .mtable .col-align-l > .vlist-t {\n  text-align: left;\n}\n.katex .mtable .col-align-r > .vlist-t {\n  text-align: right;\n}\n.katex .svg-align {\n  text-align: left;\n}\n.katex svg {\n  display: block;\n  position: absolute;\n  width: 100%;\n  height: inherit;\n  fill: currentColor;\n  stroke: currentColor;\n  fill-rule: nonzero;\n  fill-opacity: 1;\n  stroke-width: 1;\n  stroke-linecap: butt;\n  stroke-linejoin: miter;\n  stroke-miterlimit: 4;\n  stroke-dasharray: none;\n  stroke-dashoffset: 0;\n  stroke-opacity: 1;\n}\n.katex svg path {\n  stroke: none;\n}\n.katex img {\n  border-style: none;\n  min-width: 0;\n  min-height: 0;\n  max-width: none;\n  max-height: none;\n}\n.katex .stretchy {\n  width: 100%;\n  display: block;\n  position: relative;\n  overflow: hidden;\n}\n.katex .stretchy::before,\n.katex .stretchy::after {\n  content: \"\";\n}\n.katex .hide-tail {\n  width: 100%;\n  position: relative;\n  overflow: hidden;\n}\n.katex .halfarrow-left {\n  position: absolute;\n  left: 0;\n  width: 50.2%;\n  overflow: hidden;\n}\n.katex .halfarrow-right {\n  position: absolute;\n  right: 0;\n  width: 50.2%;\n  overflow: hidden;\n}\n.katex .brace-left {\n  position: absolute;\n  left: 0;\n  width: 25.1%;\n  overflow: hidden;\n}\n.katex .brace-center {\n  position: absolute;\n  left: 25%;\n  width: 50%;\n  overflow: hidden;\n}\n.katex .brace-right {\n  position: absolute;\n  right: 0;\n  width: 25.1%;\n  overflow: hidden;\n}\n.katex .x-arrow-pad {\n  padding: 0 0.5em;\n}\n.katex .cd-arrow-pad {\n  padding: 0 0.55556em 0 0.27778em;\n}\n.katex .x-arrow,\n.katex .mover,\n.katex .munder {\n  text-align: center;\n}\n.katex .boxpad {\n  padding: 0 0.3em;\n}\n.katex .fbox,\n.katex .fcolorbox {\n  box-sizing: border-box;\n  border: 0.04em solid;\n}\n.katex .cancel-pad {\n  padding: 0 0.2em;\n}\n.katex .cancel-lap {\n  margin-left: -0.2em;\n  margin-right: -0.2em;\n}\n.katex .sout {\n  border-bottom-style: solid;\n  border-bottom-width: 0.08em;\n}\n.katex .angl {\n  box-sizing: border-box;\n  border-top: 0.049em solid;\n  border-right: 0.049em solid;\n  margin-right: 0.03889em;\n}\n.katex .anglpad {\n  padding: 0 0.03889em;\n}\n.katex .eqn-num::before {\n  counter-increment: katexEqnNo;\n  content: \"(\" counter(katexEqnNo) \")\";\n}\n.katex .mml-eqn-num::before {\n  counter-increment: mmlEqnNo;\n  content: \"(\" counter(mmlEqnNo) \")\";\n}\n.katex .mtr-glue {\n  width: 50%;\n}\n.katex .cd-vert-arrow {\n  display: inline-block;\n  position: relative;\n}\n.katex .cd-label-left {\n  display: inline-block;\n  position: absolute;\n  right: calc(50% + 0.3em);\n  text-align: left;\n}\n.katex .cd-label-right {\n  display: inline-block;\n  position: absolute;\n  left: calc(50% + 0.3em);\n  text-align: right;\n}\n.katex-display {\n  display: block;\n  margin: 1em 0;\n  text-align: center;\n}\n.katex-display > .katex {\n  display: block;\n  text-align: center;\n  white-space: nowrap;\n}\n.katex-display > .katex > .katex-html {\n  display: block;\n  position: relative;\n}\n.katex-display > .katex > .katex-html > .tag {\n  position: absolute;\n  right: 0;\n}\n.katex-display.leqno > .katex > .katex-html > .tag {\n  left: 0;\n  right: auto;\n}\n.katex-display.fleqn > .katex {\n  text-align: left;\n  padding-left: 2em;\n}\nbody {\n  counter-reset: katexEqnNo mmlEqnNo;\n}\n"
  },
  {
    "path": "app/src/assets/globals.less",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@import 'normalize.css';\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --background-light: 0 0% 100%;\n    --background-dark: 0 0% 0%;\n    --background-hover: 5 0 98%;\n    --background-active: 5 0 96%;\n    --background-container: 0, 0%, 97%, 0.9;\n    --background-container-hover: 0, 0%, 97%, 0.95;\n    --foreground: 240 10% 3.9%;\n\n    --card: 0 0% 100%;\n    --card-hover: 0 0% 97%;\n    --card-active: 0 0% 94.5%;\n    --card-foreground: 240 10% 3.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 240 10% 3.9%;\n\n    --primary: 240 5.9% 10%;\n    --primary-foreground: 0 0% 98%;\n\n    --secondary: 240 5.9% 97.5%;\n    --secondary-foreground: 240 5.9% 10%;\n\n    --muted: 240 4.8% 95.9%;\n    --muted-foreground: 240 3.8% 46.1%;\n\n    --accent: 240 4.8% 95.9%;\n    --accent-secondary: 240 4.8% 95.9%;\n    --accent-foreground: 240 5.9% 10%;\n\n    --destructive: 0 67.22% 50.59%;\n    --destructive-foreground: 0 0% 98%;\n\n    --success: 120 100% 50%;\n    --success-foreground: 0 0% 98%;\n\n    --failure: 0 67.22% 50.59%;\n    --failure-foreground: 0 0% 98%;\n\n    --border: 240 5.9% 90%;\n    --border-hover: 240 5.9% 85%;\n    --border-active: 240 5.9% 80%;\n\n    --input: 240 5.9% 90%;\n    --input-unread: 240 5.9% 50%;\n    --ring: 240 5% 64.9%;\n\n    --text: 0 0% 0%;\n    --text-light: 0 0% 100%;\n    --text-dark: 0 0% 100%;\n    --text-secondary: 0 0% 35%;\n    --text-unread: 0 0% 50%;\n    --text-secondary-dark: 0 0% 80%;\n\n    --selection: 212 100% 41%;\n    --selection-foreground: 0 0% 98%;\n\n    --radius: 0.5rem;\n\n    --shadow: #00000005;\n\n    --gold: 40 100% 50%;\n    --gold-foreground: 35 100% 40%;\n\n    --link: 210 100% 63%;\n    --error: 20 80% 50%;\n\n    --anim-bar: no-repeat linear-gradient(#333 0 0);\n  }\n\n  .dark {\n    --background: 0 0% 0%;\n    --background-hover: 0 0% 7.8%;\n    --background-active: 0 0% 13.7%;\n    --background-container: 0, 0%, 5%, 0.9;\n    --background-container-hover: 0, 0%, 5%, 0.95;\n    --foreground: 210 40% 98%;\n\n    --card: 240 10% 3.9%;\n    --card-hover: 240 11% 12.5%;\n    --card-active: 240 9.5% 19%;\n    --card-foreground: 0 0% 98%;\n\n    --popover: 240 10% 3.9%;\n    --popover-foreground: 0 0% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 0 0 10%;\n    --secondary-foreground: 0 0% 98%;\n\n    --muted: 240 3.7% 15.9%;\n    --muted-foreground: 240 5% 64.9%;\n\n    --accent: 240 3.7% 15.9%;\n    --accent-secondary: 240 5% 15.9%;\n    --accent-foreground: 0 0% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 240 3.7% 12.9%;\n    --border-hover: 240 3.7% 17.9%;\n    --border-active: 240 3.7% 25.9%;\n    --input: 240 3.7% 15.9%;\n    --input-unread: 240 3.7% 50%;\n    --ring: 240 4.9% 83.9%;\n    --text: 0 0% 100%;\n    --text-secondary: 0 0% 80%;\n    --text-unread: 0 0% 50%;\n\n    --shadow: #ffffff05;\n    --anim-bar: no-repeat linear-gradient(#ccc 0 0);\n  }\n\n  [type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {\n    @apply border-border;\n  }\n}\n\n* {\n  --tw-ring-color: none !important;\n}\n\n.ui-input:-webkit-autofill,\n.ui-input:-moz-autofill,\n.ui-input:-ms-autofill,\n.ui-input:-o-autofill,\n.ui-input:autofill {\n  background: hsl(var(--background)) !important;\n  color: hsl(var(--foreground)) !important;\n\n  -webkit-text-fill-color: hsl(var(--foreground)) !important;\n  transition: background-color 5000s ease-in-out 0s;\n  caret-color: hsl(var(--foreground)) !important;\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/main.less",
    "content": "@import \"ui\";\n@font-family: \"HarmonyOS Sans\",\"Inter Variable\",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";\n@font-family-normal: \"HarmonyOS Sans\",\"Inter Variable\",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";\n@font-family-code: \"JetBrains Mono Variable\",\"HarmonyOS Sans\",monospace,\"Inter Variable\",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";\n@line-height: 1.5;\n@font-weight: 400;\n\n@color-scheme: light dark;\n\n@font-synthesis: none;\n@text-rendering: optimizeLegibility;\n@-webkit-font-smoothing: antialiased;\n@-moz-osx-font-smoothing: grayscale;\n@-webkit-text-size-adjust: 100%;\n\nhtml, body, #root {\n  margin: 0;\n  padding: 0;\n  width: 100%;\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: auto;\n  touch-action: pan-y;\n  -webkit-overflow-scrolling: touch;\n}\n\n:root {\n  font-family: @font-family;\n  line-height: @line-height;\n  font-weight: @font-weight;\n\n  color-scheme: @color-scheme;\n\n  font-synthesis: @font-synthesis;\n  text-rendering: @text-rendering;\n  -webkit-font-smoothing: @-webkit-font-smoothing;\n  -moz-osx-font-smoothing: @-moz-osx-font-smoothing;\n  -webkit-text-size-adjust: @-webkit-text-size-adjust;\n  --font-family: @font-family;\n  --font-family-normal: @font-family-normal;\n  --font-family-code: @font-family-code;\n\n  --navbar-height: 3.375rem;\n}\n\n.font-code {\n  font-family: @font-family-code;\n}\n\n* {\n  outline: 0;\n  box-sizing: border-box;\n  -webkit-tap-highlight-color: transparent;\n  -webkit-overflow-scrolling: touch;\n}\n\nbody {\n  scrollbar-width: none;\n\n  &::-webkit-scrollbar {\n    display: none;\n  }\n}\n\n.grow {\n  flex-grow: 1;\n}\n\nstrong {\n  font-weight: bold;\n}\n\n.jetbrains-mono {\n  font-family: @font-family-code;\n}\n\n.hover\\:bg-accent[aria-pressed=\"false\"] {\n  color: hsl(var(--text-secondary));\n\n  &:hover {\n    color: hsl(var(--text-secondary));\n    background: hsl(var(--accent-secondary));\n  }\n}\n\n.hover\\:bg-accent[aria-pressed=\"true\"] {\n  background: hsl(var(--accent));\n  color: hsl(var(--text));\n  border: 1px solid hsl(var(--border));\n}\n\n.icon-tooltip {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n\n  &.gold {\n    color: hsl(var(--gold));\n  }\n}\n\n.flex-dialog {\n  border-radius: var(--radius) !important;\n  max-height: calc(95% - 2rem) !important;\n  overflow-x: hidden;\n  overflow-y: auto;\n  scrollbar-width: none;\n  -webkit-overflow-scrolling: touch;\n  touch-action: pan-y;\n  outline: none;\n\n  @media (max-width: 520px) {\n    & {\n      margin-top: 1rem;\n      margin-bottom: 1rem;\n      transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important;\n    }\n  }\n\n  .link {\n    color: hsl(var(--text-secondary));\n    text-decoration: none;\n    transition: color 0.2s ease-in-out;\n    user-select: none;\n    cursor: pointer;\n    margin: 0.5rem auto;\n\n    &:hover {\n      color: hsl(var(--text));\n    }\n  }\n\n  .content-h-60 {\n    height: 60vh;\n  }\n\n  .content-w-980 {\n    width: 80vw;\n    max-width: 980px;\n  }\n\n  &.full-screen {\n    width: 100vw !important;\n    height: 100% !important;\n    max-width: 100vw !important;\n    max-height: 100% !important;\n    border-radius: 0 !important;\n    border: 1px solid hsl(var(--border)) !important;\n\n    .content-h-60 {\n      height: calc(100vh - 6rem);\n    }\n\n    .content-w-980 {\n      max-width: none;\n      width: calc(100vw - 2rem);\n    }\n\n    .content-h-fit {\n      height: fit-content;\n    }\n\n    .editor-preview,\n    .editor-input {\n      min-height: calc(100vh - 8.5rem) !important;\n    }\n  }\n}\n\n\n.announcement-dialog {\n  max-width: min(90vw, 720px) !important;\n}\n\n.share-dialog {\n  max-width: min(90vw, 720px) !important;\n}\n\n.record-dialog {\n  max-width: min(90vw, 1024px) !important;\n}\n\n.pre-quota-dialog {\n  max-width: min(90vw, 720px) !important;\n}\n\n.key-dialog {\n  max-width: min(90vw, 1024px) !important;\n}\n\n.fixed-dialog {\n  border-radius: var(--radius) !important;\n  max-height: calc(95% - 2rem) !important;\n  min-height: 60vh;\n  overflow-x: hidden;\n  overflow-y: auto;\n  scrollbar-width: none;\n  -webkit-overflow-scrolling: touch;\n  touch-action: pan-y;\n  outline: 0;\n\n  @media (max-width: 660px) {\n    width: 100vw !important;\n    height: 100% !important;\n    max-width: 100vw !important;\n    max-height: 100% !important;\n    border-radius: 0 !important;\n  }\n}\n\n.cent {\n  font-weight: normal !important;\n}\n\n.error-boundary {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  height: max-content;\n  min-height: calc(100% - var(--navbar-height));\n  overflow: hidden;\n  background: hsla(var(--background-container));\n  padding: 2.5rem 5rem;\n\n  @media (max-width: 720px) {\n    & {\n      padding: 2.5rem 1rem;\n    }\n  }\n\n  .error-provider {\n    text-align: center;\n    margin: 0.5rem auto;\n\n    p {\n      margin: 0.35rem;\n    }\n  }\n\n  .error-tips {\n    text-align: center;\n    padding: 1rem 2rem;\n    color: hsl(var(--text-secondary));\n  }\n}\n\n.tips-icon {\n  width: 1rem;\n  height: 1rem;\n  cursor: pointer;\n  margin-left: 0.2rem;\n  scale: 0.9;\n  color: hsl(var(--text-secondary));\n  outline: none !important;\n}\n\n.chat-logo {\n  border-radius: var(--radius);\n  user-select: none;\n}\n\n.broadcast-markdown {\n  * {\n    font-size: 0.9rem;\n    color: hsl(var(--text));\n  }\n\n  br {\n    display: none;\n  }\n}"
  },
  {
    "path": "app/src/assets/markdown/all.less",
    "content": "@import \"highlight.less\";\n@import \"style.less\";\n@import \"theme.less\";\n\n.file-instance {\n  display: flex;\n  flex-direction: column;\n  margin: 2px 0 !important;\n  margin-bottom: 4px !important;\n  padding: 0.5rem !important;\n  border-radius: var(--radius);\n\n  .file-content {\n    background: none !important;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    flex-wrap: wrap;\n  }\n\n  img {\n    max-width: 320px;\n    max-height: 240px;\n    object-fit: cover;\n    border-radius: var(--radius);\n    margin: 0.25rem 0;\n\n    @media (max-width: 768px) {\n      max-width: 100%;\n      max-height: 100%;\n    }\n  }\n\n  .download-action {\n    background: none !important;\n    border-radius: 0 !important;\n  }\n\n  @media (max-width: 668px) {\n    & {\n      white-space: pre-wrap;\n    }\n  }\n\n  svg {\n    width: 0.85rem;\n    height: 0.85rem;\n    color: hsl(var(--text));\n    flex-shrink: 0;\n  }\n\n  .name {\n    content: attr(file);\n    display: block;\n    color: hsl(var(--text));\n    margin-right: 6px;\n    font-family: var(--font-family) !important;\n  }\n}\n\n.markdown-body {\n  pre code {\n    color: #c9d1d9;\n  }\n\n  pre.file-block div {\n    background: none !important;\n  }\n\n  pre.file-block,\n  pre.file-block > div {\n    padding: 0;\n    margin: 0;\n    background: none !important;\n    box-shadow: none !important;\n\n    &:before {\n      content: none !important;\n    }\n  }\n\n  pre.file-block > div.file-instance {\n    background: hsla(var(--background-container)) !important;\n  }\n\n  .markdown-syntax {\n    position: relative;\n\n    .markdown-syntax-header {\n      position: absolute;\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: center;\n      z-index: 1;\n      top: -34px;\n      right: 0;\n      user-select: none;\n      cursor: pointer;\n      transition: 0.25s ease;\n\n      &:hover {\n        p, svg {\n          color: hsl(var(--text-dark));\n        }\n      }\n\n      p {\n        color: hsl(var(--text-secondary-dark));\n        font-size: 12px;\n        line-height: 1;\n        margin: 0 0 0 6px;\n        padding: 0;\n        transition: 0.25s ease;\n      }\n\n      svg {\n        color: hsl(var(--text-secondary-dark));\n        transition: 0.25s ease;\n      }\n    }\n  }\n}\n\n.virtual-prompt {\n  text-align: center;\n  border-radius: var(--radius);\n  border: 1px solid hsl(var(--border));\n  padding: 0.5rem;\n  margin-bottom: 0.5rem !important;\n}\n\n.virtual-action {\n  svg {\n    transform: translateY(1px);\n  }\n}\n\np:has(.virtual-action) {\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr 1fr;\n  justify-content: end;\n  margin-top: 1.25rem !important;\n\n  br {\n    display: none;\n  }\n\n  @media (max-width: 768px) {\n    grid-template-columns: 1fr 1fr;\n  }\n\n  @media (max-width: 480px) {\n    grid-template-columns: 1fr;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/markdown/highlight.less",
    "content": "/*!\n  Theme: GitHub\n  Description: Light theme as seen on github.com\n  Author: github.com\n  Maintainer: @Hirse\n  Updated: 2021-05-15\n\n  Outdated base version: https://github.com/primer/github-syntax-light\n  Current colors taken from GitHub's CSS\n  License: MIT\n*/\n\npre code.hljs {\n  display: block;\n  overflow-x: auto;\n  padding: 1em\n}\n\ncode.hljs {\n  padding: 3px 5px\n}\n\n.hljs {\n  color: #24292e;\n  background: #fff\n}\n\n.hljs-doctag,\n.hljs-keyword,\n.hljs-meta .hljs-keyword,\n.hljs-template-tag,\n.hljs-template-variable,\n.hljs-type,\n.hljs-variable.language_ {\n  color: #d73a49\n}\n\n.hljs-title,\n.hljs-title.class_,\n.hljs-title.class_.inherited__,\n.hljs-title.function_ {\n  color: #6f42c1\n}\n\n.hljs-attr,\n.hljs-attribute,\n.hljs-literal,\n.hljs-meta,\n.hljs-number,\n.hljs-operator,\n.hljs-selector-attr,\n.hljs-selector-class,\n.hljs-selector-id,\n.hljs-variable {\n  color: #005cc5\n}\n\n.hljs-meta .hljs-string,\n.hljs-regexp,\n.hljs-string {\n  color: #032f62\n}\n\n.hljs-built_in,\n.hljs-symbol {\n  color: #e36209\n}\n\n.hljs-code,\n.hljs-comment,\n.hljs-formula {\n  color: #6a737d\n}\n\n.hljs-name,\n.hljs-quote,\n.hljs-selector-pseudo,\n.hljs-selector-tag {\n  color: #22863a\n}\n\n.hljs-subst {\n  color: #24292e\n}\n\n.hljs-section {\n  color: #005cc5;\n  font-weight: 700\n}\n\n.hljs-bullet {\n  color: #735c0f\n}\n\n.hljs-emphasis {\n  color: #24292e;\n  font-style: italic\n}\n\n.hljs-strong {\n  color: #24292e;\n  font-weight: 700\n}\n\n.hljs-addition {\n  color: #22863a;\n  background-color: #f0fff4\n}\n\n.hljs-deletion {\n  color: #b31d28;\n  background-color: #ffeef0\n}\n"
  },
  {
    "path": "app/src/assets/markdown/style.less",
    "content": "/* This is a theme distributed by `starry-night`.\n * It’s based on what GitHub uses on their site.\n * See <https://github.com/wooorm/starry-night> for more info. */\n:root {\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}\n\n.dark {\n  :root {\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  }\n}\n\n.pl-c {\n  color: var(--color-prettylights-syntax-comment);\n}\n\n.pl-c1,\n.pl-s .pl-v {\n  color: var(--color-prettylights-syntax-constant);\n}\n\n.pl-e,\n.pl-en {\n  color: var(--color-prettylights-syntax-entity);\n}\n\n.pl-smi,\n.pl-s .pl-s1 {\n  color: var(--color-prettylights-syntax-storage-modifier-import);\n}\n\n.pl-ent {\n  color: var(--color-prettylights-syntax-entity-tag);\n}\n\n.pl-k {\n  color: var(--color-prettylights-syntax-keyword);\n}\n\n.pl-s,\n.pl-pds,\n.pl-s .pl-pse .pl-s1,\n.pl-sr,\n.pl-sr .pl-cce,\n.pl-sr .pl-sre,\n.pl-sr .pl-sra {\n  color: var(--color-prettylights-syntax-string);\n}\n\n.pl-v,\n.pl-smw {\n  color: var(--color-prettylights-syntax-variable);\n}\n\n.pl-bu {\n  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);\n}\n\n.pl-ii {\n  color: var(--color-prettylights-syntax-invalid-illegal-text);\n  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);\n}\n\n.pl-c2 {\n  color: var(--color-prettylights-syntax-carriage-return-text);\n  background-color: var(--color-prettylights-syntax-carriage-return-bg);\n}\n\n.pl-sr .pl-cce {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-string-regexp);\n}\n\n.pl-ml {\n  color: var(--color-prettylights-syntax-markup-list);\n}\n\n.pl-mh,\n.pl-mh .pl-en,\n.pl-ms {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-heading);\n}\n\n.pl-mi {\n  font-style: italic;\n  color: var(--color-prettylights-syntax-markup-italic);\n}\n\n.pl-mb {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-bold);\n}\n\n.pl-md {\n  color: var(--color-prettylights-syntax-markup-deleted-text);\n  background-color: var(--color-prettylights-syntax-markup-deleted-bg);\n}\n\n.pl-mi1 {\n  color: var(--color-prettylights-syntax-markup-inserted-text);\n  background-color: var(--color-prettylights-syntax-markup-inserted-bg);\n}\n\n.pl-mc {\n  color: var(--color-prettylights-syntax-markup-changed-text);\n  background-color: var(--color-prettylights-syntax-markup-changed-bg);\n}\n\n.pl-mi2 {\n  color: var(--color-prettylights-syntax-markup-ignored-text);\n  background-color: var(--color-prettylights-syntax-markup-ignored-bg);\n}\n\n.pl-mdr {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-meta-diff-range);\n}\n\n.pl-ba {\n  color: var(--color-prettylights-syntax-brackethighlighter-angle);\n}\n\n.pl-sg {\n  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);\n}\n\n.pl-corl {\n  text-decoration: underline;\n  color: var(--color-prettylights-syntax-constant-other-reference-link);\n}\n"
  },
  {
    "path": "app/src/assets/markdown/theme.less",
    "content": ".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.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\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  border-radius: 4px;\n  max-height: 50vh;\n  margin: 0.5rem auto;\n\n    &.broken {\n        display: none;\n    }\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: 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": "app/src/assets/pages/api.less",
    "content": ".api-dialog {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  height: max-content;\n  padding: 24px 0 12px;\n  gap: 24px;\n}\n\n.api-wrapper {\n  display: flex;\n  flex-direction: row;\n  gap: 6px;\n  width: 100%;\n\n  input {\n    text-align: center;\n    font-size: 16px;\n    cursor: pointer;\n    flex-grow: 1;\n  }\n\n  button {\n    flex-shrink: 0;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/article.less",
    "content": ".article-page {\n  position: relative;\n  display: flex;\n  width: 100%;\n  min-height: calc(100% - var(--navbar-height));\n  height: max-content;\n}\n\n\n.article-container {\n  display: flex;\n  flex-direction: column;\n  padding: 12px 16px;\n  gap: 6px;\n  width: 100%;\n  height: 100%;\n}\n\n.article-wrapper {\n  width: calc(96vw - 32px);\n  height: 100%;\n  margin: 1rem auto;\n  padding: 1rem;\n  max-width: 840px;\n\n  .article-title {\n    display: flex;\n    flex-direction: row;\n    user-select: none;\n    align-items: center;\n  }\n\n  .article-action {\n    @media (max-width: 768px) {\n      flex-direction: column;\n    }\n  }\n\n  .article-content {\n    display: flex;\n    flex-direction: column;\n    margin: 1rem 0;\n\n    & > * {\n      margin-bottom: 1rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/auth.less",
    "content": ".auth {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.auth-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-items: center;\n  padding: 2.5rem 2rem;\n  height: fit-content;\n  user-select: none;\n\n  .logo {\n    width: 4rem;\n    height: 4rem;\n    border-radius: var(--radius);\n  }\n\n  & > * {\n    margin-bottom: 0.5rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.auth-card {\n  width: 80vw;\n  max-width: 360px;\n  min-width: 280px;\n  margin: 1rem 0;\n}\n\n.auth-wrapper {\n  display: flex;\n  flex-direction: column;\n  padding: 1.5rem 0;\n\n  & > * {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.addition-wrapper {\n  display: flex;\n  flex-direction: column;\n  border-radius: var(--radius);\n  border: 1px solid hsla(var(--border));\n  padding: 1.25rem;\n  align-items: center;\n  transform: translateY(-1rem);\n  font-size: 0.875rem;\n  text-align: center;\n\n  a {\n    text-decoration: underline;\n    text-underline-offset: 0.25rem;\n    text-underline: 2px solid hsl(var(--text-secondary));\n    color: hsl(var(--text-secondary));\n    transition: 0.2s ease-in-out;\n    cursor: pointer;\n\n    &:hover {\n      color: hsl(var(--text));\n      text-underline-color: hsl(var(--text));\n    }\n  }\n\n  .row {\n    margin-bottom: 0.5rem;\n    .link {\n      margin-left: 0.1rem;\n    }\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/chat.less",
    "content": "@keyframes FlexInAnimationFromBottom {\n  0% {\n    opacity: .2;\n    margin-top: 20px;\n    margin-bottom: 0;\n  }\n  100% {\n    opacity: 1;\n    margin-top: 0;\n    margin-bottom: 20px;\n  }\n}\n\n.scroll-action {\n  position: absolute;\n  z-index: 12;\n  opacity: 0;\n  right: 36px;\n  bottom: 12rem;\n  transition: .25s;\n  pointer-events: none;\n\n  button {\n    border-color: rgba(0,0,0,0) !important;\n  }\n\n  &.active {\n    opacity: 0.8;\n    pointer-events: all;\n  }\n\n  @media (max-width: 668px) {\n    bottom: 8.5rem;\n  }\n}\n\n.message {\n  display: flex;\n  gap: 6px;\n  flex-direction: column;\n  max-width: 100%;\n\n  pre {\n    scrollbar-width: thin;\n\n    &::-webkit-scrollbar {\n      height: 6px;\n    }\n  }\n\n  &:last-child {\n    animation: FlexInAnimationFromBottom 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s 1 normal forwards running;\n\n    .bing {\n      animation: fadein 0.2s ease-in-out;\n\n      @keyframes fadein {\n        from { opacity: 0.5; }\n        to   { opacity: 1; }\n      }\n    }\n  }\n\n  .content-wrapper {\n    display: flex;\n    flex-direction: row;\n    max-width: 100%;\n\n    .message-toolbar {\n      display: flex;\n      flex-direction: column;\n      padding: 0 4px;\n      user-select: none;\n      height: max-content;\n      margin-top: auto;\n      gap: 4px;\n\n      svg {\n        cursor: pointer;\n        color: hsl(var(--text-secondary));\n        transition: 0.25s;\n\n        &:hover {\n          color: hsl(var(--text));\n        }\n      }\n    }\n  }\n\n  .message-quota {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    user-select: none;\n    gap: 4px;\n    cursor: pointer;\n    border: 1px solid hsl(var(--input));\n    border-radius: var(--radius);\n    transition: 0.2s linear;\n    padding: 4px 8px;\n    width: max-content;\n    height: max-content;\n    white-space: nowrap;\n    margin-left: 3rem;\n    transition-property: border-color, color, background-color, width;\n\n    &.subscription {\n      svg, span {\n        color: hsl(var(--gold));\n      }\n    }\n\n    .quota {\n      font-size: 14px;\n      color: hsl(var(--text-secondary));\n      transition: .25s;\n    }\n\n    .icon {\n      color: hsl(var(--text-secondary));\n    }\n\n    &:hover {\n      border-color: hsl(var(--border-hover));\n    }\n  }\n\n  .message-content {\n    display: flex;\n    flex: 1 1 auto;\n    min-width: 0;\n    flex-direction: column;\n    max-width: 100%;\n    padding: 8px 16px;\n    border-radius: var(--radius);\n    transition: 0.25s linear;\n  }\n\n  .message-avatar-wrapper {\n    .message-avatar {\n      display: flex;\n      width: 2.25rem;\n      height: 2.25rem;\n      border-radius: var(--radius);\n      text-align: center;\n      font-size: 0.785rem;\n    }\n\n    flex-shrink: 0;\n    margin-left: 0.5rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    width: 2.25rem;\n    height: 2.25rem;\n    user-select: none;\n  }\n\n  &.user {\n    align-items: flex-end;\n  }\n\n  @media (max-width: 768px) {\n    .content-wrapper {\n      flex-direction: column !important;\n      align-items: flex-start;\n\n      .message-avatar-wrapper {\n        margin-left: 0;\n        margin-right: 0;\n        margin-bottom: 0.5rem;\n      }\n    }\n\n    .message-toolbar {\n      padding: 0 !important;\n    }\n\n    .message-quota {\n      margin-left: 0;\n    }\n\n    &.user {\n      .content-wrapper {\n        align-items: flex-end;\n      }\n    }\n  }\n\n  &.user {\n    .content-wrapper {\n      flex-direction: row-reverse;\n    }\n  }\n\n  &.assistant, &.system {\n    align-items: flex-start;\n\n    .message-avatar-wrapper {\n      margin-right: 0.5rem;\n      margin-left: 0;\n    }\n  }\n}\n\n.markdown-body {\n  max-width: 100%;\n  padding: 4px 0;\n  background: none !important;\n  color: hsl(var(--text));\n\n  .prompt-row {\n    border: 1px solid hsl(var(--border));\n    border-radius: var(--radius);\n    margin: 0.25rem 0;\n    white-space: nowrap;\n\n    .grow {\n      min-width: 0.75rem;\n    }\n\n    .value {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      font-family: var(--font-family);\n      margin: 0 !important;\n    }\n\n    svg {\n      transform: translateY(1px);\n    }\n  }\n\n  ol, ul, menu {\n    list-style: inherit;\n  }\n\n  pre, code {\n    font-family: var(--font-family-code) !important;\n    background: rgb(40, 44, 52);\n  }\n\n  pre {\n    // width: calc(100% - 8px);\n    box-shadow: #0005 0 2px 2px;\n    border-radius: var(--radius);\n\n    &:before {\n      content: \"\";\n      display: block;\n      background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAACCCAYAAADVN8idAAAgAElEQVR4nO2de5QU5Zn/v1VdVX2/zQwMzDCDgCBKOIx4myXLRlnYGDlhzWWDSTxkhXBQo2iS34kmavb3C5qo5+yqqBs5xNG4ZpVskjXk6BrhqAkbdoyXgSUoiqgMzDjAzPS1+lLX3x/TYNU7F6C7untm+vn8Ne/bVdVvP+8777fe2/MABEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQExKu2BtN03SyHGVhxdS61jk+77xWr3dWk9c7Y4okTakThbqAIIa8POcTeF4EAM0w1KxhZtKamhxUtcETinKiN5s92p3Nfngok31vx/HB7mr/FmLisaItMGv2NPfclqnCrKYGoXVqWJxWF+TrAj4u5JE4n+jiRZMzoWmmmlPMTDpjJgdTxuDxhNrX2691HzmuffhBX/7gjj3pD6v9W4iJx9TFwXqxWWrlG6UmforYiIhQb4ZcEcPPBzjJ5eZd4AHA0GGYip7nZSPNJfU44tqAcUI9ZhxTetUepfv4W6mBav+W08FxRUvZ0P3F3jjehHBByM+3RyNLLw6H29vCwQubPJ6ZhY/aS3x0JwD05nKH9yRSXW8kEp2dsfiu/UnZKPG5xCRiQYuHb5/vvfyieZ4lbXO8FzU1uE62vwtLfHQXAPT064f3Hsq++cZ7ud2vHci+uv9IjtofYWP6VfWfEud7F2Gu9wJMEacVsteW+NgOAMAJtQ8Hs2+rB7J7P35h4C8lPtNxaloI2+tDkRUNDSuvqG9YPsfvnY/SRe9M6TwkywdeGRjcuaO///nOgWS8Qt9LjCP+ap6v/m8X+1de3ua78twmaT5KF70zpev9XuXAq3syL+54S97+2nsZan81SMN8v9tzaXApvziwBDOkky9epQrf6RgSxqPKYeOt9O7cn1O7+g/I+TJ/52mpSSG8aXbL51ZNa/zCeX7/QlRO/Eaj811Z3re979h/PvLBkf+qclmICvCtlfUrP78k8JX5LdJCVE78RqPrwBFl3+92p3/56PMDz1e5LEQFaPrClEvFvw4uN2d65qD8wnc6OozDuUPmrtTve5478Wa1ClEzQtgW8bu/1ty8dnXT9DWFrDEFkOcAURIhCiJEQYDL5YLocoEXePDgwPHcKeOZpgnTMGHAhKEZUHUduq5D1TSomgo1r+IM5qE6TcD4Ze/HT//70Z6OPYnqvyURzrHoHK/7a8tCG1ZfHvrHQtaYAsgBkCQXRJGDKLggCIDg4uHiOfCFtsdxQ/9DpsnBNE0YhgndMKHpBjQNUDUdqmpCUXScwX9bl2lyxrZXk08+80pi696PstT+JhnN1zdf7Voe/nwhOaYAGpwJSRTBSQK4ocYH3sXD5eIB3gWe42AU2h9vcjBMEzB06LoBQzcATYepaTAVDYqqgjdPKxUdnAld3xl/4eiW3udK/7Vnx6QXwraI372uteWmVY2NX8ZpxM/r8cDjluCRJEhu0dFyKHkVOUVBLq8gm8ud7vLO7ceO/erxw0ceIUGc2Cw6x+ted2Xk1lVLAqtxGvHzelzwuAV43DzcEu9oOfKKgVzeQC6vIZvTT3d51/bd6W2Pvxh/kARx4tNyy4zV3NLQ3xWSowqg4HGD94jg3SJcDvd/el6FkVdh5FRouTGbVAcAmLuSLx156Og2RwsxBpNaCH98/rnrvz6jeS3GEECf1wuf1wOf112yMc4U0zSRyeaRyeaQyWbHurTzF0d7On7wzvtbK1IwwlHuWdN4w9eXh9ZjDAH0eUX4vTx8XlfF2p9hmshmdchZDZnsmKLY9fTO5JY7nzq2pSIFIxyl5ZvTV3JXRr9YSI4ogJzPA7fXDc4nga9g+zMzCvLZPMzMqIOCIUF8MfabIz/7uOxT9pNSCNfNbP7MrbNn3RYSXFGMIIKiICLg98Lv98LFO/vmfbbohgFZziItZ6Fq6kiXdCY1LfbgBx/d9/jhnj9UunzE2bNuRXTZLV+quyPk46MYQQRFgUfALyLgF+CqbvODrgPpjIa0rELVRpzA70pmjNiDvx68p2NH7OVKl484e6ZfVf8p1zUN63mfK4CRBFBwQQp44fJ7wFe5ARq6AV3OQUlnAW3El7IOyEZa3XZiazl3m04qIVwQ8vPfnzt709K6umUYQQDdbhFBfwB+n8fx73YCOZNDSk4jnx9ZEHcNDr78k4Mf3EVHL8YnC1o8/G3X1N/7Nwt9yzGCALolF0IBAX6fUIXSnR45oyGZ1pBXRuyQuv74v9md923rv52OXoxfWu+cuQFt/ksxggAKkgQh6IXL765CyU6PLuehpWRoijbSxx3YI/+5++7DZZmdmDRC+I3WpiU/nDvnXoHnl7KfSYKAUCg4bgWQRc7kkEymoGjDG4RqGH/YdPDQD37e3bu7CkUjRmHNsujSO6+tv18SuOEzECKHSFAatwLIImc0xFMKVHX4/6iimZ13Pz3wvadeju2qQtGIUZj+2boLhOumbeQEiGBEkBdFSCHfuBVAFl3OQ0lmYKjDBgQdpmaoesfxB3tfGjzg5HdOCiG8f8G8Gwq7QW2dEM8B4VAIoaDfse+qJMmUjFgiOdJHndt6P37qe/vf+2mly0QM5761jRsLu0Fto0COMxEJeRAOTgwBZEmkNMQT+ZF2nHY9+0qq4/Yn+h6pfKkIFstuUJsAGpwJTzgIMeSrUslKQ01mkEukRtpx2mHsiP/Oyd2lE14If31J2wMXR8LtYETQ5/UiGglCcLkc+Z5qoak6YsnUSJtqOt+IJzq/9Pqeb1ejXMQQv7qj5eGLz/N8GowI+rwi6sIiBKEyGxDKhaaZGEzkR9pU0/X6u/k//cM93TdXo1zEEK2bZm3E+d5FYESQ83ngi/gBYWL3f9B0ZOLySJtqOvBOdm/3XR9uduJrJqwQtteHIg9ccP5jBVdoNhGsi4QRDEzMt6DRSKUzGIwn2OzO3lzu8Lfffud68k5TWS6b54s8cP20Jwqu0GwiWB+REAw4u/282qTSKgbiCpvd1dOvH/7OY33XkXeaytIw3+/23dL0w4IrNJsIeqJBuILeKpWsPOipLHKxFJvdwR1X++TNvT8q1TvNhBTCFVPrWh9euOBxL88vt+a7RQnRaAhuaXJ1QifJKyoG40koir1DyhrGzpv37V9Hzr0rw4q2wKyHvjXt5z43Z1uPlkQXGqISJIfPAI4XFMVAf0yBotpHh5m8uWvjo33X7tyTpvZXAaYuDtZ7vtu8CW7+BtsHkgBfXQicNDGn4k+HqWjIDCYBdjNN3vhp7p977irFufeEE8KrpjXM+deFC57igCXWfL/Pg/popGJnsaqFaZoYiMUhM1MFJrD7xn3717zQ13+oSkWrCT53UWjuo7c0PsMDF1nz/T4BDVHPKW8vkxXT5NAfy0HOaGz+mzdu7v3qf72ZPlilotUEjZeEp7hva74XzChQ9HngqQ+f8vYyWeFNE7mBFFSm/+NMbM3e33PH8dcTJ4p57oQSwhVT61q3Llr4DCuCoWAA0XCw2KJMSGKJFJKptC3PBHav37vvqzQyLA8r2gKztnxn+n+wIhgKCqgLT4wdeU4xmMgjmRouhusf6P0ijQzLw9TFwXrPD1ruByuCIS+kSG31f1osjXwqY8vjTGzN/uTI94sZGZYqhBWbA2qvD0UeXrjgcVYEI6FgzYkgAETDQURC9t/NAUseXrjg8fb6UKRKxZq0XDbPF3noW9N+zopgNCTVnAgCQF3YjUhIsuVxnHnR5m9Ne/qyeT5qfw7TMN/v9ny3eRMYEXSHAzUnggAgRANwhwO2PJPDes93mzc1zK/8OZGKCeEDF5z/GLsmGA2HEA4FRrtl0hMOBRANh2x5Xp5f/sAF5z9WpSJNWh64ftoT7JpgNOxGODQ516PPhEhIRJR5CfC5uaX/cv20J6pUpEmL75amH7JrglIkACE8uTYFng1C2AcpwvT/bv4G/8amH1a6LBURwl9f0vaAJVAugKGR4EQ9H+gkoaB/2MiwyeOZ+etL2h6oUpEmHb+6o+VhS6BcAEMjwYl6PtBJwkFh2MiwucE18z/uaH24SkWadLRumrXREigXwJAITtTzgU4ihnzDR4ZTxWmtm2ZtrGQ5yi6E9y+YdwN7TjAUDNT0SJAlHAogFLTZo/3iSLj9/gXzbhjtHuLMuG9t40b2nGA4KNT0SJAlEhIRsr8UXHjJee5P33vdtJuqVabJQvP1zVez5wTFkJ9E0IIQ9sEdtNljLc73LpqxoenqSpWhrEL4jdamJazHGL/PU5NrgqcjGh7mQq59ddP0Nd9obVoy2j3E2KxZFl3Keozx+4Rh04HE0Joh40LuwmuuCK5dsyw6zOUhcWZM/2zdBazHGNHngRShmTAWIRqAaO//1vIrIp+f/tm6Cyrx/WXbNbog5Oe3X7L4VavvULcooXFq3aQ/IlEspmmi78Sg7Zyhahh/+PvX31pGjrrPjgUtHv4//9+MP1l9h0qiC9Oneif9EYliMU0OHx/P2s4ZKprZ+YV/OvppctR99rQ8e8FjnIANpzIkAcHG+kl/RKJYeNNE6ljMds7Q1IwtR645cP3p7h23u0a/P3f2JtaBdjQaIhEcA47jUBexb54Ref4z3587e1OVijRhue2a+ntZB9oNUYlEcAw4zkRD1L5eKAlc+23X1N9bpSJNWFrvnLmh4ED7FL66EIngGBgcB1+dvf/jBF5svXPmhlFucYyyCOG6mc2fKYRSOkVdJDxpPcY4iVsSURcJ2/KW1tUtWzez+TNVKtKEY92K6LJCKKVT1Ecmr8cYJ5EkHvURuxj+zULf8rUrostGuYVgmH5V/afYUEqeaHDSeoxxEk4S4Inals7Wos1/6fSr6j9Vzu8tS89w6+xZt8GyLujzeied79ByEgz44PPafA223zr7nNuqVZ6Jxi1fqrsDlnVBn1ecdL5Dy0kwIMLntTl7vvDWIZsSZ4Drmob1sIgg5/NMOt+h5cQV9IJj1gvF1VPWl/M7HRfCH59/7vpCZPmhL+CAaA0eGC2VKHOkIiQI0R+ff25ZG8Nk4J41jTcUIssDGJruqwuTCJ4tdWG3bQNByMdH717TWPYpqolOyzenryxElgcwFErJR5tjzhpfxG+fRvbzgZZvTl9Zru9zVAjbIn7312c0r4VlNBgOhSZ8KKVqIIgu9rB9+9dnNK9tC0+Q6JxVYNE5XvfXl4fWwzIajIQ8Ez6UUjUQBA4R++7aC69dHtqw6Bwvtb8x4K6MfhHWKdFwcOKHUqoGgmvIdp+wtmDbsuCoEK5rbbkJFhGUBIEOzZdAKOiHJNjWFdrXzWyhs12jsO7KyK2wiKAocnRovgTCQQGiaHuJuLBgY2IEWm6Zsdqa5kWRzguWgBjygRftszmsjZ3CMSFsi/jdqxobv2zNC4VoSrRUQozjgVWNjV+mUeFwFp3jda9aErD9k0SC0miXE2cIa8NVSwKraVQ4MtzS0N/BMhqUSARLhrHh2oKNHccxIfxas31K1O0W2QPiRBH4fV64RVtn1P61oelnwsLXloU2wDIadEsu9oA4UQR+nwC3ZN8489UrwrRWzdB8fbPNC4ogSXDR+2rJuPxuCMxu23J4nHFMCAseZE4R9JMLNacI2t0P4StN06+tUlHGLQUPMqcIBUgEnYK1JWtrAmA9yAi0S9QxBPvy2lrX8shVTn+HI0J40+yWz1nTokCjQSfx+7wQhU/myjmAZ21ey3xrZb1tN5ko8DQadBC/T4AofNJVcJzJ38jYvJZp+sKUS20ZgotGgw7i8rshWDYcmRxczVdPuWiMW84aR4Rw1bTGL8AyLRrw09uQ0zA2bS/YnADw+SWBr8AyLRrw03EJp2FseuGqIZsTAMS/Di6HdW0wQP2f0/B2m67llgY/6+jzS31Ae30ocp7fv9Ca5ychdBzWpuf5/QspgC/wV/N89fNbJFv7C/hpNOg0AWaEPb9FWkgBfIcC7pozPXOseS4/zYY5DWtTfqZnjpMBfEsWwhUNDSvBeJFx8eTKymlcPD/M20zB9jXN3y72rwTjRcZFzc9xXC4M8zazYrF/VbXKM17wXBpcCsaLDE8N0HF4Fz/M20zB9s48v9QHXFHfYPPp6PPS21C58DG71q+or1s+yqU1w+Vtviutab+XOqFy4ffaR4Ws7WsRfnHAFibNTSdLygZrW9b2pVBSr7Eg5Ofn+L3zrXlsZ004B/uSMcfvn78g5K/Znn9Bi4c/t0li2h958SgXXsa25zZJ8xe0eGq2/QEAZkgzrUnOR2dXy8Uw2zK2L4WSGnF7NLIUlmlRr8dDYZbKCMdx8HrswXsLdVCTtM/3Xg7LtKjX46L2V0Z4joPXY58evWyoDmoSNiKC4HGDp/ZXNniOg+CxD7ScikpRkhBeHA7b4r153PQ2VG5YG7N1UEtcNM9jmxrxuGmTTLlhbXwxUwe1hDjfuwiW9UHeQ7uVyw1j47WFOij9uaXc3BYOXmhNeyQSwnLD2pitg1qibY7XdpbI467tWbpKwNp4EVMHNcVc7wXWJO8mISw3w2zM1EHRzy3l5iaP59QcLc8BEjWEssPa2FoHtUZTg+vUb+c4E24KvFt23BJvC8/UbKmDmmOKOO3knwZnwkX9X9lxuUV7eCZLHZRC0T3Hiql1rda0SNHnK4ab+Ydj66IWWNEWmGVNSyJNi1YKye57dFhd1AJTFwfrrWlJpP6vUrC2ZuuiGIoWwjk+7zxYNspYXYAR5YWxdXuhLmqK2dPcc8GEXCIqAxuaqVAXNYXYLLXCen5QohexSuESmXXCobooiaKFsNXrtb0FigI1hErB2pqti1qgZarAtD86NlEpWFuzdVEL8I1SkzXNUf9XMUzR3v7YuiiGooWwyeudYU27KAp9xWBtzdZFLdDUINjeAqkfqhysrZvqxZqbmueniI22DHoRqxyMrYfVRREULYRTJGmKNS2SEFYM1tZsXdQCU8P2RXKB3FpVDNbWUyO8IxsWJhQRwbYuRW7VKscwWzN1UdQzi72xThTqbA8SqCFUCp7x5crWRS1QF+Rtv9nF0xphpWBtHQ3WXvszQy6bw3EXCWHFYG3N1kUxFF17AUEM2R9EHVGl4JmOiK2LWiDg4+ztj4SwYrC2DjJ1UQsYft4eeZynGbGKwdh6WF0U88hib/TynC1sOkcdUcVgbc3WRS3gkZj2R66tKgZra7YuagFOctl8fZFrtcrBW88RYnhdFPXMYm8UeN62h5U6osrB2pqti1pAdLHtzxztUsJhWFsLAldz7Y932ftOg9pfxTCY/o+ti2KgiW1iQmJSx0MQhEMULYSaYajWtGlSx1QpWFuzdVELaJrJtD+akagUrK3ZuqgFDB2GNc1T+6sYPNP/sXVR1DOLvTFrmBlr2jRICCsFa+usYWRGuXTSklOY9kcvYhWDtTVbF7WAqeh5a9qg9lcxDOalg8sb+VEuPWOKFsK0piataYOEsGKwtk5rWnKUSyct6YxJ7a9KsLZOMXVRC/CykbZlGHqVSlKDsLbO6OmRLzxzihbCQVUbtKYNo+TRKXGGsLZm66IWGEwZtt+skxBWDNbWsVTttT8uqcetaV2n/q9SsLZm66IYihbCE4pywppWdXojqhSsrU/k7XVRCxyPa33WtEYdUcVgbX08bvSNcunkJa4NWJMGtb+KMczWTF0UQ9FC2JvNHrWmdRLCisHaujdnr4taoHdA7bamNa1aJak9WFuzdVELGCfUY7YMjfq/isHYelhdFEHRQtidzX5oTavUE1UM1tZsXdQCR45rTPujjqhSsLbuZuqiFjCOKb3WtEn9X8Vgbc3WRTEULYSHMtn3AHSeTKtaze2grhqqYmsInYW6qCk+6MsfBNB1Mq2qtEZYKRhbd304VBc1hdqjdAPoOJk2FRLCSsHYuqNQFyVRtBDuOD5o+3JVISGsFHlVsaXZuqgFduxJ20YhikIjwkrB2pqti1rg+Fsp27qUolL/VylYW7N1UQwleZbpzeUOn/zbMAElT42h3OQYG1vroNbo6ddP/XYTQF6hDQvlJq8YsI4HrXVQc5xQT20S4k0OOvV/ZUfPqzbnBdxx1ZGNWiUJ4Z5EqsuazinKaJcSDqEwNmbroJbYeyj7pjWdy5MQlptc3j4FyNZBTXEw+7Y1aZAQlh3Wxub79joolpKE8I1EotOazuVJCMsNa2O2DmqJN97L7bam2U6acB72ZYOtg1pCPZDdC8s6oZEjISw3jI07CnVQMiUJYWcsvguWDTPZXI5cXZUR0zSRzeWsWZ2FOqhJXjuQfRWWDTPZnE6ursqIYZrI5mzrg12FOqhJPn5h4C/WtJbLU/srJ8aQja2wdVAsJQnh/qRsHJLlA9a8TLZkt2/EKGSyNhHEIVk+sD8p1+x84P4jOeP9XsXW/rJZ2jRTLljbvt+rHNh/JFez7Q8AcFSxrZGaGZoVKxc6qy2M7Uuh5DBMrwwM7rSm2c6acA72JYO1fS3y6p7Mi9a0nKXp0XLB2pa1fS1ivJW2TQ3naSBQNljbmm+mHZuWL1kId/T3Pw/L9Ggmm4VOfkcdRzcMZLJZa1bnjhP9z1erPOOFHW/J22GZHs1kdZCTI+fR9SHbWugq2L6myf05tQvW84SZHLlbKwOGbsDM2AZZHdnXk44tC5UshJ0Dyfi7srzPmifL2dEuJ4okzdj0XVne1zmYLNnZ7ETntfcy8QNHFFv7S2doVOg0aWbK70C3uu+19zI13/76D8h543DukDVPl2lWzGmG2fSj3KH+AxnHht+ORKjf3nfsP2EZFbKdNlE6sixbk52/7Tv2q2qVZbzxu93pX8IyKkzLtHvPadKyfTS4/X9Sv6xWWcYb5q7U72EZFSpp6v+chrFph/7fyd87+XxHhPCRD478l4lPogSrmgo5Q29FTiFnsjb/jiZgPPrBkZeqWKRxxaPPDzxvmpyl/RmQaVToGHJGg6p9Mt1nmpzxr88P1Py0/El6njvxJmfik39QTYcu01qhU+hy3uZomzOh9zzX7+j5VUeEEAB+2fvx09Z0Si45ViJRIJWyBwDf1tv7VJWKMm7Z9mrySWs6mSYhdArWlqytCUDfGX8BllGhlpLHuJo4GxhbdhRs7SiOCeG/H+3pgGV6NJ+nUaETyJks61u085mjvU9WqTjjlmdeSWyFZXo0r+g0KnQAOaMhb/ct2vXMK/Gt1SrPeOXolt7nrGlN0WhU6AC6nIfGODRnbe0EjgnhnoSc337Mvm6VTKacenzNkkzaR9bbjx371Z4E/Yex7P0om9++O73NmhdP0ZmuUmFt+Nvd6Wf2fpSj9jcC5q7kS7CuFSYzY1xNnAmMDTsKNnYcx4QQAB4/fOQRWEaFiqYhSVMERZNIyVDssbc6CzYmRuDxF+MPggnNlEjRqLBYEillWMiljhdjm6tVnvHOkYeO2l7EDFWFSmJYNGoyA4OJNMHa2CkcFcI9CTn/C2aKNJFIQlPpYNfZoqk6komkNavzF0d7Omg0ODp7P8rmn96Z3AKLGMaTOWgaub06WzTNRDxh64S6nt6Z3EKjwbExX4z9BpZRYS6Rouj1xaDpQ7b7hI6CbcuCo0IIAD945/2tSU2LnUwbAGI0RXrWxJIpWI/lJjUt9oN33qe1mdNw51PHtiQzxqn2Z5ocBhN0nOJsGUzkbeGWErIZu/OpY1uqVqAJwpGfffw8ZOPUegZvcsjEaVbsbMnEZVu4JchG+sjPPi7bTmXHhRAAHvzgo/vAeJtJpWmK4ExJpTPDvMgUbEqcAQ/+evAe2LzNqEilSQzPlGRaG+ZF5qHfDNxTrfJMNNRtJ7aC8Tajp+hs4Zmip7LDvMgUbFo2yiKEjx/u+cOuwcGXrXmD8QTyFMX+tOQVFYPxhC1v1+Dgy48f7vlDlYo04ejYEXv5j/syNj+sA3EFCgXuPS15xcBg3D77+cf/ze7s2BF7eZRbCIaPXxj4C/bIf4Z1ijSWgqnQevXpMBUNuZh9ShR75D87FWViNMoihADwk4Mf3KUahq3zjsWSFKZpDEzTRCxmWxeEahh/+MnBD+6qUpEmLPc9O3C7opm2WI39MQWmdbqFsGGaHAZi9l2iimZ23vds/+1VKtKEpfvuw1tMzbC9+WcGk+Cp/xsV3jSRGbT3f6ZmqN13Hy77lHzZhHB/UjY2HTz0A1jPFqoKBmI1755wVAZi8WFnBn908P3baznUUrHsP5Iz7n564HuwTJEqqo7+GJ1tHY3+WA6KfWNb193/NvC9/UdrPNRSkWhPHN8My6gQiobcAO2XGI3cQAqwj5o79I7jD1biu8smhADw8+7e3dt6P34KFjGUMznEEtQYWGKJFOuAoPPZ3t4nn+r+uGYj0JfKUy/Hdj37SqoDFjGUMxoGE7TxkWUwkWcdEHQ9+0qq46lXYjUb+LlUPv794NvGjvjvYBFDNZODFiOvWyxaLA2VWRc0dsR/1/vS4IHR7nGSsgohAHxv/3s/fSOe6IRFDJOpNBJJagwnSSTTSKZs9uh8PZ740237D9IuvRK5/Ym+R15/N/8nWMQwmdIQT9J69UniSRVJ+3nLrtffzf/p9if66MxqiRzd0vsc3snuhUUM86kMtARtHjyJlsggb3cj2cG9ndlbDg8yo1F2IQSAL72+59u9uZwtmnA8maLD9gCSKRlx5nhJby53+Muv7/k/VSrSpOMf7um+uadfZ9qfQoftMXRoPp60rwv29OuH/+Ge7purVKRJR/ddH27mjqt91rx8Ik2H7TF0aD6fsA+KuONq3+EfflRRxw0VEUIA+Pbb71yfNQzbTr5YIlnTI8NEMo2Y/dA8srqx89v737m+SkWatHznsb7rMnnTNs0XS+RremQYT6qIMWcsM3lz13ce67uuSkWatMibe3+EvPFTa54ST9f0yFBLZKDEmf4/b/xU3tzzo0qXpWJC2DmQjN+8b/86E9htzY8nUzW5ZhhLpCGUJnwAAAm3SURBVIaNBE1g981/2b+OAu46z2vvZeIbH+271jQ5W/iWeFKpyTXDwUR+2EjQNLk3Nz7Sdy0F3HWe/gNyPvfPPXdxJmzn4fKJdE2uGWqx9PCRoImtuX/uucvJgLtnSsWEEAB2HB/svnHf/jWsGCZTafQPxmriaIVpmugfjLFrgjCB3Tfu27dmx/HB7ioVbdKzc0+6+8bNvV9lxTCZ0nBiMF8TRytMk8OJAYVdE4Rpcm/esLl39c69aWp/ZeL4W6mB7P09dwwTw1QGSn9tHK3gTRNKf5JdEwRnYmv2/iN3HH8rNVCNchX9n1+KaK2YWtf68MIFj3t5frk13y1KiEZDcEti0c8ezwwdlk9CUexv4lnd2HnzX/avIxGsDCvaArMe+ta0n/vc3FJrviS60BCVIEkVfT+sGHnFwEBMYY9IIJM3d218pO9aEsHKMHVxsN7z3eZNcPM32D6QBPjqQuAkoUolKy+mog2dE2QcC/A545HMv/T831JEkONKe4mtihACQHt9KPLABec/1uTxzATQbv2sLhJGMOAr6fnjjVQ6M8xjDIDO3lzu8Lf3v3M9TYdWlsvm+SL/cv20J5obXDMBXGj9rD4iIRiYXC9jqbSKgfiwsFRdPf364e881ncdTYdWlob5frfvlqYfYoo4DcBa62eeaBCuoLdKJSsPeirLeowBgA7uuNonb+75UanToRNWCE/y60vaHrg4Em4HI4Y+rxfRUBCC6HLke6qFpuqIJVOs71CgcESCdodWl/+4o/XhS85zfxqMGPq8LtSF3RCEiT1dqmkmBhN51ncoUDgiQbtDq0vrplkbcb53ERgx5Hwe+CJ+QJjY/R80HZm4zPoOBQpHJJzaHTrhhRAA7l8w74bVTdPXgBFDAIiGQwgF/Y59VyVJpuRhu0ILdD7b2/sknRMcH9x73bSbrrkiuBaMGHIAImE3wsGJOVWVSCmIJ1SM8J/a9ewrqQ46Jzg+mLGh6Wp+ReTzYMTQ4Ex4wkGIoYk5O6YmR9gVOkSHsSP+OyfPCU4KIQSAb7Q2Lblr7pwfizz/GfYzSRAQCgXg902M6QI5k0UymWaD6gIY8h36o4Pv304eY8YXa5ZFl955bf39ksANexkTRQ6RoAS/b2IIopzREB8eVBfAkO/Qu/9t4HvkMWZ8Mf2zdRcI103byAkQwQgiL4qQQj64/O4qle7s0OU8lBGC6gLoMDVD1TuOP+i0x5hJI4QAsCDk578/d/ampXV1yzDC6NDtFhH0+8etIMqZLFKpDOsv9CSdfxyM7bz34KF/It+h45MFLR7+tmvq7/2bhb7lYEaHAOCWXAgFhHEriHJGQzKtIa+MGAi264//m91537P9t5Pv0PFL650zN6DNfykYMQQAQRIgBP3jVhB1OQ8tJUMbOcpGB/bIfy6XA+1JJYQnWTez+TO3zp51W0hwRTGCIIqCiIDfC7/fCxdf3R1+umEgLWchyzLUkSNRdyY1LfbAhx/9pOOjHnoLnwCsXRFdduuX6u4I+fgoRhBEUeAR8IsI+AS4qryEo+tAOqMgLetQtRH1rSshm7GHfjNwD4VSmhhMv6r+U65rGtbzPlcAIwgiBBekgBcuvwe8q7r9n6Eb0OUclHQWGLn/64BspNVtJ7aWM5TSpBTCk/z4/HPXf31G81qMIIYn8Xm98Hnd8Hk9JRvjTDFNE5lsDplsfqRNMFY6n+7p3XrH2wc7xrqIGJ/cvaZxw7XLQxswghiexOd1we8V4PW6wFeo/RmmiWxWh5wdFkCXpevpncktFFl+YtLyzekruSujXywkhwsihjbVuL1ucD6pYu0PBqBn88hn8yNtgjlJBwCYL8Z+U87I8ieZ1EIIAG0Rv3tda8tNqxobv4wxBBEAvB4PPG4JHkmC5HZ2+3sur0JRFOTyCrK504by6fztsWPbOg4f+emehFx7bksmEYvO8brXXRm5ddWSwGqMIYgA4PW44HEL8Lh5uB0+i5hXDOTyBnJ5DdncmOIHAF2/3Z1+puPF2Oa9H+Wo/U1wWm6ZsZpbGvq7QnJEQQQAweMG7xHBu0W4HO7/9LwKI6/CyKnQxm5SQwK4K/nSkYeObnO0EGMw6YXwJG1hv/trM5rXfqVp+rXckEecMUURGDqgL0oCREGAy+WC6HKB53nwPAeO504ZzzRNmIYJwzBhGAZUXYeu61A1DaqiQVUVnMGiSqcJGNt6e5965mjvkySAk4tF53jdX70ivH715aF/5DiTx2lEERhaUxRFDqLggiAAgouHi+eG2h/HgeOG/odMk4NpDrU/3TCh6QY0DVA1HapqQlH0kXZ+snSZJmdsezX55DOvxLeSAE4+Zmxoutq1PHKVycGFMQQRGNpxKokiOEkAN9T4wLt4uFw8wLvAcyaMQv/HmyYMkwMMHbpuwNANQNNhahpMRYOiquBP73WpgzOh6zvjL1QyasRJakYIrdw0u+Vzq6Y1fuE8v38hzkAQy0znu7K877d9x3716AdHXqpyWYgKcOPK+pWrlgS+Mr9FWogzEMQy03WgW923/X9Sv/zX5wfKPgVFVJ/mq6dcxC0Nfpaf6ZmD0whiBejAR7lD+n8nf9/zXP+bp7+8PNSkEJ6kvT4UWdHQsPKK+rrlc/z++aicKHYekuUDrwwM7txxov958gpTm1w2zxdZsdi/6vI235XnNknzUTlR7Hq/Vznw6p7MizvekreTV5japGG+3+25NLiUXxxYghnSzEJ2uYVxaL/DUeWw+WZ6d/b15K5qOMlmqWkhtLIg5Ofbo5GlF4fD7W3h4IUF121A6eLYCQzFCNyTSHW9kUh0dsbiu+gIBGFlQYuHv2y+9/KL53mWLJrjvajgug0oXRy7gKEYgXsPZd98473c7tcOZF/df4SOQBB2pl9V/ylxvncR5novKLhuA0oXxg5gKEag+X72bfVAdm85d38WCwnhGKyYWtc6x+ed1+r1zmryemdMkaQpdaJQFxDEkJfnfALPiwCgGYaaNYxMWtOSg6o2eCKvnOjNZY92Z7MfHspk3yNn2EQxrGgLzJo9zT23Zaowq6lebJ0a4adFg0Jd0MeFPBLnEwRuqP1ppppTzEwqYyZjKW3weNzo6x1Qu7uPax9+2Jc/uGNP+sNq/xZi4jF1cbBebJZa+UapiZ8iNiIi1JshV8Tw8wFOcrl511D0IUOHweWNPDJ6mkvqccS1AeOEesw4pvSqPUp3tSJCnA2VOjFAEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAQxzvj/snGtbrdYI/0AAAAASUVORK5CYII=);\n      height: 32px;\n      width: 100%;\n      background-size: 40px;\n      background-repeat: no-repeat;\n      margin-bottom: 0;\n      border-top-left-radius: 5px;\n      border-top-right-radius: 5px;\n    }\n  }\n\n  .code-block {\n    position: relative;\n    overflow-x: visible !important;\n\n    &:before {\n      content: attr(lang);\n      font-size: 12px;\n      position: absolute;\n      top: -34px;\n      right: 0;\n      line-height: 1;\n      z-index: 1;\n    }\n  }\n\n  .code-inline {\n    margin: 0 2px;\n  }\n\n  pre, pre div {\n    background: #1a1a1a !important;\n  }\n\n  img {\n    background: none !important;\n  }\n\n  * {\n    // text wrap\n    word-wrap: anywhere;\n    word-break: break-word;\n  }\n}\n\n.bing {\n  display: inline-flex;\n  flex-direction: row;\n  align-items: center;\n  vertical-align: center;\n  gap: 6px;\n  color: #2f7eee;\n  background: #e8f2ff;\n  border-radius: 12px;\n  padding: 6px 12px;\n  font-size: 16px;\n  margin: 6px 0;\n  width: max-content;\n  user-select: none;\n  word-wrap: anywhere;\n  max-width: 100%;\n\n  svg {\n    width: 20px;\n    height: 20px;\n    flex-shrink: 0;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/generation.less",
    "content": ".generation-page {\n  position: relative;\n  display: flex;\n  width: 100%;\n  height: calc(100% - var(--navbar-height));\n\n  .login-action {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    margin: auto;\n    transform: translateY(-28px);\n\n    .tip {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      margin-bottom: max(6vh, 24px);\n      user-select: none;\n    }\n\n    .text {\n      font-size: 1rem;\n    }\n  }\n}\n\n.generation-container {\n  display: flex;\n  flex-direction: column;\n  padding: 12px 16px;\n  gap: 6px;\n  width: 100%;\n  height: 100%;\n\n  .action {\n    flex-shrink: 0;\n  }\n\n  .generation-wrapper {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    flex-grow: 1;\n    padding: 15vh 0;\n\n    .box {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      width: 80%;\n      max-width: 680px;\n      height: max-content;\n      margin: 6px 0;\n      gap: 12px;\n\n      .message-box {\n        width: 100%;\n        height: max-content;\n        min-height: 120px;\n        border-radius: var(--radius);\n        border: 1px solid hsl(var(--border));\n        color: hsl(var(--text-secondary));\n        padding: 0.6rem 1rem;\n        font-size: 10px;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .quota-box {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        width: max-content;\n        height: max-content;\n        border: 1px solid hsl(var(--border));\n        border-radius: var(--radius);\n        user-select: none;\n        padding: 4px 12px;\n        transition: .2s;\n        cursor: pointer;\n\n        &:hover {\n          border: 1px solid hsl(var(--border-hover));\n        }\n      }\n\n      .hash-box {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        justify-content: center;\n        gap: 1rem;\n        flex-wrap: wrap;\n        width: max-content;\n        height: max-content;\n        padding: 1rem 1.5rem;\n\n        .download-box {\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n          justify-content: center;\n          gap: 0.5rem;\n          min-width: 10rem;\n          width: max-content;\n          height: max-content;\n          padding: 1rem 1.6rem;\n          text-decoration: none;\n          cursor: pointer;\n          user-select: none;\n          border-radius: var(--radius);\n          border: 1px solid hsl(var(--border));\n          transition: .2s;\n          color: hsl(var(--text));\n          font-size: 1rem;\n          font-weight: 500;\n          background: hsla(var(--background-container));\n\n          &:hover {\n            border: 1px solid hsl(var(--border-hover));\n          }\n        }\n      }\n    }\n\n    .product {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      text-align: center;\n      font-size: min(2rem, 7vw);\n      gap: 12px;\n      user-select: none;\n      white-space: nowrap;\n\n      img {\n        width: min(3rem, 14vw);\n        height: min(3rem, 14vw);\n      }\n    }\n  }\n}\n\n.model-box {\n  width: 80%;\n  margin: 0 auto;\n  max-width: 680px;\n}\n\n.generate-box {\n  display: flex;\n  flex-direction: row;\n  width: 80%;\n  margin: 2rem auto;\n  max-width: 680px;\n\n  .input {\n    flex-grow: 1;\n    text-align: center;\n    font-size: 1.25rem;\n    height: 46px;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border-hover));\n    letter-spacing: 1px;\n    margin-right: 8px;\n  }\n\n  .action {\n    width: 46px;\n    height: 46px;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/home.less",
    "content": ".main {\n  position: relative;\n  display: inline-flex;\n  flex-direction: row;\n  width: 100%;\n  height: calc(100% - var(--navbar-height));\n  overflow: hidden;\n\n  @media (orientation: portrait) {\n    flex-direction: column-reverse;\n\n    .home-page {\n      height: calc(100% - 9rem);\n      width: 100%;\n    }\n\n    .toolbar {\n      flex-direction: row;\n      height: max-content;\n      width: 100%;\n      border-right: none;\n      border-top: 1px solid hsl(var(--border));\n\n        & > * {\n            margin-bottom: 0;\n            margin-right: auto;\n\n            &:last-child {\n            margin-right: 0.5rem;\n            }\n\n          &:first-child {\n            margin-left: 0.5rem;\n          }\n        }\n    }\n\n    .sidebar.open {\n      width: 100% !important;\n    }\n\n    .sidebar .sidebar-menu {\n      display: none;\n    }\n  }\n}\n\n.model-market {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  padding: 0 1.5rem;\n  height: 100%;\n  overflow: auto;\n  scrollbar-width: thin;\n\n  .market-wrapper {\n    width: 100%;\n    height: 100%;\n    margin: 0 auto;\n\n    max-width: 1400px;\n  }\n\n  @media (max-width: 768px) {\n    padding: 0 1rem;\n  }\n\n  .title {\n    font-size: 24px;\n  }\n\n  & > * {\n    flex-shrink: 0;\n  }\n\n  .search-bar-wrapper {\n    margin: 1rem auto;\n    width: calc(100% - 1rem);\n\n    .search-bar {\n      position: relative;\n      margin-bottom: 0.5rem;\n    }\n\n    .search-icon {\n      position: absolute;\n      top: 50%;\n      left: 1rem;\n      transform: translateY(-50%);\n      width: 1rem;\n      height: 1rem;\n    }\n\n    .clear-icon {\n      position: absolute;\n      top: 50%;\n      right: 1rem;\n      transform: translateY(-50%);\n      width: 1rem;\n      height: 1rem;\n      opacity: 0;\n      transition: 0.25s;\n      cursor: pointer;\n\n      &.active {\n        opacity: .8;\n\n        &:hover {\n          opacity: 1;\n        }\n      }\n    }\n\n    .input-box {\n      padding: 0 2.5rem;\n    }\n  }\n\n  .model-list {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    justify-content: center;\n    width: 100%;\n\n    @media (max-width: 1024px) {\n      grid-template-columns: repeat(2, 1fr);\n    }\n\n    @media (max-width: 768px) {\n      grid-template-columns: 1fr;\n    }\n  }\n\n  .model-item {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    user-select: none;\n    padding: 0.5rem 0.5rem;\n    margin: 0.5rem;\n    border: 1px solid hsl(var(--border-hover));\n    transition: 0.25s;\n    transition-property: border-color, padding, background, box-shadow;\n    cursor: pointer;\n    animation: fadein 0.25s forwards ease-in-out;\n    opacity: 0;\n    width: calc(100% - 1rem);\n    min-width: 0;\n\n    svg {\n      flex-shrink: 0;\n    }\n\n    @keyframes fadein {\n      from { opacity: 0; transform: translateY(2.5rem); }\n      to   { opacity: 1; transform: translateY(0); }\n    }\n\n    .model-name {\n      color: hsl(var(--text));\n      font-weight: medium;\n    }\n\n    .model-id {\n      color: hsl(var(--text-secondary));\n      font-size: 14px;\n      margin-top: 0.25rem;\n      background: hsl(var(--muted));\n    }\n\n    .model-description {\n      color: hsl(var(--text-secondary));\n      margin-bottom: 0.25rem;\n    }\n\n    &:hover {\n      background: hsla(var(--background-hover));\n      border-color: hsl(var(--border-active));\n\n      .grip-icon {\n        opacity: 1;\n      }\n    }\n\n    &.active {\n      border-color: hsl(var(--border-active));\n      box-shadow: 0 0 0 1px hsl(var(--text-secondary));\n\n      .grip-icon {\n        opacity: 1;\n      }\n    }\n\n    .grip-icon {\n      opacity: 0.6;\n      transition: 0.25s;\n    }\n\n    .model-avatar {\n      border-radius: 50%;\n      width: 3rem;\n      height: 3rem;\n\n      @media (max-width: 768px) {\n        width: 2.5rem;\n        height: 2.5rem;\n      }\n    }\n\n    .model-action {\n      button {\n        background: none !important;\n      }\n\n      @keyframes rotate {\n        0% {\n          transform: rotate(0deg);\n        }\n        25% {\n          transform: rotate(25deg);\n        }\n        50% {\n          transform: rotate(0deg);\n        }\n        75% {\n          transform: rotate(-25deg);\n        }\n        100% {\n          transform: rotate(0deg);\n        }\n      }\n\n      svg {\n        animation: rotate 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);\n      }\n    }\n\n    .market-tip {\n      transform: translateY(1px);\n    }\n\n    .model-name {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n\n      .badge {\n        transform: translateY(-2px);\n      }\n\n      &.pro {\n        p {\n          // gold color gradient\n          background: linear-gradient(to right, hsl(45, 100%, 70%) 0%, hsl(46, 100%, 58%) 50%, hsl(46, 100%, 50%) 100%);\n          -webkit-background-clip: text;\n          -webkit-text-fill-color: transparent;\n        }\n\n        .badge {\n          color: rgb(164, 128, 0) !important;\n          background: rgb(255, 231, 145) !important;\n        }\n      }\n    }\n\n    .model-tag {\n      display: flex;\n      flex-direction: row;\n      flex-wrap: wrap;\n      margin-top: 0.25rem;\n      transform: translateY(0.25rem);\n    }\n  }\n\n  .market-header {\n    position: relative;\n    width: 100%;\n    height: max-content;\n    margin-top: 1.5rem;\n\n\n    .header-bar {\n      width: 0.75rem;\n      margin: 0 1rem;\n      aspect-ratio: .75;\n      background:\n              var(--anim-bar) 0%   50%,\n              var(--anim-bar) 50%  50%,\n              var(--anim-bar) 100% 50%;\n      animation: l7 1s infinite linear alternate;\n\n      @keyframes l7 {\n        0%  {background-size: 20% 50% ,20% 50% ,20% 50% }\n        20% {background-size: 20% 20% ,20% 50% ,20% 50% }\n        40% {background-size: 20% 100%,20% 20% ,20% 50% }\n        60% {background-size: 20% 50% ,20% 100%,20% 20% }\n        80% {background-size: 20% 50% ,20% 50% ,20% 100%}\n        100%{background-size: 20% 50% ,20% 50% ,20% 50% }\n      }\n\n      &.reverse {\n        background:\n                var(--anim-bar) 100% 50%,\n                var(--anim-bar) 50%  50%,\n                var(--anim-bar) 0%   50%;\n      }\n    }\n\n    .close-action {\n      position: absolute;\n    }\n  }\n\n  .market-footer {\n    margin-top: 3rem;\n    margin-bottom: 2.5rem;\n    user-select: none;\n    padding: 0 1rem;\n\n    a {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: center;\n      font-size: 14px;\n      color: hsl(var(--text-secondary));\n      transition: 0.25s;\n      width: max-content;\n      max-width: 100%;\n      margin: 0 auto;\n      text-align: center;\n\n      svg {\n        flex-shrink: 0;\n      }\n\n      &:hover {\n        color: hsl(var(--text));\n      }\n    }\n  }\n}\n\n.conversation-name {\n  color: hsl(var(--text));\n  font-weight: bold !important;\n  word-wrap: anywhere;\n}\n\n.chat-action {\n  @keyframes up {\n    0% {\n      opacity: 0;\n      transform: translateY(100%);\n    }\n    100% {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n\n  animation: up 0.2s ease-in-out;\n}\n\n.web {\n  color: hsl(var(--input-unread));\n  transition: .25s linear;\n\n  &.enable {\n    color: hsl(var(--text));\n  }\n}\n\n.toolbar {\n  position: relative;\n  display: flex;\n  flex-shrink: 0;\n  flex-direction: column;\n  height: 100%;\n  margin: 0;\n  width: max-content;\n  padding: 0.5rem;\n  background: hsl(var(--background));\n  border-right: 1px solid hsl(var(--border));\n  transition: 0.25s;\n\n  &.stacked {\n    @media (orientation: landscape) {\n      width: 0;\n      padding-left: 0;\n      padding-right: 0;\n      border-right: 0;\n    }\n\n    @media (orientation: portrait) {\n      height: 0;\n      padding-top: 0;\n      padding-bottom: 0;\n      border-top: 0;\n    }\n\n    .toolbar-text {\n      display: none;\n    }\n\n    button {\n      width: 0;\n      flex-shrink: 1;\n    }\n  }\n\n  .bar-kit {\n    position: absolute;\n    opacity: 0;\n    transition: 0.25s ease-in-out;\n    cursor: pointer;\n    z-index: 64;\n    border-radius: 0.25rem;\n    background: hsl(var(--background));\n    width: max-content;\n    height: max-content;\n    border: 1px solid hsl(var(--border-active));\n\n    @media (orientation: portrait) {\n      top: 0;\n      left: 50%;\n      transform: translate(-50%, -100%);\n      padding: 0 0.75rem;\n      border-bottom-left-radius: 0;\n      border-bottom-right-radius: 0;\n\n      svg {\n        rotate: 0deg;\n      }\n\n      &.stacked svg {\n        rotate: 180deg;\n      }\n    }\n\n    @media (orientation: landscape) {\n      top: 50%;\n      right: 0;\n      transform: translate(100%, -50%);\n      padding: 0.75rem 0;\n      border-top-left-radius: 0;\n      border-bottom-left-radius: 0;\n\n      svg {\n        rotate: 90deg;\n      }\n\n      &.stacked svg {\n        rotate: -90deg;\n      }\n    }\n  }\n\n  &:hover, &:focus-within, &:focus, &:active {\n    .bar-kit {\n      opacity: 1;\n      pointer-events: all;\n    }\n  }\n\n  & > * {\n    margin-bottom: 0.75rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.sidebar {\n  display: flex;\n  flex-shrink: 0;\n  flex-direction: column;\n  width: 0;\n  height: 100%;\n  padding: 0;\n  margin: 0;\n  background: hsl(var(--background));\n  transition: 0.2s ease-in-out;\n  transition-property: width, background, box-shadow, border-right, opacity;\n  border-right: 0;\n  pointer-events: none;\n  opacity: 0;\n  overflow-x: hidden;\n\n  &.open {\n    width: 260px;\n    border-right: 1px solid hsl(var(--border));\n    pointer-events: auto;\n    opacity: 1;\n  }\n\n  &.hidden {\n    width: 0;\n    border-right: 0;\n  }\n\n  .sidebar-content {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    height: 100%;\n    padding: 4px;\n  }\n\n  .sidebar-menu {\n    height: max-content;\n    width: 100%;\n\n    .sidebar-wrapper {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: center;\n      height: max-content;\n      width: calc(100% - 0.5rem);\n      margin: 0.25rem;\n\n      img {\n        width: 2.5rem;\n        height: 2.5rem;\n        padding: 0.2rem;\n        border-radius: .5rem;\n        transform: translateY(0.05rem);\n        flex-shrink: 0;\n      }\n\n      .username {\n        margin: 0 auto 0 8px;\n        color: hsl(var(--text));\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        font-size: 14px;\n        font-family: var(--font-family-normal);\n      }\n\n      svg {\n        color: hsl(var(--text-secondary));\n      }\n    }\n  }\n\n  .conversation-list {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n    width: 100%;\n    height: 100%;\n    padding: 6px 0;\n    overflow-x: hidden;\n    overflow-y: auto;\n    touch-action: pan-y;\n    user-select: none;\n    scrollbar-width: none;\n    -webkit-overflow-scrolling: touch;\n    transition: 0.2s ease-in-out;\n\n    .empty {\n      color: hsl(var(--text-secondary));\n      font-size: 14px;\n      margin: auto;\n      user-select: none;\n    }\n\n    .conversation {\n      display: flex;\n      flex-direction: row;\n      vertical-align: center;\n      align-items: center;\n      width: calc(100% - 12px);\n      height: max-content;\n      cursor: pointer;\n      margin: 0 6px;\n      padding: 10px 12px;\n      border-radius: var(--radius);\n      border: 1px solid hsl(var(--border));\n      transition: 0.2s ease-in-out;\n      background: hsl(var(--card));\n\n      .more {\n        color: hsl(var(--text-secondary));\n        scale: 0;\n        opacity: 1;\n        transition: 0.2s;\n        transition-property: color, opacity;\n        border: 1px solid var(--border);\n        outline: none;\n        height: 0;\n        width: 0;\n\n        &:hover {\n          color: hsl(var(--text));\n        }\n      }\n\n      &:hover {\n        border-color: hsl(var(--border-active));\n\n        .id {\n          color: hsl(var(--text));\n        }\n      }\n\n      &.active {\n        background: hsl(var(--card-hover));\n        border-color: hsl(var(--border-active));\n\n        .id {\n          color: hsl(var(--text));\n        }\n      }\n    }\n\n    .id {\n        color: hsl(var(--text-unread));\n    }\n\n    svg {\n      flex-shrink: 0;\n    }\n\n    .title {\n      flex-grow: 1;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      font-size: 16px;\n      user-select: none;\n      margin: 0 4px;\n      color: hsl(var(--text));\n    }\n\n    &::-webkit-scrollbar {\n      width: 6px;\n    }\n  }\n\n  .sidebar-action {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: nowrap;\n    margin-bottom: 0.25rem;\n\n    .refresh-action {\n      &.active {\n        svg {\n          animation: RotateAnimation 0.5s linear infinite;\n\n          @keyframes RotateAnimation {\n            from {\n              transform: rotate(0deg);\n            }\n            to {\n              transform: rotate(360deg);\n            }\n          }\n        }\n      }\n    }\n  }\n\n  .login-action {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    align-items: center;\n    width: calc(100% - 1rem);\n    margin: auto auto 0.5rem;\n\n    svg {\n      transform: translateY(1px);\n    }\n  }\n\n  @media (max-width: 768px) {\n    &.open {\n      width: max(30vw, 180px);\n    }\n  }\n\n  @media (max-width: 468px) {\n    // sidebar collapsed\n    &.open {\n      width: calc(100% - 3.5rem) !important;\n    }\n    &.open ~ .chat-container {\n      width: 0;\n    }\n  }\n}\n\n.chat-container {\n  flex: 1 1 auto;\n  min-width: 0;\n  height: 100%;\n  transition: width 0.2s ease-in-out;\n\n  .chat-wrapper {\n    display: inline-flex;\n    flex-direction: column;\n    width: 100%;\n    height: 100%;\n  }\n\n  .tooltip {\n    user-select: none;\n\n    strong {\n      font-weight: 600 !important;\n    }\n  }\n\n  .chat-product {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    width: 100%;\n    overflow: hidden;\n    justify-content: center;\n    align-items: center;\n\n    button {\n      margin: 0.5rem 0;\n      white-space: nowrap;\n    }\n\n    .space-footer {\n      position: absolute;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      bottom: 6px;\n      user-select: none;\n      color: hsl(var(--text-secondary));\n      padding: 1rem;\n      z-index: 10;\n\n      * {\n        font-size: 14px;\n      }\n\n      p {\n        white-space: pre-wrap;\n        text-align: center;\n\n        &:first-child {\n          margin-bottom: 4px;\n        }\n      }\n\n      a {\n        color: hsl(var(--text));\n        text-decoration: none;\n        transition: 0.25s;\n        cursor: pointer;\n      }\n    }\n  }\n\n  .chat-content {\n    overscroll-behavior: none;\n    flex: 1 1;\n    width: 100%;\n    overflow: hidden;\n\n    .chat-messages-wrapper {\n      width: 100%;\n      height: 100%;\n      display: flex;\n      flex-direction: column;\n      padding: 18px 24px;\n    }\n\n    .message {\n      margin-bottom: 0.75rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n\n  .chat-input {\n    flex-shrink: 0;\n    width: 100%;\n    overflow: hidden;\n\n    .input-wrapper {\n      position: relative;\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      flex-wrap: nowrap;\n      width: 100%;\n      height: min-content;\n\n      .chat-box {\n        position: relative;\n        flex-grow: 1;\n      }\n\n      .input-box {\n        resize: none;\n        width: 100%;\n        color: hsl(var(--text));\n        white-space: pre-wrap;\n        padding-right: 3.25rem;\n\n        &.align {\n          text-align: center;\n        }\n\n        &::placeholder {\n          color: hsl(var(--text-secondary));\n          opacity: 1;\n          transition: opacity 0.3s ease, visibility 0.3s ease;\n        }\n\n        &:active::placeholder,\n        &:focus::placeholder {\n          opacity: 0;\n          visibility: hidden;\n        }\n\n        @-moz-document url-prefix() {\n          &::-moz-placeholder {\n            opacity: 1;\n            transition: opacity 0.3s ease, visibility 0.3s ease;\n            visibility: visible;\n          }\n\n          &:active::-moz-placeholder,\n          &:focus::-moz-placeholder {\n            opacity: 0;\n            visibility: hidden;\n          }\n        }\n      }\n\n      .action-button {\n        position: absolute;\n        right: 0.75rem;\n        bottom: 0.75rem;\n      }\n    }\n\n    .input-options {\n      margin: 12px auto -8px;\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: center;\n      flex-wrap: wrap;\n      gap: 4px;\n      height: min-content;\n    }\n  }\n}\n\n.share-wrapper {\n  display: flex;\n  flex-direction: row;\n  gap: 6px;\n  width: 100%;\n\n  input {\n    text-align: center;\n    font-size: 16px;\n    cursor: pointer;\n    flex-grow: 1;\n  }\n\n  button {\n    flex-shrink: 0;\n  }\n}\n\n.contact-image {\n  margin-top: 0.75rem;\n  max-width: min(60vw, 420px);\n  max-height: 50vh;\n  border-radius: var(--radius);\n}\n\n.version {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  user-select: none;\n  font-size: 14px;\n  color: hsl(var(--text-secondary));\n  transform: translateY(4px);\n  width: max-content;\n  margin: 0 auto;\n\n  .app {\n    margin-right: 2px;\n    padding: 2px;\n    width: 24px;\n    height: 24px;\n    color: hsl(var(--text-secondary));\n    cursor: pointer;\n    transition: 0.25s;\n    rotate: 0;\n\n    &:hover {\n      color: hsl(var(--text));\n      rotate: 30deg;\n    }\n  }\n\n  p {\n    font: var(--font-family-normal);\n    transform: translateY(-1px);\n  }\n}\n\n.tag-item {\n  flex-shrink: 0;\n  margin-right: 0.25rem;\n  padding: 0.15rem 0.5rem;\n  border: 1px solid hsl(var(--text-secondary) / 0.25);\n  background: hsl(var(--muted));\n  border-radius: 4px;\n  font-size: 12px;\n  margin-bottom: 0.25rem;\n  transition: 0.2s;\n\n  &.clickable {\n    cursor: pointer;\n  }\n\n  &:hover {\n    background: hsl(var(--muted-foreground) / 0.25);\n    color: hsl(var(--text));\n  }\n\n  &.pro {\n    color: hsl(var(--gold)) !important;\n  }\n\n  &:last-child {\n    margin-right: 0;\n  }\n}\n\n.conversation-id {\n  color: hsl(var(--text));\n\n  &:before {\n    content: \"#\";\n    color: hsl(var(--text-secondary));\n    font-size: 12px;\n  }\n}"
  },
  {
    "path": "app/src/assets/pages/navbar.less",
    "content": ".navbar {\n  display: flex;\n  flex-direction: row;\n  align-content: center;\n  vertical-align: center;\n  user-select: none;\n  height: var(--navbar-height);\n  padding: 0.5rem 0.5rem;\n  background: hsl(var(--background));\n  border-bottom: 1px solid hsl(var(--border));\n\n  .items {\n    width: 100%;\n    height: 100%;\n    margin: 0;\n    padding-right: 0.5rem;\n    display: flex;\n    flex-direction: row;\n    align-content: center;\n    vertical-align: center;\n  }\n\n  .logo {\n    border-radius: var(--radius);\n    cursor: pointer;\n  }\n\n  button {\n    white-space: nowrap;\n  }\n}\n\n.avatar {\n  outline: 0;\n  user-select: none;\n\n  img {\n    cursor: pointer;\n  }\n}\n\ndiv[data-radix-popper-content-wrapper=\"\"] {\n  user-select: none;\n\n  div.relative {\n    cursor: pointer;\n  }\n\n  .username {\n    color: hsl(var(--text));\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    max-width: 120px;\n\n    &:before {\n      content: \"@\";\n      font-size: 12px;\n      margin-right: 1px;\n      color: hsl(var(--text-secondary));\n    }\n  }\n\n  .action-button {\n    width: calc(100% - 4px);\n    cursor: pointer;\n    margin: 8px 2px 2px;\n    height: max-content !important;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/notify.less",
    "content": ".notify-container {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  background: hsla(var(--background-container));\n}\n"
  },
  {
    "path": "app/src/assets/pages/package.less",
    "content": ".package-wrapper {\n  display: flex;\n  flex-direction: column;\n\n  .package {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n\n    .package-title {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      gap: 4px;\n      font-size: 0.85rem;\n      color: hsl(var(--text));\n      user-select: none;\n\n      svg {\n        transform: translateY(1px);\n      }\n    }\n\n    .package-content {\n      font-size: 0.8rem;\n      color: hsl(var(--text-secondary));\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/preset.less",
    "content": "\n\n.mask-container {\n  height: max-content;\n  overflow-x: hidden;\n  overflow-y: auto;\n  scrollbar-width: thin;\n  padding: 1rem 0.5rem 0.5rem;\n}\n\n.mask-drawer-viewport {\n  width: 80vw;\n\n  @media (max-width: 768px) {\n    margin-left: 0.5rem;\n    margin-right: 0.5rem;\n    width: calc(100% - 1rem) !important;\n  }\n}\n\n.mask-picker-dialog {\n  width: max-content !important;\n  height: max-content !important;\n  padding: 0 !important;\n\n  .picker {\n    --epr-category-navigation-button-size: 28px;\n    --epr-search-input-bg-color: hsl(var(--background));\n    --epr-search-border-color: hsl(var(--border-hover));\n    --epr-bg-color: hsl(var(--background));\n    --epr-category-label-bg-color: hsl(var(--background));\n\n    img {\n      padding: 0.5rem;\n    }\n\n    .epr-icn-search {\n      width: 1rem;\n      height: 1rem;\n      transform: translateY(-0.55rem);\n    }\n\n    .epr-body {\n      scrollbar-width: thin;\n      -ms-overflow-style: none;\n\n      &::-webkit-scrollbar {\n        width: 6px;\n      }\n    }\n\n    .epr-search-container {\n      input {\n        border: 1px solid hsl(var(--border));\n      }\n    }\n\n    .epr-cat-btn {\n      &:focus:before {\n        border: none;\n      }\n    }\n\n    * {\n      font-family: var(--font-family) !important;\n      font-size: 0.85rem !important;\n    }\n  }\n}\n\n.mask-editor-container {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n  padding: 2rem 0;\n\n  .mask-conversation-list {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    height: max-content;\n    padding: 0.5rem 1.5rem;\n    margin: 1rem 0;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    color: hsl(var(--text));\n\n    .mask-conversation-title {\n      margin-top: 0.5rem;\n      margin-bottom: 1rem;\n    }\n\n    .mask-conversation-wrapper {\n      display: flex;\n      flex-direction: column;\n      margin-bottom: 0.5rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n\n    .mask-conversation {\n      display: flex;\n      flex-direction: row;\n      padding: 0.25rem;\n    }\n\n    .mask-conversation:hover ~ .mask-actions {\n      .mask-action {\n        border-color: hsl(var(--border-hover));\n      }\n    }\n\n    .mask-actions {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      justify-content: center;\n      margin-top: 0.5rem;\n\n      .mask-action {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        border: 1px solid hsl(var(--border));\n        width: 36px;\n        height: 24px;\n        border-radius: 12px;\n        margin: 0 0.25rem;\n        transition: 0.2s;\n\n        svg {\n          height: 16px;\n          width: 16px;\n          padding: 1px;\n        }\n\n        &.disabled {\n          cursor: not-allowed;\n        }\n\n        &:hover:not(.disabled) {\n          border-color: hsl(var(--border-hover));\n          background-color: hsl(var(--background-hover));\n        }\n      }\n    }\n  }\n\n  .mask-editor-row {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n  }\n\n  .mask-editor-column {\n    display: flex;\n    flex-direction: row;\n    width: 100%;\n    height: max-content;\n    padding: 0.5rem;\n    color: hsl(var(--text));\n    align-items: center;\n    border-bottom: 1px solid hsl(var(--border));\n\n    &:last-child {\n      border-bottom: none;\n    }\n\n    & > p {\n      margin-left: 1rem;\n      user-select: none;\n      white-space: nowrap;\n    }\n\n    & > * {\n      margin-right: 0.5rem;\n\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n  }\n}\n\n.mask-wrapper {\n  display: flex;\n  flex-direction: column;\n\n  .mask-header {\n    display: flex;\n    flex-direction: row;\n    width: 100%;\n  }\n\n  .mask-col {\n    display: flex;\n    flex-direction: column;\n    margin: 0.5rem 0;\n    width: 100%;\n\n    .mask-col-title {\n      color: hsl(var(--text));\n      margin: 0.5rem auto 0.5rem 0.5rem;\n    }\n  }\n}\n\n.mask-list {\n  display: flex;\n  flex-direction: column;\n  user-select: none;\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n  overflow: hidden;\n\n  &::-webkit-scrollbar {\n    width: 0.25rem;\n  }\n\n  .mask-item {\n    &:last-child {\n      border-bottom: none;\n    }\n  }\n}\n\n.mask-item {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  flex-shrink: 0;\n  cursor: pointer;\n  border-bottom: 1px solid hsl(var(--border));\n  padding: 1rem 0;\n  transition: 0.2s ease-in-out;\n\n  .mask-avatar {\n    width: 2.25rem;\n    height: 2.25rem;\n    padding: 0.5rem;\n    margin-right: 0.75rem;\n    margin-left: 1rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--border));\n    transition: 0.2s ease-in-out;\n  }\n\n  .mask-content {\n    display: flex;\n    flex-direction: column;\n\n    .mask-name {\n      color: hsl(var(--text));\n      margin-right: 0.5rem;\n    }\n\n    .mask-info {\n      font-size: 12px;\n      color: hsl(var(--text-secondary));\n      white-space: nowrap;\n      max-width: max-content;\n    }\n  }\n\n  &:hover {\n    background-color: hsl(var(--background-hover));\n\n    .mask-avatar {\n      border-color: hsl(var(--border-active));\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/quota.less",
    "content": ".buy-interface {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  flex-basis: auto;\n  flex-shrink: 0;\n  width: 100%;\n  height: max-content;\n}\n\n.buy-action {\n  width: 100%;\n  margin-top: 1.5rem;\n\n  .buy-button {\n    width: 100%;\n    transition: .25s;\n  }\n}\n\n.quota-dialog {\n  max-width: min(90vw, 680px) !important;\n}\n\n.amount-container {\n  display: flex;\n  flex-direction: column;\n  gap: 24px;\n  width: 100%;\n  align-items: center;\n}\n\n.other-wrapper {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  gap: 12px;\n  user-select: none;\n\n  @media (max-width: 460px) {\n    & {\n      flex-direction: column;\n    }\n  }\n\n  .amount-number {\n    color: hsl(var(--text));\n    transform: translateY(-2px);\n    white-space: nowrap;\n  }\n\n  .amount-input-box {\n    position: relative;\n    width: max-content;\n    max-width: 100%;\n    height: max-content;\n    margin: 0 auto;\n\n    .amount-input {\n      color: hsl(var(--text));\n      font-size: 16px;\n      text-align: center;\n    }\n\n    svg {\n      color: hsl(var(--text));\n      position: absolute;\n      top: 50%;\n      left: 12px;\n      user-select: none;\n      transform: translateY(-50%);\n    }\n  }\n}\n\n.line {\n  background: hsl(var(--border));\n  width: 1px;\n  min-height: 0;\n  height: 100%;\n\n  @media (max-width: 980px) {\n    & {\n      display: none;\n    }\n  }\n}\n\n.amount-wrapper {\n  display: inline-grid;\n  gap: 12px;\n  justify-content: center;\n  width: 100%;\n\n  grid-template-columns: 1fr 1fr 1fr 1fr;\n\n  @media (max-width: 760px) {\n    grid-template-columns: 1fr 1fr;\n  }\n\n  @media (max-width: 380px) {\n    grid-template-columns: 1fr;\n  }\n\n  .amount {\n    position: relative;\n    display: flex;\n    padding: 1rem 0.5rem;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    border: 1px solid hsl(var(--border));\n    border-radius: var(--radius);\n    user-select: none;\n    cursor: pointer;\n    gap: 4px;\n    min-width: 6rem;\n    width: 100%;\n    transition: .1s linear;\n    text-align: center;\n\n    &.active {\n      border-color: hsl(var(--text-secondary));\n\n      .amount-desc,\n      .other {\n        color: hsl(var(--text));\n      }\n    }\n\n    .amount-title {\n      display: flex;\n      flex-direction: row;\n      gap: 2px;\n      color: hsl(var(--text));\n      font-size: 16px;\n      align-items: center;\n\n      svg {\n        transform: translateY(1px);\n        width: 14px;\n        height: 14px;\n      }\n    }\n\n    .amount-desc {\n      color: hsl(var(--text-secondary));\n      transition: .1s linear;\n    }\n\n    .other {\n      font-size: 12px;\n      color: hsl(var(--text-secondary));\n      transition: .1s linear;\n\n      &:after {\n        content: '...';\n        font-size: 10px;\n      }\n    }\n  }\n}\n\n.grow {\n  flex-grow: 1;\n}\n\n.product-item {\n  display: flex;\n  flex-direction: column;\n  padding: 4px 2px;\n  width: 100%;\n  gap: 4px;\n\n  .row {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    gap: 4px;\n\n    .column {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      gap: 4px;\n\n      svg {\n        flex-shrink: 0;\n      }\n    }\n\n    .info {\n      margin-top: 6px;\n    }\n  }\n\n  .title {\n    color: hsl(var(--text));\n  }\n\n  .desc {\n    color: hsl(var(--text-secondary));\n  }\n}\n\n.quota-tip {\n  color: hsl(var(--text));\n  text-align: center;\n  align-items: center;\n\n  a {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/record.less",
    "content": ".record-wrapper > * {\n  max-width: 100%;\n}\n\n.record-area {\n  display: block !important;\n}\n\n.stats-boxes {\n  .stats-box {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n    height: max-content;\n    padding: 0.75rem 1.5rem;\n    border-radius: var(--radius);\n    background: hsl(var(--muted) / 0.25);\n    box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);\n    user-select: none;\n    width: 100%;\n\n    margin-right: 1.5rem;\n    margin-left: auto;\n\n    &:last-child {\n      margin-right: auto;\n    }\n\n    & > * {\n      flex-shrink: 0;\n    }\n\n    .box-wrapper {\n      flex-grow: 1;\n\n      .box-title {\n        font-size: 1rem;\n        margin-bottom: 0.5rem;\n      }\n\n      .box-value {\n        font-size: 1.5rem;\n        font-weight: normal;\n      }\n    }\n\n    .box-icon {\n      width: max-content;\n      height: max-content;\n      transform: translate(0.25rem, 0.25rem);\n      border-radius: 0.25rem;\n      svg {\n        width: 2rem;\n        height: 2rem;\n        stroke-width: 1;\n      }\n    }\n  }\n}"
  },
  {
    "path": "app/src/assets/pages/settings.less",
    "content": ".settings-dialog {\n  max-width: min(90vw, 660px) !important;\n}\n\n.settings-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: center;\n  height: 100%;\n\n  .info-box {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    user-select: none;\n    color: hsl(var(--text));\n\n    & > * {\n      margin-bottom: 0.5rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n\n.settings-wrapper {\n  width: 100%;\n  height: max-content;\n  margin: 1.5rem 0.5rem;\n\n  .settings-segment {\n    margin-bottom: 1rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.settings-segment {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  color: hsl(var(--text));\n  border: 1px solid hsl(var(--border));\n  border-radius: var(--radius);\n  padding: 0.75rem 0;\n\n  .item {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    user-select: none;\n    padding: 0 1rem;\n\n    .value {\n      transition: .1s;\n    }\n\n    .slider-value {\n      min-width: 2rem;\n    }\n\n    input {\n      text-align: center;\n      max-width: 4rem;\n      max-height: 1.75rem;\n\n      &.large-value {\n        max-width: 6rem;\n      }\n    }\n\n    button:not(.tips-trigger):not(.set-action) {\n      margin: 0.25rem 1rem;\n    }\n\n    .select {\n      margin: 0 !important;\n      padding: 0.25rem 0.75rem !important;\n\n      span {\n        font-size: 0.8rem !important;\n        margin: 0 0.5rem;\n      }\n    }\n\n    .name {\n      display: flex;\n      flex-direction: row;\n      text-align: start;\n      align-items: center;\n\n      .tips-trigger {\n        transform: translateY(1px);\n      }\n    }\n  }\n\n  & > .item {\n    margin-bottom: 0.75rem;\n    padding-bottom: 0.75rem;\n    border-bottom: 1px solid hsl(var(--border));\n\n    &:last-child {\n      margin-bottom: 0;\n      padding-bottom: 0;\n      border-bottom: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/assets/pages/share-manager.less",
    "content": ".share-table {\n  width: 100%;\n  height: max-content;\n}\n"
  },
  {
    "path": "app/src/assets/pages/sharing.less",
    "content": ""
  },
  {
    "path": "app/src/assets/pages/subscription.less",
    "content": ".sub-dialog {\n  width: max-content !important;\n  max-width: min(90vw, 1200px) !important;\n}\n\n.sub-wrapper {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.sub-row {\n  display: flex;\n  flex-direction: column;\n  padding: 0.5rem 1rem;\n  margin-bottom: 8px;\n  align-items: center;\n  width: 100%;\n\n  .sub-column {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    margin-bottom: 4px;\n    width: 100%;\n\n    svg {\n      flex-shrink: 0;\n      transform: translateY(1px);\n    }\n\n    .sub-value {\n      display: flex;\n      flex-direction: row;\n      align-items: center;\n      gap: 2px;\n\n      p {\n        font-weight: bolder;\n      }\n    }\n\n    &:first-child {\n      margin-bottom: 8px;\n    }\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.plan-wrapper {\n  margin-top: 0.5rem;\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr;\n  gap: 1rem;\n  justify-content: center;\n  width: 100%;\n\n  @media (max-width: 870px) {\n    grid-template-columns: 1fr;\n  }\n\n  &.disable {\n    grid-template-columns: 1fr;\n  }\n}\n\n.plan {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  padding: 1rem;\n  color: hsl(var(--text));\n  width: 100%;\n\n  &.standard {\n    border-color: hsl(var(--text-secondary));\n  }\n\n  .title {\n    text-align: center;\n    font-size: 16px;\n    margin: 4px;\n  }\n\n  .award {\n    display: flex;\n    flex-direction: column;\n    color: hsl(var(--gold));\n    justify-content: center;\n    align-items: center;\n    user-select: none;\n    font-size: 0.75rem;\n    line-height: 0.8rem;\n    margin-bottom: 1rem;\n    padding: 0.5rem;\n    border-radius: var(--radius);\n    border: 1px solid hsl(var(--gold));\n\n    svg {\n      flex-shrink: 0;\n      transform: translateY(1px);\n    }\n\n    div {\n      word-break: break-word;\n      white-space: break-spaces;\n      text-align: center;\n    }\n  }\n\n  .price-wrapper {\n    position: relative;\n    width: max-content;\n    margin: 0 auto;\n\n    .price {\n      font-size: 18px;\n      font-weight: bold;\n      margin: 2px auto;\n\n      .tax {\n        color: hsl(var(--text-secondary));\n      }\n    }\n\n    .annotate {\n      position: absolute;\n      font-size: 14px;\n      margin-top: 0.25rem;\n      font-weight: normal;\n      color: hsl(var(--text-secondary));\n      right: 0;\n      bottom: 0;\n      transform: translateX(calc(100% + 0.1rem));\n    }\n  }\n\n  .desc {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n    margin: 0.5rem 0;\n\n    .api-tip {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n    }\n\n    div {\n\n      svg {\n        flex-shrink: 0;\n      }\n    }\n  }\n\n  .action {\n    margin-top: auto !important;\n  }\n}\n\n.upgrade-wrapper {\n  margin: 24px auto 8px;\n\n  .price {\n    font-size: 14px;\n    margin-top: 12px;\n    text-align: center;\n    transform: translateY(12px);\n\n    .tax {\n      color: hsl(var(--text-secondary));\n    }\n  }\n}\n\n@media (max-width: 460px) {\n  .plan {\n    min-width: 0 !important;\n  }\n}\n"
  },
  {
    "path": "app/src/assets/ui.less",
    "content": "@import \"fonts/all\";\n\n.gold-text {\n  color: hsl(var(--gold)) !important;\n}\n\n.spinner {\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 2px;\n  background: hsl(var(--text));\n  transition: 0.25s linear;\n  transition-property: width, opacity;\n  z-index: 1024;\n  user-select: none;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n}\n\n.select-group {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  padding: 6px 8px;\n  border-radius: 4px;\n  user-select: none;\n  justify-content: center;\n\n  &.mobile {\n    text-align: center;\n\n    & span {\n      margin: 0 auto;\n    }\n  }\n\n  .select-group-item {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    padding: 0.35rem 0.8rem;\n    margin: 0.25rem;\n    border: 1px solid hsl(var(--border));\n    border-radius: 4px;\n    transition: .2s;\n    cursor: pointer;\n    font-size: 16px;\n    background: hsl(var(--background));\n    color: hsl(var(--text));\n\n    &:hover {\n      background: hsl(var(--accent-secondary));\n    }\n\n    &.active {\n      border-color: hsl(var(--border-hover));\n      background: hsl(var(--accent));\n    }\n  }\n}\n\n.select-element.badge {\n  user-select: none;\n  transition: .2s;\n  padding-left: 0.45rem;\n  padding-right: 0.45rem;\n\n  &.badge-default {\n    background: hsl(var(--primary)) !important;\n\n    &:hover {\n      background: hsl(var(--primary)) !important;\n    }\n  }\n\n  &.badge-gold {\n    color: rgb(164, 128, 0) !important;\n    background: rgb(255, 231, 145) !important;\n  }\n}\n\n.no-scrollbar {\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n\n  &::-webkit-scrollbar {\n    display: none;\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: transparent;\n  }\n}\n\n.thin-scrollbar {\n  scrollbar-width: thin;\n  -ms-overflow-style: none;\n\n  &::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  // update the scrollbar style\n  &::-webkit-scrollbar-thumb {\n    background: hsl(var(--border));\n    border-radius: 6px;\n  }\n\n  &::-webkit-scrollbar-thumb:hover {\n    background: hsl(var(--border-hover));\n  }\n\n  &::-webkit-scrollbar-track {\n    background: hsl(var(--background));\n  }\n}\n\n.horizontal-scrollbar {\n  --radix-scroll-area-thumb-height: 6px;\n}\n\ninput[type=\"number\"] {\n  -webkit-appearance: textfield;\n  margin: 0;\n\n  &::-webkit-inner-spin-button,\n  &::-webkit-outer-spin-button {\n    -webkit-appearance: none;\n  }\n\n  &::after {\n    content: '>';\n    position: absolute;\n    right: 5px;\n    top: 2px;\n    transform: rotate(-45deg);\n  }\n\n  &::before {\n    content: '<';\n    position: absolute;\n    right: 5px;\n    top: 20px;\n    transform: rotate(135deg);\n  }\n}\n\n.selection {\n  ::selection {\n    color: hsl(var(--selection-foreground));\n    background: hsl(var(--selection));\n  }\n\n  ::-moz-selection {\n    color: hsl(var(--selection-foreground));\n    background: hsl(var(--selection));\n  }\n}\n\n.paragraph {\n  display: flex;\n  flex-direction: column;\n  margin: 0.5rem 0;\n  padding: 0.5rem;\n  border-bottom: 1px solid hsl(var(--border));\n  color: hsl(var(--text));\n  transition: .25s;\n  cursor: pointer;\n\n  &.collapsable {\n    .paragraph-content {\n      max-height: var(--max-height);\n      will-change: max-height;\n      overflow: hidden;\n      transition: .5s;\n    }\n\n    &.collapsed {\n      padding-bottom: 1rem;\n\n      .paragraph-content {\n        max-height: 0;\n      }\n    }\n  }\n\n  .paragraph-content {\n    & > * {\n      margin-bottom: 1rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n\n  .paragraph-header {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: nowrap;\n    align-items: center;\n    transform: translateY(-0.25rem);\n  }\n\n  .paragraph-title {\n    position: relative;\n    display: flex;\n    flex-direction: row;\n    font-size: 1.05rem;\n    user-select: none;\n    line-height: 1.1rem;\n    color: hsl(var(--text-secondary));\n    transition: .25s;\n    text-decoration: none !important;\n\n    &:before {\n      content: '';\n      margin-right: 0.75rem;\n      height: 1.25rem;\n      width: 2px;\n      border-radius: 1px;\n      background: hsl(var(--text-secondary));\n      transition: .25s;\n    }\n  }\n\n  .paragraph-item {\n    display: flex;\n    flex-direction: row;\n    white-space: nowrap;\n    align-items: center;\n    font-size: 0.9rem;\n\n    &.row-layout {\n      align-items: flex-start !important;\n      flex-direction: column !important;\n\n      & > * {\n        margin-right: 0 !important;\n        margin-bottom: 0.75rem;\n\n        &:last-child {\n          margin-bottom: 0;\n        }\n      }\n    }\n\n    label {\n      font-size: 0.9rem;\n      font-weight: normal;\n    }\n\n    & > * {\n      margin-right: 1rem;\n\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n  }\n\n  .paragraph-description {\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    color: hsl(var(--text-secondary));\n    width: 100%;\n    height: max-content;\n    font-size: 0.9rem;\n    margin: 0.75rem 0 1.25rem;\n\n    svg {\n      margin-right: 0.5rem;\n      flex-shrink: 0;\n    }\n  }\n\n  .paragraph-space {\n    width: 100%;\n    height: 0.25rem;\n  }\n\n  .paragraph-content {\n    transition: 1.5s ease-in-out;\n  }\n\n  .paragraph-footer {\n    display: flex;\n    flex-direction: row;\n    margin-top: 1rem;\n\n    & > * {\n      margin-right: 0.5rem;\n\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n  }\n\n  &:hover {\n    border-color: hsl(var(--border-hover));\n\n    .paragraph-title {\n      color: hsl(var(--text));\n\n      &:before {\n        background: hsl(var(--text));\n      }\n    }\n  }\n\n  &.config-paragraph {\n    .paragraph-content {\n      input, button {\n        margin-left: auto;\n      }\n    }\n  }\n}\n\n.number-input {\n  transition: 0.25s;\n  transition-property: border-color;\n}\n\n.avatar {\n  display: flex;\n  border-radius: var(--radius);\n  text-align: center;\n\n  p {\n    margin: auto;\n    color: hsl(var(--text-dark));\n  }\n}\n\n.text-secondary {\n  color: hsl(var(--text-secondary)) !important;\n}\n\n.chart-tooltip,\n.recharts-tooltip-wrapper > .rounded-tremor-default.text-tremor-default.border {\n  @keyframes fadeIn {\n    0% {\n      opacity: 0;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n\n  animation: fadeIn 0.5s;\n  position: absolute;\n  z-index: 64;\n}\n\n.border-input:focus {\n  border-color: hsl(var(--border));\n}\n\n\n.animate-fade-in {\n  @keyframes fadeIn {\n    0% {\n      opacity: 0;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n\n  animation: fadeIn 0.5s forwards;\n}\n\n.text-common {\n  color: hsl(var(--text)) !important;\n}\n\n.text-md {\n  font-size: 0.95rem;\n}\n\n.error-border {\n  border-color: hsl(var(--destructive)) !important;\n}\n\n.button-wrapper.is-loading {\n  .loading-hidden {\n    display: none;\n  }\n}"
  },
  {
    "path": "app/src/components/Avatar.tsx",
    "content": "import { deeptrainApiEndpoint, useDeeptrain } from \"@/conf/env.ts\";\nimport { ImgHTMLAttributes, useMemo, useState, useEffect } from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { getUserInfo, UserInfo, initialUserInfo } from \"@/api/auth.ts\";\nimport md5 from \"crypto-js/md5\";\nimport { getConfig } from \"@/admin/api/system\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { setAvatar } from \"@/store/avatar\";\n\nexport interface AvatarProps extends ImgHTMLAttributes<HTMLElement> {\n  username: string;\n}\n\nasync function checkGravatar(\n  gravatar_endpoint: string,\n  email: string,\n): Promise<boolean> {\n  if (!email || email === \"root@example.com\") {\n    return false;\n  }\n\n  const trimmedEmail = email.trim().toLowerCase();\n  const hash = md5(trimmedEmail).toString();\n  const uri = `${gravatar_endpoint}/avatar/${hash}?d=404`;\n\n  try {\n    const response = await fetch(uri);\n\n    if (response.ok) {\n      return true;\n    }\n    console.info(\"[avatar] gravatar not found:\", trimmedEmail);\n    return false;\n  } catch (error) {\n    console.error(\"[avatar] request failed:\", error);\n    return false;\n  }\n}\nfunction Avatar({ username, ...props }: AvatarProps) {\n  const dispatch = useDispatch();\n  const cachedAvatarBlob = useSelector(\n    (state: any) => state.avatar.avatars[username],\n  );\n\n  const [userInfo, setUserInfo] = useState<UserInfo>(initialUserInfo);\n  const [hasAvatar, setHasAvatar] = useState(false);\n  const [gravatar_endpoint, setGravatarEndpoint] = useState<string>(\"\");\n\n  useEffect(() => {\n    getUserInfo().then((info) => setUserInfo(info?.data ?? initialUserInfo));\n  }, []);\n\n  useEffect(() => {\n    getConfig().then((config) => {\n      if (\n        config.data.general.gravatar === undefined ||\n        config.data.general.gravatar === \"\"\n      ) {\n        setGravatarEndpoint(\"\");\n        return;\n      }\n      setGravatarEndpoint(config.data.general.gravatar);\n    });\n  }, []);\n\n  useEffect(() => {\n    if (cachedAvatarBlob !== null) {\n      setHasAvatar(true);\n      return;\n    }\n    checkGravatar(gravatar_endpoint, userInfo.email).then((hasAvatar) => {\n      setHasAvatar(hasAvatar);\n      if (hasAvatar) {\n        const avatarUrl = getGravatarUrl(userInfo.email);\n        fetch(avatarUrl)\n          .then((response) => response.blob())\n          .then((blob) => {\n            dispatch(setAvatar({ username, blob }));\n          });\n      }\n    });\n  }, [gravatar_endpoint, userInfo]);\n\n  const code = useMemo(\n    () => (username?.length > 0 ? username[0].toUpperCase() : \"A\"),\n    [username],\n  );\n\n  const background = useMemo(() => {\n    const colors = [\n      \"bg-gradient-to-br from-red-500 to-orange-500\",\n      \"bg-gradient-to-br from-yellow-500 to-green-500\",\n      \"bg-gradient-to-br from-green-500 to-teal-500\",\n      \"bg-gradient-to-br from-indigo-500 to-purple-500\",\n      \"bg-gradient-to-br from-purple-500 to-pink-500\",\n      \"bg-gradient-to-br from-sky-500 to-blue-500\",\n      \"bg-gradient-to-br from-pink-500 to-rose-500\",\n    ];\n    const index = code.charCodeAt(0) % colors.length;\n    return colors[index];\n  }, [username]);\n  const getGravatarUrl = (email: string | undefined) => {\n    if (!email) return \"\";\n    const trimmedEmail = email.trim().toLowerCase();\n    const hash = md5(trimmedEmail).toString();\n    if (!gravatar_endpoint) return \"\";\n    return `${gravatar_endpoint}/avatar/${hash}?d=identicon`;\n  };\n\n  const avatarSrc =\n    useDeeptrain && username.length > 0\n      ? `${deeptrainApiEndpoint}/avatar/${username}`\n      : hasAvatar && cachedAvatarBlob\n      ? URL.createObjectURL(cachedAvatarBlob)\n      : hasAvatar && userInfo.email\n      ? getGravatarUrl(userInfo.email)\n      : \"\";\n\n  return avatarSrc ? (\n    <img\n      {...props}\n      className={cn(\"w-10 h-10\", props.className)}\n      src={avatarSrc}\n      alt=\"\"\n    />\n  ) : (\n    <div\n      {...props}\n      className={cn(\"avatar w-10 h-10 shadow\", background, props.className)}\n    >\n      <p className={`text-white`}>{code}</p>\n    </div>\n  );\n}\n\nexport default Avatar;\n"
  },
  {
    "path": "app/src/components/EditorProvider.tsx",
    "content": "import {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"./ui/dialog.tsx\";\nimport { Maximize, Image, MenuSquare, PanelRight, Eraser } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nimport \"@/assets/common/editor.less\";\nimport { Textarea } from \"./ui/textarea.tsx\";\nimport Markdown from \"./Markdown.tsx\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Toggle } from \"./ui/toggle.tsx\";\nimport { mobile } from \"@/utils/device.ts\";\nimport { Button } from \"./ui/button.tsx\";\nimport { ChatAction } from \"@/components/home/assemblies/ChatAction.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\ntype RichEditorProps = {\n  value: string;\n  onChange: (value: string) => void;\n  maxLength?: number;\n\n  formatter?: (value: string) => string;\n  isInvalid?: (value: string) => boolean;\n  title?: string;\n\n  open?: boolean;\n  setOpen?: (open: boolean) => void;\n  children?: React.ReactNode;\n\n  submittable?: boolean;\n  onSubmit?: (value: string) => void;\n  closeOnSubmit?: boolean;\n};\n\nfunction RichEditor({\n  value,\n  onChange,\n  maxLength,\n  formatter,\n  submittable,\n  isInvalid,\n  onSubmit,\n  setOpen,\n  closeOnSubmit,\n}: RichEditorProps) {\n  const { t } = useTranslation();\n  const input = useRef(null);\n  const [openPreview, setOpenPreview] = useState(!mobile);\n  const [openInput, setOpenInput] = useState(true);\n\n  const formattedValue = useMemo(() => {\n    return formatter ? formatter(value) : value;\n  }, [value, formatter]);\n  const invalid = useMemo(() => {\n    return isInvalid ? isInvalid(value) : false;\n  }, [value, isInvalid]);\n\n  const handler = () => {\n    if (!input.current) return;\n    const target = input.current as HTMLElement;\n    const preview = target.parentElement?.querySelector(\n      \".editor-preview\",\n    ) as HTMLElement | null;\n    if (!preview) {\n      setTimeout(handler, 100);\n      return;\n    }\n\n    const listener = () => {\n      preview.style.height = `${target.clientHeight}px`;\n    };\n    target.addEventListener(\"transitionstart\", listener);\n    setInterval(listener, 250);\n    target.addEventListener(\"scroll\", () => {\n      preview.scrollTop = target.scrollTop;\n    });\n\n    preview.style.height = `${target.clientHeight}px`;\n\n    if (openInput) target.focus();\n  };\n  useEffect(handler, [input]);\n\n  return (\n    <div className={`editor-container`}>\n      <div className={`editor-toolbar`}>\n        <Button\n          variant={`outline`}\n          className={`h-8 w-8 p-0`}\n          onClick={() => value && onChange(\"\")}\n        >\n          <Eraser className={`h-3.5 w-3.5`} />\n        </Button>\n        <div className={`grow`} />\n        <Toggle\n          variant={`outline`}\n          className={`h-8 w-8 p-0`}\n          pressed={openInput && !openPreview}\n          onClick={() => {\n            setOpenPreview(false);\n            setOpenInput(true);\n          }}\n        >\n          <MenuSquare className={`h-3.5 w-3.5`} />\n        </Toggle>\n\n        <Toggle\n          variant={`outline`}\n          className={`h-8 w-8 p-0`}\n          pressed={openInput && openPreview}\n          onClick={() => {\n            setOpenPreview(true);\n            setOpenInput(true);\n          }}\n        >\n          <PanelRight className={`h-3.5 w-3.5`} />\n        </Toggle>\n\n        <Toggle\n          variant={`outline`}\n          className={`h-8 w-8 p-0`}\n          pressed={!openInput && openPreview}\n          onClick={() => {\n            setOpenPreview(true);\n            setOpenInput(false);\n          }}\n        >\n          <Image className={`h-3.5 w-3.5`} />\n        </Toggle>\n      </div>\n      <div className={`editor-wrapper`}>\n        <div\n          className={cn(\n            \"editor-object\",\n            openInput && \"show-editor\",\n            openPreview && \"show-preview\",\n          )}\n        >\n          {openInput && (\n            <Textarea\n              placeholder={t(\"chat.placeholder-raw\")}\n              value={value}\n              className={cn(\n                `editor-input transition-all`,\n                invalid && `error-border`,\n              )}\n              id={`editor`}\n              maxLength={maxLength}\n              onChange={(e) => onChange(e.target.value)}\n              ref={input}\n            />\n          )}\n          {openPreview &&\n            (formattedValue ? (\n              <Markdown\n                className={`editor-preview`}\n                children={formattedValue}\n              />\n            ) : (\n              <div\n                className={`editor-preview inline-flex text-secondary text-xs items-center justify-center whitespace-pre-wrap`}\n              >\n                <Image\n                  className={`h-3.5 w-3.5 mr-1 shrink-0 inline-flex translate-y-[1px]`}\n                />\n                {t(\"chat.empty-preview\")}\n              </div>\n            ))}\n        </div>\n      </div>\n      {submittable && (\n        <div className={`editor-footer mt-2 flex flex-row`}>\n          <Button\n            variant={`outline`}\n            className={`ml-auto mr-2`}\n            onClick={() => setOpen?.(false)}\n          >\n            {t(\"cancel\")}\n          </Button>\n          <Button\n            variant={`default`}\n            onClick={() => {\n              onSubmit?.(value);\n              (closeOnSubmit ?? true) && setOpen?.(false);\n            }}\n          >\n            {t(\"submit\")}\n          </Button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction EditorProvider(props: RichEditorProps) {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <Dialog open={props.open} onOpenChange={props.setOpen}>\n        {!props.setOpen && (\n          <DialogTrigger asChild>\n            {props.children ?? (\n              <ChatAction text={t(\"editor\")} className={`hidden md:flex`}>\n                <Maximize className={`h-4 w-4`} />\n              </ChatAction>\n            )}\n          </DialogTrigger>\n        )}\n        <DialogContent className={`editor-dialog flex-dialog`} couldFullScreen>\n          <DialogHeader>\n            <DialogTitle>{props.title ?? t(\"edit\")}</DialogTitle>\n            <DialogDescription asChild>\n              <RichEditor {...props} />\n            </DialogDescription>\n          </DialogHeader>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\nexport default EditorProvider;\n\nexport function JSONEditorProvider({ ...props }: RichEditorProps) {\n  return (\n    <EditorProvider\n      {...props}\n      formatter={(value) => `\\`\\`\\`json\\n${value}\\n\\`\\`\\``}\n      isInvalid={(value) => {\n        try {\n          JSON.parse(value);\n          return false;\n        } catch (e) {\n          return true;\n        }\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "app/src/components/Emoji.tsx",
    "content": "import { cn } from \"@/components/ui/lib/utils.ts\";\n\nexport function getEmojiSource(emoji: string): string {\n  return `https://registry.npmmirror.com/@lobehub/fluent-emoji-3d/latest/files/assets/${emoji}.webp`;\n}\n\ntype EmojiProps = {\n  emoji: string;\n  className?: string;\n};\n\nfunction Emoji({ emoji, className }: EmojiProps) {\n  return (\n    <img\n      className={cn(\"select-none\", className)}\n      src={getEmojiSource(emoji)}\n      alt={\"\"}\n    />\n  );\n}\n\nexport default Emoji;\n"
  },
  {
    "path": "app/src/components/ErrorBoundary.tsx",
    "content": "import React from \"react\";\nimport { AlertCircle, Download } from \"lucide-react\";\nimport { withTranslation, WithTranslation } from \"react-i18next\";\nimport { version } from \"@/conf/bootstrap.ts\";\nimport { getMemoryPerformance } from \"@/utils/app.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { saveAsFile } from \"@/utils/dom.ts\";\n\ntype ErrorBoundaryProps = { children: React.ReactNode } & WithTranslation;\n\nclass ErrorBoundary extends React.Component<\n  ErrorBoundaryProps,\n  { errorCaught: Error | null }\n> {\n  constructor(props: ErrorBoundaryProps) {\n    super(props);\n    this.state = { errorCaught: null };\n  }\n\n  static getDerivedStateFromError(error: Error) {\n    return { errorCaught: error };\n  }\n\n  render() {\n    const { t } = this.props;\n    const ua = navigator.userAgent || \"unknown\";\n    const memory = getMemoryPerformance();\n    const time = new Date().toLocaleString();\n    const stamp = new Date().getTime();\n    const path = window.location.pathname;\n\n    const message = `Raised-Path: ${path}\\nApp-Version: ${version}\\nMemory-Usage: ${\n      !isNaN(memory) ? memory.toFixed(2) + \" MB\" : \"unknown\"\n    }\\nLocale-Time: ${time}\\nError-Message: ${\n      this.state.errorCaught?.message || \"unknown\"\n    }\\nUser-Agent: ${ua}\\nStack-Trace: ${\n      this.state.errorCaught?.stack || \"unknown\"\n    }`;\n\n    return this.state.errorCaught ? (\n      <div className={`error-boundary`}>\n        <AlertCircle className={`h-12 w-12 mt-4 mb-6`} />\n        <p className={`select-none text-2xl mb-4`}>{t(\"fatal\")}</p>\n        <div className={`error-provider`}>\n          <p>Raised-Path: {path}</p>\n          <p>App-Version: {version}</p>\n          <p>\n            Memory-Usage:{\" \"}\n            {!isNaN(memory) ? memory.toFixed(2) + \" MB\" : \"unknown\"}\n          </p>\n          <p>Locale-Time: {time}</p>\n          <p>Error-Message: {this.state.errorCaught.message}</p>\n          <p>User-Agent: {ua}</p>\n        </div>\n        <div className={`error-action mt-4 mb-4`}>\n          <Button onClick={() => saveAsFile(`error-${stamp}.log`, message)}>\n            <Download className={`h-4 w-4 mr-2`} />\n            {t(\"download-fatal-log\")}\n          </Button>\n        </div>\n        <div className={`error-tips select-none align-center`}>\n          <p>{t(\"fatal-tips\")}</p>\n        </div>\n      </div>\n    ) : (\n      this.props.children\n    );\n  }\n}\n\nexport default withTranslation()(ErrorBoundary);\n"
  },
  {
    "path": "app/src/components/FileProvider.tsx",
    "content": "import { useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport {\n  Loader2,\n  Paperclip,\n  X,\n  FileIcon,\n  FileTextIcon,\n  FileImageIcon,\n  FileVideoIcon,\n  FileAudioIcon,\n  FileSpreadsheetIcon,\n  FileArchiveIcon,\n  FileCodeIcon,\n  FileJsonIcon,\n  FileVideo2Icon,\n  FileDigitIcon,\n  AlarmClock,\n} from \"lucide-react\";\n\nimport \"@/assets/common/file.less\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"./ui/dialog\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDraggableInput } from \"@/utils/dom.ts\";\nimport { FileObject, FileArray, quickBlobParser } from \"@/api/file.ts\";\nimport { useSelector } from \"react-redux\";\nimport { getModelFromId, isHighContextModel } from \"@/conf/model.ts\";\nimport { selectModel, selectSupportModels } from \"@/store/chat.ts\";\nimport { ChatAction } from \"@/components/home/assemblies/ChatAction.tsx\";\nimport { blobEvent } from \"@/events/blob.ts\";\nimport { isB64Image } from \"@/utils/base.ts\";\nimport { toast } from \"sonner\";\nimport { Badge } from \"./ui/badge.tsx\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { cn } from \"./ui/lib/utils.ts\";\nimport { Progress } from \"./ui/progress.tsx\";\n\nconst MaxFileSize = 1024 * 1024 * 100; // 100MB File Size Limit\nconst MaxPromptSize = 10 * 1024; // 10KB Prompt Size Limit (to avoid token overflow)\n\ntype FileTask = {\n  id: number;\n  file: File;\n  progress: number;\n};\n\ntype FileTaskState = {\n  tasks: FileTask[];\n};\n\nfunction fileTaskReducer(state: FileTaskState, action: any): FileTaskState {\n  switch (action.type) {\n    case \"add\":\n      return { ...state, tasks: [...state.tasks, action.payload] };\n    case \"remove\":\n      return {\n        ...state,\n        tasks: state.tasks.filter((task) => task.id !== action.payload),\n      };\n    case \"update-progress\":\n      return {\n        ...state,\n        tasks: state.tasks.map((task) =>\n          task.id === action.payload.id\n            ? { ...task, progress: action.payload.progress }\n            : task,\n        ),\n      };\n    default:\n      return state;\n  }\n}\n\ntype FileProviderProps = {\n  files: FileArray;\n  dispatch: (action: Record<string, any>) => void;\n};\n\nfunction FileProvider({ files, dispatch }: FileProviderProps) {\n  const { t } = useTranslation();\n  const model = useSelector(selectModel);\n  const [open, setOpen] = useState(false);\n  const [loading, setLoading] = useState(false);\n\n  const [tasks, taskDispatch] = useReducer(fileTaskReducer, {\n    tasks: [],\n  } as FileTaskState);\n\n  const supportModels = useSelector(selectSupportModels);\n\n  useEffect(() => {\n    blobEvent.bind(async (file: File | File[]) => {\n      setOpen?.(true);\n      await triggerFile(Array.isArray(file) ? file : [file]);\n    });\n  }, []);\n\n  const triggerFile = async (files: (File | null)[]) => {\n    setLoading(true);\n    for (const file of files) {\n      if (!file) continue;\n      if (file.size > MaxFileSize) {\n        toast.error(t(\"file.over-size\"), {\n          description: t(\"file.over-size-prompt\", {\n            size: (MaxFileSize / 1024 / 1024).toFixed(),\n          }),\n        });\n      } else {\n        const id = Date.now();\n        taskDispatch({\n          type: \"add\",\n          payload: { id, file, progress: 0 },\n        });\n\n        const info = getModelFromId(supportModels, model);\n        const task = quickBlobParser(\n          file,\n          info ?? {\n            id: model,\n            ocr_model: false,\n            vision_model: false,\n            reverse_model: false,\n          },\n          (progress) => {\n            console.debug(\n              `[parser] task ${id} progress: ${progress.toFixed(2)}%`,\n            );\n            taskDispatch({\n              type: \"update-progress\",\n              payload: { id, progress },\n            });\n          },\n        );\n\n        toast.promise(task, {\n          loading: t(\"file.uploading-prompt\"),\n          success: (content: string) => {\n            addFile({ name: file.name, content, size: file.size });\n            taskDispatch({\n              type: \"remove\",\n              payload: id,\n            });\n            return t(\"file.parse-success-prompt\", { file: file.name });\n          },\n          error: (error: Error) => {\n            taskDispatch({\n              type: \"remove\",\n              payload: id,\n            });\n            return t(\"file.parse-error-prompt\", { reason: error.message });\n          },\n        });\n      }\n    }\n    setLoading(false);\n  };\n\n  function addFile(file: FileObject) {\n    console.debug(\n      `[file] new file was added (filename: ${file.name}, size: ${file.size}, prompt: ${file.content.length})`,\n    );\n    if (\n      file.content.length > MaxPromptSize &&\n      !isHighContextModel(supportModels, model) &&\n      !isB64Image(file.content)\n    ) {\n      file.content = file.content.slice(0, MaxPromptSize);\n      toast(t(\"file.max-length\"), {\n        description: t(\"file.max-length-prompt\"),\n      });\n    }\n\n    dispatch({ type: \"add\", payload: file });\n  }\n\n  function removeFile(index: number) {\n    dispatch({ type: \"remove\", payload: index });\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <ChatAction text={t(\"file.file\")} active={files.length}>\n          <Paperclip className={`h-4 w-4`} />\n        </ChatAction>\n      </DialogTrigger>\n      <DialogContent className={`file-dialog flex-dialog`}>\n        <DialogHeader>\n          <DialogTitle className=\"flex flex-row items-center\">\n            {t(\"file.file\")}\n            <Badge variant=\"secondary\" className=\"ml-2\">\n              {files.length}\n            </Badge>\n          </DialogTitle>\n          <DialogDescription asChild>\n            <motion.div\n              className={`file-wrapper`}\n              initial={{ opacity: 0, y: -20 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.3 }}\n            >\n              <AnimatePresence key=\"files\">\n                <FileList value={files} removeFile={removeFile} />\n              </AnimatePresence>\n              <AnimatePresence key=\"tasks\">\n                {tasks.tasks.map((task, index) => (\n                  <motion.div\n                    key={task.id}\n                    initial={{ opacity: 0, y: 20 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -20 }}\n                    transition={{ duration: 0.1, delay: index * 0.1 }}\n                  >\n                    <FileTaskItem task={task} />\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n              <motion.div\n                initial={{ opacity: 0, scale: 0.95 }}\n                animate={{ opacity: 1, scale: 1 }}\n                transition={{ delay: 0.2, duration: 0.3 }}\n              >\n                <FileInput\n                  loading={loading}\n                  id={\"file\"}\n                  className={\"file\"}\n                  handleEvent={triggerFile}\n                />\n              </motion.div>\n            </motion.div>\n          </DialogDescription>\n        </DialogHeader>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ntype FileTaskItemProps = {\n  task: FileTask;\n};\n\nfunction FileTaskItem({ task }: FileTaskItemProps) {\n  return (\n    <div className=\"w-full h-fit flex flex-row items-center py-0.5 select-none\">\n      <AlarmClock className=\"w-3.5 h-3.5 mr-1\" />\n      <div className=\"truncate\">{task.file.name}</div>\n      <div className=\"mr-1 ml-auto text-xs\">{task.progress.toFixed()}%</div>\n      <Progress value={task.progress} className=\"w-16 md:w-24 h-2\" />\n    </div>\n  );\n}\n\ntype FileBadgeProps = {\n  name: string;\n};\n\nfunction getFileExtension(name: string) {\n  return name.split(\".\").pop()?.toLowerCase() || \"\";\n}\n\nfunction getFileIcon(name: string) {\n  const extension = getFileExtension(name);\n  switch (extension) {\n    case \"pdf\":\n      return FileTextIcon;\n    case \"doc\":\n    case \"docx\":\n    case \"txt\":\n      return FileDigitIcon;\n    case \"xls\":\n    case \"xlsx\":\n    case \"csv\":\n      return FileSpreadsheetIcon;\n    case \"ppt\":\n    case \"pptx\":\n      return FileVideo2Icon;\n    case \"jpg\":\n    case \"jpeg\":\n    case \"png\":\n    case \"gif\":\n    case \"svg\":\n      return FileImageIcon;\n    case \"mp4\":\n    case \"avi\":\n    case \"mov\":\n      return FileVideoIcon;\n    case \"mp3\":\n    case \"wav\":\n      return FileAudioIcon;\n    case \"zip\":\n    case \"rar\":\n    case \"7z\":\n      return FileArchiveIcon;\n    case \"js\":\n    case \"ts\":\n    case \"py\":\n    case \"java\":\n    case \"cpp\":\n    case \"c\":\n    case \"h\":\n    case \"rs\":\n    case \"swift\":\n    case \"kt\":\n    case \"ktm\":\n    case \"php\":\n    case \"rb\":\n    case \"sh\":\n    case \"html\":\n    case \"css\":\n    case \"scss\":\n    case \"less\":\n    case \"sass\":\n    case \"styl\":\n    case \"vue\":\n    case \"svelte\":\n    case \"astro\":\n    case \"tsx\":\n    case \"jsx\":\n      return FileCodeIcon;\n    case \"json\":\n    case \"xml\":\n    case \"jsonl\":\n    case \"yaml\":\n    case \"yml\":\n    case \"toml\":\n    case \"ini\":\n    case \"cfg\":\n    case \"conf\":\n      return FileJsonIcon;\n    default:\n      return FileIcon;\n  }\n}\n\nfunction FileIconObject({ name }: FileBadgeProps) {\n  const IconComponent = useMemo(() => getFileIcon(name), [name]);\n\n  return (\n    <div className=\"w-fit h-fit relative\">\n      <IconComponent className=\"stroke-[1.25] h-8 w-8 text-primary/70 group-hover:text-primary transition-colors duration-200\" />\n    </div>\n  );\n}\n\nfunction FileBadge({ name }: FileBadgeProps) {\n  const extension = getFileExtension(name);\n  return (\n    <span\n      className={cn(\n        \"px-1 inline-block mr-1 rounded-sm bg-muted/50 text-2xs text-primary\",\n        {\n          // pdf&ppt: red-500\n          \"bg-red-500/10 text-red-500\":\n            extension === \"pdf\" || extension === \"ppt\" || extension === \"pptx\",\n          // doc: blue-500\n          \"bg-blue-500/10 text-blue-500\":\n            extension === \"doc\" || extension === \"docx\",\n          // xls: green-500\n          \"bg-green-500/10 text-green-500\":\n            extension === \"xls\" || extension === \"xlsx\" || extension === \"csv\",\n          // json/xml/etc: orange-500\n          \"bg-orange-500/10 text-orange-500\":\n            extension === \"json\" ||\n            extension === \"xml\" ||\n            extension === \"jsonl\" ||\n            extension === \"yaml\" ||\n            extension === \"yml\" ||\n            extension === \"toml\" ||\n            extension === \"ini\" ||\n            extension === \"cfg\" ||\n            extension === \"conf\",\n          // code: violet-500\n          \"bg-violet-500/10 text-violet-500\":\n            extension === \"js\" ||\n            extension === \"ts\" ||\n            extension === \"py\" ||\n            extension === \"java\" ||\n            extension === \"cpp\" ||\n            extension === \"go\" ||\n            extension === \"c\" ||\n            extension === \"h\" ||\n            extension === \"rs\" ||\n            extension === \"swift\" ||\n            extension === \"kt\" ||\n            extension === \"ktm\" ||\n            extension === \"php\" ||\n            extension === \"rb\" ||\n            extension === \"sh\" ||\n            extension === \"html\" ||\n            extension === \"css\" ||\n            extension === \"scss\" ||\n            extension === \"less\" ||\n            extension === \"sass\" ||\n            extension === \"styl\" ||\n            extension === \"vue\" ||\n            extension === \"svelte\" ||\n            extension === \"astro\" ||\n            extension === \"tsx\" ||\n            extension === \"jsx\" ||\n            extension === \"ts\" ||\n            extension === \"jsx\",\n        },\n      )}\n    >\n      {extension.toUpperCase()}\n    </span>\n  );\n}\n\ntype FileListProps = {\n  value: FileArray;\n  removeFile: (index: number) => void;\n};\n\nfunction FileList({ value, removeFile }: FileListProps) {\n  if (value.length === 0) return null;\n\n  const listVariants = {\n    hidden: { opacity: 0, height: 0 },\n    visible: { opacity: 1, height: \"auto\" },\n  };\n\n  const itemVariants = {\n    hidden: { opacity: 0, y: 10 },\n    visible: { opacity: 1, y: 0 },\n  };\n\n  return (\n    <motion.div\n      className={`file-list grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 md:gap-2.5 justify-center`}\n      initial=\"hidden\"\n      animate=\"visible\"\n      variants={listVariants}\n    >\n      <AnimatePresence>\n        {value.map((file, index) => (\n          <motion.div\n            className={`relative h-fit pt-3 flex flex-col items-center justify-between bg-gradient-to-tr from-background to-muted/25 border hover:border-primary/40 cursor-pointer rounded-lg p-2 shadow-sm transition-all duration-200 ease-in-out group md:pt-4 md:p-3`}\n            key={index}\n            initial=\"hidden\"\n            animate=\"visible\"\n            exit=\"hidden\"\n            variants={itemVariants}\n            transition={{ delay: index * 0.1 }}\n          >\n            <div className=\"flex pt-1 flex-col items-center w-full h-fit\">\n              <FileIconObject name={file.name} />\n              <span\n                className={`mt-0.5 text-xs font-medium truncate max-w-[95%] text-center md:mt-1 md:text-sm`}\n              >\n                {file.name}\n              </span>\n            </div>\n            <div className=\"flex flex-col items-center\">\n              <span\n                className={`text-[10px] text-muted-foreground flex flex-row items-center mt-0.5 md:text-xs`}\n              >\n                <FileBadge name={file.name} />\n                {((file.size || file.content.length) / 1024).toFixed(2)}KB\n              </span>\n              <button\n                className=\"absolute group w-fit h-fit top-1 right-1 p-0.5 rounded-full hover:bg-secondary/10 transition-colors duration-200 md:top-2 md:right-2 md:p-1\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  removeFile(index);\n                }}\n              >\n                <X\n                  className={`h-3 w-3 text-secondary hover:text-destructive transition-colors duration-200 md:h-4 md:w-4`}\n                />\n              </button>\n            </div>\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\ntype FileInputProps = {\n  id: string;\n  loading: boolean;\n  className?: string;\n  handleEvent: (files: (File | null)[]) => void;\n};\n\nfunction FileInput({ id, loading, className, handleEvent }: FileInputProps) {\n  const { t } = useTranslation();\n  const ref = useRef(null);\n\n  useEffect(() => {\n    return useDraggableInput(window.document.body, handleEvent);\n  }, []);\n\n  return (\n    <>\n      <label className={`drop-window`} htmlFor={id} ref={ref}>\n        {loading && <Loader2 className={`h-4 w-4 animate-spin mr-2`} />}\n        <p>{t(\"file.drop\")}</p>\n\n        <div className=\"mt-6 flex flex-wrap justify-center gap-2\">\n          {[\n            { icon: FileIcon, text: \"Text\" },\n            { icon: FileVideo2Icon, text: \"PPT\" },\n            { icon: FileDigitIcon, text: \"Word\" },\n            { icon: FileTextIcon, text: \"PDF\" },\n            { icon: FileSpreadsheetIcon, text: \"Excel\" },\n            { icon: FileImageIcon, text: \"Image\" },\n            { icon: FileAudioIcon, text: \"Audio\" },\n            { icon: FileCodeIcon, text: \"Code\" },\n            { icon: FileJsonIcon, text: \"Data\" },\n          ].map((item, index) => (\n            <motion.div\n              key={index}\n              className=\"flex flex-col items-center\"\n              initial={{ opacity: 0, y: 10 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ delay: index * 0.1, duration: 0.3 }}\n            >\n              <div className=\"w-10 h-10 rounded-full bg-muted/50 p-2 hover:bg-muted/70 transition-colors duration-200\">\n                <item.icon className=\"w-full h-full text-secondary stroke-[1.5]\" />\n              </div>\n              <span className=\"mt-0.5 text-xs font-medium text-muted-foreground\">\n                {item.text}\n              </span>\n            </motion.div>\n          ))}\n        </div>\n      </label>\n      <input\n        id={id}\n        type=\"file\"\n        className={className}\n        onChange={(e) => handleEvent(Array.from(e.target?.files || []))}\n        accept=\"*\"\n        style={{ display: \"none\" }}\n        multiple={true}\n        // on transfer file\n        onPaste={(e) => {\n          const items = e.clipboardData.items;\n          const files = Array.from(items).filter(\n            (item) => item.kind === \"file\",\n          );\n          handleEvent(files.map((file) => file.getAsFile()));\n        }}\n      />\n    </>\n  );\n}\n\nexport default FileProvider;\n"
  },
  {
    "path": "app/src/components/FileViewer.tsx",
    "content": "import {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"./ui/dialog\";\nimport { useTranslation } from \"react-i18next\";\nimport React from \"react\";\nimport { Heading2, Paperclip, Text } from \"lucide-react\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport { CodeMarkdown } from \"@/components/Markdown.tsx\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group.tsx\";\n\ntype FileViewerProps = {\n  filename: string;\n  content: string;\n  children: React.ReactNode;\n  asChild?: boolean;\n};\n\nenum viewerType {\n  Text = \"text\",\n  Image = \"image\",\n}\n\nfunction FileViewer({ filename, content, children, asChild }: FileViewerProps) {\n  const { t } = useTranslation();\n\n  const [renderedType, setRenderedType] = React.useState(viewerType.Text);\n\n  return (\n    <Dialog>\n      <DialogTrigger asChild={asChild}>{children}</DialogTrigger>\n      <DialogContent className={`flex-dialog`}>\n        <DialogHeader>\n          <DialogTitle className={`flex flex-row items-center select-none`}>\n            <Paperclip className={`h-4 w-4 mr-2`} />\n            {filename ?? t(\"file.file\")}\n          </DialogTitle>\n        </DialogHeader>\n        <div className={`file-viewer-action`}>\n          <ToggleGroup variant={`outline`} type={`single`} value={renderedType}>\n            <ToggleGroupItem\n              value={viewerType.Text}\n              onClick={() => setRenderedType(viewerType.Text)}\n            >\n              <Text className={`h-4 w-4`} />\n            </ToggleGroupItem>\n            <ToggleGroupItem\n              value={viewerType.Image}\n              onClick={() => setRenderedType(viewerType.Image)}\n            >\n              <Heading2 className={`h-4 w-4`} />\n            </ToggleGroupItem>\n          </ToggleGroup>\n        </div>\n        <div className={`file-viewer-content`}>\n          {renderedType === viewerType.Text ? (\n            <Textarea\n              className={`file-viewer-textarea thin-scrollbar`}\n              value={content}\n              rows={15}\n              readOnly\n            />\n          ) : (\n            <div>\n              <CodeMarkdown\n                filename={filename}\n                codeStyle={`overflow-auto max-h-[60vh] thin-scrollbar`}\n              >\n                {content}\n              </CodeMarkdown>\n            </div>\n          )}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default FileViewer;\n"
  },
  {
    "path": "app/src/components/I18nProvider.tsx",
    "content": "import { Button } from \"./ui/button.tsx\";\nimport { Languages } from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"./ui/dropdown-menu.tsx\";\nimport { langsProps, setLanguage } from \"@/i18n.ts\";\nimport { useTranslation } from \"react-i18next\";\n\nfunction I18nProvider() {\n  const { i18n } = useTranslation();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"icon\">\n          <Languages className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        {Object.entries(langsProps).map(([key, value]) => (\n          <DropdownMenuCheckboxItem\n            key={key}\n            checked={i18n.language === key}\n            onClick={() => setLanguage(i18n, key)}\n          >\n            {value}\n          </DropdownMenuCheckboxItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport default I18nProvider;\n"
  },
  {
    "path": "app/src/components/Loader.tsx",
    "content": "import \"@/assets/common/loader.less\";\n\ntype LoaderProps = {\n  className?: string;\n  prompt?: string;\n};\n\nfunction Loader({ className, prompt }: LoaderProps) {\n  return (\n    <div className={`loader-wrapper ${className}`}>\n      <div className={`loader`} />\n      <p>{prompt}</p>\n    </div>\n  );\n}\n\nexport default Loader;\n"
  },
  {
    "path": "app/src/components/MCPResultDebug.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface MCPResultDebugProps {\n  toolCall: {\n    function: {\n      arguments: string;\n    };\n    result?: string;\n    error?: string;\n  };\n}\n\nexport function MCPResultDebug({ toolCall }: MCPResultDebugProps): JSX.Element {\n  const { t } = useTranslation();\n\n  const hasResult = !!toolCall.result;\n  const hasError = !!toolCall.error;\n\n  const defaultTab = hasResult ? \"result\" : hasError ? \"error\" : \"arguments\";\n\n  const formatContent = (content: string): string => {\n    try {\n      const parsed = JSON.parse(content);\n      return JSON.stringify(parsed, null, 2);\n    } catch {\n      return content;\n    }\n  };\n\n  const formattedArguments = formatContent(toolCall.function.arguments);\n  const formattedResult = toolCall.result ? formatContent(toolCall.result) : '';\n  const formattedError = toolCall.error ? formatContent(toolCall.error) : '';\n\n  const [contentHeights, setContentHeights] = useState<{\n    arguments: number;\n    result: number;\n    error: number;\n  }>({ arguments: 0, result: 0, error: 0 });\n\n  const argumentsRef = useRef<HTMLPreElement>(null);\n  const resultRef = useRef<HTMLPreElement>(null);\n  const errorRef = useRef<HTMLPreElement>(null);\n\n  useEffect(() => {\n    const measureHeight = () => {\n      const newHeights = {\n        arguments: argumentsRef.current?.scrollHeight || 0,\n        result: resultRef.current?.scrollHeight || 0,\n        error: errorRef.current?.scrollHeight || 0,\n      };\n      setContentHeights(newHeights);\n    };\n\n    const timeout = setTimeout(measureHeight, 100);\n    return () => clearTimeout(timeout);\n  }, [formattedArguments, formattedResult, formattedError]);\n\n  const SCROLL_THRESHOLD = 200;\n\n  const ContentWrapper = ({ \n    children, \n    shouldScroll, \n    className \n  }: { \n    children: React.ReactNode; \n    shouldScroll: boolean;\n    className?: string;\n  }) => {\n    if (shouldScroll) {\n      return (\n        <ScrollArea className={`mcp-debug-scroll-area ${className || ''}`}>\n          {children}\n        </ScrollArea>\n      );\n    }\n    return <div className={`mcp-debug-content ${className || ''}`}>{children}</div>;\n  };\n\n  return (\n    <div className=\"px-3 pb-3\">\n      <div className=\"debug-panel mt-4 border-t pt-4\">\n        <Tabs defaultValue={defaultTab} className=\"w-full\">\n          <TabsList className=\"grid w-full grid-cols-auto mcp-debug-tabs\">\n            <TabsTrigger value=\"arguments\" className=\"text-xs\">\n              {t(\"plugin.mcp.raw-arguments\")}\n            </TabsTrigger>\n            {hasResult && (\n              <TabsTrigger value=\"result\" className=\"text-xs\">\n                {t(\"plugin.mcp.result\")}\n              </TabsTrigger>\n            )}\n            {hasError && (\n              <TabsTrigger value=\"error\" className=\"text-xs\">\n                {t(\"plugin.mcp.error\")}\n              </TabsTrigger>\n            )}\n          </TabsList>\n          \n          <TabsContent value=\"arguments\" className=\"mt-3\">\n            <ContentWrapper shouldScroll={contentHeights.arguments > SCROLL_THRESHOLD}>\n              <pre \n                ref={argumentsRef}\n                className=\"text-xs bg-muted/50 rounded p-3 font-mono whitespace-pre-wrap break-words min-w-0\"\n              >\n                {formattedArguments}\n              </pre>\n            </ContentWrapper>\n          </TabsContent>\n          \n          {hasResult && (\n            <TabsContent value=\"result\" className=\"mt-3\">\n              <ContentWrapper shouldScroll={contentHeights.result > SCROLL_THRESHOLD}>\n                <pre \n                  ref={resultRef}\n                  className=\"text-xs bg-green-500/10 border border-green-500/20 rounded p-3 font-mono whitespace-pre-wrap break-words min-w-0\"\n                >\n                  {formattedResult}\n                </pre>\n              </ContentWrapper>\n            </TabsContent>\n          )}\n          \n          {hasError && (\n            <TabsContent value=\"error\" className=\"mt-3\">\n              <ContentWrapper shouldScroll={contentHeights.error > SCROLL_THRESHOLD}>\n                <pre \n                  ref={errorRef}\n                  className=\"text-xs bg-red-500/10 border border-red-500/20 rounded p-3 font-mono whitespace-pre-wrap break-words min-w-0\"\n                >\n                  {formattedError}\n                </pre>\n              </ContentWrapper>\n            </TabsContent>\n          )}\n        </Tabs>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/MCPResultPanel.tsx",
    "content": "import { \n  CheckCircle, \n  XCircle, \n  Loader2,\n  Copy,\n  Bug,\n  BugOff,\n  Edit,\n  Check\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { MCPResultDebug } from \"./MCPResultDebug\";\nimport { useClipboard } from \"@/utils/dom\";\n\ninterface ToolArgumentEditorProps {\n  paramKey: string;\n  paramValue: unknown;\n  onValueChange: (key: string, value: unknown) => void;\n}\n\nfunction ToolArgumentEditor({ \n  paramKey, \n  paramValue, \n  onValueChange \n}: ToolArgumentEditorProps): JSX.Element {\n  const { t } = useTranslation();\n  const copy = useClipboard();\n  const [isEditing, setIsEditing] = useState(false);\n  const [editValue, setEditValue] = useState(String(paramValue || ''));\n\n  const handleSave = () => {\n    try {\n      const parsedValue = editValue.startsWith('{') || editValue.startsWith('[') \n        ? JSON.parse(editValue) \n        : editValue;\n      onValueChange(paramKey, parsedValue);\n    } catch {\n      onValueChange(paramKey, editValue);\n    }\n    setIsEditing(false);\n  };\n\n  const handleCopy = async () => {\n    await copy(String(paramValue));\n  };\n\n  const displayValue = typeof paramValue === 'object' \n    ? JSON.stringify(paramValue, null, 2) \n    : String(paramValue);\n\n  return (\n    <div className=\"tool-param-item flex items-center gap-3 py-1.5 px-2 rounded hover:bg-muted/20 transition-colors\">\n      <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n        <span className=\"text-xs font-medium text-muted-foreground whitespace-nowrap\">{paramKey}:</span>\n        {isEditing ? (\n          <textarea\n            value={editValue}\n            onChange={(e) => setEditValue(e.target.value)}\n            className=\"flex-1 text-xs bg-background border rounded px-2 py-1 min-h-[24px] font-mono resize-none\"\n            onKeyDown={(e) => {\n              if (e.key === 'Escape') {\n                setIsEditing(false);\n              } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n                handleSave();\n              }\n            }}\n          />\n        ) : (\n          <div className=\"overflow-x-auto\">\n            <pre className=\"flex-1 text-xs text-foreground whitespace-pre-wrap font-mono bg-muted/30 rounded px-2 py-1 min-h-[24px] leading-relaxed break-words min-w-0\">\n              {displayValue}\n            </pre>\n          </div>\n        )}\n      </div>\n      <div className=\"flex items-center gap-1 shrink-0 self-center\">\n        <button\n          onClick={handleCopy}\n          className=\"p-0.5 text-muted-foreground hover:text-foreground transition-colors\"\n          title={t(\"plugin.mcp.copy-param-value\")}\n        >\n          <Copy className=\"h-3 w-3\" />\n        </button>\n        <button\n          onClick={() => {\n            if (isEditing) {\n              handleSave();\n            } else {\n              setEditValue(displayValue);\n              setIsEditing(true);\n            }\n          }}\n          className=\"p-0.5 text-muted-foreground hover:text-foreground transition-colors\"\n          title={isEditing ? t(\"plugin.mcp.save\") : t(\"plugin.mcp.edit\")}\n        >\n          {isEditing ? <Check className=\"h-3 w-3\" /> : <Edit className=\"h-3 w-3\" />}\n        </button>\n      </div>\n    </div>\n  );\n}\n\ninterface SingleToolCallPanelProps {\n  toolCall: {\n    index: number;\n    type: string;\n    id: string;\n    function: {\n      name: string;\n      arguments: string;\n    };\n    status?: \"start\" | \"executing\" | \"success\" | \"error\";\n    result?: string;\n    error?: string;\n  };\n  pluginName?: string;\n}\n\nexport function SingleToolCallPanel({ \n  toolCall, \n  pluginName = \"MCP\" \n}: SingleToolCallPanelProps): JSX.Element {\n  const { t } = useTranslation();\n  const [showDebug, setShowDebug] = useState(false);\n  \n  const getStatusIcon = () => {\n    switch (toolCall.status) {\n      case \"start\":\n      case \"executing\":\n        return <Loader2 className=\"h-4 w-4 animate-spin text-blue-500\" />;\n      case \"success\":\n        return <CheckCircle className=\"h-4 w-4 text-green-500\" />;\n      case \"error\":\n        return <XCircle className=\"h-4 w-4 text-red-500\" />;\n      default:\n        return <CheckCircle className=\"h-4 w-4 text-green-500\" />;\n    }\n  };\n\n  const getStatusDescription = () => {\n    switch (toolCall.status) {\n      case \"start\":\n        return t(\"plugin.mcp.status-prepare\");\n      case \"executing\":\n        return t(\"plugin.mcp.status-executing\");\n      case \"success\":\n        return t(\"plugin.mcp.status-success\");\n      case \"error\":\n        return t(\"plugin.mcp.status-error\");\n      default:\n        return t(\"plugin.mcp.status-success\");\n    }\n  };\n\n  const argumentsObj = (() => {\n    try {\n      return JSON.parse(toolCall.function.arguments);\n    } catch {\n      return { value: toolCall.function.arguments };\n    }\n  })();\n\n  return (\n    <div className=\"single-tool-call border rounded-md mb-3 bg-muted/20\">\n      <div className=\"flex items-center justify-between gap-2 px-3 py-2 border-b\">\n        <div className=\"flex items-center gap-2\">\n          <div className=\"flex flex-col items-start\">\n            <span className=\"text-sm font-medium\">{pluginName} / {toolCall.function.name}</span>\n            <span className=\"text-xs text-muted-foreground flex items-center gap-1\">\n              {getStatusIcon()}\n              {getStatusDescription()}\n            </span>\n          </div>\n        </div>\n        \n        <button\n          onClick={() => setShowDebug(!showDebug)}\n          className=\"px-2 py-1 text-xs bg-muted hover:bg-muted/80 border rounded transition-colors flex items-center gap-1\"\n          title={showDebug ? t(\"plugin.mcp.hide-debug\") : t(\"plugin.mcp.show-debug\")}\n        >\n          {showDebug ? <BugOff className=\"h-3 w-3\" /> : <Bug className=\"h-3 w-3\" />}\n          DEBUG\n        </button>\n      </div>\n\n      <div className=\"px-3 py-3\">\n        <div className=\"text-sm font-medium text-muted-foreground mb-2\">{t(\"plugin.mcp.tool-arguments\")}</div>\n        {Object.keys(argumentsObj).length > 0 ? (\n          <div className=\"border rounded-md bg-muted/30 divide-y divide-border/50\">\n            {Object.entries(argumentsObj).map(([key, value]) => (\n              <ToolArgumentEditor\n                key={key}\n                paramKey={key}\n                paramValue={value}\n                onValueChange={() => {}}\n              />\n            ))}\n          </div>\n        ) : (\n          <div className=\"text-sm text-muted-foreground bg-muted/50 rounded p-3\">\n            {t(\"plugin.mcp.no-arguments-needed\")}\n          </div>\n        )}\n      </div>\n\n      {showDebug && (\n        <MCPResultDebug toolCall={toolCall} />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/Markdown.tsx",
    "content": "import ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\nimport remarkBreaks from \"remark-breaks\";\nimport rehypeKatex from \"rehype-katex\";\nimport rehypeRaw from \"rehype-raw\";\nimport \"@/assets/markdown/all.less\";\nimport { useEffect, useMemo } from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport Label from \"@/components/markdown/Label.tsx\";\nimport Link from \"@/components/markdown/Link.tsx\";\nimport Code, { CodeProps } from \"@/components/markdown/Code.tsx\";\nimport Image from \"@/components/markdown/Image.tsx\";\nimport Video from \"@/components/markdown/Video.tsx\";\n\ntype MarkdownProps = {\n  children: string;\n  className?: string;\n  acceptHtml?: boolean;\n  codeStyle?: string;\n  loading?: boolean;\n};\n\nfunction MarkdownContent({\n  children,\n  className,\n  acceptHtml,\n  codeStyle,\n  loading,\n}: MarkdownProps) {\n  useEffect(() => {\n    document.querySelectorAll(\".file-instance\").forEach((el) => {\n      const parent = el.parentElement as HTMLElement;\n      if (!parent.classList.contains(\"file-block\"))\n        parent.classList.add(\"file-block\");\n    });\n  }, [children]);\n\n  const rehypePlugins = useMemo(() => {\n    const plugins = [rehypeKatex as any];\n    return acceptHtml ? [...plugins, rehypeRaw] : plugins;\n  }, [acceptHtml]);\n\n  const components = useMemo(() => {\n    return {\n      p: Label,\n      a: Link,\n      img: (props: React.ImgHTMLAttributes<HTMLImageElement>) => {\n        if (props.alt === \"video\") {\n          return (\n            <Video\n              src={props.src || \"\"}\n              alt={props.alt}\n              className={props.className}\n            />\n          );\n        }\n        return <Image {...props} />;\n      },\n      code: (props: CodeProps) => (\n        <Code {...props} loading={loading} codeStyle={codeStyle} />\n      ),\n    };\n  }, [codeStyle]);\n\n  return (\n    <ReactMarkdown\n      remarkPlugins={[remarkMath, remarkGfm, remarkBreaks]}\n      rehypePlugins={rehypePlugins}\n      className={cn(\"markdown-body\", className)}\n      children={children}\n      skipHtml={acceptHtml}\n      components={components}\n    />\n  );\n}\n\nfunction Markdown({\n  children,\n  acceptHtml,\n  codeStyle,\n  className,\n  loading,\n}: MarkdownProps) {\n  // memoize the component\n  return useMemo(\n    () => (\n      <MarkdownContent\n        children={children}\n        acceptHtml={acceptHtml}\n        codeStyle={codeStyle}\n        className={className}\n        loading={loading}\n      />\n    ),\n    [children, acceptHtml, codeStyle, className, loading],\n  );\n}\n\ntype CodeMarkdownProps = MarkdownProps & {\n  filename: string;\n  language?: string;\n};\n\nexport function CodeMarkdown({\n  filename,\n  language,\n  ...props\n}: CodeMarkdownProps) {\n  const suffix =\n    language ?? (filename.includes(\".\") ? filename.split(\".\").pop() : \"\");\n  const children = useMemo(() => {\n    const content = props.children.toString();\n\n    return `\\`\\`\\`${suffix}\\n${content}\\n\\`\\`\\``;\n  }, [props.children]);\n\n  return <Markdown {...props}>{children}</Markdown>;\n}\n\nexport default Markdown;\n"
  },
  {
    "path": "app/src/components/Message.tsx",
    "content": "import { Message, UserRole } from \"@/api/types.tsx\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport {\n  CalendarCheck2,\n  CircleSlash,\n  Cloud,\n  CloudCog,\n  Copy,\n  File,\n  Loader2,\n  SquareMousePointer,\n  PencilLine,\n  Power,\n  RotateCcw,\n  Trash,\n} from \"lucide-react\";\nimport { filterMessage } from \"@/utils/processor.ts\";\nimport {\n  copyClipboard,\n  isContainDom,\n  saveAsFile,\n} from \"@/utils/dom.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport React, { Ref, useRef, useState } from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport EditorProvider from \"@/components/EditorProvider.tsx\";\nimport Avatar from \"@/components/Avatar.tsx\";\nimport { useSelector } from \"react-redux\";\nimport { selectUsername } from \"@/store/auth.ts\";\nimport { appLogo } from \"@/conf/env.ts\";\nimport { motion } from \"framer-motion\";\nimport { ThinkContent } from \"@/components/ThinkContent\";\n\ntype MessageProps = {\n  index: number;\n  message: Message;\n  end?: boolean;\n  username?: string;\n  onEvent?: (event: string, index?: number, message?: string) => void;\n  ref?: Ref<HTMLElement>;\n  sharing?: boolean;\n\n  selected?: boolean;\n  onFocus?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;\n  onFocusLeave?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;\n};\n\nfunction MessageSegment(props: MessageProps) {\n  const ref = useRef(null);\n  const { message } = props;\n\n  return (\n    <div\n      className={`message ${message.role}`}\n      ref={ref}\n      onClick={props.onFocus}\n      onMouseEnter={props.onFocus}\n      onMouseLeave={(event) => {\n        try {\n          if (isContainDom(ref.current, event.relatedTarget as HTMLElement))\n            return;\n          props.onFocusLeave && props.onFocusLeave(event);\n        } catch (e) {\n          console.debug(`[message] cannot leave focus: ${e}`);\n        }\n      }}\n    >\n      <MessageContent {...props} />\n      <MessageQuota message={message} />\n    </div>\n  );\n}\n\ntype MessageQuotaProps = {\n  message: Message;\n};\n\nfunction MessageQuota({ message }: MessageQuotaProps) {\n  const [detail, setDetail] = useState(false);\n\n  if (message.role === UserRole) return null;\n\n  return (\n    message.quota &&\n    message.quota !== 0 && (\n      <motion.div\n        className={cn(\"message-quota\", message.plan && \"subscription\")}\n        onClick={() => setDetail(!detail)}\n        initial={{ opacity: 0, scale: 0.8 }}\n        animate={{ opacity: 1, scale: 1 }}\n        transition={{ duration: 0.3, ease: \"easeOut\" }}\n        whileHover={{ scale: 1.05 }}\n        whileTap={{ scale: 0.95 }}\n      >\n        <motion.div\n          initial={{ rotate: 0 }}\n          animate={{ rotate: detail ? 360 : 0 }}\n          transition={{ duration: 0.5, ease: \"easeInOut\" }}\n        >\n          {message.plan ? (\n            <CalendarCheck2 className={`h-4 w-4 icon`} />\n          ) : detail ? (\n            <CloudCog className={`h-4 w-4 icon`} />\n          ) : (\n            <Cloud className={`h-4 w-4 icon`} />\n          )}\n        </motion.div>\n        <motion.span\n          className={`quota`}\n          initial={{ y: 10, opacity: 0 }}\n          animate={{ y: 0, opacity: 1 }}\n          transition={{ delay: 0.2, duration: 0.3 }}\n        >\n          {(message.quota < 0 ? 0 : message.quota).toFixed(detail ? 6 : 2)}\n        </motion.span>\n      </motion.div>\n    )\n  );\n}\n\ntype MessageMenuProps = {\n  children?: React.ReactNode;\n  message: Message;\n  end?: boolean;\n  index: number;\n  onEvent?: (event: string, index?: number, message?: string) => void;\n  editedMessage?: string;\n  setEditedMessage: (message: string) => void;\n  setOpen: (open: boolean) => void;\n  align?: \"start\" | \"end\";\n};\n\nfunction MessageMenu({\n  children,\n  align,\n  message,\n  end,\n  index,\n  onEvent,\n  editedMessage,\n  setEditedMessage,\n  setOpen,\n}: MessageMenuProps) {\n  const { t } = useTranslation();\n  const isAssistant = message.role === \"assistant\";\n  const notInOutput = message.end !== false;\n  const disableDelete = isAssistant && end && !notInOutput;\n  const [dropdown, setDropdown] = useState(false);\n\n  return (\n    <DropdownMenu open={dropdown} onOpenChange={setDropdown}>\n      <DropdownMenuTrigger className={cn(`flex flex-row outline-none`)}>\n        {children}\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align={align}>\n        {isAssistant && end ? (\n          <DropdownMenuItem\n            onClick={() => {\n              onEvent && onEvent(message.end !== false ? \"restart\" : \"stop\");\n              setDropdown(false);\n            }}\n          >\n            {notInOutput ? (\n              <>\n                <RotateCcw className={`h-4 w-4 mr-1.5`} />\n                {t(\"message.restart\")}\n              </>\n            ) : (\n              <>\n                <Power className={`h-4 w-4 mr-1.5`} />\n                {t(\"message.stop\")}\n              </>\n            )}\n          </DropdownMenuItem>\n        ) : (\n          notInOutput && (\n            <DropdownMenuItem\n              onClick={() => {\n                onEvent && onEvent(\"restart\");\n                setDropdown(false);\n              }}\n            >\n              <RotateCcw className={`h-4 w-4 mr-1.5`} />\n              {t(\"message.restart\")}\n            </DropdownMenuItem>\n          )\n        )}\n        <DropdownMenuItem\n          onClick={() => copyClipboard(filterMessage(message.content))}\n        >\n          <Copy className={`h-4 w-4 mr-1.5`} />\n          {t(\"message.copy\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          onClick={() => {\n            const input = document.getElementById(\"input\") as HTMLInputElement;\n            if (input) {\n              input.value = filterMessage(message.content);\n              input.focus();\n            }\n          }}\n        >\n          <SquareMousePointer className={`h-4 w-4 mr-1.5`} />\n          {t(\"message.use\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          disabled={disableDelete}\n          onClick={() => {\n            editedMessage?.length === 0 && setEditedMessage(message.content);\n            setOpen(true);\n          }}\n        >\n          <PencilLine className={`h-4 w-4 mr-1.5`} />\n          {t(\"message.edit\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          disabled={disableDelete}\n          onClick={() => onEvent && onEvent(\"remove\", index)}\n        >\n          <Trash className={`h-4 w-4 mr-1.5`} />\n          {t(\"message.remove\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          onClick={() =>\n            saveAsFile(\n              `message-${message.role}.txt`,\n              filterMessage(message.content),\n            )\n          }\n        >\n          <File className={`h-4 w-4 mr-1.5`} />\n          {t(\"message.save\")}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction MessageContent({\n  message,\n  end,\n  index,\n  onEvent,\n  selected,\n  username,\n}: MessageProps) {\n  const isUser = message.role === \"user\";\n  const hasContent = message.content.length > 0;\n  const isAssistant = message.role === \"assistant\";\n  const isOutput = message.end === false;\n  const user = useSelector(selectUsername);\n\n  const [open, setOpen] = useState(false);\n  const [editedMessage, setEditedMessage] = useState<string | undefined>(\"\");\n\n  // parse think content\n  const parseThinkContent = (content: string) => {\n    // check if there is a start tag\n\n    const startMatch = content.match(/<think>\\n?(.*?)(?:<\\/think>|$)/s);\n    if (startMatch) {\n      const thinkContent = startMatch[1];\n      // if there is an end tag, remove the whole matching part;\n      // if not, keep the remaining content\n      const hasEndTag = content.includes('</think>');\n      const restContent = hasEndTag ? \n        content.replace(startMatch[0], \"\").trim() :\n        content.substring(content.indexOf('<think>') + 7).trim();\n      \n      return {\n        thinkContent,\n        restContent: hasEndTag ? restContent : '',\n        isComplete: hasEndTag\n      };\n    }\n    return null;\n  };\n\n  const parsedContent = message.content.length ? parseThinkContent(message.content) : null;\n\n  return (\n    <div className={\"content-wrapper\"}>\n      <EditorProvider\n        submittable={true}\n        onSubmit={(value) => onEvent && onEvent(\"edit\", index, value)}\n        open={open}\n        setOpen={setOpen}\n        value={editedMessage ?? \"\"}\n        onChange={setEditedMessage}\n      />\n      <div className={`message-avatar-wrapper`}>\n        {!selected ? (\n          isUser ? (\n            <Avatar\n              className={`message-avatar animate-fade-in`}\n              username={username ?? user}\n            />\n          ) : (\n            <img\n              src={appLogo}\n              alt={``}\n              className={`message-avatar animate-fade-in`}\n            />\n          )\n        ) : (\n          <MessageMenu\n            message={message}\n            end={end}\n            index={index}\n            onEvent={onEvent}\n            editedMessage={editedMessage}\n            setEditedMessage={setEditedMessage}\n            setOpen={setOpen}\n            align={isUser ? \"end\" : \"start\"}\n          >\n            <div\n              className={`message-avatar flex flex-row items-center justify-center cursor-pointer select-none opacity-0 animate-fade-in`}\n            >\n              <PencilLine className={`h-4 w-4`} />\n            </div>\n          </MessageMenu>\n        )}\n      </div>\n      <div\n        className={`relative message-content dark:bg-muted/40 border dark:border-transparent hover:border-border`}\n      >\n        {hasContent ? (\n          <>\n            {parsedContent ? (\n              <>\n                <ThinkContent \n                  content={parsedContent.thinkContent} \n                  isComplete={parsedContent.isComplete}\n                />\n                {parsedContent.restContent && (\n                  <Markdown\n                    loading={message.end === false}\n                    children={message.content}\n                    acceptHtml={false}\n                  />\n                )}\n              </>\n            ) : (\n              <Markdown\n                loading={message.end === false}\n                children={message.content}\n                acceptHtml={false}\n              />\n            )}\n          </>\n        ) : message.end === true ? (\n          <CircleSlash className={`h-5 w-5 m-1`} />\n        ) : (\n          <Loader2 className={`h-5 w-5 m-1 animate-spin`} />\n        )}\n\n        {isAssistant && hasContent && isOutput && (\n          <Loader2\n            className={`absolute right-0 bottom-0 h-3.5 w-3.5 m-1 animate-spin`}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default MessageSegment;\n"
  },
  {
    "path": "app/src/components/ModelAvatar.tsx",
    "content": "import { isUrl } from \"@/utils/base.ts\";\nimport { Model } from \"@/api/types.tsx\";\n\nimport {\n  Claude,\n  Gemini,\n  Gemma,\n  OpenAI,\n  Spark,\n  Qwen,\n  Baichuan,\n  ByteDance,\n  Meta,\n  Copilot,\n  Hunyuan,\n  Midjourney,\n  Stability,\n  Moonshot,\n  LLaVA,\n  DeepSeek,\n  Grok,\n  Minimax,\n  Mistral,\n  Dalle,\n  Rwkv,\n  Cloudflare,\n  Cohere,\n  Fireworks,\n  Groq,\n  OpenRouter,\n  Perplexity,\n  GithubCopilot,\n  Suno,\n  Qingyan,\n  IconAvatarProps,\n  Azure,\n  Coze,\n  Dify\n} from \"@lobehub/icons\";\nimport React from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\ntype ModelAvatarProps = {\n  model: Model | { id: string; name: string; avatar?: string };\n  className?: string;\n  size?: number;\n};\n\nconst builtinAvatars: Record<string, React.ExoticComponent<IconAvatarProps>> = {\n  openai: OpenAI.Avatar,\n  \"gpt-3.5\": OpenAI.Avatar,\n  \"gpt-4\": OpenAI.Avatar,\n  dalle: Dalle.Avatar,\n  \"dall-e\": Dalle.Avatar,\n\n  azure: Azure.Avatar,\n\n  claude: Claude.Avatar,\n  anthropic: Claude.Avatar,\n\n  gemini: Gemini.Avatar,\n  palm: Gemma.Avatar,\n  gemma: Gemma.Avatar,\n  \"chat-bison\": Gemma.Avatar, // \"chat-bision\" is a typo, but we need to keep it for compatibility\n  google: Gemini.Avatar,\n\n  glm: Qingyan.Avatar,\n  zhipu: Qingyan.Avatar,\n\n  spark: Spark.Avatar,\n\n  tongyi: Qwen.Avatar,\n  qwen: Qwen.Avatar,\n\n  baichuan: Baichuan.Avatar,\n\n  byte: ByteDance.Avatar,\n  bytedance: ByteDance.Avatar,\n  skylark: ByteDance.Avatar,\n\n  meta: Meta.Avatar,\n  llama: Meta.Avatar,\n\n  bing: Copilot.Avatar,\n\n  hunyuan: Hunyuan.Avatar,\n\n  midjourney: Midjourney.Avatar,\n\n  stability: Stability.Avatar,\n  \"stable-diffusion\": Stability.Avatar,\n  stablediffusion: Stability.Avatar,\n  sd: Stability.Avatar,\n\n  moonshot: Moonshot.Avatar,\n  kimi: Moonshot.Avatar,\n\n  llava: LLaVA.Avatar,\n\n  deepseek: DeepSeek.Avatar,\n  \"deep-seek\": DeepSeek.Avatar,\n\n  coze: Coze.Avatar,\n\n  dify: Dify.Avatar,\n\n  grok: Grok.Avatar,\n  minimax: Minimax.Avatar,\n  abab: Minimax.Avatar,\n  mistral: Mistral.Avatar,\n\n  rwkv: Rwkv.Avatar,\n\n  cf: Cloudflare.Combine,\n  cloudflare: Cloudflare.Combine,\n\n  command: Cohere.Avatar,\n  cohere: Cohere.Avatar,\n\n  firework: Fireworks.Avatar,\n\n  groq: Groq.Avatar,\n\n  router: OpenRouter.Avatar,\n\n  perplexity: Perplexity.Avatar,\n\n  copilot: GithubCopilot.Avatar,\n\n  suno: Suno.Avatar,\n};\n\nfunction getAvatarType(id: string): string | undefined {\n  if (id.includes(\"gpt-3.5\")) return \"gpt3\";\n  if (id.includes(\"gpt-4\") || id.includes(\"o1\")) return \"gpt4\";\n}\n\nfunction ModelAvatar({ model, className, size }: ModelAvatarProps) {\n  const avatarSize = size ?? 42;\n  \n  if (isUrl(model.avatar ?? \"\")) {\n    return (\n      <div \n        style={{\n          width: avatarSize,\n          height: avatarSize,\n          minWidth: avatarSize,\n          minHeight: avatarSize\n        }}\n        className={cn(\n          \"relative flex items-center justify-center overflow-hidden\",\n          // using scale to make the avatar smaller\n          className?.includes(\"h-4\") && \"scale-[0.85]\",\n          className\n        )}\n      >\n        <img\n          src={model.avatar}\n          alt={model.name}\n          className=\"rounded-full object-cover w-full h-full\"\n          style={{\n            transform: className?.includes(\"h-4\") ? \"scale(1.15)\" : \"none\"\n          }}\n        />\n      </div>\n    );\n  }\n\n  // if key is include, return value (reactelement)\n  const id = model.id.toLowerCase();\n  const key = Object.keys(builtinAvatars).find((key) => id.includes(key));\n  const Avatar = key ? builtinAvatars[key] : OpenAI.Avatar;\n\n  return (\n    <Avatar\n      size={avatarSize}\n      className={className}\n      // @ts-ignore\n      type={getAvatarType(id)}\n    />\n  );\n}\n\nexport default ModelAvatar;\n\nexport type ChannelTypeAvatarProps = {\n  type: string;\n  size?: number;\n  className?: string;\n};\n\nexport function ChannelTypeAvatar({\n  type,\n  size,\n  className,\n}: ChannelTypeAvatarProps) {\n  const key = Object.keys(builtinAvatars).find((key) => type.includes(key));\n  const Avatar = key ? builtinAvatars[key] : OpenAI.Avatar;\n\n  return <Avatar size={size ?? 42} className={className} />;\n}\n"
  },
  {
    "path": "app/src/components/OperationAction.tsx",
    "content": "import React from \"react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip.tsx\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover.tsx\";\nimport { Button, type ButtonProps } from \"@/components/ui/button.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\ntype ActionProps = ButtonProps & {\n  tooltip?: string;\n  children: React.ReactNode;\n  onClick?: () => any;\n  native?: boolean;\n  variant?:\n    | \"secondary\"\n    | \"default\"\n    | \"destructive\"\n    | \"outline\"\n    | \"ghost\"\n    | \"link\"\n    | null\n    | undefined;\n};\nfunction OperationAction({\n  tooltip,\n  children,\n  onClick,\n  variant,\n  native,\n  className,\n  ...props\n}: ActionProps) {\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          {variant === \"destructive\" ? (\n            <Popover>\n              <PopoverTrigger asChild>\n                <Button\n                  size={`icon`}\n                  className={cn(!native && `w-8 h-8`, className)}\n                  variant={variant}\n                  {...props}\n                >\n                  {children}\n                </Button>\n              </PopoverTrigger>\n              <PopoverContent className={`w-max`}>\n                <Button\n                  className={`flex flex-row items-center mx-1`}\n                  onClick={onClick}\n                  variant={variant}\n                >\n                  {children}\n                  <p className={`ml-1 translate-y-[-1px]`}>{tooltip}</p>\n                </Button>\n              </PopoverContent>\n            </Popover>\n          ) : (\n            <Button\n              size={`icon`}\n              className={cn(!native && `w-8 h-8`, className)}\n              onClick={onClick}\n              variant={variant}\n              {...props}\n            >\n              {children}\n            </Button>\n          )}\n        </TooltipTrigger>\n        <TooltipContent>{tooltip}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n\nexport default OperationAction;\n"
  },
  {
    "path": "app/src/components/Paragraph.tsx",
    "content": "import React from \"react\";\nimport { Info } from \"lucide-react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\n\nexport type ParagraphProps = {\n  isPro?: boolean;\n  title?: string;\n  children: React.ReactNode;\n  className?: string;\n  configParagraph?: boolean;\n  isCollapsed?: boolean;\n};\n\nfunction Paragraph({\n  title,\n  children,\n  className,\n  configParagraph,\n  isCollapsed,\n  isPro,\n}: ParagraphProps) {\n  return (\n    <Accordion type={`single`} collapsible={isCollapsed} defaultValue={\"item\"}>\n      <AccordionItem\n        value={`item`}\n        className={cn(\n          `paragraph`,\n          configParagraph && `config-paragraph`,\n          className,\n        )}\n      >\n        <AccordionTrigger className={`paragraph-header`}>\n          <div className={`paragraph-title flex flex-row items-center`}>\n            {title ?? \"\"}\n            {isPro && (\n              <Badge className={`ml-2`} variant={`gold`}>\n                Pro\n              </Badge>\n            )}\n          </div>\n        </AccordionTrigger>\n        <AccordionContent className={`paragraph-content mt-2`}>\n          {children}\n        </AccordionContent>\n      </AccordionItem>\n    </Accordion>\n  );\n}\n\nfunction ParagraphItem({\n  children,\n  className,\n  rowLayout,\n}: {\n  children: React.ReactNode;\n  className?: string;\n  rowLayout?: boolean;\n}) {\n  return (\n    <div className={cn(\"paragraph-item\", className, rowLayout && \"row-layout\")}>\n      {children}\n    </div>\n  );\n}\n\ntype ParagraphDescriptionProps = {\n  children: string;\n  border?: boolean;\n  hideIcon?: boolean;\n  className?: string;\n  classNameMarkdown?: string;\n};\n\nexport function ParagraphDescription({\n  children,\n  border,\n  hideIcon,\n  className,\n  classNameMarkdown,\n}: ParagraphDescriptionProps) {\n  return (\n    <div\n      className={cn(\n        \"paragraph-description\",\n        border && `px-3 py-2 border rounded-lg`,\n        className,\n      )}\n    >\n      {!hideIcon && <Info size={16} />}\n      <Markdown\n        children={children}\n        className={cn(\"leading-6\", classNameMarkdown)}\n      />\n    </div>\n  );\n}\n\nexport function ParagraphSpace() {\n  return <div className={`paragraph-space`} />;\n}\n\nfunction ParagraphFooter({ children }: { children: React.ReactNode }) {\n  return <div className={`paragraph-footer`}>{children}</div>;\n}\n\nexport default Paragraph;\nexport { ParagraphItem, ParagraphFooter };\n"
  },
  {
    "path": "app/src/components/PopupDialog.tsx",
    "content": "import {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { Alert, AlertDescription } from \"./ui/alert\";\nimport { AlertCircle } from \"lucide-react\";\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { Calendar } from \"@/components/ui/calendar.tsx\";\nimport { Separator } from \"@/components/ui/separator.tsx\";\nimport { Combobox } from \"@/components/ui/combo-box.tsx\";\nimport { MultiCombobox } from \"./ui/multi-combobox\";\n\nexport enum popupTypes {\n  Text = \"text\",\n  Number = \"number\",\n  Switch = \"switch\",\n  Clock = \"clock\",\n  List = \"list\",\n  MultiList = \"multi-list\",\n  Empty = \"empty\",\n}\n\ntype ParamProps = {\n  dataList?: string[];\n  dataListTranslated?: string;\n};\n\nexport type PopupDialogProps = {\n  title: string;\n  description?: string;\n  name?: string;\n  placeholder?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => string;\n  params?: ParamProps;\n  onSubmit?: (value: string) => Promise<boolean>;\n  destructive?: boolean;\n  disabled?: boolean;\n\n  type?: popupTypes;\n\n  open: boolean;\n  setOpen: (open: boolean) => void;\n\n  cancelLabel?: string;\n  confirmLabel?: string;\n\n  componentProps?: any;\n  alert?: string;\n};\n\ntype PopupFieldProps = PopupDialogProps & {\n  value: string;\n  setValue: (value: string) => void;\n};\n\nfunction PopupField({\n  type,\n  setValue,\n  onValueChange,\n  params,\n  value,\n  placeholder,\n  componentProps,\n}: PopupFieldProps) {\n  switch (type) {\n    case popupTypes.Text:\n      return (\n        <Input\n          onChange={(e) => {\n            setValue(\n              onValueChange ? onValueChange(e.target.value) : e.target.value,\n            );\n          }}\n          value={value}\n          placeholder={placeholder}\n          {...componentProps}\n        />\n      );\n\n    case popupTypes.Clock:\n      return <CalendarComp value={value} onValueChange={(v) => setValue(v)} />;\n\n    case popupTypes.List:\n      return (\n        <Combobox\n          value={value}\n          onChange={(v) => setValue(v)}\n          list={params?.dataList || []}\n          listTranslated={params?.dataListTranslated || \"\"}\n        />\n      );\n\n    case popupTypes.MultiList:\n      return (\n        <MultiCombobox\n          value={(value || \"\").split(\",\")}\n          onChange={(v) => setValue(v.filter((i) => i.trim()).join(\",\"))}\n          list={params?.dataList || []}\n          listTranslate={params?.dataListTranslated || \"\"}\n          placeholder={placeholder || value || \"\"}\n        />\n      );\n\n    case popupTypes.Number:\n      return (\n        <NumberInput\n          type={`number`}\n          value={parseFloat(value)}\n          onValueChange={(v) => setValue(v.toString())}\n          placeholder={placeholder}\n          {...componentProps}\n        />\n      );\n\n    case popupTypes.Switch:\n      return (\n        <Switch\n          checked={value === \"true\"}\n          onCheckedChange={(state: boolean) => {\n            setValue(state.toString());\n          }}\n          {...componentProps}\n        />\n      );\n\n    case popupTypes.Empty:\n      return null;\n\n    default:\n      return null;\n  }\n}\n\nfunction fixedZero(val: number) {\n  return val < 10 ? `0${val}` : val.toString();\n}\n\nfunction CalendarComp(props: {\n  value: string;\n  onValueChange: (v: string) => void;\n}) {\n  const { value, onValueChange } = props;\n  const { t } = useTranslation();\n\n  const convertedDate = useMemo(() => {\n    const date = new Date(value.split(\" \")[0] || \"1970-01-01\");\n    console.log(`[calendar] converted date:`, date);\n    return date;\n  }, [value]);\n\n  const onDateChange = (date: Date, overrideTime?: boolean) => {\n    const v = `${date.getFullYear()}-${fixedZero(\n      date.getMonth() + 1,\n    )}-${fixedZero(date.getDate())}`;\n    const t = !overrideTime\n      ? value.split(\" \")[1] || \"00:00:00\"\n      : `${fixedZero(date.getHours())}:${fixedZero(\n          date.getMinutes(),\n        )}:${fixedZero(date.getSeconds())}`;\n\n    console.log(`[calendar] clicked date: [${v} ${t}]`);\n    onValueChange(`${v} ${t}`);\n  };\n\n  const [month, setMonth] = useState(convertedDate);\n  useEffect(() => {\n    setMonth(convertedDate);\n  }, [convertedDate]);\n\n  return (\n    <div\n      className={`flex flex-col gap-2 items-center justify-center px-2 w-full h-fit`}\n    >\n      <Calendar\n        className={`scale-90 md:scale-100`}\n        mode=\"single\"\n        month={month}\n        onMonthChange={(date) => date && setMonth(date)}\n        selected={convertedDate}\n        onSelect={(date) => date && onDateChange(date)}\n      />\n      <Input\n        value={value}\n        onChange={(e) => onValueChange(e.target.value)}\n        placeholder={t(\"date.pick\")}\n        className={`w-full text-center`}\n      />\n      <Separator />\n      <div className={`flex flex-row w-full flex-wrap`}>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() => onDateChange(new Date(\"1970-01-01 00:00:00\"), true)}\n        >\n          {t(\"date.clean\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() => onDateChange(new Date(), true)}\n        >\n          {t(\"date.today\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(convertedDate.setDate(convertedDate.getDate() + 1)),\n            )\n          }\n        >\n          {t(\"date.add-day\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(convertedDate.setDate(convertedDate.getDate() - 1)),\n            )\n          }\n        >\n          {t(\"date.sub-day\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(convertedDate.setMonth(convertedDate.getMonth() + 1)),\n            )\n          }\n        >\n          {t(\"date.add-month\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(convertedDate.setMonth(convertedDate.getMonth() - 1)),\n            )\n          }\n        >\n          {t(\"date.sub-month\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(\n                convertedDate.setFullYear(convertedDate.getFullYear() + 1),\n              ),\n            )\n          }\n        >\n          {t(\"date.add-year\")}\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`m-0.5 shrink-0`}\n          onClick={() =>\n            onDateChange(\n              new Date(\n                convertedDate.setFullYear(convertedDate.getFullYear() - 1),\n              ),\n            )\n          }\n        >\n          {t(\"date.sub-year\")}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction PopupDialog(props: PopupDialogProps) {\n  const {\n    title,\n    description,\n    name,\n    type,\n    defaultValue,\n    onSubmit,\n    open,\n    setOpen,\n    cancelLabel,\n    confirmLabel,\n    destructive,\n    disabled,\n    alert,\n  } = props;\n\n  const { t } = useTranslation();\n  const [value, setValue] = useState<string>(defaultValue || \"\");\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          <DialogDescription className={`pt-1.5`}>\n            {description}\n          </DialogDescription>\n        </DialogHeader>\n        {!(type === popupTypes.Empty) && (\n          <div className={`pt-1 flex flex-row items-center justify-center`}>\n            <span className={`mr-4 whitespace-nowrap`}>{name}</span>\n            <PopupField {...props} value={value} setValue={setValue} />\n          </div>\n        )}\n        {alert && (\n          <Alert className={`pb-3 select-none text-secondary`}>\n            <AlertCircle className=\"text-secondary mt-[1px] h-4 w-4\" />\n            <AlertDescription className={`mt-[1px]`}>{alert}</AlertDescription>\n          </Alert>\n        )}\n        <DialogFooter>\n          <Button\n            unClickable\n            variant={`outline`}\n            onClick={() => setOpen(false)}\n          >\n            {cancelLabel || t(\"cancel\")}\n          </Button>\n          <Button\n            unClickable\n            disabled={disabled}\n            variant={destructive ? `destructive` : `default`}\n            loading={true}\n            onClick={async () => {\n              if (!onSubmit) return;\n\n              const status: boolean = await onSubmit(value);\n              if (status) {\n                setOpen(false);\n                setValue(defaultValue || \"\");\n              }\n            }}\n          >\n            {confirmLabel || t(\"confirm\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ntype PopupAlertDialogProps = {\n  title: string;\n  description?: string;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  cancelLabel?: string;\n  confirmLabel?: string;\n  destructive?: boolean;\n  disabled?: boolean;\n  onSubmit?: () => Promise<boolean>;\n};\n\nexport function PopupAlertDialog({\n  title,\n  description,\n  open,\n  setOpen,\n  cancelLabel,\n  confirmLabel,\n  destructive,\n  disabled,\n  onSubmit,\n}: PopupAlertDialogProps) {\n  const { t } = useTranslation();\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          {description && (\n            <AlertDialogDescription>{description}</AlertDialogDescription>\n          )}\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>{cancelLabel || t(\"cancel\")}</AlertDialogCancel>\n          <Button\n            disabled={disabled}\n            unClickable\n            variant={destructive ? `destructive` : `default`}\n            loading={true}\n            onClick={async () => {\n              if (!onSubmit) return;\n              const status: boolean = await onSubmit();\n              if (status) {\n                setOpen(false);\n              }\n            }}\n          >\n            {confirmLabel || t(\"confirm\")}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n\nexport default PopupDialog;\n"
  },
  {
    "path": "app/src/components/ProjectLink.tsx",
    "content": "import { Button } from \"./ui/button.tsx\";\nimport { useConversationActions, useMessages } from \"@/store/chat.ts\";\nimport { MessageSquarePlus } from \"lucide-react\";\nimport Github from \"@/components/ui/icons/Github.tsx\";\nimport { openWindow } from \"@/utils/device.ts\";\n\nfunction ProjectLink() {\n  const messages = useMessages();\n  const { toggle } = useConversationActions();\n\n  return messages.length > 0 ? (\n    <Button\n      variant=\"outline\"\n      size=\"icon-md\"\n      className=\"rounded-full overflow-hidden\"\n      onClick={async () => await toggle(-1)}\n    >\n      <MessageSquarePlus className={`h-4 w-4`} />\n    </Button>\n  ) : (\n    <Button\n      variant=\"outline\"\n      size=\"icon-md\"\n      className=\"rounded-full overflow-hidden\"\n      onClick={() => openWindow(\"https://github.com/coaidev/coai\")}\n    >\n      <Github className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100\" />\n    </Button>\n  );\n}\n\nexport default ProjectLink;\n"
  },
  {
    "path": "app/src/components/ReloadService.tsx",
    "content": "import { version } from \"@/conf/bootstrap.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { getMemory, setMemory } from \"@/utils/memory.ts\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport { toast } from \"sonner\";\n\nfunction ReloadPrompt() {\n  const { t } = useTranslation();\n\n  const before = getMemory(\"version\");\n  if (version.length === 0) {\n    return <></>;\n  }\n  if (before.length > 0 && before !== version) {\n    setMemory(\"version\", version);\n\n    setTimeout(() => {\n      toast.success(t(\"service.update-success\"), {\n        description: (\n          <p>\n            <Badge variant={`outline`} className={`font-medium mr-1`}>\n              v{version}\n            </Badge>\n            {t(\"service.update-success-prompt\")}\n          </p>\n        ),\n      });\n    }, 2500);\n\n    console.debug(\n      `[service] service worker updated (from ${before} to ${version})`,\n    );\n  }\n  setMemory(\"version\", version);\n\n  return <></>;\n}\n\nexport default ReloadPrompt;\n"
  },
  {
    "path": "app/src/components/Require.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo } from \"react\";\nimport { isEmailValid } from \"@/utils/form.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\nfunction Required() {\n  return <span className={`text-red-500 mr-0.5`}>*</span>;\n}\n\nexport type LengthRangeRequiredProps = {\n  content: string;\n  min: number;\n  max: number;\n  hideOnEmpty?: boolean;\n};\n\nexport function LengthRangeRequired({\n  content,\n  min,\n  max,\n  hideOnEmpty,\n}: LengthRangeRequiredProps) {\n  const { t } = useTranslation();\n  const onDisplay = useMemo(() => {\n    if (hideOnEmpty && content.length === 0) return false;\n    return content.length < min || content.length > max;\n  }, [content, min, max, hideOnEmpty]);\n\n  return (\n    <span\n      className={cn(\n        \"ml-1 text-red-500 transition-opacity\",\n        !onDisplay && \"opacity-0\",\n      )}\n    >\n      ({t(\"auth.length-range\", { min, max })})\n    </span>\n  );\n}\n\nexport type SameRequiredProps = {\n  content: string;\n  compare: string;\n  hideOnEmpty?: boolean;\n};\n\nexport function SameRequired({\n  content,\n  compare,\n  hideOnEmpty,\n}: SameRequiredProps) {\n  const { t } = useTranslation();\n  const onDisplay = useMemo(() => {\n    if (hideOnEmpty && compare.length === 0) return false;\n    return content !== compare;\n  }, [content, compare, hideOnEmpty]);\n\n  return (\n    <span\n      className={cn(\n        \"ml-1 text-red-500 transition-opacity\",\n        !onDisplay && \"opacity-0\",\n      )}\n    >\n      ({t(\"auth.same-rule\")})\n    </span>\n  );\n}\n\nexport type EmailRequireProps = {\n  content: string;\n  hideOnEmpty?: boolean;\n};\n\nexport function EmailRequire({ content, hideOnEmpty }: EmailRequireProps) {\n  const { t } = useTranslation();\n  const onDisplay = useMemo(() => {\n    if (hideOnEmpty && content.length === 0) return false;\n    return !isEmailValid(content);\n  }, [content, hideOnEmpty]);\n\n  return (\n    <span\n      className={cn(\n        \"ml-1 text-red-500 transition-opacity\",\n        !onDisplay && \"opacity-0\",\n      )}\n    >\n      ({t(\"auth.invalid-email\")})\n    </span>\n  );\n}\n\nexport default Required;\n"
  },
  {
    "path": "app/src/components/SelectGroup.tsx",
    "content": "import {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"./ui/select\";\nimport { mobile } from \"@/utils/device.ts\";\nimport React, { useEffect, useState } from \"react\";\nimport { Badge } from \"./ui/badge.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport Tips from \"./Tips.tsx\";\n\nexport type SingleSelectItemBadgeProps = {\n  variant: string;\n  name?: string;\n  icon?: React.ReactNode;\n  className?: string;\n  tooltip?: string;\n};\n\nexport type SelectItemBadgeProps =\n  | SingleSelectItemBadgeProps\n  | SingleSelectItemBadgeProps[];\n\nexport type SelectItemProps = {\n  name: string;\n  value: string;\n  badge?: SelectItemBadgeProps;\n  tag?: any;\n  icon?: React.ReactNode;\n};\n\ntype SelectGroupProps = {\n  current: SelectItemProps;\n  list: SelectItemProps[];\n  onChange?: (select: string) => void;\n  maxElements?: number;\n  className?: string;\n  classNameDesktop?: string;\n  classNameMobile?: string;\n  side?: \"left\" | \"right\" | \"top\" | \"bottom\";\n\n  selectGroupTop?: SelectItemProps;\n  selectGroupBottom?: SelectItemProps;\n};\n\nexport function SingleGroupSelectItemBadge(props: SingleSelectItemBadgeProps) {\n  const Comp = (\n    <Badge\n      className={cn(\n        \"w-5 h-5 p-1 rounded-sm ml-1.5 hover\",\n        {\n          \"text-primary bg-primary/5 hover:bg-primary/10\":\n            props.variant === \"default\",\n          \"text-amber-600 bg-amber-500/20 hover:bg-amber-500/30\":\n            props.variant === \"gold\",\n          \"text-blue-600 bg-blue-500/20 hover:bg-blue-500/30\":\n            props.variant === \"multi-modal\",\n          \"text-green-600 bg-green-500/20 hover:bg-green-500/30\":\n            props.variant === \"web\",\n          \"text-purple-600 bg-purple-500/20 hover:bg-purple-500/30\":\n            props.variant === \"high-quality\",\n          \"text-red-600 bg-red-500/20 hover:bg-red-500/30\":\n            props.variant === \"high-price\",\n          \"text-gray-600 bg-gray-500/20 hover:bg-gray-500/30\":\n            props.variant === \"open-source\",\n          \"text-indigo-600 bg-indigo-500/20 hover:bg-indigo-500/30\":\n            props.variant === \"image-generation\",\n          \"text-yellow-600 bg-yellow-500/20 hover:bg-yellow-500/30\":\n            props.variant === \"fast\",\n          \"text-orange-600 bg-orange-500/20 hover:bg-orange-500/30\":\n            props.variant === \"unstable\",\n          \"text-teal-600 bg-teal-500/20 hover:bg-teal-500/30\":\n            props.variant === \"high-context\",\n          \"text-emerald-600 bg-emerald-500/20 hover:bg-emerald-500/30\":\n            props.variant === \"free\",\n        },\n        props.className,\n      )}\n    >\n      {props.icon}\n      {props.name}\n    </Badge>\n  );\n\n  return props.tooltip ? <Tips trigger={Comp}>{props.tooltip}</Tips> : Comp;\n}\n\nfunction GroupSelectItemBadge(props: { data: SelectItemBadgeProps }) {\n  return Array.isArray(props.data) ? (\n    props.data.map((badge) => <SingleGroupSelectItemBadge {...badge} />)\n  ) : (\n    <SingleGroupSelectItemBadge {...props.data} />\n  );\n}\n\nexport function GroupSelectItem(props: SelectItemProps) {\n  return (\n    <div\n      className={`mr-1 flex flex-row items-center align-center whitespace-nowrap text-sm`}\n    >\n      {props.icon && <div className={`mr-1.5`}>{props.icon}</div>}\n      {props.value}\n      {props.badge && <GroupSelectItemBadge data={props.badge} />}\n    </div>\n  );\n}\n\nfunction SelectGroupDesktop(props: SelectGroupProps) {\n  const max: number = props.maxElements || 5;\n  const range = props.list.length > max ? max : props.list.length;\n  const display = props.list.slice(0, range);\n  const hidden = props.list.slice(range);\n\n  return (\n    <div className={`select-group`}>\n      {display.map((select: SelectItemProps, idx: number) => (\n        <div\n          key={idx}\n          onClick={() => props.onChange?.(select.name)}\n          className={`select-group-item ${\n            select == props.current ? \"active\" : \"\"\n          }`}\n        >\n          <GroupSelectItem {...select} />\n        </div>\n      ))}\n\n      {props.list.length > max && (\n        <Select\n          defaultValue={\"...\"}\n          value={props.current?.name || \"...\"}\n          onValueChange={(value: string) => props.onChange?.(value)}\n        >\n          <SelectTrigger\n            className={`w-max gap-1 select-group-item ${\n              hidden.includes(props.current) ? \"active\" : \"\"\n            }`}\n          >\n            <SelectValue asChild>\n              <span>\n                {hidden.includes(props.current) ? (\n                  <GroupSelectItem {...props.current} />\n                ) : (\n                  \"...\"\n                )}\n              </span>\n            </SelectValue>\n          </SelectTrigger>\n          <SelectContent\n            className={`${props.className} ${props.classNameDesktop}`}\n          >\n            {props.selectGroupTop && (\n              <SelectItem\n                value={props.selectGroupTop.name}\n                onClick={() => props.onChange?.(props.selectGroupTop!.name)}\n              >\n                <GroupSelectItem {...props.selectGroupTop} />\n              </SelectItem>\n            )}\n\n            {hidden.map((select: SelectItemProps, idx: number) => (\n              <SelectItem key={idx} value={select.name}>\n                <GroupSelectItem {...select} />\n              </SelectItem>\n            ))}\n\n            {props.selectGroupBottom && (\n              <SelectItem\n                value={props.selectGroupBottom.name}\n                onClick={() => props.onChange?.(props.selectGroupBottom!.name)}\n              >\n                <GroupSelectItem {...props.selectGroupBottom} />\n              </SelectItem>\n            )}\n          </SelectContent>\n        </Select>\n      )}\n    </div>\n  );\n}\n\nfunction SelectGroupMobile(props: SelectGroupProps) {\n  return (\n    <div className={`mb-2 w-full`}>\n      <Select\n        value={props.current?.name || \"\"}\n        onValueChange={(value: string) => {\n          props.onChange?.(value);\n        }}\n      >\n        <SelectTrigger className=\"select-group mobile whitespace-nowrap flex-nowrap\">\n          <SelectValue placeholder={props.current?.value || \"\"} />\n        </SelectTrigger>\n        <SelectContent\n          className={`${props.className} ${props.classNameMobile}`}\n        >\n          {props.selectGroupTop && (\n            <SelectItem\n              value={props.selectGroupTop.name}\n              onClick={() => props.onChange?.(props.selectGroupTop!.name)}\n            >\n              <GroupSelectItem {...props.selectGroupTop} />\n            </SelectItem>\n          )}\n\n          {props.list.map((select: SelectItemProps, idx: number) => (\n            <SelectItem\n              className={`whitespace-nowrap`}\n              key={idx}\n              value={select.name}\n            >\n              <GroupSelectItem {...select} />\n            </SelectItem>\n          ))}\n\n          {props.selectGroupBottom && (\n            <SelectItem\n              value={props.selectGroupBottom.name}\n              onClick={() => props.onChange?.(props.selectGroupBottom!.name)}\n            >\n              <GroupSelectItem {...props.selectGroupBottom} />\n            </SelectItem>\n          )}\n        </SelectContent>\n      </Select>\n    </div>\n  );\n}\n\nfunction SelectGroup(props: SelectGroupProps) {\n  const [state, setState] = useState(mobile);\n  useEffect(() => {\n    window.addEventListener(\"resize\", () => {\n      setState(mobile);\n    });\n  }, []);\n\n  return state ? (\n    <SelectGroupMobile {...props} />\n  ) : (\n    <SelectGroupDesktop {...props} />\n  );\n}\n\nexport default SelectGroup;\n"
  },
  {
    "path": "app/src/components/ThemeProvider.tsx",
    "content": "import { createContext, useContext, useEffect, useState, ReactNode } from \"react\";\nimport { Moon, Sun, Monitor } from \"lucide-react\";\n\nimport { Button } from \"./ui/button\";\nimport { getMemory, setMemory } from \"@/utils/memory.ts\";\nimport { themeEvent } from \"@/events/theme.ts\";\n\nconst defaultTheme: Theme = \"dark\";\n\nexport type Theme = \"dark\" | \"light\" | \"system\";\n\ntype ThemeProviderProps = {\n  children?: ReactNode;\n  defaultTheme?: Theme;\n};\n\ntype ThemeProviderState = {\n  theme: Theme;\n  setTheme: (theme: Theme) => void;\n  toggleTheme?: () => void;\n};\n\nexport function activeTheme(theme: Theme) {\n  const root = window.document.documentElement;\n\n  root.classList.remove(\"light\", \"dark\");\n  let actualTheme = theme;\n  if (theme === \"system\") {\n    actualTheme = window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n      ? \"dark\"\n      : \"light\";\n  }\n\n  root.classList.add(actualTheme);\n  setMemory(\"theme\", theme);\n  themeEvent.emit(actualTheme);\n}\n\nexport function getTheme() {\n  return (getMemory(\"theme\") as Theme) || defaultTheme;\n}\n\n// system -> dark -> light -> system\nfunction getNextTheme(current: Theme): Theme {\n  return current === \"system\" ? \"dark\" : current === \"dark\" ? \"light\" : \"system\";\n}\n\nconst initialState: ThemeProviderState = {\n  theme: \"system\",\n  setTheme: (theme: Theme) => {\n    activeTheme(theme);\n  },\n  toggleTheme: () => {\n    const key = getMemory(\"theme\");\n    const current = (key.length > 0 ? (key as Theme) : defaultTheme) as Theme;\n    const next = getNextTheme(current);\n    activeTheme(next);\n  },\n};\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState);\n\nexport function ThemeProvider({\n  defaultTheme = \"dark\",\n  ...props\n}: ThemeProviderProps) {\n  const [theme, setTheme] = useState<Theme>(\n    () => (getMemory(\"theme\") as Theme) || defaultTheme,\n  );\n\n  useEffect(() => {\n    const root = window.document.documentElement;\n\n    root.classList.remove(\"light\", \"dark\");\n\n    if (theme === \"system\") {\n      const systemTheme = window.matchMedia(\"(prefers-color-scheme: dark)\")\n        .matches\n        ? \"dark\"\n        : \"light\";\n\n      root.classList.add(systemTheme);\n      return;\n    }\n\n    root.classList.add(theme);\n  }, [theme]);\n\n  const value = {\n    theme,\n    setTheme: (newTheme: Theme) => {\n      activeTheme(newTheme);\n      setTheme(newTheme);\n    },\n    toggleTheme: () => {\n      const nextTheme: Theme = getNextTheme(theme);\n      activeTheme(nextTheme);\n      setTheme(nextTheme);\n    },\n  };\n\n  return <ThemeProviderContext.Provider {...props} value={value} />;\n}\n\nexport const useTheme = () => {\n  const context = useContext(ThemeProviderContext);\n\n  if (context === undefined)\n    throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n  return context;\n};\n\nexport function ThemeToggle({ className, size = \"icon\" }: { className?: string; size?: \"icon\" | \"icon-md\" }) {\n  const { theme, toggleTheme } = useTheme();\n\n  return (\n    <Button\n      variant=\"outline\"\n      size={size}\n      onClick={() => toggleTheme?.()}\n      className={`!m-0 ${className || ''}`}\n    >\n      <Sun\n        className={`h-4 w-4 transition-all ${theme === \"light\" ? \"relative rotate-0 scale-100\" : \"absolute -rotate-90 scale-0\"}`}\n      />\n      <Moon\n        className={`h-4 w-4 transition-all ${theme === \"dark\" ? \"relative rotate-0 scale-100\" : \"absolute rotate-90 scale-0\"}`}\n      />\n      <Monitor\n        className={`h-4 w-4 transition-all ${theme === \"system\" ? \"relative rotate-0 scale-100\" : \"absolute rotate-90 scale-0\"}`}\n      />\n    </Button>\n  );\n}\n\nexport default ThemeToggle;\n"
  },
  {
    "path": "app/src/components/ThinkContent.tsx",
    "content": "import { useState } from \"react\";\nimport { ChevronDown, ChevronUp, Brain, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/components/ui/lib/utils\";\nimport Markdown from \"@/components/Markdown\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useTranslation } from \"react-i18next\";\n\ninterface ThinkContentProps {\n  content: string;\n  isComplete?: boolean;\n}\n\nexport function ThinkContent({ content, isComplete = true }: ThinkContentProps) {\n  const [isExpanded, setIsExpanded] = useState(true);\n  const { t } = useTranslation();\n\n  const toggleExpand = () => {\n    setIsExpanded(!isExpanded);\n  };\n\n  return (\n    <div className=\"think-content-wrapper my-2 rounded-lg border bg-muted/40 dark:bg-muted/20\">\n      <Button\n        variant=\"ghost\"\n        onClick={toggleExpand}\n        className=\"w-full flex items-center justify-between p-2 hover:bg-muted/60\"\n      >\n        <div className=\"flex items-center gap-2\">\n          <Brain className=\"h-4 w-4\" />\n          <span className=\"text-sm font-medium\">{t(\"message.thinking-process\")}</span>\n          {!isComplete && (\n            <Loader2 className=\"h-3 w-3 animate-spin\" />\n          )}\n        </div>\n        {isExpanded ? (\n          <ChevronUp className=\"h-4 w-4\" />\n        ) : (\n          <ChevronDown className=\"h-4 w-4\" />\n        )}\n      </Button>\n      \n      <AnimatePresence mode=\"wait\">\n        {isExpanded && (\n          <motion.div\n            key=\"think-content\"\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2 }}\n            className=\"overflow-auto\"\n          >\n            <div className={cn(\"p-3 pt-0 text-sm\", !isComplete && \"opacity-80\")}>\n              <Markdown>{content}</Markdown>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/TickButton.tsx",
    "content": "import { Button, ButtonProps } from \"@/components/ui/button.tsx\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { isAsyncFunc } from \"@/utils/base.ts\";\n\nexport interface TickButtonProps extends ButtonProps {\n  tick: number;\n  onTickChange?: (tick: number) => void;\n  onClick?: (\n    e: React.MouseEvent<HTMLButtonElement>,\n  ) => boolean | Promise<boolean>;\n}\n\nfunction TickButton({\n  tick,\n  onTickChange,\n  onClick,\n  children,\n  ...props\n}: TickButtonProps) {\n  const stamp = useRef(0);\n  const [timer, setTimer] = useState(0);\n\n  useEffect(() => {\n    setInterval(() => {\n      const offset = Math.floor((Number(Date.now()) - stamp.current) / 1000);\n      let value = tick - offset;\n      if (value <= 0) value = 0;\n      setTimer(value);\n      onTickChange && onTickChange(value);\n    }, 250);\n  }, []);\n\n  const onReset = () => (stamp.current = Number(Date.now()));\n\n  // if is async function, use this:\n  const onTrigger = isAsyncFunc(onClick)\n    ? async (e: React.MouseEvent<HTMLButtonElement>) => {\n        if (timer !== 0 || !onClick) return;\n        if (await onClick(e)) onReset();\n      }\n    : (e: React.MouseEvent<HTMLButtonElement>) => {\n        if (timer !== 0 || !onClick) return;\n        if (onClick(e)) onReset();\n      };\n\n  return (\n    <Button {...props} onClick={onTrigger}>\n      {timer === 0 ? children : `${timer}s`}\n    </Button>\n  );\n}\n\nexport function useTicker(\n  interval?: number,\n  onTickerEnd?: () => any,\n): {\n  tick: number;\n  triggerTicker: () => void;\n} {\n  const stamp = useRef(0);\n  const [tick, setTick] = useState(0);\n  const step = interval || 60;\n\n  const triggerTicker = () => (stamp.current = Number(Date.now()));\n\n  useEffect(() => {\n    setInterval(() => {\n      const offset = Math.floor((Number(Date.now()) - stamp.current) / 1000);\n      setTick(step - offset);\n    }, 250);\n  }, []);\n\n  useEffect(() => {\n    if (stamp.current === 0) return;\n    if (tick === 0) {\n      onTickerEnd && onTickerEnd();\n      stamp.current = 0;\n    }\n  }, [tick]);\n\n  return {\n    tick: tick < 0 ? 0 : tick > step ? step : tick, // fix negative value\n    triggerTicker,\n  };\n}\n\nexport default TickButton;\n"
  },
  {
    "path": "app/src/components/Tips.tsx",
    "content": "import {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip.tsx\";\nimport { HelpCircle } from \"lucide-react\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport Clickable from \"@/components/ui/clickable.tsx\";\n\ntype TipsProps = {\n  content?: string;\n  align?: \"start\" | \"end\" | \"center\" | undefined;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\" | undefined;\n  trigger?: React.ReactNode;\n  children?: React.ReactNode;\n  className?: string;\n  classNameTrigger?: string;\n  classNamePopup?: string;\n  hideTimeout?: number;\n  notHide?: boolean;\n\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onClicked?: () => void;\n  asChild?: boolean;\n};\n\nfunction Tips({\n  content,\n  align,\n  side,\n  trigger,\n  children,\n  className,\n  classNameTrigger,\n  classNamePopup,\n  hideTimeout,\n  notHide,\n  open,\n  onOpenChange,\n  onClicked,\n  asChild,\n}: TipsProps) {\n  const timeout = hideTimeout ?? 2500;\n  const comp = useMemo(\n    () => (\n      <>\n        {content && <p className={`text-center`}>{content}</p>}\n        {children}\n      </>\n    ),\n    [content, children],\n  );\n\n  const [drop, setDrop] = onOpenChange\n    ? [open, onOpenChange]\n    : React.useState(false);\n  const [tooltip, setTooltip] = React.useState(false);\n\n  const task = useRef<NodeJS.Timeout>();\n\n  useEffect(() => {\n    if (notHide) return;\n    drop\n      ? (task.current = setTimeout(() => setDrop(false), timeout))\n      : clearTimeout(task.current);\n  }, [drop]);\n\n  useEffect(() => {\n    if (!tooltip) return;\n\n    setTooltip(false);\n    !drop && setDrop(true);\n  }, [drop, tooltip]);\n\n  return (\n    <DropdownMenu\n      open={drop}\n      onOpenChange={(open) => {\n        setDrop(open);\n        open && onClicked && onClicked();\n      }}\n    >\n      <DropdownMenuTrigger\n        asChild={asChild}\n        className={cn(\n          `tips-trigger select-none outline-none`,\n          classNameTrigger,\n        )}\n        onClick={onClicked}\n      >\n        <TooltipProvider>\n          <Tooltip open={tooltip} onOpenChange={setTooltip}>\n            <TooltipTrigger asChild>\n              <Clickable>\n                {trigger ?? (\n                  <HelpCircle className={cn(\"tips-icon\", className)} />\n                )}\n              </Clickable>\n            </TooltipTrigger>\n            <TooltipContent className=\"hidden\" />\n          </Tooltip>\n        </TooltipProvider>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        className={cn(\n          \"px-3 py-1.5 cursor-pointer text-sm min-w-0 max-w-[90vw]\",\n          classNamePopup,\n        )}\n        side={side ?? \"top\"}\n        align={align}\n      >\n        {comp}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport default Tips;\n"
  },
  {
    "path": "app/src/components/TrendBadge.tsx",
    "content": "import React from \"react\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nexport type TrendBadgeProps = {\n  current: number;\n  previous: number;\n};\n\nexport const TrendBadge: React.FC<TrendBadgeProps> = ({\n  current,\n  previous,\n}) => {\n  const trend = previous === 0 ? 0 : ((current - previous) / previous) * 100;\n  const percentage = Math.abs(trend).toFixed(1);\n\n  return trend < 0 ? (\n    <span className=\"inline-flex items-center gap-x-1 rounded-tremor-small px-2 py-1 text-tremor-label font-semibold text-red-700 ring-1 ring-inset ring-tremor-ring dark:text-red-500 dark:ring-dark-tremor-ring\">\n      <ChevronDown className=\"-ml-0.5 h-4 w-4\" aria-hidden={true} />\n      {percentage}%\n    </span>\n  ) : (\n    <span className=\"inline-flex items-center gap-x-1 rounded-tremor-small px-2 py-1 text-tremor-label font-semibold text-green-700 ring-1 ring-inset ring-tremor-ring dark:text-green-500 dark:ring-dark-tremor-ring\">\n      <ChevronUp className=\"-ml-0.5 h-4 w-4\" aria-hidden={true} />\n      {percentage}%\n    </span>\n  );\n};\n"
  },
  {
    "path": "app/src/components/VoiceProvider.tsx",
    "content": "import { ChatAction } from \"@/components/home/assemblies/ChatAction.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { toast } from \"sonner\";\nimport { Mic } from \"lucide-react\";\n\nexport function VoiceAction() {\n  const { t } = useTranslation();\n\n  return (\n    <ChatAction\n      text={t(\"chat.voice\")}\n      onClick={() => toast.info(t(\"coming-soon\"))}\n    >\n      <Mic className={\"w-4 h-4\"} />\n    </ChatAction>\n  );\n}\n"
  },
  {
    "path": "app/src/components/WarningButton.tsx",
    "content": "import React, { useState } from \"react\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogEmoji,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { useTranslation } from \"react-i18next\";\n\ntype WarningButtonProps = ButtonProps & {\n  title?: string;\n  description?: string;\n  confirmText?: string;\n  cancelText?: string;\n  children?: React.ReactNode;\n};\n\nexport default function WarningButton({\n  variant,\n  title,\n  description,\n  confirmText,\n  cancelText,\n  children,\n  className,\n  onClick,\n  ...props\n}: WarningButtonProps) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n\n  const handleClick = onClick\n    ? async () => {\n        //@ts-ignore\n        await onClick?.();\n        setOpen(false);\n      }\n    : undefined;\n\n  return (\n    <AlertDialog open={open} onOpenChange={setOpen}>\n      <AlertDialogTrigger asChild>\n        <Button\n          variant={variant}\n          className={cn(\"flex items-center\", className)}\n          {...props}\n        >\n          {children}\n        </Button>\n      </AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogEmoji emoji=\"1f641\" />\n          <AlertDialogTitle>{title || t(\"are-you-sure\")}</AlertDialogTitle>\n          <AlertDialogDescription>\n            {description || t(\"this-action-cannot-be-undone\")}\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>{cancelText || t(\"cancel\")}</AlertDialogCancel>\n          <AlertDialogAction asChild>\n            <Button\n              unClickable\n              loading={true}\n              onClick={handleClick}\n              variant={`destructive`}\n            >\n              {confirmText || t(\"confirm\")}\n            </Button>\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/admin/ChannelSettings.tsx",
    "content": "import { useEffect, useReducer, useState } from \"react\";\nimport ChannelTable from \"@/components/admin/assemblies/ChannelTable.tsx\";\nimport ChannelEditor from \"@/components/admin/assemblies/ChannelEditor.tsx\";\nimport { Channel, getChannelInfo } from \"@/admin/channel.ts\";\nimport { useSearchParams } from \"react-router-dom\";\n\nconst initialProxyState = {\n  proxy: \"\",\n  proxy_type: 0,\n  username: \"\",\n  password: \"\",\n};\n\nconst initialState: Channel = {\n  id: -1,\n  type: \"openai\",\n  name: \"\",\n  models: [],\n  priority: 0,\n  weight: 1,\n  retry: 3,\n  secret: \"\",\n  endpoint: getChannelInfo().endpoint,\n  mapper: \"\",\n  state: true,\n  group: [],\n  proxy: { ...initialProxyState },\n};\n\nfunction reducer(state: Channel, action: any): Channel {\n  switch (action.type) {\n    case \"type\":\n      const isChanged =\n        getChannelInfo(state.type).endpoint !== state.endpoint &&\n        state.endpoint.trim() !== \"\";\n      const endpoint = isChanged\n        ? state.endpoint\n        : getChannelInfo(action.value).endpoint;\n      return { ...state, endpoint, type: action.value };\n    case \"name\":\n      return { ...state, name: action.value };\n    case \"models\":\n      return { ...state, models: action.value };\n    case \"add-model\":\n      if (state.models.includes(action.value) || action.value === \"\") {\n        return state;\n      }\n      return { ...state, models: [...state.models, action.value] };\n    case \"add-models\":\n      const models = action.value.filter(\n        (model: string) => !state.models.includes(model) && model !== \"\",\n      );\n      return { ...state, models: [...state.models, ...models] };\n    case \"remove-model\":\n      return {\n        ...state,\n        models: state.models.filter((model) => model !== action.value),\n      };\n    case \"clear-models\":\n      return { ...state, models: [] };\n    case \"priority\":\n      return { ...state, priority: action.value };\n    case \"weight\":\n      return { ...state, weight: action.value };\n    case \"secret\":\n      return { ...state, secret: action.value };\n    case \"endpoint\":\n      return { ...state, endpoint: action.value };\n    case \"mapper\":\n      return { ...state, mapper: action.value };\n    case \"retry\":\n      return { ...state, retry: action.value };\n    case \"clear\":\n      return { ...initialState };\n    case \"add-group\":\n      return {\n        ...state,\n        group: state.group ? [...state.group, action.value] : [action.value],\n      };\n    case \"remove-group\":\n      return {\n        ...state,\n        group: state.group\n          ? state.group.filter((group) => group !== action.value)\n          : [],\n      };\n    case \"set-group\":\n      return { ...state, group: action.value };\n    case \"set-proxy\":\n      return {\n        ...state,\n        proxy: {\n          proxy: action.value as string,\n          proxy_type: state?.proxy?.proxy_type || 0,\n          password: state?.proxy?.password || \"\",\n          username: state?.proxy?.username || \"\",\n        },\n      };\n    case \"set-proxy-type\":\n      return {\n        ...state,\n        proxy: {\n          proxy: state?.proxy?.proxy || \"\",\n          proxy_type: action.value as number,\n          password: state?.proxy?.password || \"\",\n          username: state?.proxy?.username || \"\",\n        },\n      };\n    case \"set-proxy-username\":\n      return {\n        ...state,\n        proxy: {\n          proxy: state?.proxy?.proxy || \"\",\n          proxy_type: state?.proxy?.proxy_type || 0,\n          password: state?.proxy?.password || \"\",\n          username: action.value as string,\n        },\n      };\n    case \"set-proxy-password\":\n      return {\n        ...state,\n        proxy: {\n          proxy: state?.proxy?.proxy || \"\",\n          proxy_type: state?.proxy?.proxy_type || 0,\n          password: action.value as string,\n          username: state?.proxy?.username || \"\",\n        },\n      };\n    case \"set-first-message-as-user\":\n      return { ...state, first_message_as_user: action.value };\n    case \"set-merge-consecutive-user-messages\":\n      return { ...state, merge_consecutive_user_messages: action.value };\n    case \"set\":\n      return { ...state, ...action.value };\n    case \"import\":\n      return { ...state, ...action.value, id: state.id, state: state.state };\n    default:\n      return state;\n  }\n}\n\nfunction ChannelSettings() {\n  const [search] = useSearchParams();\n\n  const [enabled, setEnabled] = useState<boolean>(\n    search.get(\"editor_id\") !== null && search.get(\"editor_id\") !== \"empty\",\n  );\n  const [id, setId] = useState<number>(\n    search.get(\"editor_id\") !== null && search.get(\"editor_id\") !== \"empty\"\n      ? parseInt(search.get(\"editor_id\") || \"-1\")\n      : -1,\n  );\n\n  const [data, setData] = useState<Channel[]>([]);\n  const [edit, dispatch] = useReducer(reducer, { ...initialState });\n\n  useEffect(() => {\n    // set uri to ?editor_id=${id} if enabled is true, otherwise remove it\n    if (enabled) {\n      window.history.replaceState({}, \"\", `?editor_id=${id}`);\n    } else {\n      window.history.replaceState({}, \"\", \"?editor_id=empty\");\n    }\n  }, [enabled, id]);\n\n  return (\n    <>\n      <ChannelTable\n        setEnabled={setEnabled}\n        setId={setId}\n        display={!enabled}\n        dispatch={dispatch}\n        data={data}\n        setData={setData}\n      />\n      <ChannelEditor\n        setEnabled={setEnabled}\n        id={id}\n        display={enabled}\n        edit={edit}\n        data={data}\n        dispatch={dispatch}\n      />\n    </>\n  );\n}\n\nexport default ChannelSettings;\n"
  },
  {
    "path": "app/src/components/admin/ChargeWidget.tsx",
    "content": "import { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport {\n  ChargeProps,\n  chargeTypes,\n  defaultChargeType,\n  nonBilling,\n  timesBilling,\n  tokenBilling,\n} from \"@/admin/charge.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useMemo, useReducer, useState } from \"react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  Activity,\n  AlertCircle,\n  BoxIcon,\n  Cloud,\n  Copy,\n  DownloadCloud,\n  Eraser,\n  EyeOff,\n  KanbanSquareDashed,\n  Minus,\n  PencilLine,\n  Plus,\n  RotateCw,\n  Search,\n  Settings2,\n  Trash,\n  UploadCloud,\n} from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n  DropdownMenuItem,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport {\n  Command,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table.tsx\";\nimport OperationAction from \"@/components/OperationAction.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport {\n  deleteCharge,\n  listCharge,\n  setCharge,\n  syncCharge,\n  fetchUpstreamCharge,\n} from \"@/admin/api/charge.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { getQuerySelector, scrollUp, useClipboard } from \"@/utils/dom.ts\";\nimport PopupDialog, { popupTypes } from \"@/components/PopupDialog.tsx\";\nimport { getV1Path } from \"@/api/v1.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog.tsx\";\nimport { getUniqueList, isEnter, parseNumber } from \"@/utils/base.ts\";\nimport { defaultChannelModels } from \"@/admin/channel.ts\";\nimport { getPricing } from \"@/admin/datasets/charge.ts\";\nimport { useAllModels } from \"@/admin/hook.tsx\";\nimport { toast } from \"sonner\";\nimport { formatDecimal } from \"@/utils/base.ts\";\n\nconst initialState: ChargeProps = {\n  id: -1,\n  type: defaultChargeType,\n  models: [],\n  anonymous: false,\n  input: 0,\n  output: 0,\n};\n\nfunction reducer(state: ChargeProps, action: any): ChargeProps {\n  switch (action.type) {\n    case \"set\":\n      return { ...action.payload };\n    case \"set-models\":\n      return { ...state, models: action.payload };\n    case \"add-model\":\n      const model = action.payload.trim();\n      if (model.length === 0 || state.models.includes(model)) return state;\n      return { ...state, models: [...state.models, model] };\n    case \"toggle-model\":\n      if (action.payload.trim().length === 0) return state;\n      return state.models.includes(action.payload)\n        ? {\n            ...state,\n            models: state.models.filter((model) => model !== action.payload),\n          }\n        : { ...state, models: [...state.models, action.payload] };\n    case \"remove-model\":\n      return {\n        ...state,\n        models: state.models.filter((model) => model !== action.payload),\n      };\n    case \"set-type\":\n      return { ...state, type: action.payload };\n    case \"set-anonymous\":\n      return { ...state, anonymous: action.payload };\n    case \"set-input\":\n      return { ...state, input: action.payload };\n    case \"set-output\":\n      return { ...state, output: action.payload };\n    case \"clear\":\n      return initialState;\n    case \"clear-param\":\n      return { ...initialState, id: state.id };\n    default:\n      return state;\n  }\n}\n\nfunction preflight(state: ChargeProps): ChargeProps {\n  state.models = state.models\n    .map((model) => model.trim())\n    .filter((model) => model.length > 0);\n  switch (state.type) {\n    case nonBilling:\n      state.input = 0;\n      state.output = 0;\n      break;\n    case timesBilling:\n      state.input = 0;\n      state.anonymous = false;\n      break;\n    case tokenBilling:\n      state.anonymous = false;\n      break;\n  }\n\n  if (state.input < 0) state.input = 0;\n  if (state.output < 0) state.output = 0;\n\n  return state;\n}\n\ntype SyncDialogProps = {\n  current: string[];\n  builtin: boolean;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  onRefresh: () => void;\n  system: string;\n};\n\nfunction SyncDialog({\n  builtin,\n  current,\n  open,\n  setOpen,\n  onRefresh,\n  system,\n}: SyncDialogProps) {\n  const { t } = useTranslation();\n\n  const [siteCharge, setSiteCharge] = useState<ChargeProps[]>([]);\n  const [siteOpen, setSiteOpen] = useState(false);\n\n  const [overwrite, setOverwrite] = useState(false);\n  const siteModels = useMemo(\n    () => siteCharge.flatMap((charge) => charge.models),\n    [siteCharge],\n  );\n  const influencedModels = useMemo(\n    () =>\n      overwrite\n        ? siteModels\n        : siteModels.filter((model) => !current.includes(model)),\n    [overwrite, siteModels, current],\n  );\n\n  return (\n    <>\n      <PopupDialog\n        type={popupTypes.Number}\n        title={t(\"admin.charge.sync-builtin\")}\n        name={t(\"admin.charge.usd-currency\")}\n        open={open && builtin}\n        setOpen={setOpen}\n        defaultValue={\"7.1\"}\n        onSubmit={async (_currency: string): Promise<boolean> => {\n          const currency = parseNumber(_currency);\n          const pricing = getPricing(currency);\n\n          setSiteCharge(pricing);\n          setSiteOpen(true);\n\n          return true;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.Text}\n        title={t(\"admin.charge.sync\")}\n        name={t(\"admin.charge.sync-site\")}\n        placeholder={t(\"admin.charge.sync-placeholder\")}\n        open={open && !builtin}\n        setOpen={setOpen}\n        defaultValue={\"https://api.chatnio.net\"}\n        alert={system === \"\" ? t(\"admin.coai-format-only\") : undefined}\n        onSubmit={async (endpoint): Promise<boolean> => {\n          const path = system === \"newapi\"\n            ? `${endpoint.replace(/\\/$/, \"\")}/api/ratio_config`\n            : getV1Path(\"/v1/charge\", { endpoint });\n          const resp = await fetchUpstreamCharge({ endpoint, system });\n\n          if (!resp.status || resp.data.length === 0) {\n            toast.error(t(\"admin.charge.sync-failed\"), {\n              description: t(\"admin.charge.sync-failed-prompt\", {\n                endpoint: path,\n              }),\n            });\n            return false;\n          }\n\n          setSiteCharge(resp.data);\n          setSiteOpen(true);\n          return true;\n        }}\n      />\n      <Dialog open={siteOpen} onOpenChange={setSiteOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.charge.sync-option\")}</DialogTitle>\n            <DialogDescription className={`pt-1.5`}>\n              {t(\"admin.charge.sync-prompt\", {\n                length: siteModels.length,\n                influence: influencedModels.length,\n              })}\n            </DialogDescription>\n          </DialogHeader>\n          <div className={`pt-1 flex flex-row items-center justify-center`}>\n            <span className={`mr-4 whitespace-nowrap`}>\n              {t(\"admin.charge.sync-overwrite\")}\n            </span>\n            <Switch checked={overwrite} onCheckedChange={setOverwrite} />\n          </div>\n          <DialogFooter>\n            <Button\n              unClickable\n              variant={`outline`}\n              onClick={() => {\n                setSiteOpen(false);\n                setSiteCharge([]);\n              }}\n            >\n              {t(\"cancel\")}\n            </Button>\n            <Button\n              unClickable\n              loading={true}\n              variant={overwrite ? `destructive` : `default`}\n              onClick={async () => {\n                const resp = await syncCharge({\n                  data: siteCharge,\n                  overwrite,\n                });\n                withNotify(t, resp, true);\n\n                if (resp.status) {\n                  setOpen(false);\n                  setSiteOpen(false);\n                  setSiteCharge([]);\n\n                  onRefresh();\n                }\n              }}\n            >\n              {t(\"admin.charge.sync-confirm\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\ntype ChargeActionProps = {\n  loading: boolean;\n  onRefresh: () => void;\n  currentModels: string[];\n};\n\nfunction ChargeAction({\n  loading,\n  onRefresh,\n  currentModels,\n}: ChargeActionProps) {\n  const { t } = useTranslation();\n  const [popup, setPopup] = useState(false);\n  const [builtin, setBuiltin] = useState(false);\n  const [system, setSystem] = useState(\"\");\n\n  const open = (builtin: boolean) => {\n    setBuiltin(builtin);\n    setPopup(true);\n  };\n\n  return (\n    <div className={`flex flex-row w-full h-max`}>\n      <SyncDialog\n        builtin={builtin}\n        onRefresh={onRefresh}\n        current={currentModels}\n        open={popup}\n        setOpen={setPopup}\n        system={system}\n      />\n      <Button variant={`default`} className={`mr-2`} onClick={() => open(true)}>\n        <KanbanSquareDashed className={`w-4 h-4 mr-2`} />\n        {t(\"admin.charge.sync-builtin\")}\n      </Button>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button variant={`outline`}>\n            <Activity className={`w-4 h-4 mr-2`} />\n            {t(\"admin.charge.sync\")}\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align={`start`}>\n          <DropdownMenuItem\n            onSelect={() => {\n              setSystem(\"\");\n              open(false);\n            }}\n          >\n            CoAI\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            onSelect={() => {\n              setSystem(\"newapi\");\n              open(false);\n            }}\n          >\n            NewAPI\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <div className={`grow`} />\n      <Button variant={`outline`} size={`icon`} onClick={onRefresh}>\n        <RotateCw className={cn(\"w-4 h-4\", loading && \"animate-spin\")} />\n      </Button>\n    </div>\n  );\n}\n\ntype ChargeAlertProps = {\n  models: string[];\n  onClick: (model: string) => void;\n};\n\nfunction ChargeAlert({ models, onClick }: ChargeAlertProps) {\n  const { t } = useTranslation();\n\n  return (\n    models.length > 0 && (\n      <Alert className={`charge-alert`}>\n        <AlertTitle className={`flex flex-row items-center select-none`}>\n          <AlertCircle className=\"h-4 w-4 mr-2\" />\n          <p>{t(\"admin.charge.unused-model\")}</p>\n          <Tips content={t(\"admin.charge.unused-model-tip\")} />\n        </AlertTitle>\n        <AlertDescription className={`model-list`}>\n          {models.slice(0, 15).map((model, index) => (\n            <Button\n              key={index}\n              variant={`outline`}\n              className={`cursor-pointer h-8 select-none flex flex-row items-center`}\n              onClick={() => onClick(model)}\n            >\n              <BoxIcon className={`w-3.5 h-3.5 mr-1`} />\n              {model}\n            </Button>\n          ))}\n        </AlertDescription>\n      </Alert>\n    )\n  );\n}\n\ntype ChargeEditorProps = {\n  form: ChargeProps;\n  dispatch: (action: any) => void;\n  onRefresh: () => void;\n  usedModels: string[];\n  allModels: string[];\n};\n\nfunction ChargeEditor({\n  form,\n  dispatch,\n  onRefresh,\n  usedModels,\n  allModels,\n}: ChargeEditorProps) {\n  const { t } = useTranslation();\n\n  const [model, setModel] = useState(\"\");\n\n  const channelModels = useMemo(\n    () => getUniqueList([...allModels, ...defaultChannelModels]),\n    [allModels],\n  );\n\n  const unusedModels = useMemo(() => {\n    return channelModels.filter(\n      (model) =>\n        !form.models.includes(model) &&\n        !usedModels.includes(model) &&\n        model.trim() !== \"\",\n    );\n  }, [form.models, usedModels]);\n\n  const disabled = useMemo(() => {\n    if (model.trim() !== \"\") return false;\n    return form.models.length === 0;\n  }, [model, form.models]);\n\n  const [loading, setLoading] = useState(false);\n\n  async function post() {\n    const raw = model.trim();\n    const data = preflight({ ...form });\n    if (raw !== \"\" && !data.models.includes(raw)) {\n      data.models = [raw, ...data.models];\n      setModel(\"\");\n    }\n\n    const resp = await setCharge(data);\n    withNotify(t, resp, true);\n\n    if (resp.status) clear();\n    onRefresh();\n  }\n\n  function clear() {\n    dispatch({ type: \"clear\" });\n    setModel(\"\");\n  }\n\n  return (\n    <div className={`charge-editor`}>\n      <div className={`w-full h-max mb-5`}>\n        <RadioGroup\n          value={form.type}\n          onValueChange={(value) =>\n            dispatch({ type: \"set-type\", payload: value })\n          }\n          className={`flex flex-row gap-5 whitespace-nowrap flex-wrap`}\n        >\n          {chargeTypes.map((chargeType, index) => (\n            <div\n              className=\"flex items-center space-x-2 cursor-pointer\"\n              key={index}\n            >\n              <RadioGroupItem\n                className={`transition-all duration-200`}\n                value={chargeType}\n                id={chargeType}\n              />\n              <Label htmlFor={chargeType} className={`cursor-pointer`}>\n                {t(`admin.charge.${chargeType}`)}\n              </Label>\n            </div>\n          ))}\n        </RadioGroup>\n      </div>\n      <div className={`flex flex-row w-full h-max mb-4`}>\n        <Button\n          onClick={() => {\n            dispatch({ type: \"add-model\", payload: model });\n            setModel(\"\");\n          }}\n          size={`icon`}\n          className={`mr-2 shrink-0`}\n        >\n          <Plus className={`w-4 h-4`} />\n        </Button>\n        <Input\n          value={model}\n          onChange={(e) => setModel(e.target.value)}\n          placeholder={t(\"admin.channels.model\")}\n          onKeyDown={(e) => {\n            if (isEnter(e)) {\n              dispatch({ type: \"add-model\", payload: model });\n              setModel(\"\");\n            }\n          }}\n        />\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button size={`icon`} className={`ml-2 shrink-0`}>\n              <Search className={`w-4 h-4`} />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align={`end`} asChild>\n            <Command>\n              <CommandInput placeholder={t(\"admin.channels.search-model\")} />\n              <CommandList className={`thin-scrollbar`}>\n                {unusedModels.map((model, idx) => (\n                  <CommandItem\n                    key={idx}\n                    value={model}\n                    onSelect={(value) =>\n                      dispatch({ type: \"add-model\", payload: value })\n                    }\n                    className={`px-2`}\n                  >\n                    {model}\n                  </CommandItem>\n                ))}\n              </CommandList>\n            </Command>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n      <div className={`flex flex-col w-full h-max mb-2`}>\n        {form.models.map((model, index) => (\n          <div\n            className={`flex flex-row w-full h-max shrink-0 mb-2 select-none`}\n            key={index}\n          >\n            <Input value={model} readOnly />\n            <Button\n              onClick={() => dispatch({ type: \"remove-model\", payload: model })}\n              size={`icon`}\n              variant={`outline`}\n              className={`ml-2 shrink-0`}\n            >\n              <Minus className={`w-4 h-4`} />\n            </Button>\n          </div>\n        ))}\n      </div>\n\n      {form.type === nonBilling && (\n        <div className={`flex flex-row w-full h-max items-center mt-4 mb-6`}>\n          <EyeOff className={`w-4 h-4 mr-2`} />\n          <Label className={`grow`}>{t(\"admin.charge.anonymous\")}</Label>\n          <Switch\n            checked={form.anonymous}\n            onCheckedChange={(checked) =>\n              dispatch({ type: \"set-anonymous\", payload: checked })\n            }\n          />\n        </div>\n      )}\n\n      {form.type === timesBilling && (\n        <div className={`flex flex-row w-full h-max items-center`}>\n          <Cloud className={`w-4 h-4 mr-2`} />\n          <Label className={`grow`}>{t(\"admin.charge.time-count\")}</Label>\n          <NumberInput\n            value={form.output}\n            onValueChange={(value) =>\n              dispatch({ type: \"set-output\", payload: value })\n            }\n            acceptNegative={false}\n            className={`w-20`}\n            min={0}\n            max={99999}\n          />\n        </div>\n      )}\n\n      {form.type === tokenBilling && (\n        <div className={`flex flex-col w-full h-max gap-2`}>\n          <div className={`flex flex-row w-full h-max items-center`}>\n            <UploadCloud className={`w-4 h-4 mr-2`} />\n            <Label className={`grow`}>\n              {t(\"admin.charge.input-count\")}\n              <span className={`token`}> / 1k tokens</span>\n            </Label>\n            <NumberInput\n              value={form.input}\n              onValueChange={(value) =>\n                dispatch({ type: \"set-input\", payload: value })\n              }\n              acceptNegative={false}\n              className={`w-20`}\n              min={0}\n              max={99999}\n            />\n          </div>\n          <div className={`flex flex-row w-full h-max items-center`}>\n            <DownloadCloud className={`w-4 h-4 mr-2`} />\n            <Label className={`grow`}>\n              {t(\"admin.charge.output-count\")}\n              <span className={`token`}> / 1k tokens</span>\n            </Label>\n            <NumberInput\n              value={form.output}\n              onValueChange={(value) =>\n                dispatch({ type: \"set-output\", payload: value })\n              }\n              acceptNegative={false}\n              className={`w-20`}\n              min={0}\n              max={99999}\n            />\n          </div>\n        </div>\n      )}\n\n      <div\n        className={`flex flex-row w-full h-max mt-5 gap-2 items-center flex-wrap`}\n      >\n        <div className={`object-id`}>\n          <span className={`mr-2`}>ID</span>\n          {form.id === -1 ? (\n            <Plus className={`w-3 h-3`} />\n          ) : (\n            <span className={`id`}>{form.id}</span>\n          )}\n        </div>\n        <div className={`grow`} />\n        <Button\n          variant={`outline`}\n          size={`icon`}\n          className={`shrink-0`}\n          onClick={clear}\n        >\n          <Eraser className={`w-4 h-4`} />\n        </Button>\n        <Button\n          disabled={disabled}\n          onClick={post}\n          loading={true}\n          onLoadingChange={setLoading}\n          className={`whitespace-nowrap shrink-0`}\n        >\n          {form.id === -1 ? (\n            <>\n              {!loading && <Plus className={`w-4 h-4 mr-2`} />}\n              {t(\"admin.charge.add-rule\")}\n            </>\n          ) : (\n            <>\n              {!loading && <PencilLine className={`w-4 h-4 mr-2`} />}\n              {t(\"admin.charge.update-rule\")}\n            </>\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\ntype ChargeTableProps = {\n  data: ChargeProps[];\n  dispatch: (action: any) => void;\n  onRefresh: () => void;\n};\n\nfunction ChargeTable({ data, dispatch, onRefresh }: ChargeTableProps) {\n  const { t } = useTranslation();\n  const copy = useClipboard();\n\n  return (\n    <div className={`charge-table`}>\n      <Table classNameWrapper={`table`}>\n        <TableHeader>\n          <TableRow className={`select-none whitespace-nowrap`}>\n            <TableCell>{t(\"admin.charge.id\")}</TableCell>\n            <TableCell>{t(\"admin.charge.type\")}</TableCell>\n            <TableCell>{t(\"admin.charge.model\")}</TableCell>\n            <TableCell>{t(\"admin.charge.input\")}</TableCell>\n            <TableCell>{t(\"admin.charge.output\")}</TableCell>\n            <TableCell>{t(\"admin.charge.support-anonymous\")}</TableCell>\n            <TableCell>{t(\"admin.charge.action\")}</TableCell>\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {data.map((charge, idx) => (\n            <TableRow key={idx}>\n              <TableCell className={`charge-id`}>{charge.id}</TableCell>\n              <TableCell>\n                <Badge className={`whitespace-nowrap`}>\n                  {t(`admin.charge.${charge.type}`)}\n                </Badge>\n              </TableCell>\n              <TableCell>\n                {charge.models.map((model, index) => (\n                  <p\n                    key={index}\n                    className={`whitespace-nowrap cursor-pointer`}\n                    onClick={() => copy(model)}\n                  >\n                    {model}\n                    <Copy className={`inline w-3 h-3 ml-1`} />\n                  </p>\n                ))}\n              </TableCell>\n              <TableCell>\n                {formatDecimal(charge.input)}\n              </TableCell>\n              <TableCell>\n                {formatDecimal(charge.output)}\n              </TableCell>\n              <TableCell>{t(String(charge.anonymous))}</TableCell>\n              <TableCell>\n                <div className={`inline-flex flex-row flex-wrap gap-2`}>\n                  <OperationAction\n                    tooltip={t(\"admin.channels.edit\")}\n                    onClick={async () => {\n                      const props: ChargeProps = { ...charge };\n                      dispatch({ type: \"set\", payload: props });\n\n                      // scroll to top\n                      scrollUp(\n                        getQuerySelector(\n                          \".admin-content > .scrollarea-viewport\",\n                        )!,\n                      );\n                    }}\n                  >\n                    <Settings2 className={`h-4 w-4`} />\n                  </OperationAction>\n                  <OperationAction\n                    tooltip={t(\"admin.channels.delete\")}\n                    variant={`destructive`}\n                    onClick={async () => {\n                      const resp = await deleteCharge(charge.id);\n                      withNotify(t, resp, true);\n                      onRefresh();\n                    }}\n                  >\n                    <Trash className={`h-4 w-4`} />\n                  </OperationAction>\n                </div>\n              </TableCell>\n            </TableRow>\n          ))}\n        </TableBody>\n      </Table>\n    </div>\n  );\n}\n\nfunction ChargeWidget() {\n  const { t } = useTranslation();\n  const [data, setData] = useState<ChargeProps[]>([]);\n  const [form, dispatch] = useReducer(reducer, initialState);\n  const [loading, setLoading] = useState(false);\n\n  const { allModels, update } = useAllModels();\n\n  const currentModels = useMemo(() => {\n    return data.flatMap((charge) => charge.models);\n  }, [data]);\n\n  const usedModels = useMemo((): string[] => {\n    return data.flatMap((charge) => charge.models);\n  }, [data]);\n\n  const unusedModels = useMemo(() => {\n    if (loading) return [];\n    return allModels.filter(\n      (model) => !usedModels.includes(model) && model.trim() !== \"\",\n    );\n  }, [loading, allModels, usedModels]);\n\n  async function refresh(ignoreUpdate?: boolean) {\n    setLoading(true);\n    const resp = await listCharge();\n    if (!ignoreUpdate) await update();\n\n    setLoading(false);\n    withNotify(t, resp);\n    setData(resp.data);\n  }\n\n  useEffectAsync(async () => await refresh(true), []);\n\n  return (\n    <div className={`charge-widget`}>\n      <ChargeAction\n        loading={loading}\n        onRefresh={refresh}\n        currentModels={currentModels}\n      />\n      <ChargeAlert\n        models={unusedModels}\n        onClick={(model) => dispatch({ type: \"toggle-model\", payload: model })}\n      />\n      <ChargeEditor\n        onRefresh={refresh}\n        form={form}\n        dispatch={dispatch}\n        allModels={allModels}\n        usedModels={usedModels}\n      />\n      <ChargeTable data={data} dispatch={dispatch} onRefresh={refresh} />\n    </div>\n  );\n}\n\nexport default ChargeWidget;\n"
  },
  {
    "path": "app/src/components/admin/ChartBox.tsx",
    "content": "import ModelChart from \"@/components/admin/assemblies/ModelChart.tsx\";\nimport { useState } from \"react\";\nimport {\n  BillingChartResponse,\n  ErrorChartResponse,\n  ModelChartResponse,\n  RequestChartResponse,\n  UserTypeChartResponse,\n} from \"@/admin/types.ts\";\nimport RequestChart from \"@/components/admin/assemblies/RequestChart.tsx\";\nimport BillingChart from \"@/components/admin/assemblies/BillingChart.tsx\";\nimport ErrorChart from \"@/components/admin/assemblies/ErrorChart.tsx\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  getBillingChart,\n  getErrorChart,\n  getModelChart,\n  getRequestChart,\n  getUserTypeChart,\n} from \"@/admin/api/chart.ts\";\nimport ModelUsageChart from \"@/components/admin/assemblies/ModelUsageChart.tsx\";\nimport UserTypeChart from \"@/components/admin/assemblies/UserTypeChart.tsx\";\n\nfunction ChartBox() {\n  const [model, setModel] = useState<ModelChartResponse>({\n    date: [],\n    value: [],\n  });\n\n  const [request, setRequest] = useState<RequestChartResponse>({\n    date: [],\n    value: [],\n  });\n\n  const [billing, setBilling] = useState<BillingChartResponse>({\n    date: [],\n    value: [],\n  });\n\n  const [error, setError] = useState<ErrorChartResponse>({\n    date: [],\n    value: [],\n  });\n\n  const [user, setUser] = useState<UserTypeChartResponse>({\n    total: 0,\n    normal: 0,\n    api_paid: 0,\n    basic_plan: 0,\n    standard_plan: 0,\n    pro_plan: 0,\n  });\n\n  useEffectAsync(async () => {\n    setModel(await getModelChart());\n    setRequest(await getRequestChart());\n    setBilling(await getBillingChart());\n    setError(await getErrorChart());\n    setUser(await getUserTypeChart());\n  }, []);\n\n  return (\n    <div className={`chart-boxes`}>\n      <div className={`chart-box`}>\n        <ModelChart labels={model.date} datasets={model.value} />\n      </div>\n      <div className={`chart-box`}>\n        <ModelUsageChart labels={model.date} datasets={model.value} />\n      </div>\n      <div className={`chart-box`}>\n        <BillingChart labels={billing.date} datasets={billing.value} />\n      </div>\n      <div className={`chart-box`}>\n        <UserTypeChart data={user} />\n      </div>\n      <div className={`chart-box`}>\n        <RequestChart labels={request.date} datasets={request.value} />\n      </div>\n      <div className={`chart-box`}>\n        <ErrorChart labels={error.date} datasets={error.value} />\n      </div>\n    </div>\n  );\n}\n\nexport default ChartBox;\n"
  },
  {
    "path": "app/src/components/admin/InfoBox.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useState } from \"react\";\nimport {\n  CircleDollarSign,\n  MessageSquareDot,\n  Users2,\n  Wallet,\n} from \"lucide-react\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { getAdminInfo, initialAdminInfoState } from \"@/admin/api/chart.ts\";\nimport { InfoResponse } from \"@/admin/types.ts\";\nimport { getReadableNumber } from \"@/utils/processor.ts\";\nimport { TrendBadge } from \"@/components/TrendBadge.tsx\";\nimport { useCurrency } from \"@/store/info\";\n\nfunction InfoBox() {\n  const { t } = useTranslation();\n  const { name: currencyName } = useCurrency();\n  const [form, setForm] = useState<InfoResponse>({\n    ...initialAdminInfoState,\n  });\n\n  useEffectAsync(async () => {\n    setForm(await getAdminInfo());\n  }, []);\n\n  return (\n    <div className={`info-boxes`}>\n      <div className={`info-box`}>\n        <div className={`box-wrapper`}>\n          <div className={`box-title`}>{t(\"admin.billing-today\")}</div>\n          <div className={`flex flex-col md:flex-row md:items-end`}>\n            <div className={`box-value`}>\n              {form.billing_today.toFixed(2)}\n              <span className={`box-subvalue`}>{currencyName}</span>\n            </div>\n            <TrendBadge\n              current={form.billing_today}\n              previous={form.billing_yesterday}\n            />\n          </div>\n        </div>\n        <div className={`box-icon`}>\n          <CircleDollarSign />\n        </div>\n      </div>\n\n      <div className={`info-box`}>\n        <div className={`box-wrapper`}>\n          <div className={`box-title`}>{t(\"admin.billing-month\")}</div>\n          <div className={`flex flex-col md:flex-row md:items-end`}>\n            <div className={`box-value mr-1`}>\n              {form.billing_month.toFixed(2)}\n              <span className={`box-subvalue`}>{currencyName}</span>\n            </div>\n            <TrendBadge\n              current={form.billing_month}\n              previous={form.billing_last_month}\n            />\n          </div>\n        </div>\n        <div className={`box-icon`}>\n          <Wallet />\n        </div>\n      </div>\n\n      <div className={`info-box`}>\n        <div className={`box-wrapper`}>\n          <div className={`box-title`}>{t(\"admin.subscription-users\")}</div>\n          <div className={`box-value`}>\n            {form.subscription_count}\n            <span className={`box-subvalue`}>{t(\"admin.seat\")}</span>\n          </div>\n        </div>\n        <div className={`box-icon`}>\n          <Users2 />\n        </div>\n      </div>\n\n      <div className={`info-box`}>\n        <div className={`box-wrapper`}>\n          <div className={`box-title`}>{t(\"admin.online-chats\")}</div>\n          <div className={`box-value`}>\n            {getReadableNumber(form.online_chats)}\n          </div>\n        </div>\n        <div className={`box-icon`}>\n          <MessageSquareDot />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default InfoBox;\n"
  },
  {
    "path": "app/src/components/admin/InvitationTable.tsx",
    "content": "import {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table.tsx\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useState } from \"react\";\nimport { InvitationForm, InvitationResponse } from \"@/admin/types.ts\";\nimport { Button, TemporaryButton } from \"@/components/ui/button.tsx\";\nimport { Copy, Download, Loader2, RotateCw, Trash } from \"lucide-react\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  deleteInvitation,\n  generateInvitation,\n  getInvitationList,\n} from \"@/admin/api/chart.ts\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport { copyClipboard, saveAsFile } from \"@/utils/dom.ts\";\nimport { PaginationAction } from \"@/components/ui/pagination.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport OperationAction from \"@/components/OperationAction.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport StateBadge from \"@/components/admin/common/StateBadge.tsx\";\nimport { toast } from \"sonner\";\n\nfunction GenerateDialog({ update }: { update: () => void }) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState<boolean>(false);\n  const [type, setType] = useState<string>(\"\");\n  const [quota, setQuota] = useState<string>(\"5\");\n  const [number, setNumber] = useState<string>(\"1\");\n  const [data, setData] = useState<string>(\"\");\n\n  function getNumber(value: string): string {\n    return value.replace(/[^\\d.]/g, \"\");\n  }\n\n  async function generateCode() {\n    const data = await generateInvitation(type, Number(quota), Number(number));\n    if (data.status) {\n      setData(data.data.join(\"\\n\"));\n      update();\n    } else\n      toast.error(t(\"admin.error\"), {\n        description: data.message,\n      });\n  }\n\n  function close() {\n    setType(\"\");\n    setQuota(\"5\");\n    setNumber(\"1\");\n\n    setOpen(false);\n    setData(\"\");\n  }\n\n  function downloadCode() {\n    return saveAsFile(\"invitation.txt\", data);\n  }\n\n  return (\n    <>\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogTrigger asChild>\n          <Button>{t(\"admin.generate\")}</Button>\n        </DialogTrigger>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.generate\")}</DialogTitle>\n            <DialogDescription className={`pt-2`}>\n              <div className={`invitation-row`}>\n                <p className={`mr-4`}>{t(\"admin.type\")}</p>\n                <Input value={type} onChange={(e) => setType(e.target.value)} />\n              </div>\n              <div className={`invitation-row`}>\n                <p className={`mr-4`}>{t(\"admin.quota\")}</p>\n                <Input\n                  value={quota}\n                  onChange={(e) => setQuota(getNumber(e.target.value))}\n                />\n              </div>\n              <div className={`invitation-row`}>\n                <p className={`mr-4`}>{t(\"admin.number\")}</p>\n                <Input\n                  value={number}\n                  onChange={(e) => setNumber(getNumber(e.target.value))}\n                />\n              </div>\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button variant={`outline`} onClick={() => setOpen(false)}>\n              {t(\"admin.cancel\")}\n            </Button>\n            <Button variant={`default`} loading={true} onClick={generateCode}>\n              {t(\"admin.confirm\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n      <Dialog\n        open={data !== \"\"}\n        onOpenChange={(state: boolean) => {\n          if (!state) close();\n        }}\n      >\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.generate-result\")}</DialogTitle>\n            <DialogDescription className={`pt-4`}>\n              <Textarea value={data} rows={12} readOnly />\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button unClickable variant={`outline`} onClick={close}>\n              {t(\"close\")}\n            </Button>\n            <Button unClickable variant={`default`} onClick={downloadCode}>\n              <Download className={`h-4 w-4 mr-2`} />\n              {t(\"download\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\nfunction InvitationTable() {\n  const { t } = useTranslation();\n  const [data, setData] = useState<InvitationForm>({\n    total: 0,\n    data: [],\n  });\n  const [loading, setLoading] = useState<boolean>(false);\n  const [page, setPage] = useState<number>(0);\n\n  async function update() {\n    setLoading(true);\n    const resp = await getInvitationList(page);\n    setLoading(false);\n    if (resp.status) setData(resp as InvitationResponse);\n    else\n      toast.error(t(\"admin.error\"), {\n        description: resp.message,\n      });\n  }\n  useEffectAsync(update, [page]);\n\n  return (\n    <div className={`invitation-table`}>\n      {(data.data && data.data.length > 0) || page > 0 ? (\n        <>\n          <Table>\n            <TableHeader>\n              <TableRow className={`select-none whitespace-nowrap`}>\n                <TableHead>{t(\"admin.invitation-code\")}</TableHead>\n                <TableHead>{t(\"admin.quota\")}</TableHead>\n                <TableHead>{t(\"admin.type\")}</TableHead>\n                <TableHead>{t(\"admin.used\")}</TableHead>\n                <TableHead>{t(\"admin.used-username\")}</TableHead>\n                <TableHead>{t(\"admin.created-at\")}</TableHead>\n                <TableHead>{t(\"admin.used-at\")}</TableHead>\n                <TableHead>{t(\"admin.action\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {(data.data || []).map((invitation, idx) => (\n                <TableRow key={idx} className={`whitespace-nowrap`}>\n                  <TableCell>{invitation.code}</TableCell>\n                  <TableCell>\n                    <Badge variant={`outline`}>{invitation.quota}</Badge>\n                  </TableCell>\n                  <TableCell>\n                    <Badge>{invitation.type}</Badge>\n                  </TableCell>\n                  <TableCell>\n                    <StateBadge state={invitation.used} />\n                  </TableCell>\n                  <TableCell>{invitation.username || \"-\"}</TableCell>\n                  <TableCell>{invitation.created_at}</TableCell>\n                  <TableCell>{invitation.updated_at}</TableCell>\n                  <TableCell className={`flex gap-2`}>\n                    <TemporaryButton\n                      size={`icon`}\n                      variant={`outline`}\n                      onClick={() => copyClipboard(invitation.code)}\n                    >\n                      <Copy className={`h-4 w-4`} />\n                    </TemporaryButton>\n                    <OperationAction\n                      native\n                      tooltip={t(\"delete\")}\n                      variant={`destructive`}\n                      onClick={async () => {\n                        const resp = await deleteInvitation(invitation.code);\n                        withNotify(t, resp, true);\n\n                        resp.status && (await update());\n                      }}\n                    >\n                      <Trash className={`h-4 w-4`} />\n                    </OperationAction>\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n          <PaginationAction\n            current={page}\n            total={data.total}\n            onPageChange={setPage}\n            offset\n          />\n        </>\n      ) : (\n        <div className={`empty`}>\n          {loading ? (\n            <Loader2 className={`w-6 h-6 inline-block animate-spin`} />\n          ) : (\n            <p>{t(\"admin.empty\")}</p>\n          )}\n        </div>\n      )}\n      <div className={`invitation-action`}>\n        <div className={`grow`} />\n        <Button variant={`outline`} size={`icon`} onClick={update}>\n          <RotateCw className={`h-4 w-4`} />\n        </Button>\n        <GenerateDialog update={update} />\n      </div>\n    </div>\n  );\n}\n\nexport default InvitationTable;\n"
  },
  {
    "path": "app/src/components/admin/MenuBar.tsx",
    "content": "import { useDispatch, useSelector } from \"react-redux\";\nimport { closeMenu, selectMenu } from \"@/store/menu.ts\";\nimport React, { useMemo } from \"react\";\nimport {\n  BookCopy,\n  CalendarRange,\n  CloudCog,\n  CopyrightIcon,\n  CreditCard,\n  FileClock,\n  Gauge,\n  GitFork,\n  History,\n  Radio,\n  ServerCrash,\n  Settings,\n  Users,\n} from \"lucide-react\";\nimport router from \"@/router.tsx\";\nimport { useLocation } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\nimport { mobile } from \"@/utils/device.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\n\ntype MenuItemProps = {\n  title: string;\n  icon: React.ReactNode;\n  path: string;\n  exit?: boolean;\n  pro?: boolean;\n};\n\nfunction MenuItem({ title, icon, path, exit, pro }: MenuItemProps) {\n  const location = useLocation();\n  const dispatch = useDispatch();\n  const active = useMemo(\n    () =>\n      !exit &&\n      (location.pathname === `/admin${path}` ||\n        location.pathname + \"/\" === `/admin${path}`),\n    [location.pathname, path],\n  );\n\n  const redirect = async () => {\n    if (exit) return await router.navigate(\"/\");\n\n    if (mobile) dispatch(closeMenu());\n    await router.navigate(`/admin${path}`);\n  };\n\n  return (\n    <div className={cn(\"menu-item\", active && \"active\")} onClick={redirect}>\n      <div className={`menu-item-icon`}>{icon}</div>\n      <div className={`menu-item-title`}>{title}</div>\n\n      {pro && (\n        <Badge className={`menu-item-badge ml-2`} variant={`gold`}>\n          Pro\n        </Badge>\n      )}\n    </div>\n  );\n}\n\nfunction MenuBar() {\n  const { t } = useTranslation();\n  const open = useSelector(selectMenu);\n  return (\n    <div className={cn(\"admin-menu\", open && \"open\")}>\n      <MenuItem title={t(\"admin.dashboard\")} icon={<Gauge />} path={\"/\"} />\n      <MenuItem title={t(\"admin.user\")} icon={<Users />} path={\"/users\"} />\n      <MenuItem\n        title={t(\"admin.market.title\")}\n        icon={<BookCopy />}\n        path={\"/market\"}\n      />\n      <MenuItem\n        title={t(\"admin.broadcast\")}\n        icon={<Radio />}\n        path={\"/broadcast\"}\n      />\n      <MenuItem\n        title={t(\"admin.channel\")}\n        icon={<GitFork />}\n        path={\"/channel\"}\n      />\n      <MenuItem title={t(\"admin.prize\")} icon={<CloudCog />} path={\"/charge\"} />\n      <MenuItem\n        title={t(\"admin.subscription\")}\n        icon={<CalendarRange />}\n        path={\"/subscription\"}\n      />\n      <MenuItem\n        title={t(\"admin.payment\")}\n        icon={<CreditCard />}\n        path={\"/pay\"}\n        pro\n      />\n      <MenuItem\n        pro\n        title={t(\"record.title\")}\n        icon={<History />}\n        path={\"/record\"}\n      />\n      <MenuItem\n        // pro\n        title={t(\"admin.settings\")}\n        icon={<Settings />}\n        path={\"/system\"}\n      />\n      <MenuItem\n        title={t(\"admin.logger.title\")}\n        icon={<FileClock />}\n        path={\"/logger\"}\n      />\n      <MenuItem\n        pro\n        title={t(\"admin.cdn.warmup\")}\n        icon={<ServerCrash />}\n        path={\"/warmup\"}\n      />\n      <MenuItem\n        pro\n        title={t(\"admin.license.title\")}\n        icon={<CopyrightIcon />}\n        path={\"/license\"}\n      />\n    </div>\n  );\n}\n\nexport default MenuBar;\n"
  },
  {
    "path": "app/src/components/admin/RedeemTable.tsx",
    "content": "import {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table.tsx\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useState } from \"react\";\nimport { RedeemForm, RedeemResponse } from \"@/admin/types.ts\";\nimport { Button, TemporaryButton } from \"@/components/ui/button.tsx\";\nimport { Copy, Download, Loader2, RotateCw, Trash } from \"lucide-react\";\nimport {\n  deleteRedeem,\n  generateRedeem,\n  getRedeemList,\n} from \"@/admin/api/chart.ts\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport { copyClipboard, saveAsFile } from \"@/utils/dom.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport { PaginationAction } from \"@/components/ui/pagination.tsx\";\nimport OperationAction from \"@/components/OperationAction.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport StateBadge from \"@/components/admin/common/StateBadge.tsx\";\nimport { toast } from \"sonner\";\n\nfunction GenerateDialog({ update }: { update: () => void }) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState<boolean>(false);\n  const [quota, setQuota] = useState<string>(\"5\");\n  const [number, setNumber] = useState<string>(\"1\");\n  const [data, setData] = useState<string>(\"\");\n\n  function getNumber(value: string): string {\n    return value.replace(/[^\\d.]/g, \"\");\n  }\n\n  async function generateCode() {\n    const data = await generateRedeem(Number(quota), Number(number));\n    if (data.status) {\n      setData(data.data.join(\"\\n\"));\n      update();\n    } else {\n      toast.error(t(\"admin.error\"), {\n        description: data.message,\n      });\n    }\n  }\n\n  function close() {\n    setQuota(\"5\");\n    setNumber(\"1\");\n\n    setOpen(false);\n    setData(\"\");\n  }\n\n  function downloadCode() {\n    return saveAsFile(\"code.txt\", data);\n  }\n\n  return (\n    <>\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogTrigger asChild>\n          <Button>{t(\"admin.generate\")}</Button>\n        </DialogTrigger>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.generate\")}</DialogTitle>\n            <DialogDescription className={`pt-2`}>\n              <div className={`redeem-row`}>\n                <p className={`mr-4`}>{t(\"admin.quota\")}</p>\n                <Input\n                  value={quota}\n                  onChange={(e) => setQuota(getNumber(e.target.value))}\n                />\n              </div>\n              <div className={`redeem-row`}>\n                <p className={`mr-4`}>{t(\"admin.number\")}</p>\n                <Input\n                  value={number}\n                  onChange={(e) => setNumber(getNumber(e.target.value))}\n                />\n              </div>\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              unClickable\n              variant={`outline`}\n              onClick={() => setOpen(false)}\n            >\n              {t(\"admin.cancel\")}\n            </Button>\n            <Button\n              unClickable\n              variant={`default`}\n              loading={true}\n              onClick={generateCode}\n            >\n              {t(\"admin.confirm\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n      <Dialog\n        open={data !== \"\"}\n        onOpenChange={(state: boolean) => {\n          if (!state) close();\n        }}\n      >\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.generate-result\")}</DialogTitle>\n            <DialogDescription className={`pt-4`}>\n              <Textarea value={data} rows={12} readOnly />\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button unClickable variant={`outline`} onClick={close}>\n              {t(\"close\")}\n            </Button>\n            <Button unClickable variant={`default`} onClick={downloadCode}>\n              <Download className={`h-4 w-4 mr-2`} />\n              {t(\"download\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\nfunction RedeemTable() {\n  const { t } = useTranslation();\n  const [data, setData] = useState<RedeemForm>({\n    total: 0,\n    data: [],\n  });\n  const [loading, setLoading] = useState<boolean>(false);\n  const [page, setPage] = useState<number>(0);\n\n  async function update() {\n    setLoading(true);\n    const resp = await getRedeemList(page);\n    setLoading(false);\n    if (resp.status) setData(resp as RedeemResponse);\n    else\n      toast.error(t(\"admin.error\"), {\n        description: resp.message,\n      });\n  }\n  useEffectAsync(update, [page]);\n\n  return (\n    <div className={`redeem-table`}>\n      {(data.data && data.data.length > 0) || page > 0 ? (\n        <>\n          <Table>\n            <TableHeader>\n              <TableRow className={`select-none whitespace-nowrap`}>\n                <TableHead>{t(\"admin.redeem.code\")}</TableHead>\n                <TableHead>{t(\"admin.redeem.quota\")}</TableHead>\n                <TableHead>{t(\"admin.used\")}</TableHead>\n                <TableHead>{t(\"admin.created-at\")}</TableHead>\n                <TableHead>{t(\"admin.used-at\")}</TableHead>\n                <TableHead>{t(\"admin.action\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {(data.data || []).map((redeem, idx) => (\n                <TableRow key={idx} className={`whitespace-nowrap`}>\n                  <TableCell>{redeem.code}</TableCell>\n                  <TableCell>\n                    <Badge variant={`outline`}>{redeem.quota}</Badge>\n                  </TableCell>\n                  <TableCell>\n                    <StateBadge state={redeem.used} />\n                  </TableCell>\n                  <TableCell>{redeem.created_at}</TableCell>\n                  <TableCell>{redeem.updated_at}</TableCell>\n                  <TableCell className={`flex gap-2`}>\n                    <TemporaryButton\n                      size={`icon`}\n                      variant={`outline`}\n                      onClick={() => copyClipboard(redeem.code)}\n                    >\n                      <Copy className={`h-4 w-4`} />\n                    </TemporaryButton>\n                    <OperationAction\n                      native\n                      tooltip={t(\"delete\")}\n                      variant={`destructive`}\n                      onClick={async () => {\n                        const resp = await deleteRedeem(redeem.code);\n                        withNotify(t, resp, true);\n\n                        resp.status && (await update());\n                      }}\n                    >\n                      <Trash className={`h-4 w-4`} />\n                    </OperationAction>\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n          <PaginationAction\n            current={page}\n            total={data.total}\n            onPageChange={setPage}\n            offset\n          />\n        </>\n      ) : loading ? (\n        <div className={`flex flex-col my-4 items-center`}>\n          <Loader2 className={`w-6 h-6 inline-block animate-spin`} />\n        </div>\n      ) : (\n        <p className={`empty`}>{t(\"admin.empty\")}</p>\n      )}\n      <div className={`redeem-action`}>\n        <div className={`grow`} />\n        <Button variant={`outline`} size={`icon`} onClick={update}>\n          <RotateCw className={`h-4 w-4`} />\n        </Button>\n        <GenerateDialog update={update} />\n      </div>\n    </div>\n  );\n}\n\nexport default RedeemTable;\n"
  },
  {
    "path": "app/src/components/admin/UserTable.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo, useReducer, useState } from \"react\";\nimport {\n  CommonResponse,\n  UserData,\n  UserForm,\n  UserResponse,\n} from \"@/admin/types.ts\";\nimport {\n  banUserOperation,\n  getUserList,\n  initialUserFilter,\n  quotaOperation,\n  releaseUsageOperation,\n  setAdminOperation,\n  subscriptionLevelOperation,\n  subscriptionOperation,\n  updateEmail,\n  updatePassword,\n  UserFilterProps,\n} from \"@/admin/api/chart.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table.tsx\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  ArrowDownNarrowWide,\n  CalendarCheck2,\n  CalendarClock,\n  CalendarOff,\n  CalendarPlus,\n  CloudCog,\n  CloudFog,\n  Filter,\n  KeyRound,\n  Loader2,\n  Mail,\n  MinusCircle,\n  MoreHorizontal,\n  PlusCircle,\n  RotateCw,\n  Search,\n  Shield,\n  ShieldMinus,\n} from \"lucide-react\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport PopupDialog, { popupTypes } from \"@/components/PopupDialog.tsx\";\nimport { getNumber, isEnter, parseNumber } from \"@/utils/base.ts\";\nimport { useSelector } from \"react-redux\";\nimport { selectUsername } from \"@/store/auth.ts\";\nimport { PaginationAction } from \"@/components/ui/pagination.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTrigger,\n  DialogTitle,\n  DialogFooter,\n  DialogAction,\n} from \"@/components/ui/dialog.tsx\";\nimport { RadioBox } from \"@/components/ui/radio-box.tsx\";\nimport { formReducer } from \"@/utils/form.ts\";\nimport { Separator } from \"@/components/ui/separator.tsx\";\nimport { toast } from \"sonner\";\nimport { Badge } from \"../ui/badge\";\n\ntype OperationMenuProps = {\n  user: UserData;\n  onRefresh?: () => void;\n};\n\nexport enum UserType {\n  normal = \"normal\",\n  basic_plan = \"basic_plan\",\n  standard_plan = \"standard_plan\",\n  pro_plan = \"pro_plan\",\n}\n\nexport const userTypeArray = [\n  UserType.normal,\n  UserType.basic_plan,\n  UserType.standard_plan,\n  UserType.pro_plan,\n];\n\nfunction doToast(t: any, resp: CommonResponse) {\n  if (!resp.status)\n    toast.error(t(\"admin.operate-failed\"), {\n      description: t(\"admin.operate-failed-prompt\", {\n        reason: resp.message || resp.error,\n      }),\n    });\n  else\n    toast.success(t(\"admin.operate-success\"), {\n      description: t(\"admin.operate-success-prompt\"),\n    });\n}\n\nfunction OperationMenu({ user, onRefresh }: OperationMenuProps) {\n  const { t } = useTranslation();\n\n  const username = useSelector(selectUsername);\n\n  const [passwordOpen, setPasswordOpen] = useState<boolean>(false);\n  const [emailOpen, setEmailOpen] = useState<boolean>(false);\n  const [quotaOpen, setQuotaOpen] = useState<boolean>(false);\n  const [quotaSetOpen, setQuotaSetOpen] = useState<boolean>(false);\n  const [subscriptionOpen, setSubscriptionOpen] = useState<boolean>(false);\n  const [subscriptionLevelOpen, setSubscriptionLevelOpen] =\n    useState<boolean>(false);\n  const [releaseOpen, setReleaseOpen] = useState<boolean>(false);\n  const [banOpen, setBanOpen] = useState<boolean>(false);\n  const [adminOpen, setAdminOpen] = useState<boolean>(false);\n\n  return (\n    <>\n      <PopupDialog\n        destructive={true}\n        type={popupTypes.Text}\n        title={t(\"admin.password-action\")}\n        name={t(\"auth.password\")}\n        description={t(\"admin.password-action-desc\")}\n        open={passwordOpen}\n        setOpen={setPasswordOpen}\n        defaultValue={\"\"}\n        onSubmit={async (password) => {\n          const resp = await updatePassword(user.id, password);\n          doToast(t, resp);\n\n          if (resp.status) {\n            username === user.username && location.reload();\n            onRefresh?.();\n          }\n\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        destructive={true}\n        type={popupTypes.Text}\n        title={t(\"admin.email-action\")}\n        name={t(\"admin.email\")}\n        description={t(\"admin.email-action-desc\")}\n        open={emailOpen}\n        setOpen={setEmailOpen}\n        defaultValue={user.email}\n        onSubmit={async (email) => {\n          const resp = await updateEmail(user.id, email);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.Number}\n        title={t(\"admin.quota-action\")}\n        name={t(\"admin.quota\")}\n        description={t(\"admin.quota-action-desc\")}\n        defaultValue={\"0\"}\n        onValueChange={getNumber}\n        open={quotaOpen}\n        setOpen={setQuotaOpen}\n        componentProps={{ acceptNegative: true }}\n        onSubmit={async (value) => {\n          const quota = parseNumber(value);\n          const resp = await quotaOperation(user.id, quota);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.Number}\n        title={t(\"admin.quota-set-action\")}\n        name={t(\"admin.quota\")}\n        description={t(\"admin.quota-set-action-desc\")}\n        defaultValue={user.quota.toFixed(2)}\n        onValueChange={getNumber}\n        open={quotaSetOpen}\n        setOpen={setQuotaSetOpen}\n        componentProps={{ acceptNegative: true }}\n        onSubmit={async (value) => {\n          const quota = parseNumber(value);\n          const resp = await quotaOperation(user.id, quota, true);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.Clock}\n        title={t(\"admin.subscription-action\")}\n        description={t(\"admin.subscription-action-desc\", {\n          username: user.username,\n        })}\n        defaultValue={user.expired_at || new Date().toISOString().slice(0, 19).replace('T', ' ')}\n        open={subscriptionOpen}\n        setOpen={setSubscriptionOpen}\n        onSubmit={async (value) => {\n          const resp = await subscriptionOperation(user.id, value);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.List}\n        title={t(\"admin.subscription-level\")}\n        name={t(\"admin.level\")}\n        description={t(\"admin.subscription-level-desc\")}\n        defaultValue={userTypeArray[user.level]}\n        params={{\n          dataList: userTypeArray,\n          dataListTranslated: \"admin.identity\",\n        }}\n        open={subscriptionLevelOpen}\n        setOpen={setSubscriptionLevelOpen}\n        onSubmit={async (value) => {\n          const level = userTypeArray.indexOf(value as UserType);\n          console.log(level);\n          const resp = await subscriptionLevelOperation(user.id, level);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        type={popupTypes.Empty}\n        title={t(\"admin.release-subscription-action\")}\n        name={t(\"admin.release-subscription\")}\n        description={t(\"admin.release-subscription-action-desc\")}\n        open={releaseOpen}\n        setOpen={setReleaseOpen}\n        onSubmit={async () => {\n          const resp = await releaseUsageOperation(user.id);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        disabled={username === user.username}\n        destructive={true}\n        type={popupTypes.Empty}\n        title={user.is_banned ? t(\"admin.unban-action\") : t(\"admin.ban-action\")}\n        description={\n          user.is_banned\n            ? t(\"admin.unban-action-desc\")\n            : t(\"admin.ban-action-desc\")\n        }\n        open={banOpen}\n        setOpen={setBanOpen}\n        onSubmit={async () => {\n          const resp = await banUserOperation(user.id, !user.is_banned);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n      <PopupDialog\n        disabled={username === user.username}\n        destructive={true}\n        type={popupTypes.Empty}\n        title={\n          user.is_admin\n            ? t(\"admin.cancel-admin-action\")\n            : t(\"admin.set-admin-action\")\n        }\n        description={\n          user.is_admin\n            ? t(\"admin.cancel-admin-action-desc\")\n            : t(\"admin.set-admin-action-desc\")\n        }\n        open={adminOpen}\n        setOpen={setAdminOpen}\n        onSubmit={async () => {\n          const resp = await setAdminOperation(user.id, !user.is_admin);\n          doToast(t, resp);\n\n          if (resp.status) onRefresh?.();\n          return resp.status;\n        }}\n      />\n\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button variant={`outline`} size={`icon`}>\n            <MoreHorizontal className={`h-4 w-4`} />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent className={`min-w-[8.75rem]`}>\n          <DropdownMenuItem onClick={() => setPasswordOpen(true)}>\n            <KeyRound className={`h-4 w-4 mr-2`} />\n            {t(\"admin.password-action\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setEmailOpen(true)}>\n            <Mail className={`h-4 w-4 mr-2`} />\n            {t(\"admin.email-action\")}\n          </DropdownMenuItem>\n          {user.is_banned ? (\n            <DropdownMenuItem onClick={() => setBanOpen(true)}>\n              <PlusCircle className={`h-4 w-4 mr-2`} />\n              {t(\"admin.unban-action\")}\n            </DropdownMenuItem>\n          ) : (\n            <DropdownMenuItem onClick={() => setBanOpen(true)}>\n              <MinusCircle className={`h-4 w-4 mr-2`} />\n              {t(\"admin.ban-action\")}\n            </DropdownMenuItem>\n          )}\n          {user.is_admin ? (\n            <DropdownMenuItem onClick={() => setAdminOpen(true)}>\n              <ShieldMinus className={`h-4 w-4 mr-2`} />\n              {t(\"admin.cancel-admin-action\")}\n            </DropdownMenuItem>\n          ) : (\n            <DropdownMenuItem onClick={() => setAdminOpen(true)}>\n              <Shield className={`h-4 w-4 mr-2`} />\n              {t(\"admin.set-admin-action\")}\n            </DropdownMenuItem>\n          )}\n          <DropdownMenuItem onClick={() => setQuotaOpen(true)}>\n            <CloudFog className={`h-4 w-4 mr-2`} />\n            {t(\"admin.quota-action\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setQuotaSetOpen(true)}>\n            <CloudCog className={`h-4 w-4 mr-2`} />\n            {t(\"admin.quota-set-action\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setSubscriptionOpen(true)}>\n            <CalendarClock className={`h-4 w-4 mr-2`} />\n            {t(\"admin.subscription-action\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setSubscriptionLevelOpen(true)}>\n            <CalendarCheck2 className={`h-4 w-4 mr-2`} />\n            {t(\"admin.subscription-level\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setReleaseOpen(true)}>\n            <CalendarOff className={`h-4 w-4 mr-2`} />\n            {t(\"admin.release-subscription-action\")}\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  );\n}\n\nfunction UserTable() {\n  const { t } = useTranslation();\n  const [data, setData] = useState<UserForm>({\n    total: 0,\n    data: [],\n  });\n  const [page, setPage] = useState<number>(0);\n  const [search, setSearch] = useState<string>(\"\");\n  const [loading, setLoading] = useState<boolean>(false);\n\n  const [filter, filterDispatch] = useReducer(formReducer<UserFilterProps>(), {\n    ...initialUserFilter,\n  });\n\n  const [filterDialog, setFilterDialog] = useState<boolean>(false);\n\n  const filterConds = useMemo((): number => {\n    return Object.values(filter).filter(\n      (value) => value !== \"all\" && value !== \"id-asc\",\n    ).length;\n  }, [filter]);\n\n  async function update() {\n    setLoading(true);\n    const resp = await getUserList(page, search, filter);\n    setLoading(false);\n    if (resp.status) setData(resp as UserResponse);\n    else\n      toast.error(t(\"admin.error\"), {\n        description: resp.message,\n      });\n  }\n  useEffectAsync(update, [page]);\n\n  return (\n    <div className={`user-table`}>\n      <div className={`flex flex-row mb-6`}>\n        <Dialog open={filterDialog} onOpenChange={setFilterDialog}>\n          <DialogTrigger asChild>\n            <Button size={`icon`} className={`shrink-0 mr-2`} onClick={update}>\n              <Filter className={`h-4 w-4`} />\n              {filterConds > 0 && (\n                <span className={`text-xs`}>{filterConds}</span>\n              )}\n            </Button>\n          </DialogTrigger>\n          <DialogContent>\n            <DialogTitle>{t(\"filter.filter\")}</DialogTitle>\n            <DialogDescription>\n              {t(\"filter.conds\", { count: filterConds })}\n            </DialogDescription>\n            <div className={`flex flex-col`}>\n              <RadioBox\n                title={t(\"filter.admin\")}\n                icon={<Shield />}\n                prefix=\"admin\"\n                items={[\n                  { id: \"all\", value: t(\"filter.all\") },\n                  { id: \"yes\", value: t(\"filter.admin\") },\n                  { id: \"no\", value: t(\"filter.not-admin\") },\n                ]}\n                value={filter.admin}\n                onValueChange={(value) =>\n                  filterDispatch({ type: \"update:admin\", value })\n                }\n              />\n              <RadioBox\n                title={t(\"filter.ban\")}\n                icon={<ShieldMinus />}\n                prefix=\"ban\"\n                items={[\n                  { id: \"all\", value: t(\"filter.all\") },\n                  { id: \"yes\", value: t(\"filter.banned\") },\n                  { id: \"no\", value: t(\"filter.not-banned\") },\n                ]}\n                value={filter.ban}\n                onValueChange={(value) =>\n                  filterDispatch({ type: \"update:ban\", value })\n                }\n              />\n              <RadioBox\n                title={t(\"filter.plan\")}\n                icon={<CalendarPlus />}\n                prefix=\"plan\"\n                items={[\n                  { id: \"all\", value: t(\"filter.all\") },\n                  { id: \"yes\", value: t(\"filter.subscribed\") },\n                  { id: \"no\", value: t(\"filter.unsubscribed\") },\n                ]}\n                value={filter.plan}\n                onValueChange={(value) =>\n                  filterDispatch({ type: \"update:plan\", value })\n                }\n              />\n              <Separator />\n              <RadioBox\n                title={t(\"filter.sorts.sort\")}\n                icon={<ArrowDownNarrowWide />}\n                prefix=\"sort\"\n                colLayout\n                className={`mt-2`}\n                items={[\n                  // id-desc, id-asc\n                  { id: \"id-asc\", value: t(\"filter.sorts.id-asc\") },\n                  { id: \"id-desc\", value: t(\"filter.sorts.id-desc\") },\n\n                  // quota-desc, quota-asc\n                  { id: \"quota-asc\", value: t(\"filter.sorts.quota-asc\") },\n                  { id: \"quota-desc\", value: t(\"filter.sorts.quota-desc\") },\n\n                  // used-quota-desc, used-quota-asc\n                  {\n                    id: \"used-quota-asc\",\n                    value: t(\"filter.sorts.used-quota-asc\"),\n                  },\n                  {\n                    id: \"used-quota-desc\",\n                    value: t(\"filter.sorts.used-quota-desc\"),\n                  },\n\n                  // plan-desc, plan-asc\n                  { id: \"plan-asc\", value: t(\"filter.sorts.plan-asc\") },\n                  { id: \"plan-desc\", value: t(\"filter.sorts.plan-desc\") },\n                ]}\n                value={filter.sort}\n                onValueChange={(value) =>\n                  filterDispatch({ type: \"update:sort\", value })\n                }\n              />\n            </div>\n            <DialogFooter>\n              <DialogAction onClick={() => setFilterDialog(false)}>\n                {t(\"confirm\")}\n              </DialogAction>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n        <Input\n          className={`search`}\n          placeholder={t(\"admin.search-username\")}\n          value={search}\n          onChange={(e) => setSearch(e.target.value)}\n          onKeyDown={async (e) => {\n            if (isEnter(e)) await update();\n          }}\n        />\n        <Button size={`icon`} className={`flex-shrink-0 ml-2`} onClick={update}>\n          <Search className={`h-4 w-4`} />\n        </Button>\n      </div>\n      {(data.data && data.data.length > 0) || page > 0 ? (\n        <>\n          <Table>\n            <TableHeader>\n              <TableRow className={`select-none whitespace-nowrap`}>\n                <TableHead>ID</TableHead>\n                <TableHead>{t(\"admin.username\")}</TableHead>\n                <TableHead>{t(\"admin.email\")}</TableHead>\n                <TableHead>{t(\"admin.quota\")}</TableHead>\n                <TableHead>{t(\"admin.used-quota\")}</TableHead>\n                <TableHead>{t(\"admin.is-subscribed\")}</TableHead>\n                <TableHead>{t(\"admin.level\")}</TableHead>\n                <TableHead>{t(\"admin.total-month\")}</TableHead>\n                <TableHead>{t(\"admin.expired-at\")}</TableHead>\n                <TableHead>{t(\"admin.is-banned\")}</TableHead>\n                <TableHead>{t(\"admin.is-admin\")}</TableHead>\n                <TableHead>{t(\"admin.action\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {(data.data || []).map((user, idx) => (\n                <TableRow key={idx}>\n                  <TableCell>{user.id}</TableCell>\n                  <TableCell className={`whitespace-nowrap`}>\n                    {user.username}\n                  </TableCell>\n                  <TableCell className={`whitespace-nowrap`}>\n                    {user.email || \"-\"}\n                  </TableCell>\n                  <TableCell>{user.quota}</TableCell>\n                  <TableCell>{user.used_quota}</TableCell>\n                  <TableCell>\n                    {t(user.is_subscribed.toString())}\n                    <Tips\n                      className={`inline-block`}\n                      content={t(\"admin.is-subscribed-tips\")}\n                    />\n                  </TableCell>\n                  <TableCell className={`whitespace-nowrap`}>\n                    <Badge variant={`outline`}>\n                      {t(`admin.identity.${userTypeArray[user.level]}`)}\n                    </Badge>\n                  </TableCell>\n                  <TableCell>{user.total_month}</TableCell>\n                  <TableCell className={`whitespace-nowrap`}>\n                    {user.expired_at || \"-\"}\n                  </TableCell>\n                  <TableCell>{t(user.is_banned.toString())}</TableCell>\n                  <TableCell>{t(user.is_admin.toString())}</TableCell>\n                  <TableCell>\n                    <OperationMenu user={user} onRefresh={update} />\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n          <PaginationAction\n            current={page}\n            total={data.total}\n            onPageChange={setPage}\n            offset\n          />\n        </>\n      ) : loading ? (\n        <div className={`flex flex-col mb-4 mt-12 items-center`}>\n          <Loader2 className={`w-6 h-6 inline-block animate-spin`} />\n        </div>\n      ) : (\n        <div className={`empty`}>\n          <p>{t(\"admin.empty\")}</p>\n        </div>\n      )}\n      <div className={`user-action`}>\n        <div className={`grow`} />\n        <Button variant={`outline`} size={`icon`} onClick={update}>\n          <RotateCw className={`h-4 w-4`} />\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport default UserTable;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/BillingChart.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { AreaChart } from \"@tremor/react\";\nimport { useCurrency } from \"@/store/info\";\n\ntype BillingChartProps = {\n  labels: string[];\n  datasets: number[];\n};\nfunction BillingChart({ labels, datasets }: BillingChartProps) {\n  const { t } = useTranslation();\n  const { symbol } = useCurrency();\n\n  const data = useMemo(() => {\n    return datasets.map((data, index) => ({\n      date: labels[index],\n      [t(\"admin.billing\")]: data,\n    }));\n  }, [labels, datasets, t(\"admin.billing\")]);\n\n  const mrr = useMemo(() => {\n    // datasets sum\n    return datasets.reduce((acc, curr) => acc + curr, 0);\n  }, [datasets]);\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <p>{t(\"admin.billing-chart\")}</p>\n        {labels.length === 0 && (\n          <Loader2 className={`h-4 w-4 inline-block animate-spin`} />\n        )}\n\n        <div\n          className={`ml-auto bg-orange-500/20 text-orange-500 px-1 rounded-sm text-xs py-0.5`}\n        >\n          MRR {symbol}\n          {mrr.toFixed(2)}\n        </div>\n      </div>\n      <AreaChart\n        className={`common-chart`}\n        data={data}\n        categories={[t(\"admin.billing\")]}\n        index={\"date\"}\n        colors={[\"orange\"]}\n        showAnimation={true}\n        valueFormatter={(value) => `${symbol}${value.toFixed(2)}`}\n      />\n    </div>\n  );\n}\n\nexport default BillingChart;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/BroadcastTable.tsx",
    "content": "import {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table.tsx\";\nimport { useState } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { selectInit } from \"@/store/auth.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  BroadcastInfo,\n  createBroadcast,\n  getBroadcastList,\n  removeBroadcast,\n  updateBroadcast,\n} from \"@/api/broadcast.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { extractMessage } from \"@/utils/processor.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  AlertCircle,\n  Edit,\n  Eye,\n  Loader2,\n  MoreVertical,\n  Plus,\n  RotateCcw,\n  Trash,\n} from \"lucide-react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport EditorProvider from \"@/components/EditorProvider.tsx\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport {\n  AlertDialog,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog.tsx\";\nimport { DialogClose } from \"@radix-ui/react-dialog\";\nimport { toast } from \"sonner\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\n\ntype CreateBroadcastDialogProps = {\n  onCreated?: () => void;\n};\n\nfunction CreateBroadcastDialog(props: CreateBroadcastDialogProps) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState<boolean>(false);\n  const [content, setContent] = useState<string>(\"\");\n  const [notifyAll, setNotifyAll] = useState<boolean>(false);\n\n  async function postBroadcast() {\n    const broadcast = content.trim();\n    if (broadcast.length === 0) return;\n    const resp = await createBroadcast(broadcast, notifyAll);\n    if (resp.status) {\n      toast.success(t(\"admin.post-success\"), {\n        description: t(\"admin.post-success-prompt\"),\n      });\n\n      setContent(\"\");\n      setNotifyAll(false);\n\n      setOpen(false);\n      props.onCreated?.();\n    } else {\n      toast.error(t(\"admin.post-failed\"), {\n        description: t(\"admin.post-failed-prompt\", { reason: resp.error }),\n      });\n    }\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant={`default`}>\n          <Plus className={`w-4 h-4 mr-1`} />\n          {t(\"admin.create-broadcast\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"admin.create-broadcast\")}</DialogTitle>\n          <DialogDescription asChild>\n            <div className={`pt-4`}>\n              <p className=\"text-sm text-secondary mb-6 border p-2 rounded-md\">\n                {t(\"admin.broadcast-tip\")}\n              </p>\n              <Textarea\n                placeholder={t(\"admin.broadcast-placeholder\")}\n                value={content}\n                rows={5}\n                onChange={(e) => setContent(e.target.value)}\n              />\n\n              <div className=\"flex items-center space-x-2 mt-4\">\n                <Checkbox\n                  id=\"notify-all\"\n                  checked={notifyAll}\n                  onCheckedChange={(checked) =>\n                    setNotifyAll(checked as boolean)\n                  }\n                />\n                <Label\n                  htmlFor=\"notify-all\"\n                  className=\"text-sm font-medium text-primary cursor-pointer\"\n                >\n                  {t(\"admin.notify-all\")}\n                </Label>\n              </div>\n            </div>\n          </DialogDescription>\n        </DialogHeader>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button variant={`outline`}>{t(\"admin.cancel\")}</Button>\n          </DialogClose>\n          <Button\n            unClickable\n            variant={`default`}\n            onClick={postBroadcast}\n            loading={true}\n          >\n            {t(\"admin.post\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ntype BroadcastItemProps = {\n  item: BroadcastInfo;\n  onRefresh?: () => void;\n};\n\nfunction BroadcastItem({ item, onRefresh }: BroadcastItemProps) {\n  const { t } = useTranslation();\n\n  const [open, setOpen] = useState<boolean>(false);\n  const [dialogOpen, setDialogOpen] = useState<boolean>(false);\n  const [value, setValue] = useState<string>(\"\");\n\n  return (\n    <TableRow>\n      <EditorProvider\n        title={t(\"admin.view\")}\n        value={value || item.content}\n        onChange={setValue}\n        open={open}\n        setOpen={setOpen}\n        submittable\n        onSubmit={async (value: string) => {\n          const resp = await updateBroadcast(item.index, value);\n          withNotify(t, resp, true);\n          onRefresh?.();\n        }}\n      />\n      <AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>\n        <AlertDialogTrigger asChild></AlertDialogTrigger>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>{t(\"admin.delete-broadcast\")}</AlertDialogTitle>\n            <AlertDialogDescription>\n              <p>{t(\"admin.delete-broadcast-desc\")}</p>\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <Button unClickable variant={`outline`}>\n              {t(\"cancel\")}\n            </Button>\n            <Button\n              unClickable\n              variant={`destructive`}\n              onClick={async () => {\n                const resp = await removeBroadcast(item.index);\n                withNotify(t, resp, true);\n                onRefresh?.();\n                if (resp.status) setDialogOpen(false);\n              }}\n            >\n              {t(\"delete\")}\n            </Button>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n      <TableCell>{item.index}</TableCell>\n      <TableCell>{extractMessage(item.content, 25)}</TableCell>\n      <TableCell>{item.poster}</TableCell>\n      <TableCell>{item.created_at}</TableCell>\n      <TableCell>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant={`outline`} size={`icon`}>\n              <MoreVertical className={`w-4 h-4`} />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align={`end`}>\n            <DropdownMenuItem onClick={() => setOpen(true)}>\n              <Eye className={`w-4 h-4 mr-1.5`} />\n              {t(\"admin.view\")}\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => setOpen(true)}>\n              <Edit className={`w-4 h-4 mr-1.5`} />\n              {t(\"edit\")}\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => setDialogOpen(true)}>\n              <Trash className={`w-4 h-4 mr-1.5`} />\n              {t(\"delete\")}\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nfunction BroadcastTable() {\n  const { t } = useTranslation();\n  const init = useSelector(selectInit);\n  const [data, setData] = useState<BroadcastInfo[]>([]);\n  const [loading, setLoading] = useState<boolean>(false);\n\n  const doRefresh = async () => {\n    if (!init) return;\n\n    setLoading(true);\n    setData(await getBroadcastList());\n    setLoading(false);\n  };\n\n  useEffectAsync(doRefresh, [init]);\n\n  return (\n    <div className={`broadcast-table whitespace-nowrap`}>\n      <div className={`broadcast-action flex flex-row flex-nowrap w-full mb-4`}>\n        <Button\n          variant={`outline`}\n          size={`icon`}\n          className={`select-none`}\n          onClick={async () => {\n            setData(await getBroadcastList());\n          }}\n        >\n          <RotateCcw className={`w-4 h-4`} />\n        </Button>\n        <div className={`grow`} />\n        <CreateBroadcastDialog onCreated={doRefresh} />\n      </div>\n      <Alert className={`pb-2 mb-4`}>\n        <AlertCircle className={`h-4 w-4`} />\n        <AlertDescription className={`break-all whitespace-pre-wrap`}>\n          {t(\"admin.broadcast-tip\")}\n        </AlertDescription>\n      </Alert>\n\n      {data.length ? (\n        <Table>\n          <TableHeader>\n            <TableRow className={`select-none whitespace-nowrap`}>\n              <TableHead>ID</TableHead>\n              <TableHead>{t(\"admin.broadcast-content\")}</TableHead>\n              <TableHead>{t(\"admin.poster\")}</TableHead>\n              <TableHead>{t(\"admin.post-at\")}</TableHead>\n              <TableHead>{t(\"admin.action\")}</TableHead>\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {data.map((item, idx) => (\n              <BroadcastItem key={idx} item={item} onRefresh={doRefresh} />\n            ))}\n          </TableBody>\n        </Table>\n      ) : (\n        <div className={`text-center select-none my-8`}>\n          {loading ? (\n            <Loader2 className={`w-6 h-6 inline-block animate-spin`} />\n          ) : (\n            <p className={`empty`}>{t(\"admin.empty\")}</p>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default BroadcastTable;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/ChannelEditor.tsx",
    "content": "import Tips from \"@/components/Tips.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectGroup,\n  SelectTrigger,\n  SelectValue,\n  NativeSelectTrigger,\n} from \"@/components/ui/select.tsx\";\nimport {\n  Channel,\n  channelGroups,\n  ChannelTypes,\n  getChannelInfo,\n  proxyType,\n  ProxyTypes,\n} from \"@/admin/channel.ts\";\nimport { CommonResponse, withNotify } from \"@/api/common.ts\";\nimport { FlexibleTextarea, Textarea } from \"@/components/ui/textarea.tsx\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMemo, useState } from \"react\";\nimport Required from \"@/components/Require.tsx\";\nimport {\n  BookDashed,\n  Loader2,\n  Paintbrush,\n  Plus,\n  Search,\n  Kanban,\n  X,\n} from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport {\n  Command,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport {\n  createChannel,\n  getChannel,\n  updateChannel,\n} from \"@/admin/api/channel.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport Paragraph, {\n  ParagraphDescription,\n  ParagraphItem,\n  ParagraphSpace,\n} from \"@/components/Paragraph.tsx\";\nimport { MultiCombobox } from \"@/components/ui/multi-combobox.tsx\";\nimport { useChannelModels } from \"@/admin/hook.tsx\";\nimport { isEnter } from \"@/utils/base.ts\";\nimport { TypeBadge } from \"@/components/admin/assemblies/ChannelTable.tsx\";\nimport { useClipboard } from \"@/utils/dom.ts\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\n\ntype CustomActionProps = {\n  onPost: (model: string) => void;\n};\nfunction CustomAction({ onPost }: CustomActionProps) {\n  const { t } = useTranslation();\n  const [model, setModel] = useState(\"\");\n\n  function post() {\n    const data = model.trim();\n    if (data === \"\") return;\n    onPost(data);\n    setModel(\"\");\n  }\n\n  return (\n    <div className={`flex flex-row grow gap-0 custom-action`}>\n      <Input\n        value={model}\n        placeholder={t(\"admin.channels.add-custom-model\")}\n        className={`rounded-r-none`}\n        onChange={(e) => setModel(e.target.value)}\n        onKeyDown={(e) => {\n          if (isEnter(e)) post();\n        }}\n      />\n      <Button className={`rounded-l-none`} onClick={post}>\n        {t(\"add\")}\n      </Button>\n    </div>\n  );\n}\n\nfunction validator(state: Channel): boolean {\n  return (\n    state.name.trim() !== \"\" &&\n    state.models.length > 0 &&\n    state.secret.trim() !== \"\" &&\n    state.endpoint.trim() !== \"\"\n  );\n}\n\nfunction handler(data: Channel): Channel {\n  data.models = data.models.filter((model) => model.trim() !== \"\");\n  data.name = data.name.trim();\n  data.secret = data.secret\n    .trim()\n    .split(\"\\n\")\n    .filter((line) => line.trim() !== \"\")\n    .join(\"\\n\");\n  data.endpoint = data.endpoint.trim();\n  data.endpoint.endsWith(\"/\") && (data.endpoint = data.endpoint.slice(0, -1));\n\n  data.mapper = data.mapper\n    .trim()\n    .split(\"\\n\")\n    .filter((line) => {\n      if (line.trim() === \"\") return false;\n      const values = line.split(\">\");\n      return (\n        values.length === 2 &&\n        values[0].trim() !== \"\" &&\n        values[1].trim() !== \"\"\n      );\n    })\n    .join(\"\\n\");\n  data.group = data.group\n    ? data.group.filter((group) => group.trim() !== \"\")\n    : [];\n\n  if (\n    data.proxy &&\n    data.proxy.proxy.trim() === \"\" &&\n    data.proxy.proxy_type !== 0\n  ) {\n    data.proxy.proxy_type = 0;\n  }\n  return data;\n}\n\ntype ChannelEditorProps = {\n  display: boolean;\n  id: number;\n  setEnabled: (enabled: boolean) => void;\n  edit: Channel;\n  dispatch: (action: any) => void;\n  data: Channel[];\n};\n\nfunction ChannelEditor({\n  display,\n  id,\n  edit,\n  dispatch,\n  setEnabled,\n  data,\n}: ChannelEditorProps) {\n  const { t } = useTranslation();\n  const copy = useClipboard();\n  const info = useMemo(() => getChannelInfo(edit.type), [edit.type]);\n  const { channelModels } = useChannelModels();\n  const unusedModels = useMemo(() => {\n    return channelModels.filter(\n      (model) => !edit.models.includes(model) && model !== \"\",\n    );\n  }, [channelModels, edit.models]);\n  const enabled = useMemo(() => validator(edit), [edit]);\n\n  const [loading, setLoading] = useState(false);\n\n  function close(clear?: boolean) {\n    if (clear) dispatch({ type: \"clear\" });\n    setEnabled(false);\n  }\n\n  async function post() {\n    const data = handler(edit);\n    console.debug(`[channel] preflight channel data`, data);\n\n    const resp =\n      id === -1 ? await createChannel(data) : await updateChannel(id, data);\n    withNotify(t, resp as CommonResponse, true);\n\n    if (resp.status) {\n      close(true);\n    }\n  }\n\n  useEffectAsync(async () => {\n    if (id === -1) dispatch({ type: \"clear\" });\n    else {\n      setLoading(true);\n      const resp = await getChannel(id);\n      setLoading(false);\n      withNotify(t, resp as CommonResponse);\n      if (resp.data) dispatch({ type: \"set\", value: resp.data });\n    }\n  }, [id]);\n\n  return (\n    display && (\n      <div className={`channel-editor`}>\n        <div className={`flex flex-row items-center mb-4`}>\n          {loading && (\n            <Button variant={`outline`} className={`mr-2`}>\n              <Loader2 className={`h-4 w-4 animate-spin mr-2 shrink-0`} />\n              {t(\"admin.channels.loading\")}\n            </Button>\n          )}\n          <Button\n            variant={`outline`}\n            className={`mr-2 shrink-0`}\n            onClick={() => dispatch({ type: \"clear\" })}\n          >\n            <Paintbrush className={`h-4 w-4 mr-1.5`} />\n            {t(\"admin.channels.new\")}\n          </Button>\n          <Select\n            value={``}\n            onValueChange={(value) => {\n              const chan = data.find(\n                (channel) => channel.id.toString() === value,\n              );\n              if (!chan) return;\n\n              dispatch({ type: \"import\", value: chan });\n              console.debug(`[channel] import channel template: `, chan);\n            }}\n          >\n            <NativeSelectTrigger asChild>\n              <Button variant={`outline`} className={`mr-2 shrink-0`}>\n                <Kanban className={`h-4 w-4 mr-1.5`} />\n                {t(\"admin.channels.import\")}\n              </Button>\n            </NativeSelectTrigger>\n            <SelectContent>\n              {(data || []).map((channel, idx) => (\n                <SelectItem key={idx} value={channel.id.toString()}>\n                  <div className={`flex flex-row items-center w-full`}>\n                    <BookDashed className={`h-4 w-4 mr-1`} />\n                    {channel.name}\n\n                    <span className={`text-secondary ml-1`}>#{channel.id}</span>\n                    <TypeBadge\n                      type={channel.type}\n                      className={`ml-1`}\n                      variant={`outline`}\n                    />\n                  </div>\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n          <div className={`grow`} />\n          <Button\n            variant={`outline`}\n            size={`icon`}\n            className={`shrink-0`}\n            onClick={() => close()}\n          >\n            <X className={`h-4 w-4`} />\n          </Button>\n        </div>\n        <div className={`channel-wrapper w-full h-max`}>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              <Required />\n              {t(\"admin.channels.name\")}\n              <Tips content={t(\"admin.channels.name-tip\")} />\n            </div>\n            <Input\n              value={edit.name}\n              placeholder={t(\"admin.channels.name-placeholder\")}\n              onChange={(e) =>\n                dispatch({ type: \"name\", value: e.target.value })\n              }\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              <Required />\n              {t(\"admin.channels.type\")}\n            </div>\n            <Select\n              value={edit.type}\n              onValueChange={(value) => dispatch({ type: \"type\", value })}\n            >\n              <SelectTrigger>\n                <SelectValue placeholder={t(\"admin.channels.type\")} />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectGroup>\n                  {Object.entries(ChannelTypes).map(([key, value], idx) => (\n                    <SelectItem key={idx} value={key}>\n                      {value}\n                    </SelectItem>\n                  ))}\n                </SelectGroup>\n              </SelectContent>\n            </Select>\n            {info.description && (\n              <Markdown className={`channel-description mt-4 mb-1`}>\n                {info.description}\n              </Markdown>\n            )}\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              <Required />\n              {t(\"admin.channels.model\")}\n            </div>\n            <div className={`channel-model-wrapper`}>\n              {edit.models.map((model: string, idx: number) => (\n                <div\n                  className={`channel-model-item cursor-pointer`}\n                  key={idx}\n                  onClick={() => copy(model)}\n                >\n                  {model}\n                  <X\n                    className={`remove-action`}\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      dispatch({ type: \"remove-model\", value: model });\n                    }}\n                  />\n                </div>\n              ))}\n            </div>\n            <div className={`channel-model-action mt-4`}>\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button>\n                    <Search className={`h-4 w-4 mr-2`} />\n                    {t(\"admin.channels.add-model\")}\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align={`start`} asChild>\n                  <Command>\n                    <CommandInput\n                      placeholder={t(\"admin.channels.search-model\")}\n                    />\n                    <CommandList className={`thin-scrollbar`}>\n                      {unusedModels.map((model, idx) => (\n                        <CommandItem\n                          key={idx}\n                          value={model}\n                          onSelect={() =>\n                            dispatch({ type: \"add-model\", value: model })\n                          }\n                          className={`px-2`}\n                        >\n                          {model}\n                        </CommandItem>\n                      ))}\n                    </CommandList>\n                  </Command>\n                </DropdownMenuContent>\n              </DropdownMenu>\n              <CustomAction\n                onPost={(model) => {\n                  const models = model.split(\" \");\n                  dispatch({ type: \"add-models\", value: models });\n                }}\n              />\n              <Button\n                onClick={() =>\n                  dispatch({ type: \"add-models\", value: info.models })\n                }\n              >\n                {t(\"admin.channels.fill-template-models\", {\n                  number: info.models.length,\n                })}\n              </Button>\n              <Button\n                variant={`outline`}\n                onClick={() => dispatch({ type: \"clear-models\" })}\n              >\n                {t(\"admin.channels.clear-models\")}\n              </Button>\n            </div>\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              <Required />\n              {t(\"admin.channels.secret\")}\n            </div>\n            <Textarea\n              value={edit.secret}\n              placeholder={t(\"admin.channels.secret-placeholder\", {\n                format: info.format,\n              })}\n              onChange={(e) =>\n                dispatch({ type: \"secret\", value: e.target.value })\n              }\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              <Required />\n              {t(\"admin.channels.endpoint\")}\n            </div>\n            <Input\n              value={edit.endpoint}\n              placeholder={t(\"admin.channels.endpoint-placeholder\")}\n              onChange={(e) =>\n                dispatch({ type: \"endpoint\", value: e.target.value })\n              }\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              {t(\"admin.channels.priority\")}\n              <Tips content={t(\"admin.channels.priority-tip\")} />\n            </div>\n            <NumberInput\n              value={edit.priority}\n              acceptNegative={true}\n              onValueChange={(value) => dispatch({ type: \"priority\", value })}\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              {t(\"admin.channels.weight\")}\n              <Tips content={t(\"admin.channels.weight-tip\")} />\n            </div>\n            <NumberInput\n              value={edit.weight}\n              min={1}\n              onValueChange={(value) => dispatch({ type: \"weight\", value })}\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              {t(\"admin.channels.retry\")}\n              <Tips content={t(\"admin.channels.retry-tip\")} />\n            </div>\n            <NumberInput\n              value={edit.retry}\n              min={1}\n              onValueChange={(value) => dispatch({ type: \"retry\", value })}\n            />\n          </div>\n          <div className={`channel-row`}>\n            <div className={`channel-content`}>\n              {t(\"admin.channels.mapper\")}\n              <Tips content={t(\"admin.channels.mapper-tip\")} />\n            </div>\n            <FlexibleTextarea\n              rows={5}\n              value={edit.mapper}\n              placeholder={t(\"admin.channels.mapper-placeholder\")}\n              onChange={(e) =>\n                dispatch({ type: \"mapper\", value: e.target.value })\n              }\n            />\n          </div>\n          <Paragraph title={t(\"admin.channels.advanced\")} isCollapsed={true}>\n            <ParagraphItem>\n              <div className={`channel-row column-layout`}>\n                <div className={`channel-content flex flex-row items-center justify-between w-full`}>\n                  <div className={`flex flex-row items-center gap-1`}>\n                    {t(\"admin.channels.first-message-as-user\")}\n                    <Tips content={t(\"admin.channels.first-message-as-user-tip\")} />\n                  </div>\n                  <Switch\n                    checked={edit.first_message_as_user || false}\n                    onCheckedChange={(value) =>\n                      dispatch({ type: \"set-first-message-as-user\", value })\n                    }\n                  />\n                </div>\n              </div>\n            </ParagraphItem>\n            <ParagraphDescription>\n              {t(\"admin.channels.first-message-as-user-desc\")}\n            </ParagraphDescription>\n            <ParagraphSpace />\n            <ParagraphItem>\n              <div className={`channel-row column-layout`}>\n                <div className={`channel-content flex flex-row items-center justify-between w-full`}>\n                  <div className={`flex flex-row items-center gap-1`}>\n                    {t(\"admin.channels.merge-consecutive-user-messages\")}\n                    <Tips content={t(\"admin.channels.merge-consecutive-user-messages-tip\")} />\n                  </div>\n                  <Switch\n                    checked={edit.merge_consecutive_user_messages || false}\n                    onCheckedChange={(value) =>\n                      dispatch({ type: \"set-merge-consecutive-user-messages\", value })\n                    }\n                  />\n                </div>\n              </div>\n            </ParagraphItem>\n            <ParagraphDescription>\n              {t(\"admin.channels.merge-consecutive-user-messages-desc\")}\n            </ParagraphDescription>\n            <ParagraphSpace />\n            <ParagraphItem>\n              <div className={`channel-row column-layout`}>\n                <div className={`channel-content`}>\n                  {t(\"admin.channels.group\")}\n                  <Tips content={t(\"admin.channels.group-tip\")} />\n                </div>\n                <MultiCombobox\n                  className={`w-full max-w-full`}\n                  value={edit.group || []}\n                  align={`end`}\n                  onChange={(value: string[]) =>\n                    dispatch({ type: \"set-group\", value })\n                  }\n                  list={channelGroups}\n                  listTranslate={\"admin.channels.groups\"}\n                  placeholder={t(\"admin.channels.group-placeholder\", {\n                    length: (edit.group || []).length,\n                  })}\n                />\n              </div>\n            </ParagraphItem>\n            <ParagraphDescription>\n              {t(\"admin.channels.group-desc\")}\n            </ParagraphDescription>\n            <ParagraphSpace />\n            <ParagraphItem>\n              <div className={`channel-row column-layout`}>\n                <div className={`channel-content`}>\n                  {t(\"admin.channels.proxy-type\")}\n                </div>\n                <Select\n                  value={(edit.proxy?.proxy_type || 0).toString()}\n                  onValueChange={(value: string) =>\n                    dispatch({ type: \"set-proxy-type\", value: parseInt(value) })\n                  }\n                >\n                  <SelectTrigger>\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectGroup>\n                      <SelectItem value={\"0\"}>\n                        {ProxyTypes[proxyType.NoneProxy]}\n                      </SelectItem>\n                      <SelectItem value={\"1\"}>\n                        {ProxyTypes[proxyType.HttpProxy]}\n                      </SelectItem>\n                      <SelectItem value={\"2\"}>\n                        {ProxyTypes[proxyType.HttpsProxy]}\n                      </SelectItem>\n                      <SelectItem value={\"3\"}>\n                        {ProxyTypes[proxyType.Socks5Proxy]}\n                      </SelectItem>\n                    </SelectGroup>\n                  </SelectContent>\n                </Select>\n              </div>\n            </ParagraphItem>\n            <ParagraphItem>\n              <div className={`channel-content`}>\n                {t(\"admin.channels.proxy-endpoint\")}\n              </div>\n              <Input\n                value={edit.proxy?.proxy || \"\"}\n                placeholder={t(\"admin.channels.proxy-endpoint-placeholder\")}\n                onChange={(e) =>\n                  dispatch({ type: \"set-proxy\", value: e.target.value })\n                }\n                disabled={edit.proxy?.proxy_type === 0}\n              />\n            </ParagraphItem>\n            <ParagraphItem>\n              <div className={`channel-content`}>\n                {t(\"admin.channels.proxy-username\")}\n              </div>\n              <Input\n                value={edit.proxy?.username || \"\"}\n                placeholder={t(\"admin.channels.proxy-username-placeholder\")}\n                onChange={(e) =>\n                  dispatch({\n                    type: \"set-proxy-username\",\n                    value: e.target.value,\n                  })\n                }\n                disabled={edit.proxy?.proxy_type === 0}\n              />\n            </ParagraphItem>\n            <ParagraphItem>\n              <div className={`channel-content`}>\n                {t(\"admin.channels.proxy-password\")}\n              </div>\n              <Input\n                value={edit.proxy?.password || \"\"}\n                placeholder={t(\"admin.channels.proxy-password-placeholder\")}\n                onChange={(e) =>\n                  dispatch({\n                    type: \"set-proxy-password\",\n                    value: e.target.value,\n                  })\n                }\n                disabled={edit.proxy?.proxy_type === 0}\n              />\n            </ParagraphItem>\n            <ParagraphDescription>\n              {t(\"admin.channels.proxy-desc\")}\n            </ParagraphDescription>\n          </Paragraph>\n        </div>\n        <div className={`mt-4 flex flex-row w-full h-max pr-2 items-center`}>\n          <div className={`object-id`}>\n            <span className={`mr-2`}>ID</span>\n            {edit.id === -1 ? (\n              <Plus className={`w-3 h-3`} />\n            ) : (\n              <span className={`id`}>{edit.id}</span>\n            )}\n          </div>\n          <div className={`grow`} />\n          <Button variant={`outline`} onClick={() => close()}>\n            {t(\"cancel\")}\n          </Button>\n          <Button\n            className={`ml-2`}\n            loading={true}\n            onClick={post}\n            disabled={!enabled}\n          >\n            {t(\"confirm\")}\n          </Button>\n        </div>\n      </div>\n    )\n  );\n}\n\nexport default ChannelEditor;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/ChannelTable.tsx",
    "content": "import {\n  ColumnsVisibilityBar,\n  Table,\n  TableBody,\n  TableCell,\n  TableHeader,\n  TableRow,\n  useColumnsVisibility,\n} from \"@/components/ui/table.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport {\n  Activity,\n  ArrowDown10,\n  Blocks,\n  Check,\n  Circle,\n  Plus,\n  RotateCw,\n  Search,\n  Settings2,\n  Sheet,\n  SquareAsterisk,\n  Trash,\n  Weight,\n  Workflow,\n  X,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport OperationAction from \"@/components/OperationAction.tsx\";\nimport { Dispatch, useEffect, useMemo, useState } from \"react\";\nimport { Channel, getShortChannelType } from \"@/admin/channel.ts\";\nimport { withNotify } from \"@/api/common.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  activateChannel,\n  deactivateChannel,\n  deleteChannel,\n  listChannel,\n} from \"@/admin/api/channel.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { getApiModels } from \"@/api/v1.ts\";\nimport { getHostName } from \"@/utils/base.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { DialogClose } from \"@radix-ui/react-dialog\";\nimport { ChannelTypeAvatar } from \"@/components/ModelAvatar.tsx\";\n\ntype ChannelTableProps = {\n  display: boolean;\n  dispatch: Dispatch<any>;\n  setId: (id: number) => void;\n  setEnabled: (enabled: boolean) => void;\n  data: Channel[];\n  setData: (data: Channel[]) => void;\n};\n\ntype TypeBadgeProps = {\n  type: string;\n  className?: string;\n  variant?:\n    | \"default\"\n    | \"secondary\"\n    | \"destructive\"\n    | \"outline\"\n    | \"gold\"\n    | null\n    | undefined;\n};\n\nexport function TypeBadge({ type, className, variant }: TypeBadgeProps) {\n  const content = useMemo(() => getShortChannelType(type), [type]);\n\n  return (\n    <Badge\n      className={cn(`select-none w-max cursor-pointer`, className)}\n      variant={variant}\n    >\n      {content || type}\n    </Badge>\n  );\n}\n\ntype SyncDialogProps = {\n  dispatch: Dispatch<any>;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n};\n\nfunction SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) {\n  const { t } = useTranslation();\n  const [endpoint, setEndpoint] = useState<string>(\"https://api.openai.com\");\n  const [secret, setSecret] = useState<string>(\"\");\n\n  const submit = async (endpoint: string): Promise<boolean> => {\n    endpoint = endpoint.trim();\n    endpoint.endsWith(\"/\") && (endpoint = endpoint.slice(0, -1));\n\n    const resp = await getApiModels(secret, { endpoint });\n    withNotify(t, resp, true);\n\n    if (!resp.status) return false;\n\n    const name = getHostName(endpoint).replace(/\\./g, \"-\");\n    const data: Channel = {\n      id: -1,\n      name,\n      type: \"openai\",\n      models: resp.data,\n      priority: 0,\n      weight: 1,\n      retry: 3,\n      secret,\n      endpoint,\n      mapper: \"\",\n      state: true,\n      group: [],\n      proxy: { proxy: \"\", proxy_type: 0, username: \"\", password: \"\" },\n    };\n\n    dispatch({ type: \"set\", value: data });\n    return true;\n  };\n\n  return (\n    <>\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.channels.joint\")}</DialogTitle>\n          </DialogHeader>\n          <div className={`pt-2 flex flex-col`}>\n            <div className={`flex flex-row items-center mb-4`}>\n              <Label className={`mr-2 whitespace-nowrap`}>\n                {t(\"admin.channels.joint-endpoint\")}\n              </Label>\n              <Input\n                value={endpoint}\n                onChange={(e) => setEndpoint(e.target.value)}\n                placeholder={t(\"admin.channels.upstream-endpoint-placeholder\")}\n              />\n            </div>\n            <div className={`flex flex-row items-center`}>\n              <Label className={`mr-2 whitespace-nowrap`}>\n                {t(\"admin.channels.secret\")}\n              </Label>\n              <Input\n                value={secret}\n                onChange={(e) => setSecret(e.target.value)}\n                placeholder={t(\"admin.channels.sync-secret-placeholder\")}\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <DialogClose asChild>\n              <Button variant={`outline`}>{t(\"cancel\")}</Button>\n            </DialogClose>\n            <Button\n              unClickable\n              className={`mb-1`}\n              onClick={async () => {\n                const status = await submit(endpoint);\n                status && setOpen(false);\n              }}\n            >\n              {t(\"confirm\")}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\nfunction ChannelTable({\n  display,\n  dispatch,\n  setId,\n  setEnabled,\n  data,\n  setData,\n}: ChannelTableProps) {\n  const { t } = useTranslation();\n  const [search, setSearch] = useState<string>(\"\");\n  const [loading, setLoading] = useState<boolean>(false);\n  const [open, setOpen] = useState<boolean>(false);\n\n  const [blockDisplayType, setBlockDisplayType] = useState<boolean>(false);\n\n  const refresh = async () => {\n    setLoading(true);\n    const resp = await listChannel();\n    setLoading(false);\n    if (!resp.status) withNotify(t, resp);\n    else setData(resp.data);\n  };\n  useEffectAsync(refresh, []);\n  useEffectAsync(refresh, [display]);\n\n  useEffect(() => {\n    if (display) setId(-1);\n  }, [display]);\n\n  const { bar, toggle, merge } = useColumnsVisibility(\n    {\n      id: true,\n      name: true,\n      type: true,\n      priority: true,\n      weight: true,\n      [\"secret-number\"]: true,\n      [\"retry-name\"]: true,\n      state: true,\n      action: true,\n    },\n    { translatePrefix: \"admin.channels\" },\n  );\n\n  const channels = useMemo(() => {\n    const v = data || [];\n    const s = search.trim().toLowerCase();\n    if (s.trim() === \"\") return v;\n\n    return v.filter((x) => {\n      return (\n        x.name.toLowerCase().includes(s) ||\n        x.type.toLowerCase().includes(s) ||\n        x.secret.toLowerCase().includes(s) ||\n        x.models.some((m) => m.toLowerCase().includes(s))\n      );\n    });\n  }, [search, data]);\n\n  return (\n    display && (\n      <div>\n        <SyncDialog\n          open={open}\n          setOpen={setOpen}\n          dispatch={(action) => {\n            dispatch(action);\n            setEnabled(true);\n            setId(-1);\n          }}\n        />\n        <div className={`flex flex-row w-full h-max`}>\n          <Button\n            className={`mr-2 shrink-0`}\n            onClick={() => {\n              setEnabled(true);\n              setId(-1);\n            }}\n          >\n            <Plus className={`h-4 w-4 mr-1`} />\n            {t(\"admin.channels.create\")}\n          </Button>\n          <Button\n            className={`mr-2 shrink-0`}\n            variant={`outline`}\n            onClick={() => setOpen(true)}\n          >\n            <Activity className={`h-4 w-4 mr-1`} />\n            {t(\"admin.channels.joint\")}\n          </Button>\n          <Button\n            variant={`outline`}\n            size={`icon`}\n            className={`ml-auto`}\n            onClick={() => setBlockDisplayType(!blockDisplayType)}\n          >\n            {blockDisplayType ? (\n              <Blocks className={`h-4 w-4`} />\n            ) : (\n              <Sheet className={`h-4 w-4`} />\n            )}\n          </Button>\n          <ColumnsVisibilityBar bar={bar} toggle={toggle} />\n        </div>\n        <div className={`flex flex-row items-center mt-4`}>\n          <Button className={`shrink-0 mr-2`} size={`icon`}>\n            <Search className={`h-4 w-4`} />\n          </Button>\n          <Input\n            className={`grow`}\n            value={search}\n            onChange={(e) => setSearch(e.target.value)}\n            placeholder={t(\"admin.channels.search-channel\")}\n          />\n          <Button\n            variant={`outline`}\n            size={`icon`}\n            className={`ml-2 shrink-0`}\n            onClick={refresh}\n          >\n            <RotateCw className={cn(`h-4 w-4`, loading && `animate-spin`)} />\n          </Button>\n        </div>\n        {!blockDisplayType ? (\n          <Table className={`channel-table mt-4`}>\n            <TableHeader>\n              <TableRow className={`select-none whitespace-nowrap`}>\n                <TableCell className={merge(\"id\")}>\n                  {t(\"admin.channels.id\")}\n                </TableCell>\n                <TableCell className={merge(\"name\")}>\n                  {t(\"admin.channels.name\")}\n                </TableCell>\n                <TableCell className={merge(\"type\")}>\n                  {t(\"admin.channels.type\")}\n                </TableCell>\n                <TableCell className={merge(\"priority\")}>\n                  {t(\"admin.channels.priority\")}\n                </TableCell>\n                <TableCell className={merge(\"weight\")}>\n                  {t(\"admin.channels.weight\")}\n                </TableCell>\n                <TableCell className={merge(\"secret-number\")}>\n                  {t(\"admin.channels.secret-number\")}\n                </TableCell>\n                <TableCell className={merge(\"retry-name\")}>\n                  {t(\"admin.channels.retry-name\")}\n                </TableCell>\n                <TableCell className={merge(\"state\")}>\n                  {t(\"admin.channels.state\")}\n                </TableCell>\n                <TableCell className={merge(\"action\")}>\n                  {t(\"admin.channels.action\")}\n                </TableCell>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {channels.map((chan, idx) => (\n                <TableRow key={idx}>\n                  <TableCell className={merge(\"id\", `channel-id select-none`)}>\n                    #{chan.id}\n                  </TableCell>\n                  <TableCell className={merge(\"name\")}>{chan.name}</TableCell>\n                  <TableCell className={merge(\"type\")}>\n                    <TypeBadge type={chan.type} />\n                  </TableCell>\n                  <TableCell className={merge(\"priority\")}>\n                    {chan.priority}\n                  </TableCell>\n                  <TableCell className={merge(\"weight\")}>\n                    {chan.weight}\n                  </TableCell>\n                  <TableCell className={merge(\"secret-number\")}>\n                    {chan.secret.split(\"\\n\").filter((x) => x).length}\n                  </TableCell>\n                  <TableCell className={merge(\"retry-name\")}>\n                    {chan.retry}\n                  </TableCell>\n                  <TableCell className={merge(\"state\")}>\n                    {chan.state ? (\n                      <Check className={`h-4 w-4 text-green-500`} />\n                    ) : (\n                      <X className={`h-4 w-4 text-destructive`} />\n                    )}\n                  </TableCell>\n                  <TableCell\n                    className={merge(\"action\", `flex flex-row flex-wrap gap-2`)}\n                  >\n                    <OperationAction\n                      tooltip={t(\"admin.channels.edit\")}\n                      onClick={() => {\n                        setEnabled(true);\n                        setId(chan.id);\n                      }}\n                    >\n                      <Settings2 className={`h-4 w-4`} />\n                    </OperationAction>\n                    {chan.state ? (\n                      <OperationAction\n                        tooltip={t(\"admin.channels.disable\")}\n                        variant={`destructive`}\n                        onClick={async () => {\n                          const resp = await deactivateChannel(chan.id);\n                          withNotify(t, resp, true);\n                          await refresh();\n                        }}\n                      >\n                        <X className={`h-4 w-4`} />\n                      </OperationAction>\n                    ) : (\n                      <OperationAction\n                        tooltip={t(\"admin.channels.enable\")}\n                        onClick={async () => {\n                          const resp = await activateChannel(chan.id);\n                          withNotify(t, resp, true);\n                          await refresh();\n                        }}\n                      >\n                        <Check className={`h-4 w-4`} />\n                      </OperationAction>\n                    )}\n                    <OperationAction\n                      tooltip={t(\"admin.channels.delete\")}\n                      variant={`destructive`}\n                      onClick={async () => {\n                        const resp = await deleteChannel(chan.id);\n                        withNotify(t, resp, true);\n                        await refresh();\n                      }}\n                    >\n                      <Trash className={`h-4 w-4`} />\n                    </OperationAction>\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n        ) : (\n          <div className={`grid grid-cols-1 md:grid-cols-2 gap-4 mt-4`}>\n            {channels.map((chan, idx) => (\n              <div\n                key={idx}\n                onClick={() => {\n                  setEnabled(true);\n                  setId(chan.id);\n                }}\n                className={`flex flex-col p-4 border rounded-md cursor-pointer select-none hover:bg-background-hover transition`}\n              >\n                <div className={`flex flex-row items-center w-full`}>\n                  <Circle\n                    className={cn(\n                      `h-3 w-3 stroke-[3.5] mr-1.5`,\n                      chan.state ? `text-green-500` : `text-destructive`,\n                    )}\n                  />\n                  <span className={`mr-1`}>{chan.name}</span>\n                  <Badge variant={`outline`} className={`select-none`}>\n                    #{chan.id}\n                  </Badge>\n                  <TypeBadge type={chan.type} className={`ml-auto`} />\n                </div>\n                <div className={`mt-1 grid grid-cols-2 gap-1`}>\n                  <div className={`flex flex-row items-center`}>\n                    <ArrowDown10 className={`h-3.5 w-3.5`} />\n                    <Label className={`whitespace-nowrap ml-1 mr-2`}>\n                      {t(\"admin.channels.priority\")}\n                    </Label>\n                    <span className={`font-bold`}>{chan.priority}</span>\n                  </div>\n                  <div className={`flex flex-row items-center`}>\n                    <Weight className={`h-3.5 w-3.5`} />\n                    <Label className={`whitespace-nowrap ml-1 mr-2`}>\n                      {t(\"admin.channels.weight\")}\n                    </Label>\n                    <span className={`font-bold`}>{chan.weight}</span>\n                  </div>\n                  <div className={`flex flex-row items-center`}>\n                    <SquareAsterisk className={`h-3.5 w-3.5`} />\n                    <Label className={`whitespace-nowrap ml-1 mr-2`}>\n                      {t(\"admin.channels.secret-number\")}\n                    </Label>\n                    <span className={`font-bold`}>\n                      {chan.secret.split(\"\\n\").filter((x) => x).length}\n                    </span>\n                  </div>\n                  <div className={`flex flex-row items-center`}>\n                    <Workflow className={`h-3.5 w-3.5`} />\n                    <Label className={`whitespace-nowrap ml-1 mr-2`}>\n                      {t(\"admin.channels.retry-name\")}\n                    </Label>\n                    <span className={`font-bold`}>{chan.retry}</span>\n                  </div>\n                </div>\n                <div className={`flex flex-row items-center space-x-1 mt-2`}>\n                  <OperationAction\n                    tooltip={t(\"admin.channels.edit\")}\n                    onClick={() => {\n                      setEnabled(true);\n                      setId(chan.id);\n                    }}\n                  >\n                    <Settings2 className={`h-4 w-4`} />\n                  </OperationAction>\n                  {chan.state ? (\n                    <OperationAction\n                      tooltip={t(\"admin.channels.disable\")}\n                      variant={`destructive`}\n                      onClick={async () => {\n                        const resp = await deactivateChannel(chan.id);\n                        withNotify(t, resp, true);\n                        await refresh();\n                      }}\n                    >\n                      <X className={`h-4 w-4`} />\n                    </OperationAction>\n                  ) : (\n                    <OperationAction\n                      tooltip={t(\"admin.channels.enable\")}\n                      onClick={async () => {\n                        const resp = await activateChannel(chan.id);\n                        withNotify(t, resp, true);\n                        await refresh();\n                      }}\n                    >\n                      <Check className={`h-4 w-4`} />\n                    </OperationAction>\n                  )}\n                  <OperationAction\n                    tooltip={t(\"admin.channels.delete\")}\n                    variant={`destructive`}\n                    onClick={async () => {\n                      const resp = await deleteChannel(chan.id);\n                      withNotify(t, resp, true);\n                      await refresh();\n                    }}\n                  >\n                    <Trash className={`h-4 w-4`} />\n                  </OperationAction>\n                  <div className={`grow`} />\n                  <ChannelTypeAvatar type={chan.type} size={28} />\n                </div>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    )\n  );\n}\n\nexport default ChannelTable;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/ErrorChart.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { AreaChart } from \"@tremor/react\";\nimport { getReadableNumber } from \"@/utils/processor.ts\";\n\ntype ErrorChartProps = {\n  labels: string[];\n  datasets: number[];\n};\nfunction ErrorChart({ labels, datasets }: ErrorChartProps) {\n  const { t } = useTranslation();\n  const data = useMemo(() => {\n    return datasets.map((data, index) => ({\n      date: labels[index],\n      [t(\"admin.times\")]: data,\n    }));\n  }, [labels, datasets, t(\"admin.times\")]);\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <p>{t(\"admin.error-chart\")}</p>\n        {labels.length === 0 && (\n          <Loader2 className={`h-4 w-4 inline-block animate-spin`} />\n        )}\n      </div>\n      <AreaChart\n        className={`common-chart`}\n        data={data}\n        categories={[t(\"admin.times\")]}\n        index={\"date\"}\n        colors={[\"red\"]}\n        showAnimation={true}\n        valueFormatter={(value) => getReadableNumber(value, 1)}\n      />\n    </div>\n  );\n}\n\nexport default ErrorChart;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/ModelChart.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { BarChart4, LineChartIcon, Loader2 } from \"lucide-react\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { AreaChart, BarChart } from \"@tremor/react\";\nimport { getReadableNumber } from \"@/utils/processor.ts\";\nimport { getModelColor } from \"@/admin/colors.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\n\ntype ModelChartProps = {\n  labels: string[];\n  datasets: {\n    model: string;\n    data: number[];\n  }[];\n};\n\nfunction ModelChart({ labels, datasets }: ModelChartProps) {\n  const { t } = useTranslation();\n  const [area, setArea] = useState(false);\n  const data = useMemo(() => {\n    return labels.map((label, idx) => {\n      const v: Record<string, any> = { date: label };\n      datasets.forEach((dataset) => {\n        if (dataset.data[idx] === 0 && !area) return;\n        v[dataset.model] = dataset.data[idx];\n      });\n\n      return v;\n    });\n  }, [area, labels, datasets]);\n\n  const categories = useMemo(\n    () => datasets.map((dataset) => dataset.model),\n    [datasets],\n  );\n\n  const colors = useMemo(\n    () => datasets.map((dataset) => getModelColor(dataset.model)),\n    [datasets],\n  );\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <div className={`flex flex-row items-center`}>\n          {t(\"admin.model-chart\")}\n          <Tips content={t(\"admin.model-chart-tip\")} />\n        </div>\n        {labels.length === 0 && (\n          <Loader2 className={`h-4 w-4 inline-block animate-spin`} />\n        )}\n        <div className={`grow`} />\n        <Button\n          variant={`ghost`}\n          size={`icon-sm`}\n          onClick={() => setArea(!area)}\n        >\n          {area ? (\n            <BarChart4 className={`h-4 w-4`} />\n          ) : (\n            <LineChartIcon className={`h-4 w-4`} />\n          )}\n        </Button>\n      </div>\n      {!area ? (\n        <BarChart\n          className={`common-chart`}\n          data={data}\n          index={\"date\"}\n          layout={`horizontal`}\n          stack={true}\n          categories={categories}\n          colors={colors}\n          valueFormatter={(value) => getReadableNumber(value, 1, true)}\n          showLegend={false}\n        />\n      ) : (\n        <AreaChart\n          className={`common-chart`}\n          data={data}\n          index={\"date\"}\n          stack={true}\n          categories={categories}\n          colors={colors}\n          valueFormatter={(value) => getReadableNumber(value, 1, true)}\n          showLegend={false}\n        />\n      )}\n    </div>\n  );\n}\n\nexport default ModelChart;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/ModelUsageChart.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Loader2 } from \"lucide-react\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { sum } from \"@/utils/base.ts\";\nimport { DonutChart, Legend } from \"@tremor/react\";\nimport { getReadableNumber } from \"@/utils/processor.ts\";\nimport { getModelColor } from \"@/admin/colors.ts\";\n\ntype ModelChartProps = {\n  labels: string[];\n  datasets: {\n    model: string;\n    data: number[];\n  }[];\n};\n\ntype DataUsage = {\n  name: string;\n  value: number;\n};\n\nfunction ModelUsageChart({ labels, datasets }: ModelChartProps) {\n  const { t } = useTranslation();\n\n  const usage = useMemo((): Record<string, number> => {\n    const usage: Record<string, number> = {};\n    datasets.forEach((dataset) => {\n      usage[dataset.model] = sum(dataset.data);\n    });\n    return usage;\n  }, [datasets]);\n\n  const data = useMemo((): DataUsage[] => {\n    const models: string[] = Object.keys(usage);\n    const data: number[] = models.map((model) => usage[model]);\n\n    return models.map(\n      (model, i): DataUsage => ({ name: model, value: data[i] }),\n    );\n  }, [usage]);\n\n  const sorted = useMemo(() => {\n    return data.sort((a, b) => b.value - a.value);\n  }, [data]);\n\n  const categories = useMemo(() => {\n    return sorted.map(\n      (item) => `${item.name} (${getReadableNumber(item.value, 1)})`,\n    );\n  }, [sorted]);\n\n  type CustomTooltipTypeDonut = {\n    payload: any;\n    active: boolean | undefined;\n    label: any;\n  };\n\n  const customTooltip = (props: CustomTooltipTypeDonut) => {\n    const { payload, active } = props;\n    if (!active || !payload) return null;\n    const categoryPayload = payload?.[0];\n    if (!categoryPayload) return null;\n    return (\n      <div className=\"chart-tooltip min-w-56 w-max z-10 rounded-tremor-default border border-tremor-border bg-tremor-background p-2 text-tremor-default shadow-tremor-dropdown\">\n        <div className=\"flex flex-1 space-x-2.5\">\n          <div\n            className={`flex w-1.5 flex-col bg-${categoryPayload?.color} rounded`}\n          />\n          <div className=\"w-full\">\n            <div className=\"flex items-center justify-between space-x-8\">\n              <p className=\"whitespace-nowrap text-right text-tremor-content\">\n                {categoryPayload.name}\n              </p>\n              <p className=\"whitespace-nowrap text-right font-medium text-tremor-content-emphasis\">\n                {getReadableNumber(categoryPayload.value, 1)} tokens\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <div className={`flex flex-row items-center`}>\n          {t(\"admin.model-usage-chart\")}\n          <Tips content={t(\"admin.model-chart-tip\")} />\n        </div>\n        {labels.length === 0 && (\n          <Loader2 className={`h-4 w-4 inline-block animate-spin`} />\n        )}\n      </div>\n      <div className={`flex flex-row`}>\n        <DonutChart\n          className={`common-chart p-4 w-[50%]`}\n          variant={`donut`}\n          data={data}\n          showAnimation={true}\n          valueFormatter={(value) => getReadableNumber(value, 1)}\n          customTooltip={customTooltip}\n          colors={data.map((item) => getModelColor(item.name))}\n        />\n        <Legend\n          className={`common-chart p-2 w-[50%] z-0`}\n          // keep 6 items max\n          categories={categories.slice(0, 6)}\n          colors={sorted.slice(0, 6).map((item) => getModelColor(item.name))}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default ModelUsageChart;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/RequestChart.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { AreaChart } from \"@tremor/react\";\nimport { getReadableNumber } from \"@/utils/processor.ts\";\n\ntype RequestChartProps = {\n  labels: string[];\n  datasets: number[];\n};\n\nfunction RequestChart({ labels, datasets }: RequestChartProps) {\n  const { t } = useTranslation();\n  const data = useMemo(() => {\n    return datasets.map((data, index) => ({\n      date: labels[index],\n      [t(\"admin.requests\")]: data,\n    }));\n  }, [labels, datasets, t(\"admin.requests\")]);\n\n  const rpm = useMemo(() => {\n    // request per month, sum of datasets\n    return datasets.reduce((acc, curr) => acc + curr, 0);\n  }, [datasets]);\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <p>{t(\"admin.request-chart\")}</p>\n        {labels.length === 0 && (\n          <Loader2 className={`h-4 w-4 inline-block animate-spin`} />\n        )}\n        <div\n          className={`ml-auto bg-blue-500/20 text-blue-500 px-1 rounded-sm text-xs py-0.5`}\n        >\n          RPM {getReadableNumber(rpm)}\n        </div>\n      </div>\n      <AreaChart\n        className={`common-chart`}\n        data={data}\n        categories={[t(\"admin.requests\")]}\n        index={\"date\"}\n        colors={[\"blue\"]}\n        showAnimation={true}\n        valueFormatter={(value) => getReadableNumber(value, 1)}\n      />\n    </div>\n  );\n}\n\nexport default RequestChart;\n"
  },
  {
    "path": "app/src/components/admin/assemblies/UserTypeChart.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Filter, Loader2 } from \"lucide-react\";\nimport { UserTypeChartResponse } from \"@/admin/types.ts\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { DonutChart, Legend } from \"@tremor/react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { MultiCombobox } from \"@/components/ui/multi-combobox.tsx\";\n\ntype UserTypeChartProps = {\n  data: UserTypeChartResponse;\n};\n\nenum UserType {\n  normal = \"normal\",\n  api_paid = \"api_paid\",\n  basic_plan = \"basic_plan\",\n  standard_plan = \"standard_plan\",\n  pro_plan = \"pro_plan\",\n}\n\ntype UserStatus = {\n  name: string;\n  value: number;\n};\n\nfunction UserTypeChart({ data }: UserTypeChartProps) {\n  const { t } = useTranslation();\n\n  const [display, setDisplay] = useState<UserType[]>([\n    UserType.normal,\n    UserType.api_paid,\n    UserType.basic_plan,\n    UserType.standard_plan,\n    UserType.pro_plan,\n  ]);\n\n  const chart = useMemo((): UserStatus[] => {\n    return [\n      display.includes(UserType.normal) && {\n        name: t(\"admin.identity.normal\"),\n        value: data.normal,\n      },\n      display.includes(UserType.api_paid) && {\n        name: t(\"admin.identity.api_paid\"),\n        value: data.api_paid,\n      },\n      display.includes(UserType.basic_plan) && {\n        name: t(\"admin.identity.basic_plan\"),\n        value: data.basic_plan,\n      },\n      display.includes(UserType.standard_plan) && {\n        name: t(\"admin.identity.standard_plan\"),\n        value: data.standard_plan,\n      },\n      display.includes(UserType.pro_plan) && {\n        name: t(\"admin.identity.pro_plan\"),\n        value: data.pro_plan,\n      },\n    ].filter((item) => item) as UserStatus[];\n  }, [display, data, t(\"admin.identity.normal\")]);\n\n  return (\n    <div className={`chart`}>\n      <div className={`chart-title mb-2`}>\n        <div className={`flex flex-row items-center w-full`}>\n          <div>\n            {t(\"admin.user-type-chart\")}\n            <Tips\n              className={`translate-y-[2px]`}\n              content={t(\"admin.user-type-chart-tip\")}\n            />\n          </div>\n          {data.total === 0 && (\n            <Loader2 className={`h-4 w-4 ml-1 animate-spin`} />\n          )}\n\n          <div className={`grow`} />\n          <MultiCombobox\n            value={display}\n            align={`end`}\n            onChange={(value) => setDisplay(value as UserType[])}\n            list={[\n              UserType.normal,\n              UserType.api_paid,\n              UserType.basic_plan,\n              UserType.standard_plan,\n              UserType.pro_plan,\n            ]}\n            listTranslate={`admin.identity`}\n          >\n            <Button variant={`ghost`} size={`icon-sm`}>\n              <Filter className={`h-4 w-4`} />\n            </Button>\n          </MultiCombobox>\n        </div>\n      </div>\n      <div className={`flex flex-row`}>\n        <DonutChart\n          className={`common-chart p-4 w-[50%]`}\n          variant={`donut`}\n          data={chart}\n          showAnimation={true}\n          colors={[\"blue\", \"cyan\", \"indigo\", \"violet\", \"fuchsia\"]}\n        />\n        <Legend\n          className={`common-chart p-4 w-[50%]`}\n          categories={chart.map((item) => item.name)}\n          colors={[\"blue\", \"cyan\", \"indigo\", \"violet\", \"fuchsia\"]}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default UserTypeChart;\n"
  },
  {
    "path": "app/src/components/admin/common/StateBadge.tsx",
    "content": "import { Badge } from \"@/components/ui/badge.tsx\";\nimport { useTranslation } from \"react-i18next\";\n\nexport type StateBadgeProps = {\n  state: boolean;\n};\n\nexport default function StateBadge({ state }: StateBadgeProps) {\n  const { t } = useTranslation();\n\n  return <Badge variant=\"outline\">{t(`admin.used-${state}`)}</Badge>;\n}\n"
  },
  {
    "path": "app/src/components/app/Announcement.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useMemo, useState } from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n  DialogAction,\n} from \"@/components/ui/dialog\";\nimport { ArrowRight, Bell, Check, Megaphone } from \"lucide-react\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { BroadcastInfo, getBroadcastList } from \"@/api/broadcast.ts\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport Contact from \"./Contact\";\nimport { useSelector } from \"react-redux\";\nimport { infoAnnouncementSelector, infoBroadcastSelector } from \"@/store/info\";\n\ntype AnnouncementProps = {\n  className?: string;\n  children?: React.ReactNode;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n};\n\nfunction Announcement({\n  className,\n  children,\n  open,\n  setOpen,\n}: AnnouncementProps) {\n  const { t } = useTranslation();\n\n  const announcement = useSelector(infoAnnouncementSelector);\n  const broadcast = useSelector(infoBroadcastSelector);\n\n  const [dialog, setDialog] = useState<boolean>(false);\n\n  const displayBroadcastMessage = useMemo(() => {\n    const segs = broadcast.message.split(\"\\n\");\n\n    // if > 6 lines, only show the part above\n    if (segs.length > 6) {\n      return segs.slice(0, 6).join(\"\\n\") + \" ...\";\n    }\n\n    return broadcast.message;\n  }, [broadcast.message]);\n\n  const TriggerComp = children || (\n    <div\n      className={cn(\n        \"flex items-center w-full p-4 rounded-lg border shadow-sm hover:bg-muted/50 hover:border-primary/50 transition duration-300 cursor-pointer\",\n        \"bg-gradient-to-br from-background to-muted/50\",\n        className,\n      )}\n    >\n      <Markdown acceptHtml={true} className=\"p-0 text-sm text-secondary\">\n        {displayBroadcastMessage || t(\"no-announcement\")}\n      </Markdown>\n    </div>\n  );\n\n  return (\n    <>\n      <BroadcastList open={dialog} setOpen={setDialog} />\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogTrigger asChild>{TriggerComp}</DialogTrigger>\n        <DialogContent\n          className={`announcement-dialog flex-dialog`}\n          couldFullScreen\n        >\n          <DialogHeader notTextCentered>\n            <DialogTitle className={\"flex flex-row items-center select-none\"}>\n              <Bell className=\"inline-block w-4 h-4 mr-2\" />\n              <p className={`translate-y-[-1px]`}>{t(\"announcement\")}</p>\n            </DialogTitle>\n            <DialogDescription asChild>\n              <ScrollArea\n                className={`w-full h-[60vh] md:h-[70vh] px-2.5 content-h-fit`}\n                type={`always`}\n              >\n                <Contact />\n                {broadcast.message.length > 0 && (\n                  <Alert\n                    className={`mt-1.5 mb-4 mx-0.5 w-[calc(100%-0.5rem)] pb-3.5`}\n                  >\n                    <Megaphone className=\"select-none h-5 w-5\" />\n                    <AlertTitle\n                      className={`flex flex-row items-center select-none font-bold`}\n                    >\n                      {t(\"notify\")}\n                      {broadcast.firstReceived && (\n                        <Badge className={`ml-1.5`}>{t(\"new-notify\")}</Badge>\n                      )}\n                      <div className={`grow`} />\n                      <div\n                        className={`ml-0.5 font-normal select-none cursor-pointer flex flex-row items-center transition text-blue-500 opacity-95 hover:opacity-100 group`}\n                        onClick={() => setDialog(true)}\n                      >\n                        <ArrowRight\n                          className={`h-3.5 w-3.5 transition mr-1 group-hover:translate-x-0.5`}\n                        />\n                        {t(\"view-all\")}\n                      </div>\n                    </AlertTitle>\n                    <AlertDescription\n                      className={`mt-2 text-common whitespace-pre-wrap broadcast-markdown`}\n                    >\n                      <Markdown acceptHtml={true}>{broadcast.message}</Markdown>\n                    </AlertDescription>\n                  </Alert>\n                )}\n                <Markdown acceptHtml={true}>\n                  {announcement || t(\"empty\")}\n                </Markdown>\n              </ScrollArea>\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <DialogAction onClick={() => setOpen(false)}>\n              <Check className=\"w-4 h-4 mr-1\" />\n              {t(\"i-know\")}\n            </DialogAction>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n\ntype BroadcastListProps = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n};\nfunction BroadcastList({ open, setOpen }: BroadcastListProps) {\n  const { t } = useTranslation();\n  const [data, setData] = useState<BroadcastInfo[]>([]);\n\n  useEffectAsync(async () => {\n    if (!open || data.length > 0) return;\n    setData(await getBroadcastList());\n  }, [open]);\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent\n        className={`announcement-dialog flex-dialog`}\n        couldFullScreen\n      >\n        <DialogHeader notTextCentered>\n          <DialogTitle className={\"flex flex-row items-center select-none\"}>\n            <Megaphone className=\"inline-block w-4 h-4 mr-2\" />\n            <p className={`translate-y-[-1px]`}>{t(\"notify\")}</p>\n          </DialogTitle>\n          <DialogDescription asChild>\n            <ScrollArea\n              className={`w-full h-[60vh] md:h-[70vh] px-2.5 content-h-fit`}\n              type={`always`}\n            >\n              {data.map((item, index) => (\n                <Alert\n                  key={index}\n                  className={`mt-1.5 mb-4 mx-0.5 w-[calc(100%-0.5rem)] pb-3.5`}\n                >\n                  <Megaphone className=\"select-none h-5 w-5\" />\n                  <AlertTitle\n                    className={`flex flex-row items-center select-none font-bold`}\n                  >\n                    <Badge className={`h-5 mr-2`}>#{item.index}</Badge>{\" \"}\n                    <p className={`font-normal text-sm`}>{item.created_at}</p>\n                  </AlertTitle>\n                  <AlertDescription\n                    className={`mt-2 text-common whitespace-pre-wrap`}\n                  >\n                    {item.content}\n                  </AlertDescription>\n                </Alert>\n              ))}\n            </ScrollArea>\n          </DialogDescription>\n        </DialogHeader>\n        <DialogFooter>\n          <DialogAction onClick={() => setOpen(false)}>\n            <Check className=\"w-4 h-4 mr-1\" />\n            {t(\"i-know\")}\n          </DialogAction>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default Announcement;\n"
  },
  {
    "path": "app/src/components/app/AppProvider.tsx",
    "content": "import { ThemeProvider } from \"@/components/ThemeProvider.tsx\";\nimport DialogManager from \"@/dialogs\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { bindMarket, getApiPlans } from \"@/api/v1.ts\";\nimport { useDispatch } from \"react-redux\";\nimport {\n  stack,\n  updateMasks,\n  updateSupportModels,\n  useMessageActions,\n} from \"@/store/chat.ts\";\nimport { dispatchSubscriptionData, setTheme } from \"@/store/globals.ts\";\nimport { infoEvent } from \"@/events/info.ts\";\nimport { setForm } from \"@/store/info.ts\";\nimport { themeEvent } from \"@/events/theme.ts\";\nimport { useEffect } from \"react\";\n\nfunction AppProvider({ children }: { children?: React.ReactNode }) {\n  const dispatch = useDispatch();\n  const { receive } = useMessageActions();\n\n  useEffect(() => {\n    infoEvent.bind((data) => dispatch(setForm(data)));\n    themeEvent.bind((theme) => dispatch(setTheme(theme)));\n\n    stack.setCallback(async (id, message) => {\n      await receive(id, message);\n    });\n  }, []);\n\n  useEffectAsync(async () => {\n    updateSupportModels(dispatch, await bindMarket());\n    dispatchSubscriptionData(dispatch, await getApiPlans());\n    await updateMasks(dispatch);\n  }, []);\n\n  return (\n    <ThemeProvider>\n      <DialogManager />\n      {children}\n    </ThemeProvider>\n  );\n}\n\nexport default AppProvider;\n"
  },
  {
    "path": "app/src/components/app/Contact.tsx",
    "content": "import { useSelector } from \"react-redux\";\nimport { infoContactSelector } from \"@/store/info.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { TwitchIcon } from \"lucide-react\";\n\nfunction Contact() {\n  const { t } = useTranslation();\n  const contact = useSelector(infoContactSelector);\n  const showTrigger = contact && contact.length > 0;\n\n  if (!showTrigger) {\n    return null;\n  }\n\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <p\n          className={`text-sm text-primary font-medium cursor-pointer flex flex-row items-center mx-auto w-fit h-fit mt-0.5 mb-2.5 px-2.5 py-1 rounded-full border`}\n        >\n          <TwitchIcon className={`w-3.5 h-3.5 mr-1`} />\n          {t(\"contact.community\")}\n        </p>\n      </DialogTrigger>\n      <DialogContent className={`flex-dialog`}>\n        <DialogHeader>\n          <DialogTitle>{t(\"contact.community\")}</DialogTitle>\n          <DialogDescription asChild>\n            <Markdown className={`pt-4`} acceptHtml={true}>\n              {contact}\n            </Markdown>\n          </DialogDescription>\n        </DialogHeader>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default Contact;\n"
  },
  {
    "path": "app/src/components/app/MenuBar.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  logout,\n  selectAdmin,\n  selectAuthenticated,\n  selectUsername,\n} from \"@/store/auth.ts\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport router from \"@/router.tsx\";\nimport React from \"react\";\nimport {\n  LibraryBig,\n  MessageCircle,\n  Shield,\n  User,\n  Wallet,\n} from \"lucide-react\";\nimport Icon from \"@/components/utils/Icon.tsx\";\n\ntype MenuBarProps = {\n  children: React.ReactNode;\n  className?: string;\n};\n\ntype MenuBarItemProps = {\n  icon: React.ReactElement;\n  path: string;\n  name: string;\n};\n\nconst BarItem = ({ icon, path, name }: MenuBarItemProps) => {\n  const { t } = useTranslation();\n  const navigate = () => router.navigate(path);\n\n  return (\n    <DropdownMenuItem onClick={navigate}>\n      <Icon icon={icon} className={`w-4 h-4 mr-1.5`} />\n      {t(`bar.${name}-full`)}\n    </DropdownMenuItem>\n  );\n};\n\nfunction MenuBar({ children, className }: MenuBarProps) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const auth = useSelector(selectAuthenticated);\n  const username = useSelector(selectUsername);\n  const admin = useSelector(selectAdmin);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      <DropdownMenuContent className={className} align={`end`}>\n        {auth ? (\n          <>\n            <DropdownMenuLabel className={`username`}>\n              {username}\n            </DropdownMenuLabel>\n            <DropdownMenuSeparator />\n            <BarItem icon={<MessageCircle />} path={`/`} name={\"chat\"} />\n            <BarItem icon={<LibraryBig />} path={`/model`} name={\"model\"} />\n            {/* <BarItem icon={<Compass />} path={`/preset`} name={\"preset\"} /> */}\n            <BarItem icon={<Wallet />} path={`/wallet`} name={\"wallet\"} />\n            {/* <BarItem icon={<DraftingCompass />} path={`/key`} name={\"key\"} /> */}\n            <BarItem icon={<User />} path={`/account`} name={\"account\"} />\n            {/* <BarItem icon={<PieChart />} path={`/log`} name={\"log\"} /> */}\n            {admin && (\n              <BarItem icon={<Shield />} path={`/admin`} name={\"admin\"} />\n            )}\n            <DropdownMenuSeparator />\n            <DropdownMenuItem asChild>\n              <Button\n                size={`sm`}\n                className={`action-button`}\n                onClick={() => dispatch(logout())}\n              >\n                {t(\"logout\")}\n              </Button>\n            </DropdownMenuItem>\n          </>\n        ) : (\n          <DropdownMenuItem asChild>\n            <Button\n              size={`sm`}\n              className={`h-max w-full cursor-pointer`}\n              onClick={() => router.navigate(\"/login\")}\n            >\n              {t(\"login\")}\n            </Button>\n          </DropdownMenuItem>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport default MenuBar;\n"
  },
  {
    "path": "app/src/components/app/NavBar.tsx",
    "content": "import \"@/assets/pages/navbar.less\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  selectAuthenticated,\n  selectUsername,\n  validateToken,\n} from \"@/store/auth.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { Menu, Settings2 } from \"lucide-react\";\nimport { useEffect } from \"react\";\nimport { tokenField } from \"@/conf/bootstrap.ts\";\nimport { toggleMenu } from \"@/store/menu.ts\";\nimport router from \"@/router.tsx\";\nimport MenuBar from \"./MenuBar.tsx\";\nimport { getMemory } from \"@/utils/memory.ts\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport Avatar from \"@/components/Avatar.tsx\";\nimport { appLogo } from \"@/conf/env.ts\";\nimport { refreshQuota } from \"@/store/quota.ts\";\nimport { refreshSubscription } from \"@/store/subscription.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { AppDispatch, clearCronJobs, createCronJob } from \"@/store\";\nimport { openDialog } from \"@/store/settings.ts\";\nimport ThemeToggle from \"@/components/ThemeProvider.tsx\";\nimport ProjectLink from \"@/components/ProjectLink.tsx\";\n\nfunction NavMenu() {\n  const username = useSelector(selectUsername);\n\n  return (\n    <div className={`avatar`}>\n      <MenuBar>\n        <Button\n          variant={`ghost`}\n          size={`icon-md`}\n          className={`rounded-full overflow-hidden`}\n          unClickable\n        >\n          <Avatar username={username} className={`w-9 h-9 rounded-full`} />\n        </Button>\n      </MenuBar>\n    </div>\n  );\n}\n\nfunction NavBar() {\n  const { t } = useTranslation();\n  const dispatch: AppDispatch = useDispatch();\n  useEffect(() => {\n    validateToken(dispatch, getMemory(tokenField));\n  }, []);\n  const auth = useSelector(selectAuthenticated);\n\n  useEffectAsync(async () => {\n    if (!auth) return;\n\n    const quotaTask = createCronJob(dispatch, refreshQuota, 30, true);\n    const planTask = createCronJob(dispatch, refreshSubscription, 30, true);\n\n    console.log(\n      `[cron] register quota and plan fetching tasks: ${quotaTask}, ${planTask}`,\n    );\n\n    return () => clearCronJobs([quotaTask, planTask]);\n  }, [auth]);\n\n  return (\n    <nav className={`navbar`}>\n      <div className={`items space-x-2`}>\n        <Button\n          size={`icon-md`}\n          variant={`ghost`}\n          className={`sidebar-button`}\n          onClick={() => dispatch(toggleMenu())}\n        >\n          <Menu className={`w-5 h-5`} />\n        </Button>\n        <img\n          className={`logo w-9 h-9 scale-110`}\n          src={appLogo}\n          alt=\"\"\n          onClick={() => router.navigate(\"/\")}\n        />\n        <div className={`grow`} />\n        <ProjectLink />\n        <ThemeToggle size=\"icon-md\" className={`rounded-full overflow-hidden`} />\n        <Button\n          size={`icon-md`}\n          variant={`outline`}\n          className={`rounded-full overflow-hidden`}\n          onClick={() => dispatch(openDialog())}\n        >\n          <Settings2 className={`w-4 h-4`} />\n        </Button>\n        {auth ? (\n          <NavMenu />\n        ) : (\n          <Button size={`thin`} className={`rounded-full`} onClick={goAuth}>\n            {t(\"login\")}\n          </Button>\n        )}\n      </div>\n    </nav>\n  );\n}\n\nexport default NavBar;\n"
  },
  {
    "path": "app/src/components/home/ChatInterface.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport { Message } from \"@/api/types.tsx\";\nimport { useSelector } from \"react-redux\";\nimport {\n  listenMessageEvent,\n  selectCurrent,\n  useMessages,\n} from \"@/store/chat.ts\";\nimport MessageSegment from \"@/components/Message.tsx\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { AnimatePresence, motion } from \"framer-motion\";\n\ntype ChatInterfaceProps = {\n  scrollable: boolean;\n  setTarget: (target: HTMLDivElement | null) => void;\n};\n\nconst shouldRenderMessage = (message: Message): boolean => {\n  if (message.role === \"tool\") {\n    return false;\n  }\n\n  if (message.role && message.role.startsWith(\"virtualRole::\")) {\n    return false;\n  }\n  \n  return true;\n};\n\nfunction ChatInterface({ scrollable, setTarget }: ChatInterfaceProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const messages: Message[] = useMessages();\n  const process = listenMessageEvent();\n  const current: number = useSelector(selectCurrent);\n  const [selected, setSelected] = React.useState(-1);\n\n  const renderableMessages = React.useMemo(() => {\n    return messages.filter(shouldRenderMessage);\n  }, [messages]);\n\n  useEffect(() => {\n    if (!ref.current || !scrollable) return;\n    const el = ref.current;\n    el.scrollTop = el.scrollHeight;\n  }, [renderableMessages, scrollable]);\n\n  useEffect(() => {\n    setTarget(ref.current);\n  }, [ref, setTarget]);\n\n  return (\n    <ScrollArea className=\"chat-content\" ref={ref}>\n      <AnimatePresence>\n        <motion.div className=\"chat-messages-wrapper\">\n          {renderableMessages.map((message) => {\n            const originalIndex = messages.findIndex(m => m === message);\n            \n            return (\n              <motion.div\n                key={`message-${originalIndex}`}\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: -20 }}\n                transition={{\n                  type: \"spring\",\n                  stiffness: 500,\n                  damping: 30,\n                  mass: 1,\n                  delay:\n                    message.role === \"assistant\" || message.role === \"system\"\n                      ? 0.25\n                      : 0,\n                }}\n              >\n                <MessageSegment\n                  message={message}\n                  end={originalIndex === messages.length - 1}\n                  onEvent={(event: string, index?: number, message?: string) => {\n                    process({ id: current, event, index: index ?? originalIndex, message });\n                  }}\n                  index={originalIndex}\n                  selected={selected === originalIndex}\n                  onFocus={() => setSelected(originalIndex)}\n                  onFocusLeave={() => setSelected(-1)}\n                />\n              </motion.div>\n            );\n          })}\n        </motion.div>\n      </AnimatePresence>\n    </ScrollArea>\n  );\n}\n\nexport default ChatInterface;\n"
  },
  {
    "path": "app/src/components/home/ChatSpace.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useSelector } from \"react-redux\";\nimport { selectAuthenticated } from \"@/store/auth.ts\";\nimport { infoAuthFooterSelector, infoFooterSelector } from \"@/store/info.ts\";\nimport Markdown from \"@/components/Markdown.tsx\";\nimport { motion } from \"framer-motion\";\nimport { useState } from \"react\";\nimport { ArrowRight, Megaphone } from \"lucide-react\";\nimport Clickable from \"@/components/ui/clickable\";\nimport Announcement from \"@/components/app/Announcement\";\n\nfunction Footer() {\n  const auth = useSelector(selectAuthenticated);\n  const footer = useSelector(infoFooterSelector);\n  const auth_footer = useSelector(infoAuthFooterSelector);\n\n  if (auth && auth_footer) {\n    // hide footer\n    return null;\n  }\n\n  return (\n    footer.length > 0 && (\n      <Markdown\n        className={`whitespace-pre-wrap text-secondary text-xs md:text-sm rounded-md bg-background/10 backdrop-blur-sm`}\n        acceptHtml={true}\n      >\n        {footer}\n      </Markdown>\n    )\n  );\n}\n\nfunction ChatSpace() {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n\n  return (\n    <motion.div\n      className={`chat-product`}\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5 }}\n    >\n      <motion.div\n        className=\"flex flex-col space-y-1 w-full md:max-w-2xl mb-4 md:mx-auto px-6 select-none\"\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.5, delay: 0.2 }}\n      >\n        <motion.div\n          className=\"flex flex-row items-center px-1 w-full\"\n          initial={{ opacity: 0, x: -20 }}\n          animate={{ opacity: 1, x: 0 }}\n          transition={{ duration: 0.3, delay: 0.2 }}\n        >\n          <Megaphone className=\"w-3 h-3 text-secondary mr-1\" />\n          <p className=\"text-xs font-medium text-secondary mr-auto\">\n            {t(\"new-announcement\")}\n          </p>\n          <Clickable\n            tapScale={0.9}\n            className=\"flex items-center\"\n            onClick={() => setOpen(true)}\n          >\n            <motion.button\n              className=\"w-fit h-fit\"\n              whileHover={{ scale: 1.1 }}\n              whileTap={{ scale: 0.9 }}\n            >\n              <ArrowRight className=\"w-3 h-3 text-secondary\" />\n            </motion.button>\n            <p className=\"text-xs font-medium text-secondary ml-1\">\n              {t(\"learn-more\")}\n            </p>\n          </Clickable>\n        </motion.div>\n        <Announcement open={open} setOpen={setOpen} />\n      </motion.div>\n\n      <motion.div\n        className={`space-footer`}\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.5, delay: 0.8 }}\n      >\n        <Footer />\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport default ChatSpace;\n"
  },
  {
    "path": "app/src/components/home/ChatWrapper.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport FileAction from \"@/components/FileProvider.tsx\";\nimport { useSelector } from \"react-redux\";\nimport { selectAuthenticated, selectInit } from \"@/store/auth.ts\";\nimport {\n  listenMessageEvent,\n  selectCurrent,\n  selectModel,\n  selectSupportModels,\n  useMessageActions,\n  useMessages,\n  useWorking,\n} from \"@/store/chat.ts\";\nimport { formatMessage } from \"@/utils/processor.ts\";\nimport ChatInterface from \"@/components/home/ChatInterface.tsx\";\nimport { clearHistoryState, getQueryParam } from \"@/utils/path.ts\";\nimport { forgetMemory, popMemory } from \"@/utils/memory.ts\";\nimport { alignSelector } from \"@/store/settings.ts\";\nimport { FileArray } from \"@/api/file.ts\";\nimport {\n  NewConversationAction,\n  WebAction,\n} from \"@/components/home/assemblies/ChatAction.tsx\";\nimport ChatSpace from \"@/components/home/ChatSpace.tsx\";\nimport ActionButton, {\n  ActionCommand,\n} from \"@/components/home/assemblies/ActionButton.tsx\";\nimport ChatInput from \"@/components/home/assemblies/ChatInput.tsx\";\nimport ScrollAction from \"@/components/home/assemblies/ScrollAction.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { getModelFromId } from \"@/conf/model.ts\";\nimport { ModelArea } from \"@/components/home/ModelArea.tsx\";\nimport { toast } from \"sonner\";\nimport { VoiceAction } from \"@/components/VoiceProvider.tsx\";\nimport { AnimatePresence, motion } from \"framer-motion\";\n\ntype InterfaceProps = {\n  scrollable: boolean;\n  setTarget: (instance: HTMLElement | null) => void;\n};\n\nfunction Interface(props: InterfaceProps) {\n  const messages = useMessages();\n  return messages.length > 0 ? <ChatInterface {...props} /> : <ChatSpace />;\n}\n\nfunction fileReducer(state: FileArray, action: Record<string, any>): FileArray {\n  switch (action.type) {\n    case \"add\":\n      return [...state, action.payload];\n    case \"remove\":\n      return state.filter((_, i) => i !== action.payload);\n    case \"clear\":\n      return [];\n    default:\n      return state;\n  }\n}\n\nfunction ChatWrapper() {\n  const { t } = useTranslation();\n  const { send: sendAction } = useMessageActions();\n  const process = listenMessageEvent();\n  const [files, fileDispatch] = useReducer(fileReducer, []);\n  const [input, setInput] = useState(\"\");\n  const [visible, setVisibility] = useState(false);\n  const init = useSelector(selectInit);\n  const current = useSelector(selectCurrent);\n  const auth = useSelector(selectAuthenticated);\n  const model = useSelector(selectModel);\n  const target = useRef(null);\n  const align = useSelector(alignSelector);\n\n  const working = useWorking();\n  const supportModels = useSelector(selectSupportModels);\n\n  const requireAuth = useMemo(\n    (): boolean => !!getModelFromId(supportModels, model)?.auth,\n    [model],\n  );\n\n  const [instance, setInstance] = useState<HTMLElement | null>(null);\n\n  function clearFile() {\n    fileDispatch({ type: \"clear\" });\n  }\n\n  async function processSend(\n    data: string,\n    passAuth?: boolean,\n  ): Promise<boolean> {\n    if (requireAuth && !auth && !passAuth) {\n      toast(t(\"login-require\"), {\n        description: t(\"login-require-prompt\"),\n        action: {\n          label: t(\"login\"),\n          onClick: goAuth,\n        },\n      });\n      return false;\n    }\n\n    if (working) return false;\n\n    const message: string = formatMessage(files, data);\n    if (message.length > 0 && data.trim().length > 0) {\n      if (await sendAction(message)) {\n        forgetMemory(\"history\");\n        clearFile();\n        return true;\n      }\n    }\n    return false;\n  }\n\n  async function handleSend() {\n    // because of the function wrapper, we need to update the selector state using props.\n    if (await processSend(input)) {\n      setInput(\"\");\n    }\n  }\n\n  async function handleCancel() {\n    process({ id: current, event: \"stop\" });\n  }\n\n  useEffect(() => {\n    window.addEventListener(\"load\", () => {\n      const el = document.getElementById(\"input\");\n      if (el) el.focus();\n    });\n  }, []);\n\n  useEffect(() => {\n    if (!init) return;\n    const query = getQueryParam(\"q\").trim();\n    if (query.length > 0) processSend(query).then();\n    clearHistoryState();\n  }, [init]);\n\n  useEffect(() => {\n    const history: string = popMemory(\"history\");\n    if (history.length) {\n      setInput(history);\n      toast(t(\"chat.recall\"), {\n        description: t(\"chat.recall-desc\"),\n        action: {\n          label: t(\"chat.recall-cancel\"),\n          onClick: () => {\n            setInput(\"\");\n          },\n        },\n      });\n    }\n  }, []);\n\n  return (\n    <div className={`chat-container bg-muted/25 dark:bg-muted/10`}>\n      <div className={`chat-wrapper`}>\n        <Interface setTarget={setInstance} scrollable={!visible} />\n        <div className={`chat-input border-t bg-muted/25`}>\n          <motion.div\n            className={`flex flex-row items-center p-1.5 pb-0.5`}\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.5, ease: \"easeOut\" }}\n          >\n            <AnimatePresence key=\"model\">\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3 }}\n              >\n                <ModelArea />\n              </motion.div>\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3, delay: 0.1 }}\n              >\n                <WebAction />\n              </motion.div>\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3, delay: 0.2 }}\n              >\n                <FileAction files={files} dispatch={fileDispatch} />\n              </motion.div>\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3, delay: 0.3 }}\n              >\n                <VoiceAction />\n              </motion.div>\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3, delay: 0.5 }}\n              >\n                <ScrollAction\n                  visible={visible}\n                  setVisibility={setVisibility}\n                  target={instance}\n                />\n              </motion.div>\n            </AnimatePresence>\n            <motion.div\n              className={`grow`}\n              initial={{ scaleX: 0 }}\n              animate={{ scaleX: 1 }}\n              transition={{ duration: 0.5, ease: \"easeOut\" }}\n            />\n            <AnimatePresence key=\"new\">\n              <motion.div\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={{ duration: 0.3, delay: 0.6 }}\n              >\n                <NewConversationAction />\n              </motion.div>\n            </AnimatePresence>\n          </motion.div>\n          <div className={`flex flex-col gap-2 px-3 pb-2`}>\n            <div className={`relative w-full`}>\n              <ChatInput\n                className={cn(\n                  \"rounded-none border-0 bg-transparent w-full\",\n                  align && \"align\",\n                )}\n                target={target}\n                value={input}\n                onValueChange={setInput}\n                onEnterPressed={handleSend}\n              />\n            </div>\n            <div className=\"flex items-center justify-end gap-2\">\n              <ActionCommand input={input} />\n              <ActionButton\n                working={working}\n                onClick={() => (working ? handleCancel() : handleSend())}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default ChatWrapper;\n"
  },
  {
    "path": "app/src/components/home/ConversationItem.tsx",
    "content": "import { mobile } from \"@/utils/device.ts\";\nimport { filterMessage } from \"@/utils/processor.ts\";\nimport { setMenu } from \"@/store/menu.ts\";\nimport {\n  Loader2,\n  MessageSquare,\n  MessagesSquare,\n  MoreHorizontal,\n  PencilLine,\n  Share2,\n  Trash2,\n} from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { useDispatch } from \"react-redux\";\nimport { useTranslation } from \"react-i18next\";\nimport { ConversationInstance } from \"@/api/types.tsx\";\nimport { useState } from \"react\";\nimport { useConversationActions } from \"@/store/chat.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport PopupDialog, { popupTypes } from \"@/components/PopupDialog.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport Clickable from \"@/components/ui/clickable.tsx\";\n\ntype ConversationItemProps = {\n  conversation: ConversationInstance;\n  current: number;\n  operate: (conversation: {\n    target: ConversationInstance;\n    type: string;\n  }) => void;\n};\nfunction ConversationItem({\n  conversation,\n  current,\n  operate,\n}: ConversationItemProps) {\n  const dispatch = useDispatch();\n  const { toggle } = useConversationActions();\n  const { t } = useTranslation();\n  const { rename } = useConversationActions();\n  const [open, setOpen] = useState(false);\n  const [offset, setOffset] = useState(0);\n\n  const [editDialog, setEditDialog] = useState(false);\n\n  const loading = conversation.id <= 0;\n\n  return (\n    <Clickable\n      tapScale={0.975}\n      tapDuration={0.01}\n      className={cn(\"conversation\", current === conversation.id && \"active\")}\n      onClick={async (e) => {\n        const target = e.target as HTMLElement;\n        if (\n          target.classList.contains(\"delete\") ||\n          target.parentElement?.classList.contains(\"delete\")\n        )\n          return;\n        await toggle(conversation.id);\n        if (mobile) dispatch(setMenu(false));\n      }}\n      onContextMenu={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        setOpen(true);\n      }}\n    >\n      <MessageSquare\n        className={`h-6 w-6 p-1 mr-1 text-secondary bg-input/25 rounded-sm`}\n      />\n      <div className={`title`}>{filterMessage(conversation.name)}</div>\n      <DropdownMenu\n        open={open}\n        onOpenChange={(state: boolean) => {\n          setOpen(state);\n          if (state) setOffset(new Date().getTime());\n        }}\n      >\n        <DropdownMenuTrigger\n          className={`flex flex-row outline-none`}\n          onClick={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n          }}\n        >\n          <div className={cn(\"id\", loading && \"loading\")}>\n            {loading ? (\n              <Loader2 className={`mr-0.5 h-4 w-4 animate-spin`} />\n            ) : (\n              <MoreHorizontal className={`h-4 w-4 mr-0.5`} />\n            )}\n          </div>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align={`end`}>\n          <DropdownMenuLabel\n            className={`inline-flex conversation-id text-left py-0.5 w-full`}\n          >\n            {conversation.id}\n\n            <MessagesSquare\n              className={`inline h-3.5 w-3.5 ml-auto translate-y-0.5 text-secondary`}\n            />\n          </DropdownMenuLabel>\n          <DropdownMenuSeparator />\n          <PopupDialog\n            title={t(\"conversation.edit-title\")}\n            open={editDialog}\n            setOpen={setEditDialog}\n            type={popupTypes.Text}\n            name={t(\"title\")}\n            defaultValue={conversation.name}\n            onSubmit={async (name) => {\n              const resp = await rename(conversation.id, name);\n              withNotify(t, resp, true);\n              if (!resp.status) return false;\n\n              setEditDialog(false);\n              return true;\n            }}\n          />\n          <DropdownMenuItem\n            onClick={(e) => {\n              // prevent click event from opening the dropdown menu\n              if (offset + 500 > new Date().getTime()) return;\n\n              e.preventDefault();\n              e.stopPropagation();\n\n              setEditDialog(true);\n            }}\n          >\n            <PencilLine className={`h-4 w-4 mx-1`} />\n            {t(\"conversation.edit-title\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            onClick={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n              operate({ target: conversation, type: \"share\" });\n\n              setOpen(false);\n            }}\n          >\n            <Share2 className={`h-4 w-4 mx-1`} />\n            {t(\"share.share-conversation\")}\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            onClick={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n              operate({ target: conversation, type: \"delete\" });\n\n              setOpen(false);\n            }}\n          >\n            <Trash2 className={`h-4 w-4 mx-1`} />\n            {t(\"conversation.delete-conversation\")}\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </Clickable>\n  );\n}\n\nexport default ConversationItem;\n"
  },
  {
    "path": "app/src/components/home/MaskEditor.tsx",
    "content": "import { CustomMask, initialCustomMask } from \"@/masks/types.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { selectAuthenticated } from \"@/store/auth.ts\";\nimport { themeSelector } from \"@/store/globals.ts\";\nimport React, { useState } from \"react\";\nimport { saveMask } from \"@/api/mask.ts\";\nimport { withNotify } from \"@/api/common.ts\";\nimport { updateMasks } from \"@/store/chat.ts\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n} from \"@/components/ui/drawer.tsx\";\nimport EditorProvider from \"@/components/EditorProvider.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport Emoji, { getEmojiSource } from \"@/components/Emoji.tsx\";\nimport EmojiPicker, { Theme } from \"emoji-picker-react\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { FlexibleTextarea } from \"@/components/ui/textarea.tsx\";\nimport { ChevronDown, ChevronUp, Pencil, Plus, Trash } from \"lucide-react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { getRoleIcon, Roles, UserRole } from \"@/api/types.tsx\";\nimport Icon from \"@/components/utils/Icon.tsx\";\n\nexport function maskEditorReducer(state: CustomMask, action: any): CustomMask {\n  switch (action.type) {\n    case \"update-avatar\":\n      return { ...state, avatar: action.payload };\n    case \"update-name\":\n      return { ...state, name: action.payload };\n    case \"update-description\":\n      return { ...state, description: action.payload };\n    case \"set-conversation\":\n      return {\n        ...state,\n        context: action.payload,\n      };\n    case \"new-message\":\n      return {\n        ...state,\n        context: [...state.context, { role: UserRole, content: \"\" }],\n      };\n    case \"new-message-below\":\n      return {\n        ...state,\n        context: [\n          ...state.context.slice(0, action.index + 1),\n          { role: UserRole, content: \"\" },\n          ...state.context.slice(action.index + 1),\n        ],\n      };\n    case \"update-message-role\":\n      return {\n        ...state,\n        context: state.context.map((item, idx) => {\n          if (idx === action.index) return { ...item, role: action.payload };\n          return item;\n        }),\n      };\n    case \"update-message-content\":\n      return {\n        ...state,\n        context: state.context.map((item, idx) => {\n          if (idx === action.index) return { ...item, content: action.payload };\n          return item;\n        }),\n      };\n    case \"change-index\":\n      const { from, to } = action.payload;\n      const context = [...state.context];\n      const [removed] = context.splice(from, 1);\n      context.splice(to, 0, removed);\n      return { ...state, context };\n    case \"remove-message\":\n      return {\n        ...state,\n        context: state.context.filter((_, idx) => idx !== action.index),\n      };\n    case \"reset\":\n      return { ...initialCustomMask };\n    case \"set-mask\":\n      return {\n        ...action.payload,\n      };\n    case \"import-mask\":\n      return {\n        ...action.payload,\n        description: action.payload.description || \"\",\n        id: -1,\n      };\n    default:\n      return state;\n  }\n}\n\ntype RoleActionProps = {\n  role: string;\n  onClick: (role: string) => void;\n};\n\nfunction RoleAction({ role, onClick }: RoleActionProps) {\n  const icon = getRoleIcon(role);\n\n  const toggle = () => {\n    const index = Roles.indexOf(role);\n    const next = (index + 1) % Roles.length;\n\n    onClick(Roles[next]);\n  };\n\n  return (\n    <Button\n      variant={`outline`}\n      size={`icon`}\n      className={`shrink-0`}\n      onClick={toggle}\n    >\n      <Icon icon={icon} className={`h-4 w-4`} />\n    </Button>\n  );\n}\n\ntype MaskActionProps = {\n  children: React.ReactNode;\n  disabled?: boolean;\n  onClick?: () => void;\n};\n\nfunction MaskAction({ children, disabled, onClick }: MaskActionProps) {\n  return (\n    <div\n      className={cn(`mask-action`, disabled && \"disabled\")}\n      onClick={disabled ? undefined : onClick}\n    >\n      {children}\n    </div>\n  );\n}\n\ntype CustomMaskDialogProps = {\n  mask: CustomMask;\n  dispatch: (action: any) => void;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n};\n\nfunction MaskEditor({\n  mask,\n  dispatch,\n  open,\n  onOpenChange,\n}: CustomMaskDialogProps) {\n  const { t } = useTranslation();\n\n  const auth = useSelector(selectAuthenticated);\n  const theme = useSelector(themeSelector);\n\n  const [picker, setPicker] = useState(false);\n\n  const [editor, setEditor] = useState(false);\n  const [editorIndex, setEditorIndex] = useState(-1);\n\n  const global = useDispatch();\n\n  const openEditor = (index: number) => {\n    setEditorIndex(index);\n    setEditor(true);\n  };\n\n  const post = async () => {\n    const data = { ...mask };\n    data.context = mask.context.filter(\n      (item) => item.content.trim().length > 0,\n    );\n\n    if (data.name.trim().length === 0) return;\n    if (data.context.length === 0) return;\n\n    const resp = await saveMask(data);\n    withNotify(t, resp, true);\n\n    if (!resp.status) return;\n\n    await updateMasks(global);\n    onOpenChange(false);\n  };\n\n  return (\n    <Drawer open={open} onOpenChange={onOpenChange}>\n      <DrawerContent>\n        <div className={`mask-drawer-viewport py-4 max-w-[620px] mx-auto`}>\n          <DrawerHeader>\n            <DrawerTitle className={`text-center mb-4`}>\n              {mask.id !== -1 ? t(\"mask.edit\") : t(\"mask.create\")}\n            </DrawerTitle>\n            <DrawerDescription>\n              <EditorProvider\n                value={editor ? mask.context[editorIndex].content : \"\"}\n                onChange={(content) =>\n                  dispatch({\n                    type: \"update-message-content\",\n                    index: editorIndex,\n                    payload: content,\n                  })\n                }\n                open={editor}\n                setOpen={setEditor}\n              />\n              <div\n                className={`mask-editor-container no-scrollbar max-h-[60vh] overflow-y-auto`}\n              >\n                <div className={`mask-editor-row`}>\n                  <div className={`mask-editor-column`}>\n                    <p>{t(\"mask.avatar\")}</p>\n                    <div className={`grow`} />\n\n                    <Tips\n                      trigger={\n                        <Button\n                          variant={`outline`}\n                          size={`icon`}\n                          className={`shrink-0`}\n                        >\n                          <Emoji emoji={mask.avatar} className={`h-6 w-6`} />\n                        </Button>\n                      }\n                      open={picker}\n                      onOpenChange={setPicker}\n                      align={`end`}\n                      classNamePopup={`mask-picker-dialog`}\n                      notHide\n                    >\n                      <EmojiPicker\n                        className={`picker`}\n                        height={360}\n                        lazyLoadEmojis\n                        skinTonesDisabled\n                        theme={theme as Theme}\n                        open={true}\n                        searchPlaceHolder={t(\"mask.search-emoji\")}\n                        getEmojiUrl={getEmojiSource}\n                        onEmojiClick={(emoji) => {\n                          setPicker(false);\n                          dispatch({\n                            type: \"update-avatar\",\n                            payload: emoji.unified,\n                          });\n                        }}\n                      />\n                    </Tips>\n                  </div>\n                  <div className={`mask-editor-column`}>\n                    <p>{t(\"mask.name\")}</p>\n                    <Input\n                      value={mask.name}\n                      className={`ml-4`}\n                      placeholder={t(\"mask.name-placeholder\")}\n                      onChange={(e) =>\n                        dispatch({\n                          type: \"update-name\",\n                          payload: e.target.value,\n                        })\n                      }\n                    />\n                  </div>\n                  <div className={`mask-editor-column`}>\n                    <p>{t(\"mask.description\")}</p>\n                    <FlexibleTextarea\n                      value={mask.description || \"\"}\n                      className={`ml-4`}\n                      placeholder={t(\"mask.description-placeholder\")}\n                      onChange={(e) =>\n                        dispatch({\n                          type: \"update-description\",\n                          payload: e.target.value,\n                        })\n                      }\n                    />\n                  </div>\n                </div>\n\n                <div className={`mask-conversation-list`}>\n                  <div className={`mask-conversation-title`}>\n                    {t(\"mask.conversation\")}\n                  </div>\n                  {mask.context.map((item, index) => (\n                    <div key={index} className={`mask-conversation-wrapper`}>\n                      <div className={`mask-conversation`}>\n                        <RoleAction\n                          role={item.role}\n                          onClick={(role) =>\n                            dispatch({\n                              type: \"update-message-role\",\n                              index,\n                              payload: role,\n                            })\n                          }\n                        />\n                        <FlexibleTextarea\n                          className={`ml-4`}\n                          value={item.content}\n                          onChange={(e) =>\n                            dispatch({\n                              type: \"update-message-content\",\n                              index,\n                              payload: e.target.value,\n                            })\n                          }\n                        />\n                      </div>\n                      <div className={`mask-actions`}>\n                        <MaskAction\n                          onClick={() =>\n                            dispatch({ type: \"new-message-below\", index })\n                          }\n                        >\n                          <Plus />\n                        </MaskAction>\n                        <MaskAction onClick={() => openEditor(index)}>\n                          <Pencil />\n                        </MaskAction>\n                        <MaskAction\n                          disabled={index === 0}\n                          onClick={() =>\n                            dispatch({\n                              type: \"change-index\",\n                              payload: { from: index, to: index - 1 },\n                            })\n                          }\n                        >\n                          <ChevronUp />\n                        </MaskAction>\n                        <MaskAction\n                          disabled={index === mask.context.length - 1}\n                          onClick={() =>\n                            dispatch({\n                              type: \"change-index\",\n                              payload: { from: index, to: index + 1 },\n                            })\n                          }\n                        >\n                          <ChevronDown />\n                        </MaskAction>\n                        <MaskAction\n                          disabled={mask.context.length === 1}\n                          onClick={() =>\n                            dispatch({ type: \"remove-message\", index })\n                          }\n                        >\n                          <Trash />\n                        </MaskAction>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            </DrawerDescription>\n          </DrawerHeader>\n          <DrawerFooter>\n            <Button unClickable loading={true} onClick={post} disabled={!auth}>\n              {auth ? t(\"submit\") : t(\"login-require\")}\n            </Button>\n            <DrawerClose asChild>\n              <Button unClickable variant=\"outline\">\n                {t(\"cancel\")}\n              </Button>\n            </DrawerClose>\n          </DrawerFooter>\n        </div>\n      </DrawerContent>\n    </Drawer>\n  );\n}\n\nexport default MaskEditor;\n"
  },
  {
    "path": "app/src/components/home/ModelArea.tsx",
    "content": "import SelectGroup, {\n  GroupSelectItem,\n  SelectItemProps,\n} from \"@/components/SelectGroup.tsx\";\nimport {\n  selectModel,\n  selectModelList,\n  selectSupportModels,\n  setModel,\n} from \"@/store/chat.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { selectAuthenticated } from \"@/store/auth.ts\";\nimport { Model, Plans } from \"@/api/types.tsx\";\nimport { modelEvent } from \"@/events/model.ts\";\nimport { levelSelector } from \"@/store/subscription.ts\";\nimport { teenagerSelector } from \"@/store/package.ts\";\nimport { useMemo } from \"react\";\nimport {\n  CloudOff,\n  Gem,\n  Sparkles,\n  Kanban,\n  Award,\n  EyeIcon,\n  Globe,\n  DollarSign,\n  Github,\n  Image,\n  Bolt,\n  Snail,\n  Cpu,\n  Zap,\n} from \"lucide-react\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { includingModelFromPlan } from \"@/conf/subscription.tsx\";\nimport { subscriptionDataSelector } from \"@/store/globals.ts\";\nimport router from \"@/router.tsx\";\nimport ModelAvatar from \"@/components/ModelAvatar.tsx\";\nimport {\n  NativeSelectTrigger,\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectGroup as SelectGroupUI,\n  SelectLabel,\n  SelectSeparator,\n} from \"@/components/ui/select.tsx\";\nimport { ChatAction } from \"@/components/home/assemblies/ChatAction.tsx\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport { toast } from \"sonner\";\n\nconst tagIcons: { [key: string]: React.ReactNode } = {\n  official: <Award />,\n  \"multi-modal\": <EyeIcon />,\n  web: <Globe />,\n  \"high-quality\": <Sparkles />,\n  \"high-price\": <DollarSign />,\n  \"open-source\": <Github />,\n  \"image-generation\": <Image />,\n  fast: <Bolt />,\n  unstable: <Snail />,\n  \"high-context\": <Cpu />,\n  free: <Zap />,\n};\n\nconst notDisplayTags = [\"official\", \"fast\", \"unstable\", \"free\"];\n\nfunction GetModel(models: Model[], name: string): Model {\n  return models.find((model) => model.id === name) as Model;\n}\n\ntype ModelSelectorProps = {\n  side?: \"left\" | \"right\" | \"top\" | \"bottom\";\n};\n\nfunction formatModel(\n  data: Plans,\n  model: Model,\n  level: number,\n  t: (key: string) => string,\n) {\n  let badge = [];\n  if (model.free) {\n    badge.push({\n      variant: \"default\",\n      icon: <CloudOff className={`h-3 w-3`} />,\n      tooltip: t(\"tag.free\"),\n    });\n  } else if (includingModelFromPlan(data, level, model.id)) {\n    badge.push({\n      variant: \"gold\",\n      icon: <Gem className={`h-3 w-3`} />,\n      tooltip: t(\"tag.badges.plan-included\"),\n    });\n  }\n\n  const tags = model.tag || [];\n  tags.forEach((tag) => {\n    if (tagIcons[tag] && !notDisplayTags.includes(tag)) {\n      badge.push({\n        variant: tag,\n        icon: <Icon icon={tagIcons[tag]} className={`h-3 w-3`} />,\n        tooltip: t(`tag.${tag}`),\n      });\n    }\n  });\n\n  return {\n    name: model.id,\n    value: model.name,\n    badge: badge.length > 0 ? badge : undefined,\n    icon: <ModelAvatar size={24} model={model} />,\n  } as SelectItemProps;\n}\n\nexport default function ModelFinder(props: ModelSelectorProps) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n\n  const model = useSelector(selectModel);\n  const auth = useSelector(selectAuthenticated);\n  const level = useSelector(levelSelector);\n  const student = useSelector(teenagerSelector);\n  const list = useSelector(selectModelList);\n\n  const supportModels = useSelector(selectSupportModels);\n  const modelList = useSelector(selectModelList);\n  const subscriptionData = useSelector(subscriptionDataSelector);\n\n  modelEvent.bind((target: string) => {\n    if (supportModels.find((m) => m.id === target)) {\n      if (model === target) return;\n      console.debug(`[chat] toggle model from event: ${target}`);\n      dispatch(setModel(target));\n    }\n  });\n\n  const models = useMemo(() => {\n    const raw = list.length\n      ? supportModels.filter((model) => list.includes(model.id))\n      : supportModels.filter((model) => model.default);\n\n    if (raw.length === 0)\n      raw.push({\n        name: \"default\",\n        id: \"default\",\n      } as Model);\n\n    return raw.map((model) => formatModel(subscriptionData, model, level, t));\n  }, [supportModels, subscriptionData, level, student, modelList, t]);\n\n  const current = useMemo((): SelectItemProps => {\n    const raw = models.find((item) => item.name === model);\n    return raw || models[0];\n  }, [models, model, supportModels, modelList]);\n\n  return (\n    <SelectGroup\n      current={current}\n      list={models}\n      maxElements={3}\n      side={props.side}\n      classNameMobile={`model-select-group`}\n      selectGroupTop={{\n        icon: <Sparkles size={16} />,\n        name: \"market\",\n        value: t(\"market.model\"),\n      }}\n      onChange={(value: string) => {\n        if (value === \"market\") {\n          router.navigate(\"/model\");\n          return;\n        }\n        const model = GetModel(supportModels, value);\n        console.debug(`[model] select model: ${model.name} (id: ${model.id})`);\n\n        if (!auth && model.auth) {\n          toast(t(\"login-require\"), {\n            action: {\n              label: t(\"login\"),\n              onClick: goAuth,\n            },\n          });\n          return;\n        }\n        dispatch(setModel(value));\n      }}\n    />\n  );\n}\n\nexport function ModelArea() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n\n  const model = useSelector(selectModel);\n  const auth = useSelector(selectAuthenticated);\n  const level = useSelector(levelSelector);\n  const student = useSelector(teenagerSelector);\n\n  const supportModels = useSelector(selectSupportModels);\n  const modelList = useSelector(selectModelList);\n  const subscriptionData = useSelector(subscriptionDataSelector);\n\n  modelEvent.bind((target: string) => {\n    if (supportModels.find((m) => m.id === target)) {\n      if (model === target) return;\n      console.debug(`[chat] toggle model from event: ${target}`);\n      dispatch(setModel(target));\n    }\n  });\n\n  const models = useMemo(() => {\n    const raw =\n      supportModels.length > 0\n        ? supportModels\n        : [\n            {\n              name: \"default\",\n              id: \"default\",\n            } as Model,\n          ];\n\n    return raw.map((model) => formatModel(subscriptionData, model, level, t));\n  }, [supportModels, subscriptionData, level, student, modelList, t]);\n\n  const starredModels = useMemo(() => {\n    return models.filter((model) => modelList.includes(model.name));\n  }, [models, modelList]);\n\n  const unstarredModels = useMemo(() => {\n    return models.filter((model) => !modelList.includes(model.name));\n  }, [models, modelList]);\n\n  const showStarred = starredModels.length > 0;\n\n  const current = useMemo((): SelectItemProps => {\n    const raw = models.find((item) => item.name === model);\n    return raw || models[0];\n  }, [models, model, supportModels, modelList]);\n\n  return (\n    <Select\n      value={current.name}\n      onValueChange={(value: string) => {\n        if (value === \"market\") {\n          router.navigate(\"/model\");\n          return;\n        }\n        const model = GetModel(supportModels, value);\n        console.debug(`[model] select model: ${model.name} (id: ${model.id})`);\n\n        if (!auth && model.auth) {\n          toast(t(\"login-require\"), {\n            action: {\n              label: t(\"login\"),\n              onClick: goAuth,\n            },\n          });\n          return;\n        }\n        dispatch(setModel(value));\n      }}\n    >\n      <NativeSelectTrigger>\n        <ChatAction text={t(\"model\")}>\n          <Icon icon={current.icon} className={`h-4 w-4`} />\n        </ChatAction>\n      </NativeSelectTrigger>\n      <SelectContent>\n        <SelectGroupUI>\n          <SelectLabel>{t(\"market.title\")}</SelectLabel>\n          <SelectItem value=\"market\">\n            <GroupSelectItem\n              icon={\n                <Kanban\n                  className={`h-6 w-6 p-1 rounded-full bg-amber-500/10 text-amber-500`}\n                />\n              }\n              name=\"market\"\n              value={t(\"market.model\")}\n            />\n          </SelectItem>\n        </SelectGroupUI>\n        <SelectSeparator />\n\n        {showStarred && (\n          <>\n            <SelectGroupUI>\n              <SelectLabel>{t(\"starred\")}</SelectLabel>\n              {starredModels.map((model, idx) => (\n                <SelectItem key={idx} value={model.name}>\n                  <GroupSelectItem {...model} />\n                </SelectItem>\n              ))}\n            </SelectGroupUI>\n            <SelectSeparator />\n          </>\n        )}\n\n        <SelectGroupUI>\n          <SelectLabel>{t(\"unstarred\")}</SelectLabel>\n          {unstarredModels.map((model, idx) => (\n            <SelectItem key={idx} value={model.name}>\n              <GroupSelectItem {...model} />\n            </SelectItem>\n          ))}\n        </SelectGroupUI>\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "app/src/components/home/SideBar.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { selectAuthenticated } from \"@/store/auth.ts\";\nimport {\n  selectCurrent,\n  selectHistory,\n  selectMaskItem,\n  useConversationActions,\n} from \"@/store/chat.ts\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { ConversationInstance } from \"@/api/types.tsx\";\nimport { extractMessage, filterMessage } from \"@/utils/processor.ts\";\nimport { copyClipboard } from \"@/utils/dom.ts\";\nimport { useEffectAsync, useAnimation } from \"@/utils/hook.ts\";\nimport { mobile, openWindow } from \"@/utils/device.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { selectMenu, setMenu } from \"@/store/menu.ts\";\nimport {\n  Copy,\n  Eraser,\n  Paintbrush,\n  Plus,\n  RotateCw,\n  Search,\n  User,\n} from \"lucide-react\";\nimport ConversationItem from \"./ConversationItem.tsx\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog.tsx\";\nimport { getSharedLink, shareConversation } from \"@/api/sharing.ts\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { getNumberMemory } from \"@/utils/memory.ts\";\nimport { toast } from \"sonner\";\nimport { AnimatePresence, motion } from \"framer-motion\";\n\ntype Operation = {\n  target: ConversationInstance | null;\n  type: string;\n};\n\ntype SidebarActionProps = {\n  search: string;\n  setSearch: (search: string) => void;\n  setOperateConversation: (operation: Operation) => void;\n};\n\ntype ConversationListProps = {\n  search: string;\n  operateConversation: Operation;\n  setOperateConversation: (operation: Operation) => void;\n};\n\nfunction SidebarAction({\n  search,\n  setSearch,\n  setOperateConversation,\n}: SidebarActionProps) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n\n  const {\n    toggle,\n    refresh: refreshAction,\n    removeAll: removeAllAction,\n  } = useConversationActions();\n  const refreshRef = useRef(null);\n  const [removeAll, setRemoveAll] = useState<boolean>(false);\n\n  const current = useSelector(selectCurrent);\n  const mask = useSelector(selectMaskItem);\n\n  async function handleDeleteAll(e: React.MouseEvent<HTMLButtonElement>) {\n    e.preventDefault();\n    e.stopPropagation();\n\n    (await removeAllAction())\n      ? toast.success(t(\"conversation.delete-success\"), {\n          description: t(\"conversation.delete-success-prompt\"),\n        })\n      : toast.error(t(\"conversation.delete-failed\"), {\n          description: t(\"conversation.delete-failed-prompt\"),\n        });\n\n    await refreshAction();\n    setOperateConversation({ target: null, type: \"\" });\n    setRemoveAll(false);\n  }\n\n  return (\n    <motion.div\n      className={`sidebar-action-wrapper flex flex-col w-full h-fit px-1.5`}\n      initial={{ opacity: 0, y: -20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5 }}\n    >\n      <motion.div\n        className={`sidebar-action`}\n        initial={{ scale: 0.9 }}\n        animate={{ scale: 1 }}\n        transition={{ duration: 0.3 }}\n      >\n        <motion.div whileTap={{ scale: 0.9 }}>\n          <Button\n            variant={`ghost`}\n            size={`icon`}\n            onClick={async () => {\n              await toggle(-1);\n              if (mobile) dispatch(setMenu(false));\n            }}\n          >\n            {current === -1 && mask ? (\n              <Paintbrush className={`h-4 w-4`} />\n            ) : (\n              <Plus className={`h-4 w-4`} />\n            )}\n          </Button>\n        </motion.div>\n        <div className={`grow`} />\n        <AlertDialog open={removeAll} onOpenChange={setRemoveAll}>\n          <AlertDialogTrigger asChild>\n            <motion.div whileTap={{ scale: 0.9 }}>\n              <Button variant={`ghost`} size={`icon`}>\n                <Eraser className={`h-4 w-4`} />\n              </Button>\n            </motion.div>\n          </AlertDialogTrigger>\n          <AlertDialogContent>\n            <AlertDialogHeader>\n              <AlertDialogTitle>\n                {t(\"conversation.remove-all-title\")}\n              </AlertDialogTitle>\n              <AlertDialogDescription>\n                {t(\"conversation.remove-all-description\")}\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n            <AlertDialogFooter>\n              <AlertDialogCancel>{t(\"conversation.cancel\")}</AlertDialogCancel>\n              <Button\n                variant={`destructive`}\n                loading={true}\n                onClick={handleDeleteAll}\n                unClickable\n              >\n                {t(\"conversation.delete\")}\n              </Button>\n            </AlertDialogFooter>\n          </AlertDialogContent>\n        </AlertDialog>\n        <motion.div whileTap={{ scale: 0.9 }} className={`refresh-action`}>\n          <Button\n            variant={`ghost`}\n            size={`icon`}\n            id={`refresh`}\n            ref={refreshRef}\n            onClick={() => {\n              const hook = useAnimation(refreshRef, \"active\", 500);\n              refreshAction().finally(hook);\n            }}\n          >\n            <RotateCw className={`h-4 w-4`} />\n          </Button>\n        </motion.div>\n      </motion.div>\n      <motion.div\n        className={`relative w-full h-fit`}\n        initial={{ opacity: 0, x: -20 }}\n        animate={{ opacity: 1, x: 0 }}\n        transition={{ duration: 0.5, delay: 0.2 }}\n      >\n        <Search\n          className={`absolute h-3.5 w-3.5 top-1/2 left-3.5 transform -translate-y-1/2`}\n        />\n        <Input\n          value={search}\n          onChange={(e) => setSearch(e.target.value)}\n          placeholder={t(\"conversation.search\")}\n          className={`w-full pl-9`}\n        />\n      </motion.div>\n    </motion.div>\n  );\n}\n\nfunction SidebarConversationList({\n  search,\n  operateConversation,\n  setOperateConversation,\n}: ConversationListProps) {\n  const { t } = useTranslation();\n  const { remove } = useConversationActions();\n  const auth = useSelector(selectAuthenticated);\n  const history: ConversationInstance[] = useSelector(selectHistory);\n  const [shared, setShared] = useState<string>(\"\");\n  const current = useSelector(selectCurrent);\n\n  const filteredHistory = useMemo(() => {\n    if (search.trim().length === 0) return history;\n\n    const searchItems = search\n      .trim()\n      .toLowerCase()\n      .split(\" \")\n      .filter((item) => item.length > 0);\n\n    return history.filter((conversation) => {\n      const name = conversation.name.toLowerCase();\n      const id = conversation.id.toString();\n      return searchItems.every(\n        (item) => name.includes(item) || id.includes(item),\n      );\n    });\n  }, [history, search]);\n\n  async function handleDelete(e: React.MouseEvent<HTMLButtonElement>) {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (await remove(operateConversation?.target?.id || -1))\n      toast.success(t(\"conversation.delete-success\"), {\n        description: t(\"conversation.delete-success-prompt\"),\n      });\n    else\n      toast.error(t(\"conversation.delete-failed\"), {\n        description: t(\"conversation.delete-failed-prompt\"),\n      });\n    setOperateConversation({ target: null, type: \"\" });\n  }\n\n  async function handleShare(e: React.MouseEvent<HTMLButtonElement>) {\n    e.preventDefault();\n    e.stopPropagation();\n\n    const resp = await shareConversation(operateConversation?.target?.id || -1);\n    if (resp.status) setShared(getSharedLink(resp.data));\n    else\n      toast.error(t(\"share.failed\"), {\n        description: resp.message,\n      });\n\n    setOperateConversation({ target: null, type: \"\" });\n  }\n\n  return (\n    <>\n      <div className={`conversation-list`}>\n        <AnimatePresence>\n          {filteredHistory.length ? (\n            filteredHistory.map((conversation, i) => (\n              <motion.div\n                key={conversation.id}\n                initial={{ opacity: 0, y: 20 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: -20 }}\n                transition={{ duration: 0.3, delay: i * 0.05 }}\n              >\n                <ConversationItem\n                  operate={setOperateConversation}\n                  conversation={conversation}\n                  current={current}\n                />\n              </motion.div>\n            ))\n          ) : (\n            <motion.div\n              initial={{ opacity: 0, scale: 0.8 }}\n              animate={{ opacity: 1, scale: 1 }}\n              transition={{ duration: 0.5 }}\n              className={`empty text-center px-6`}\n            >\n              {auth\n                ? t(\"conversation.empty\")\n                : t(\"conversation.empty-anonymous\")}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n      <AlertDialog\n        open={\n          operateConversation.type === \"delete\" && !!operateConversation.target\n        }\n        onOpenChange={(open) => {\n          if (!open) setOperateConversation({ target: null, type: \"\" });\n        }}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>\n              {t(\"conversation.remove-title\")}\n            </AlertDialogTitle>\n            <AlertDialogDescription>\n              {t(\"conversation.remove-description\")}\n              <strong className={`conversation-name`}>\n                {extractMessage(\n                  filterMessage(operateConversation?.target?.name || \"\"),\n                )}\n              </strong>\n              {t(\"end\")}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>{t(\"conversation.cancel\")}</AlertDialogCancel>\n            <AlertDialogAction onClick={handleDelete}>\n              {t(\"conversation.delete\")}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n\n      <AlertDialog\n        open={\n          operateConversation.type === \"share\" && !!operateConversation.target\n        }\n        onOpenChange={(open) => {\n          if (!open) setOperateConversation({ target: null, type: \"\" });\n        }}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>{t(\"share.title\")}</AlertDialogTitle>\n            <AlertDialogDescription>\n              {t(\"share.description\")}\n              <strong className={`conversation-name`}>\n                {extractMessage(\n                  filterMessage(operateConversation?.target?.name || \"\"),\n                )}\n              </strong>\n              {t(\"end\")}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>{t(\"conversation.cancel\")}</AlertDialogCancel>\n            <AlertDialogAction onClick={handleShare}>\n              {t(\"share.title\")}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n\n      <AlertDialog\n        open={shared.length > 0}\n        onOpenChange={(open) => {\n          if (!open) {\n            setShared(\"\");\n            setOperateConversation({ target: null, type: \"\" });\n          }\n        }}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>{t(\"share.success\")}</AlertDialogTitle>\n            <AlertDialogDescription>\n              <div className={`share-wrapper mt-4 mb-2`}>\n                <Input value={shared} />\n                <Button\n                  variant={`default`}\n                  size={`icon`}\n                  onClick={async () => {\n                    await copyClipboard(shared);\n                    toast.success(t(\"share.copied\"), {\n                      description: t(\"share.copied-description\"),\n                    });\n                  }}\n                >\n                  <Copy className={`h-4 w-4`} />\n                </Button>\n              </div>\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>{t(\"close\")}</AlertDialogCancel>\n            <AlertDialogAction\n              onClick={async (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                openWindow(shared, \"_blank\");\n              }}\n            >\n              {t(\"share.view\")}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </>\n  );\n}\n\nfunction SideBar() {\n  const { t } = useTranslation();\n  const { refresh, toggle } = useConversationActions();\n  const current = useSelector(selectCurrent);\n  const open = useSelector(selectMenu);\n  const auth = useSelector(selectAuthenticated);\n  const [search, setSearch] = useState<string>(\"\");\n  const [operateConversation, setOperateConversation] = useState<Operation>({\n    target: null,\n    type: \"\",\n  });\n  useEffectAsync(async () => {\n    const resp = await refresh();\n\n    const store = getNumberMemory(\"history_conversation\", -1);\n    if (current === store) return; // no need to dispatch current\n    if (store === -1) return; // -1 is default, no need to dispatch\n    if (!resp.map((item) => item.id).includes(store)) return; // not in the list, no need to dispatch\n    await toggle(store);\n  }, []);\n\n  return (\n    <div className={cn(\"sidebar\", open && \"open\")}>\n      <div className={`sidebar-content`}>\n        <SidebarAction\n          search={search}\n          setSearch={setSearch}\n          setOperateConversation={setOperateConversation}\n        />\n        <SidebarConversationList\n          search={search}\n          operateConversation={operateConversation}\n          setOperateConversation={setOperateConversation}\n        />\n        {!auth && (\n          <Button\n            className={`login-action min-h-10 h-max`}\n            variant={`default`}\n            onClick={goAuth}\n          >\n            <User className={`h-4 w-4 mr-1.5 shrink-0`} /> {t(\"login-action\")}\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default SideBar;\n"
  },
  {
    "path": "app/src/components/home/assemblies/ActionButton.tsx",
    "content": "import { Button } from \"@/components/ui/button.tsx\";\nimport {\n  CornerDownLeftIcon,\n  PauseCircle,\n  Send,\n  ArrowUpCircle,\n} from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport { useMemo } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { senderSelector } from \"@/store/settings.ts\";\nimport { useMobile } from \"@/utils/device.ts\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\ntype SendButtonProps = {\n  working: boolean;\n  onClick: () => any;\n};\n\nfunction ActionButton({ onClick, working }: SendButtonProps) {\n  const { t } = useTranslation();\n  return (\n    <motion.div\n      className={`action-button`}\n      initial={{ opacity: 0, scale: 0.9 }}\n      animate={{ opacity: 1, scale: 1 }}\n      transition={{ duration: 0.3 }}\n    >\n      <Button\n        className={`inline-flex flex-row items-center shrink-0 whitespace-nowrap`}\n        onClick={onClick}\n        unClickable\n      >\n        <div className=\"translate-y-[1px]\">\n          <motion.div\n            initial={{ rotate: 0 }}\n            animate={{ rotate: working ? 360 : 0 }}\n            transition={{ duration: 0.25 }}\n            className=\"w-fit h-fit\"\n          >\n            <Icon\n              icon={working ? <PauseCircle /> : <ArrowUpCircle />}\n              className={`h-4 w-4 shrink-0`}\n            />\n          </motion.div>\n        </div>\n\n        <motion.span\n          key={working ? \"stop\" : \"send\"}\n          initial={{ opacity: 0, y: 10 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: -10 }}\n          transition={{ duration: 0.2 }}\n          className=\"ml-1.5\"\n        >\n          {t(working ? \"stop\" : \"send\")}\n        </motion.span>\n      </Button>\n    </motion.div>\n  );\n}\n\ntype ActionCommandProps = {\n  input: string;\n};\n\nexport function ActionCommand({ input }: ActionCommandProps) {\n  const mobile = useMobile();\n  const sender = useSelector(senderSelector);\n  const display = useMemo(() => {\n    return input.split(\"\\n\").length < 2 && input.length < 10;\n  }, [input]);\n\n  return (\n    <AnimatePresence>\n      {display && (\n        <motion.div\n          className={`flex flex-col text-xs text-unread select-none`}\n          initial={{ opacity: 0, y: 10 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: 10 }}\n          transition={{ duration: 0.3 }}\n        >\n          <motion.div\n            className={`flex flex-row items-center`}\n            initial={{ x: -10, opacity: 0 }}\n            animate={{ x: 0, opacity: 1 }}\n            transition={{ delay: 0.1 }}\n          >\n            <Icon\n              icon={sender ? <Send /> : <CornerDownLeftIcon />}\n              className={`h-3 w-3 mr-1.5`}\n            />\n            Enter\n          </motion.div>\n          {!mobile && (\n            <motion.div\n              className={`flex flex-row items-center`}\n              initial={{ x: -10, opacity: 0 }}\n              animate={{ x: 0, opacity: 1 }}\n              transition={{ delay: 0.2 }}\n            >\n              <Icon\n                icon={!sender ? <Send /> : <CornerDownLeftIcon />}\n                className={`h-3 w-3 mr-1.5`}\n              />\n              Ctrl + Enter\n            </motion.div>\n          )}\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport default ActionButton;\n"
  },
  {
    "path": "app/src/components/home/assemblies/ChatAction.tsx",
    "content": "import {\n  selectWeb,\n  toggleWeb,\n  useConversationActions,\n  useMessages,\n} from \"@/store/chat.ts\";\nimport { Globe, Info, MessageSquarePlus, Wifi, WifiOff } from \"lucide-react\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { useTranslation } from \"react-i18next\";\nimport React from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { toast } from \"sonner\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport Clickable from \"@/components/ui/clickable.tsx\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip.tsx\";\n\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover.tsx\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\n\ntype ChatActionProps = {\n  style?: React.CSSProperties;\n  className?: string;\n  text?: string;\n  active?: boolean | number;\n  show?: boolean;\n  children?: React.ReactElement;\n  onClick?: () => void;\n};\n\nexport const ChatAction = ({\n  className,\n  text,\n  children,\n  active,\n  show = true,\n  onClick,\n  ...props\n}: ChatActionProps) => {\n  return (\n    <div className={cn(\n      \"transition-all duration-300\",\n      !show && \"opacity-0 pointer-events-none invisible\"\n    )}>\n      <TooltipProvider>\n        <Tooltip delayDuration={250}>\n          <TooltipTrigger>\n            <Clickable tapScale={0.9}>\n              <Button\n                size={`icon-sm`}\n                variant={`ghost`}\n                className={cn(\n                  \"group hover:bg-muted-foreground/5 mr-1\",\n                  active && `bg-muted-foreground/10 hover:bg-muted-foreground/20`,\n                  className,\n                )}\n                onClick={onClick}\n                {...props}\n              >\n                <Icon\n                  icon={children}\n                  className={cn(\n                    `h-[1.125rem] w-[1.125rem] text-unread transition shrink-0 stroke-[2]`,\n                    active && \"text-primary\",\n                  )}\n                />\n              </Button>\n            </Clickable>\n          </TooltipTrigger>\n          <TooltipContent>{text}</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  );\n};\n\nexport function WebAction() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const web = useSelector(selectWeb);\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <div>\n          <ChatAction\n            active={web}\n            text={t(\"chat.web\")}\n          >\n            <Globe className={cn(\"h-4 w-4 web\", web && \"enable\")} />\n          </ChatAction>\n        </div>\n      </PopoverTrigger>\n      <PopoverContent\n        className=\"w-64 p-3\"\n        side=\"top\"\n        align=\"start\"\n      >\n        <div className=\"space-y-4\">\n          <div className=\"flex items-center justify-between\">\n            <Label htmlFor=\"web-search-toggle\" className=\"text-sm\">{t(\"chat.web-search\")}</Label>\n            <Switch\n              id=\"web-search-toggle\"\n              checked={web}\n              onCheckedChange={() => {\n                toast(t(\"chat.web-search\"), {\n                  description: (\n                    <div className={`flex flex-col`}>\n                      <div className={`flex flex-row items-center flex-wrap`}>\n                        <Icon\n                          icon={!web ? <Wifi /> : <WifiOff />}\n                          className={`h-4 w-4 mr-1 shrink-0`}\n                        />\n                        {!web\n                          ? t(\"chat.web-enable-toast\")\n                          : t(\"chat.web-disable-toast\")}\n                      </div>\n                      <div\n                        className={`mt-1.5 flex flex-row items-center rounded-md border scale-80 py-1 px-2`}\n                      >\n                        <Icon icon={<Info />} className={`h-3 w-3 mr-1 shrink-0`} />\n                        {t(\"chat.web-enable-tip\")}\n                      </div>\n                    </div>\n                  ),\n                });\n\n                dispatch(toggleWeb());\n              }}\n            />\n          </div>\n\n          {web && (\n            <></>\n          )}\n          \n          <div className=\"rounded-md bg-muted p-2 text-xs\">\n            <div className=\"flex items-center\">\n              <Icon icon={<Info />} className=\"h-3 w-3 mr-1 shrink-0\" />\n              {t(\"chat.web-enable-tip\")}\n            </div>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport function NewConversationAction() {\n  const { t } = useTranslation();\n  const messages = useMessages();\n  const { toggle } = useConversationActions();\n\n  return (\n    <ChatAction\n      text={t(\"new-chat\")}\n      onClick={async () => messages.length > 0 && (await toggle(-1))}\n    >\n      <MessageSquarePlus className={`h-4 w-4`} />\n    </ChatAction>\n  );\n}\n"
  },
  {
    "path": "app/src/components/home/assemblies/ChatInput.tsx",
    "content": "import React from \"react\";\nimport { setMemory } from \"@/utils/memory.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { FlexibleTextarea } from \"@/components/ui/textarea.tsx\";\nimport { useSelector } from \"react-redux\";\nimport { senderSelector } from \"@/store/settings.ts\";\nimport { blobEvent } from \"@/events/blob.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { isEnter, withCtrl, withShift } from \"@/utils/base.ts\";\nimport { useMobile } from \"@/utils/device.ts\";\n\ntype ChatInputProps = {\n  className?: string;\n  target?: React.RefObject<HTMLTextAreaElement>;\n  value: string;\n  onValueChange: (value: string) => void;\n  onEnterPressed: () => void;\n};\n\nfunction ChatInput({\n  className,\n  target,\n  value,\n  onValueChange,\n  onEnterPressed,\n}: ChatInputProps) {\n  const { t } = useTranslation();\n  const sender = useSelector(senderSelector);\n\n  const mobile = useMobile();\n\n  const rows = mobile ? 3 : 5;\n  const maxRows = mobile ? 10 : 15;\n\n  return (\n    <FlexibleTextarea\n      id={`input`}\n      className={cn(\"input-box thin-scrollbar min-h-[60px]\", className)}\n      ref={target}\n      value={value}\n      rows={rows}\n      minRows={rows}\n      maxRows={maxRows}\n      onChange={(e) => {\n        onValueChange(e.target.value);\n        setMemory(\"history\", e.target.value);\n      }}\n      placeholder={t(\"chat.placeholder\")}\n      onKeyDown={async (e) => {\n        if (isEnter(e)) {\n          if (sender) {\n            // on Enter, clear the input\n            // on Ctrl + Enter or Shift + Enter, keep the input\n\n            if (!withCtrl(e) && !withShift(e)) {\n              e.preventDefault();\n              onEnterPressed();\n            } else {\n              // add Enter to the input\n              e.preventDefault();\n\n              if (!target || !target.current) return;\n              const input = target.current as HTMLTextAreaElement;\n              const value = input.value;\n              const selectionStart = input.selectionStart;\n              const selectionEnd = input.selectionEnd;\n              input.value =\n                value.slice(0, selectionStart) +\n                \"\\n\" +\n                value.slice(selectionEnd);\n              input.selectionStart = input.selectionEnd = selectionStart + 1;\n              onValueChange(input.value);\n            }\n          } else {\n            // on Enter, keep the input & on Ctrl + Enter, send the input\n            if (withCtrl(e)) {\n              e.preventDefault();\n              onEnterPressed();\n            }\n          }\n        }\n      }}\n      // on transfer file\n      onPaste={(e) => {\n        const items = e.clipboardData.items;\n        for (let i = 0; i < items.length; i++) {\n          const item = items[i];\n          if (item.kind === \"file\") {\n            const file = item.getAsFile();\n            file && blobEvent.emit(file);\n          }\n        }\n      }}\n    />\n  );\n}\n\nexport default ChatInput;\n"
  },
  {
    "path": "app/src/components/home/assemblies/ScrollAction.tsx",
    "content": "import { ChevronsDown } from \"lucide-react\";\nimport { useEffect } from \"react\";\nimport { addEventListeners, scrollDown } from \"@/utils/dom.ts\";\nimport { ChatAction } from \"@/components/home/assemblies/ChatAction.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { Message } from \"@/api/types.tsx\";\nimport { useMessages } from \"@/store/chat.ts\";\n\ntype ScrollActionProps = {\n  visible: boolean;\n  setVisibility: (visible: boolean) => void;\n  target: HTMLElement | null;\n};\n\nfunction ScrollAction(\n  { target, visible, setVisibility }: ScrollActionProps,\n) {\n  const { t } = useTranslation();\n  const messages: Message[] = useMessages();\n\n  const scrollableHandler = () => {\n    if (!target) return;\n\n    const position = target.scrollTop + target.clientHeight;\n    const height = target.scrollHeight;\n    const diff = Math.abs(position - height);\n    setVisibility(diff > 50);\n  };\n\n  useEffect(() => {\n    if (!target) return;\n    return addEventListeners(\n      target,\n      [\"scroll\", \"touchmove\"],\n      scrollableHandler,\n    );\n  }, [target]);\n\n  useEffect(() => {\n    if (!target) return;\n\n    if (target.scrollHeight <= target.clientHeight) {\n      setVisibility(false);\n    }\n  }, [messages]);\n\n  return (\n    <ChatAction\n      text={t(\"scroll-down\")}\n      onClick={() => scrollDown(target)}\n      show={visible}\n    >\n      <ChevronsDown className={`h-4 w-4`} />\n    </ChatAction>\n  );\n}\n\nexport default ScrollAction;\n"
  },
  {
    "path": "app/src/components/home/subscription/SubscriptionUsage.tsx",
    "content": "import { ValuableProgress } from \"@/components/ui/progress.tsx\";\n\ntype UsageProps = {\n  name: string;\n  usage: {\n    used: number;\n    total: number;\n  };\n};\n\nfunction SubscriptionUsage({ name, usage }: UsageProps) {\n  if (!usage) return null;\n\n  const isInfinity = usage.total === -1;\n\n  const used = usage.used;\n  const total = isInfinity ? \"∞\" : usage.total;\n\n  return (\n    <div className={`sub-column-wrapper inline-flex flex-col`}>\n      <div className={`sub-column`}>\n        <div className={`flex items-center text-sm text-secondary`}>{name}</div>\n        <div className={`grow`} />\n        <div className={`sub-value font-medium text-md`}>\n          {isInfinity ? (\n            <p>{used}</p>\n          ) : (\n            <>\n              <p>{used}</p>\n              <p className=\"text-secondary !font-normal text-sm\">/{total}</p>\n            </>\n          )}\n        </div>\n      </div>\n      <ValuableProgress\n        className={`w-full h-2`}\n        value={usage.used}\n        max={usage.total}\n      />\n    </div>\n  );\n}\n\nexport default SubscriptionUsage;\n"
  },
  {
    "path": "app/src/components/home/subscription/UpgradePlan.tsx",
    "content": "import React, { useEffect, useMemo } from \"react\";\nimport { buySubscription } from \"@/api/addition.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { DialogClose } from \"@radix-ui/react-dialog\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { expiredSelector, refreshSubscription } from \"@/store/subscription.ts\";\nimport {\n  ArrowDownCircle,\n  ArrowRight,\n  ArrowUpCircle,\n  Info,\n  Plus,\n  ShoppingCart,\n} from \"lucide-react\";\nimport { deeptrainEndpoint, useDeeptrain } from \"@/conf/env.ts\";\nimport { quotaSelector } from \"@/store/quota.ts\";\nimport { getPlanName, getPlanPrice } from \"@/conf/subscription.tsx\";\nimport { Plans } from \"@/api/types.tsx\";\nimport { subscriptionDataSelector } from \"@/store/globals.ts\";\nimport { openWindow } from \"@/utils/device.ts\";\nimport { AppDispatch } from \"@/store\";\nimport { toast } from \"sonner\";\nimport Icon from \"@/components/utils/Icon\";\nimport { useCurrency } from \"@/store/info\";\nimport Tips from \"@/components/Tips\";\n\nfunction countPrice(data: Plans, base: number, month: number): number {\n  const price = getPlanPrice(data, base) * month;\n  \n  const plan = data.find(p => p.level === base);\n  if (plan && plan.discounts) {\n    const discount = plan.discounts[month.toString()];\n    if (discount !== undefined) {\n      return price * discount;\n    }\n  }\n  \n  if (month >= 36) {\n    return price * 0.7;\n  } else if (month >= 12) {\n    return price * 0.8;\n  } else if (month >= 6) {\n    return price * 0.9;\n  }\n\n  return price;\n}\n\nfunction countUpgradePrice(\n  data: Plans,\n  level: number,\n  target: number,\n  days: number,\n): number {\n  const bias = getPlanPrice(data, target) - getPlanPrice(data, level);\n  const v = (bias / 30) * days;\n  return (v > 0 ? v + 1 : 0) + 1; // time count offset\n}\n\nfunction getDiscountPercent(data: Plans, base: number, month: number): number | null {\n  const plan = data.find(p => p.level === base);\n  if (plan && plan.discounts) {\n    const discount = plan.discounts[month.toString()];\n    if (discount !== undefined) {\n      return Math.round((1 - discount) * 100);\n    }\n  }\n\n  if (month >= 36) {\n    return 30;\n  } else if (month >= 12) {\n    return 20;\n  } else if (month >= 6) {\n    return 10;\n  }\n\n  return null;\n}\n\ntype UpgradeProps = {\n  level: number;\n  current: number;\n  isYearly?: boolean;\n};\n\nasync function callBuyAction(\n  t: any,\n  month: number,\n  level: number,\n  current: number,\n): Promise<boolean> {\n  const res = await buySubscription(month, level);\n  if (res.status) {\n    toast.success(t(\"sub.success\"), {\n      description: t(\"sub.success-prompt\", {\n        month,\n      }),\n    });\n  } else {\n    toast.error(t(\"sub.failed\"), {\n      description: useDeeptrain\n        ? t(\"sub.failed-prompt\")\n        : t(\"sub.failed-quota-prompt\", {\n            quota: current.toFixed(2),\n          }),\n\n      action: useDeeptrain\n        ? {\n            label: t(\"buy.go\"),\n            onClick: () => {\n              openWindow(`${deeptrainEndpoint}/home/wallet`);\n            },\n          }\n        : undefined,\n    });\n\n    useDeeptrain &&\n      setTimeout(() => {\n        openWindow(`${deeptrainEndpoint}/home/wallet`);\n      }, 2000);\n  }\n  return res.status;\n}\n\nasync function callMigrateAction(t: any, level: number): Promise<boolean> {\n  const res = await buySubscription(1, level);\n  if (res.status) {\n    toast.success(t(\"sub.migrate-success\"), {\n      description: t(\"sub.migrate-success-prompt\"),\n    });\n  } else {\n    toast.error(t(\"sub.migrate-failed\"), {\n      description: t(\"sub.sub-migrate-failed-prompt\", { reason: res.error }),\n    });\n  }\n  return res.status;\n}\n\nexport function Upgrade({ level, current, isYearly }: UpgradeProps) {\n  const { t } = useTranslation();\n  const expired = useSelector(expiredSelector);\n  const [open, setOpen] = React.useState(false);\n  const [month, setMonth] = React.useState(1);\n  const dispatch: AppDispatch = useDispatch();\n\n  const quota = useSelector(quotaSelector);\n\n  const subscriptionData = useSelector(subscriptionDataSelector);\n\n  const isCurrent = useMemo(() => current === level, [current, level]);\n  const isUpgrade = useMemo(() => current < level, [current, level]);\n\n  const { symbol } = useCurrency();\n\n  useEffect(() => {\n    if (isYearly) {\n      setMonth(12);\n    } else {\n      setMonth(1);\n    }\n  }, [isYearly]);\n\n  return current === 0 || current === level ? (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button\n          tapScale={0.975}\n          classNameWrapper=\"w-full\"\n          className={`action w-full`}\n          variant={`default`}\n        >\n          <Icon icon={<ShoppingCart />} className=\"h-4 w-4 mr-1.5\" />\n          {isCurrent ? t(\"sub.renew\") : t(\"sub.subscribe\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent className={`flex-dialog`}>\n        <DialogHeader>\n          <DialogTitle>{t(\"sub.select-time\")}</DialogTitle>\n        </DialogHeader>\n        <div className=\"upgrade-wrapper grid md:grid-cols-2 gap-4 w-full\">\n          {[1, 3, 6, 12].map((duration) => (\n            <button\n              key={duration}\n              className={`w-full p-4 rounded-lg border ${\n                month === duration\n                  ? \"border-primary bg-muted/20\"\n                  : \"border hover:border-primary/50\"\n              } transition-all duration-200 ease-in-out flex justify-between items-center`}\n              onClick={() => setMonth(duration)}\n            >\n              <div className=\"flex flex-col items-start\">\n                <span className=\"font-semibold\">\n                  {t(`sub.time.${duration}`)}\n                </span>\n                <div>\n                  <span className=\"text-sm\">{symbol}</span>\n                  <span className=\"text-sm font-bold\">\n                    {countPrice(subscriptionData, level, duration).toFixed(2)}\n                  </span>\n                </div>\n              </div>\n              {(() => {\n                const discount = getDiscountPercent(subscriptionData, level, duration);\n                return discount ? (\n                  <div className=\"ml-2 text-xs text-secondary !text-[#55b467] !bg-[#f4fdeb] !border !border-[#55b467]/20 px-1.5 py-0.5 rounded-full\">\n                    {discount}%\n                  </div>\n                ) : null;\n              })()}\n            </button>\n          ))}\n        </div>\n        <DialogFooter className={`translate-y-1.5`}>\n          <DialogClose asChild>\n            <Button unClickable variant={`outline`}>\n              {t(\"cancel\")}\n            </Button>\n          </DialogClose>\n          <Button\n            unClickable\n            className={`mb-1.5`}\n            onClick={async () => {\n              const res = await callBuyAction(t, month, level, quota);\n              if (res) {\n                setOpen(false);\n                dispatch(refreshSubscription());\n              }\n            }}\n          >\n            <Plus className={`h-4 w-4 mr-1`} />\n            {t(\"confirm\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  ) : (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button\n          className={`action w-full`}\n          variant={isUpgrade ? `default` : `outline`}\n          tapScale={0.975}\n          classNameWrapper=\"w-full\"\n        >\n          <Icon\n            icon={isUpgrade ? <ArrowUpCircle /> : <ArrowDownCircle />}\n            className=\"h-4 w-4 mr-1.5\"\n          />\n          {isUpgrade ? t(\"sub.upgrade\") : t(\"sub.downgrade\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent className={`flex-dialog`}>\n        <DialogHeader>\n          <DialogTitle>{t(\"sub.migrate-plan\")}</DialogTitle>\n        </DialogHeader>\n        <div\n          className={`upgrade-wrapper text-md p-2 border rounded-lg bg-secondary/50 !mt-0.5`}\n        >\n          <Info className=\"h-4 w-4 mr-1 inline-block\" />\n          {t(\"sub.migrate-plan-desc\")}\n        </div>\n        <div className=\"flex items-center justify-between space-x-2 mt-3\">\n          <div className=\"flex-1 p-3 border rounded-md bg-background\">\n            <h3 className=\"text-base font-medium\">{t(\"sub.current\")}</h3>\n            <p className=\"text-xs text-muted-foreground\">\n              {t(`sub.${getPlanName(current)}`)}\n            </p>\n            <p className=\"text-lg font-medium\">\n              {symbol}\n              {getPlanPrice(subscriptionData, current).toFixed(2)}/\n              {t(\"sub.month\")}\n            </p>\n          </div>\n\n          <ArrowRight className=\"h-5 w-5 text-muted-foreground\" />\n\n          <div className=\"flex-1 p-3 border rounded-md bg-background\">\n            <h3 className=\"text-base font-medium\">{t(\"sub.new\")}</h3>\n            <p className=\"text-xs text-muted-foreground\">\n              {t(`sub.${getPlanName(level)}`)}\n            </p>\n            <p className=\"text-lg font-medium\">\n              {symbol}\n              {getPlanPrice(subscriptionData, level).toFixed(2)}/\n              {t(\"sub.month\")}\n            </p>\n          </div>\n        </div>\n\n        {isUpgrade && (\n          <div className=\"flex items-center justify-center flex-col p-3 bg-secondary/20 rounded-md mt-2\">\n            <span className=\"mb-0.5 flex items-center\">\n              {t(\"sub.upgrade-price-label\")}\n              <Tips content={t(\"sub.upgrade-price-notice-tip\")} />\n            </span>\n            <div className=\"flex items-center px-1.5 py-0.5 rounded-lg text-amber-500 bg-amber-400/10 border border-amber-400/30\">\n              <span className=\"text-sm font-medium\">{symbol}</span>\n              <span className=\"text-md font-medium\">\n                {countUpgradePrice(\n                  subscriptionData,\n                  current,\n                  level,\n                  expired,\n                ).toFixed(2)}\n              </span>\n            </div>\n          </div>\n        )}\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button unClickable variant={`outline`}>\n              {t(\"cancel\")}\n            </Button>\n          </DialogClose>\n          <Button\n            unClickable\n            loading\n            className={`mb-1.5`}\n            onClick={async () => {\n              const res = await callMigrateAction(t, level);\n              if (res) {\n                setOpen(false);\n                dispatch(refreshSubscription());\n              }\n            }}\n          >\n            <Plus className={`h-4 w-4 mr-1`} />\n            {t(\"confirm\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/markdown/Code.tsx",
    "content": "import { MarkdownFile } from \"@/components/plugins/file.tsx\";\nimport { MarkdownProgressbar } from \"@/components/plugins/progress.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { copyClipboard } from \"@/utils/dom.ts\";\nimport { Check, Copy } from \"lucide-react\";\nimport { LightAsync as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { atomOneDark as style } from \"react-syntax-highlighter/dist/esm/styles/hljs\";\nimport React, { useMemo } from \"react\";\nimport { MarkdownMermaid } from \"@/components/plugins/mermaid.tsx\";\n\nconst LanguageMap: Record<string, string> = {\n  html: \"htmlbars\",\n  js: \"javascript\",\n  ts: \"typescript\",\n  jsx: \"javascript\",\n  tsx: \"typescript\",\n  rs: \"rust\",\n  py: \"python\",\n};\n\nexport type CodeProps = {\n  inline?: boolean;\n  className?: string;\n  children: React.ReactNode;\n  codeStyle?: string;\n  loading?: boolean;\n};\n\nfunction Code({\n  inline,\n  className,\n  children,\n  loading,\n  codeStyle,\n  ...props\n}: CodeProps) {\n  const [copied, setCopied] = React.useState(false);\n  const match = /language-(\\w+)/.exec(className || \"\");\n  const language = match ? match[1].toLowerCase() : \"unknown\";\n  if (language === \"file\") return <MarkdownFile children={children} />;\n  if (language === \"progress\")\n    return <MarkdownProgressbar children={children} />;\n  if (language === \"mermaid\") return <MarkdownMermaid children={children} />;\n\n  if (inline)\n    return (\n      <code className={cn(\"code-inline\", className)} {...props}>\n        {children}\n      </code>\n    );\n\n  return (\n    <div className={`markdown-syntax`}>\n      <div\n        className={`markdown-syntax-header`}\n        onClick={async () => {\n          const text = children?.toString() || \"\";\n          await copyClipboard(text);\n          setCopied(true);\n        }}\n      >\n        {copied ? (\n          <Check className={`h-3 w-3`} />\n        ) : (\n          <Copy className={`h-3 w-3`} />\n        )}\n        <p>{language}</p>\n      </div>\n      <SyntaxHighlighter\n        {...props}\n        children={String(children).replace(/\\n$/, \"\")}\n        style={style}\n        language={LanguageMap[language] || language}\n        PreTag=\"div\"\n        wrapLongLines={true}\n        wrapLines={true}\n        className={cn(\"code-block\", codeStyle)}\n      />\n    </div>\n  );\n}\n\nexport default function ({\n  inline,\n  className,\n  children,\n  codeStyle,\n  loading,\n  ...props\n}: CodeProps) {\n  return useMemo(() => {\n    return (\n      <Code\n        inline={inline}\n        className={className}\n        children={children}\n        codeStyle={codeStyle}\n        loading={loading}\n        {...props}\n      />\n    );\n  }, [inline, className, children, codeStyle, loading, props]);\n}\n"
  },
  {
    "path": "app/src/components/markdown/Image.tsx",
    "content": "import React, { useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { getFilenameFromURL } from \"@/utils/base.ts\";\nimport { AlertCircle, Copy, Eye, Link, Loader2, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { Skeleton } from \"@/components/ui/skeleton.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { useClipboard } from \"@/utils/dom.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { openWindow } from \"@/utils/device.ts\";\n\nexport enum ImageState {\n  Loading = \"loading\",\n  Loaded = \"loaded\",\n  Error = \"error\",\n}\nexport type ImageStateType = (typeof ImageState)[keyof typeof ImageState];\n\nexport default function Image({\n  src,\n  alt,\n  className,\n  ...props\n}: React.ImgHTMLAttributes<HTMLImageElement>) {\n  const { t } = useTranslation();\n  const copy = useClipboard();\n  const [isBase64Expanded, setIsBase64Expanded] = React.useState(false);\n\n  const filename = getFilenameFromURL(src) || \"unknown\";\n  const description = alt || filename;\n  const isBase64Image = src?.startsWith('data:image');\n\n  const imgRef = useRef<HTMLImageElement>(null);\n  const [state, setState] = React.useState<ImageStateType>(ImageState.Loading);\n\n  const isLoading = state === ImageState.Loading;\n  const isError = state === ImageState.Error;\n  const isLoaded = state === ImageState.Loaded;\n\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <div className={`flex flex-col items-center cursor-pointer`}>\n          {isLoading && (\n            <Skeleton\n              className={`relative rounded-md w-44 h-44 mx-auto my-1 flex items-center justify-center`}\n            >\n              <Loader2 className={`w-6 h-6 animate-spin`} />\n            </Skeleton>\n          )}\n\n          {isError && (\n            <div\n              className={`flex flex-col items-center text-center border rounded-md py-6 px-8 mx-auto my-1`}\n            >\n              <AlertCircle className={`h-5 w-5 text-secondary mb-1`} />\n              <span\n                className={`text-secondary mb-0 select-none text-sm whitespace-pre-wrap`}\n              >\n                {t(\"renderer.imageLoadFailed\", { src: filename })}\n              </span>\n            </div>\n          )}\n\n          <img\n            className={cn(\n              className,\n              \"select-none outline-none\",\n              !isLoaded && `hidden`,\n            )}\n            src={src}\n            ref={imgRef}\n            alt={alt || t(\"renderer.imageLoadFailed\", { src })}\n            onLoad={() => setState(ImageState.Loaded)}\n            onAbort={() => setState(ImageState.Error)}\n            onError={() => setState(ImageState.Error)}\n            {...props}\n          />\n          <span\n            className={`text-secondary text-sm mt-1 select-none max-w-[10rem] text-center truncate`}\n          >\n            {description}\n          </span>\n        </div>\n      </DialogTrigger>\n      <DialogContent className={`flex-dialog`} couldFullScreen>\n        <DialogHeader>\n          <DialogTitle className={`flex flex-row items-center`}>\n            <Eye className={`h-4 w-4 mr-1.5 translate-y-[1px]`} />\n            {t(\"renderer.viewImage\")}\n          </DialogTitle>\n        </DialogHeader>\n        <div className={`flex flex-row mb-2 items-center`}>\n          <div className={`grow`} />\n          <Button\n            size={`icon`}\n            variant={`outline`}\n            className={`ml-2`}\n            onClick={() => copy(src || \"\")}\n          >\n            <Copy className={`h-4 w-4`} />\n          </Button>\n          <Button\n            size={`icon`}\n            variant={`outline`}\n            className={`ml-2`}\n            onClick={() => openWindow(src || \"\")}\n            disabled={isError}\n          >\n            <Link className={`h-4 w-4`} />\n          </Button>\n        </div>\n        <div className={`flex flex-col items-center`}>\n          <img\n            className={cn(className, \"rounded-md select-none outline-none\")}\n            src={src}\n            alt={alt}\n            {...props}\n          />\n          <span\n            className={`text-secondary text-sm mt-2.5 text-center break-all whitespace-pre-wrap`}\n          >\n            <button\n              onClick={() => copy(src || \"\")}\n              className={`h-4 w-4 inline-block mr-1 outline-none translate-y-[2px]`}\n            >\n              <Copy className={`h-3.5 w-3.5`} />\n            </button>\n            {isBase64Image ? (\n              <>\n                <button\n                  onClick={() => setIsBase64Expanded(!isBase64Expanded)}\n                  className=\"inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md bg-primary/10 text-primary hover:bg-primary/20 transition-colors duration-200\"\n                >\n                  {isBase64Expanded ? (\n                    <ChevronUp className=\"h-3 w-3 transition-transform duration-200\" />\n                  ) : (\n                    <ChevronDown className=\"h-3 w-3 transition-transform duration-200\" />\n                  )}\n                  {t(isBase64Expanded ? \"renderer.base64ImageCollapse\" : \"renderer.base64Image\")}\n                </button>\n                <div className={`mt-2 transition-all duration-200 ${isBase64Expanded ? 'opacity-100' : 'opacity-50'}`}>\n                  {isBase64Expanded ? src : `${(src || '').substring(0, 50)}...`}\n                </div>\n              </>\n            ) : (\n              src || ''\n            )}\n          </span>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/markdown/Label.tsx",
    "content": "import {\n  CalendarPlus,\n  Cloud,\n  CloudCog,\n  Cloudy,\n  Package,\n  Plus,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport React from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { subscriptionDataSelector } from \"@/store/globals.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport router from \"@/router.tsx\";\nimport Emoji from \"../Emoji\";\nimport { cn } from \"../ui/lib/utils\";\nimport ModelAvatar from \"../ModelAvatar\";\nimport { selectSupportModels } from \"@/store/chat\";\n\ntype QuotaExceededFormProps = {\n  model: string;\n  minimum: string;\n  quota: string;\n  plan: boolean;\n};\n\nfunction QuotaExceededForm({\n  model,\n  minimum,\n  quota,\n  plan,\n}: QuotaExceededFormProps) {\n  const { t } = useTranslation();\n  const supportModels = useSelector(selectSupportModels);\n  const modelInfo = supportModels.find((m) => m.id === model);\n\n  return (\n    <div className={`flex flex-col items-center pt-4 pb-1`}>\n      <Emoji emoji={\"1f915\"} className={`w-16 h-16 m-6 mb-4`} />\n      <p className={`text-lg font-semibold !mb-1`}>\n        {t(\"oops-quota-exceeded\")}\n      </p>\n      <p className={`text-sm text-secondary px-2.5 text-center`}>\n        {t(\"oops-quota-exceeded-tip\")}\n      </p>\n      <div className=\"w-full h-fit border bg-muted/50 rounded-lg p-1.5 flex flex-col space-y-2.5 py-2.5\">\n        <div\n          className={`flex flex-row w-full items-center justify-center px-4`}\n        >\n          <Package className={`h-4 w-4 mr-1`} />\n          {t(\"model\")}\n          <div className={`grow`} />\n          <div className={`!mb-0 flex flex-row items-center space-x-1`}>\n            <ModelAvatar\n              size={24}\n              model={\n                modelInfo ?? {\n                  id: model,\n                  name: model,\n                }\n              }\n            />\n            <p className={`!mb-0`}>{modelInfo?.name ?? model}</p>\n          </div>\n        </div>\n        <div\n          className={`flex flex-row w-full items-center justify-center px-4`}\n        >\n          <Cloudy className={`h-4 w-4 mr-1`} />\n          {t(\"your-quota\")}\n          <div className={`grow`} />\n          <p className={`flex flex-row items-center font-medium !mb-0`}>\n            {quota}\n            <Cloud className={`h-4 w-4 ml-1`} />\n          </p>\n        </div>\n        <div\n          className={`flex flex-row w-full items-center justify-center px-4`}\n        >\n          <CloudCog className={`h-4 w-4 mr-1`} />\n          {t(\"min-quota\")}\n          <div className={`grow`} />\n          <p className={`flex flex-row items-center font-medium !mb-0`}>\n            {minimum}\n            <Cloud className={`h-4 w-4 ml-1`} />\n          </p>\n        </div>\n      </div>\n\n      <div\n        className={cn(\n          `mt-4 w-full h-fit grid grid-cols-1 gap-2`,\n          plan && \"md:grid-cols-2\",\n        )}\n      >\n        <Button\n          classNameWrapper={`w-full`}\n          className={`w-full`}\n          onClick={() => router.navigate(\"/wallet\")}\n        >\n          <Plus className={`h-4 w-4 mr-1`} />\n          {t(\"buy.dialog-title\")}\n        </Button>\n        {plan && (\n          <Button\n            variant={`outline`}\n            classNameWrapper={`w-full`}\n            className={`w-full`}\n            onClick={() => router.navigate(\"/wallet#plan\")}\n          >\n            <CalendarPlus className={`h-4 w-4 mr-1`} />\n            {t(\"sub.dialog-title\")}\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\ntype LabelProps = {\n  children: React.ReactNode;\n};\n\nexport default function ({ children }: LabelProps) {\n  const subscription = useSelector(subscriptionDataSelector);\n  const content = (children ?? \"\").toString();\n\n  if (content.startsWith(\"user quota\")) {\n    // if the format is `user quota is not enough error (model: gpt-3.5-turbo-1106, minimum quota: 0.01, your quota: -77.77)`, return special component\n\n    const match = content.match(\n      /user quota is not enough error \\(model: (.*), minimum quota: (.*), your quota: (.*)\\)/,\n    );\n    if (match) {\n      const [, model, minimum, quota] = match;\n      const plan = subscription\n        .flatMap((p) => p.items.map((i) => i.models.includes(model)))\n        .includes(true);\n\n      return (\n        <QuotaExceededForm\n          model={model}\n          minimum={minimum}\n          quota={quota}\n          plan={plan}\n        />\n      );\n    }\n  }\n\n  return <p>{children}</p>;\n}\n"
  },
  {
    "path": "app/src/components/markdown/Link.tsx",
    "content": "import React from \"react\";\nimport { Codepen, Codesandbox, Github, Twitter, Youtube } from \"lucide-react\";\nimport { VirtualMessage } from \"./VirtualMessage\";\n\nfunction getSocialIcon(url: string) {\n  try {\n    const { hostname } = new URL(url);\n\n    if (hostname.includes(\"github.com\"))\n      return <Github className=\"h-4 w-4 inline-block mr-0.5\" />;\n    if (hostname.includes(\"twitter.com\"))\n      return <Twitter className=\"h-4 w-4 inline-block mr-0.5\" />;\n    if (hostname.includes(\"youtube.com\"))\n      return <Youtube className=\"h-4 w-4 inline-block mr-0.5\" />;\n    if (hostname.includes(\"codepen.io\"))\n      return <Codepen className=\"h-4 w-4 inline-block mr-0.5\" />;\n    if (hostname.includes(\"codesandbox.io\"))\n      return <Codesandbox className=\"h-4 w-4 inline-block mr-0.5\" />;\n  } catch (e) {\n    return;\n  }\n}\n\ntype LinkProps = {\n  href?: string;\n  children: React.ReactNode;\n};\n\nexport default function ({ href, children }: LinkProps) {\n  const url: string = href?.toString() || \"\";\n\n  if (url.startsWith(\"https://coai.virtual/reference::\")) {\n    const referenceUrl = url.slice(\"https://coai.virtual/reference::\".length);\n    return <VirtualMessage message={`reference::${referenceUrl}`}>{children}</VirtualMessage>;\n  }\n\n  if (url.startsWith(\"https://coai.virtual\")) {\n    const message = url.slice(20);\n\n    return <VirtualMessage message={message}>{children}</VirtualMessage>;\n  }\n\n  return (\n    <a href={url} target={`_blank`} rel={`noopener noreferrer`}>\n      {getSocialIcon(url)}\n      {children}\n    </a>\n  );\n}\n"
  },
  {
    "path": "app/src/components/markdown/Reference.tsx",
    "content": "import React from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\n\ninterface ReferenceProps {\n  url: string;\n  children: React.ReactNode;\n}\n\nexport function Reference({ url, children }: ReferenceProps): JSX.Element {\n  const handleClick = (e: React.MouseEvent) => {\n    e.preventDefault();\n    window.open(url, \"_blank\", \"noopener,noreferrer\");\n  };\n\n  return (\n    <Badge \n      variant=\"outline\" \n      className=\"reference-badge inline-flex items-center py-1 px-2 gap-1.5 \n                hover:bg-primary/10 cursor-pointer transition-colors \n                rounded-md border border-primary/30 text-primary-foreground\n                bg-primary/5 font-normal text-xs\"\n      onClick={handleClick}\n    >\n      <ExternalLink className=\"h-3 w-3\" />\n      {children}\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "app/src/components/markdown/Video.tsx",
    "content": "import React, { useRef, useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSelector } from \"react-redux\";\nimport { getFilenameFromURL } from \"@/utils/base.ts\";\nimport { AlertCircle, Copy, Eye, Link, Loader2 } from \"lucide-react\";\nimport { Skeleton } from \"@/components/ui/skeleton.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { useClipboard } from \"@/utils/dom.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { openWindow } from \"@/utils/device.ts\";\nimport { RootState } from \"@/store/index.ts\";\n\nexport enum VideoState {\n  Loading = \"loading\",\n  Loaded = \"loaded\",\n  Error = \"error\",\n}\nexport type VideoStateType = (typeof VideoState)[keyof typeof VideoState];\n\ntype VideoProps = React.VideoHTMLAttributes<HTMLVideoElement> & {\n  alt?: string;\n};\n\nexport default function Video({\n  src,\n  alt,\n  className,\n  ...props\n}: VideoProps) {\n  const { t } = useTranslation();\n  const copy = useClipboard();\n  const token = useSelector((state: RootState) => state.auth.token);\n\n  const filename = getFilenameFromURL(src) || \"unknown\";\n  const description = alt || filename;\n\n  const videoRef = useRef<HTMLVideoElement>(null);\n  const [state, setState] = useState<VideoStateType>(VideoState.Loading);\n  const [videoUrl, setVideoUrl] = useState<string | null>(null);\n  const blobUrlRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    if (!src) {\n      setState(VideoState.Error);\n      return;\n    }\n\n    const fetchVideo = async () => {\n      try {\n        setState(VideoState.Loading);\n        const headers: HeadersInit = {\n          \"Content-Type\": \"video/mp4\",\n        };\n\n        if (token) {\n          headers[\"Authorization\"] = token;\n        }\n\n        const response = await fetch(src, {\n          method: \"GET\",\n          headers,\n        });\n\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n\n        const blob = await response.blob();\n        const blobUrl = URL.createObjectURL(blob);\n\n        if (blobUrlRef.current) {\n          URL.revokeObjectURL(blobUrlRef.current);\n        }\n        \n        blobUrlRef.current = blobUrl;\n        setVideoUrl(blobUrl);\n        setState(VideoState.Loaded);\n      } catch (error) {\n        console.error(\"[Video] Failed to load video:\", error);\n        setState(VideoState.Error);\n      }\n    };\n\n    fetchVideo();\n\n    return () => {\n      if (blobUrlRef.current) {\n        URL.revokeObjectURL(blobUrlRef.current);\n        blobUrlRef.current = null;\n      }\n    };\n  }, [src, token]);\n\n  const isLoading = state === VideoState.Loading;\n  const isError = state === VideoState.Error;\n  const isLoaded = state === VideoState.Loaded;\n\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <div className={`flex flex-col items-center cursor-pointer`}>\n          {isLoading && (\n            <Skeleton\n              className={`relative rounded-md w-80 h-44 mx-auto my-1 flex items-center justify-center`}\n            >\n              <Loader2 className={`w-6 h-6 animate-spin`} />\n            </Skeleton>\n          )}\n\n          {isError && (\n            <div\n              className={`flex flex-col items-center text-center border rounded-md py-6 px-8 mx-auto my-1`}\n            >\n              <AlertCircle className={`h-5 w-5 text-secondary mb-1`} />\n              <span\n                className={`text-secondary mb-0 select-none text-sm whitespace-pre-wrap`}\n              >\n                {t(\"renderer.videoLoadFailed\", { src: filename })}\n              </span>\n            </div>\n          )}\n\n          {videoUrl && (\n            <video\n              className={cn(\n                className,\n                \"select-none outline-none rounded-md max-w-[20rem] max-h-[20rem]\",\n                !isLoaded && `hidden`,\n              )}\n              src={videoUrl}\n              ref={videoRef}\n              controls\n              preload=\"metadata\"\n              onAbort={() => setState(VideoState.Error)}\n              onError={() => setState(VideoState.Error)}\n              {...props}\n            />\n          )}\n          <span\n            className={`text-secondary text-sm mt-1 select-none max-w-[20rem] text-center truncate`}\n          >\n            {description}\n          </span>\n        </div>\n      </DialogTrigger>\n      <DialogContent className={`flex-dialog`} couldFullScreen>\n        <DialogHeader>\n          <DialogTitle className={`flex flex-row items-center`}>\n            <Eye className={`h-4 w-4 mr-1.5 translate-y-[1px]`} />\n            {t(\"renderer.viewVideo\")}\n          </DialogTitle>\n        </DialogHeader>\n        <div className={`flex flex-row mb-2 items-center`}>\n          <div className={`grow`} />\n          <Button\n            size={`icon`}\n            variant={`outline`}\n            className={`ml-2`}\n            onClick={() => copy(src || \"\")}\n          >\n            <Copy className={`h-4 w-4`} />\n          </Button>\n          <Button\n            size={`icon`}\n            variant={`outline`}\n            className={`ml-2`}\n            onClick={() => openWindow(src || \"\")}\n            disabled={isError}\n          >\n            <Link className={`h-4 w-4`} />\n          </Button>\n        </div>\n        <div className={`flex flex-col items-center`}>\n          {videoUrl && (\n            <video\n              className={cn(className, \"rounded-md select-none outline-none max-w-full max-h-[80vh]\")}\n              src={videoUrl}\n              controls\n              preload=\"auto\"\n              {...props}\n            />\n          )}\n          <span\n            className={`text-secondary text-sm mt-2.5 text-center break-all whitespace-pre-wrap`}\n          >\n            <button\n              onClick={() => copy(src || \"\")}\n              className={`h-4 w-4 inline-block mr-1 outline-none translate-y-[2px]`}\n            >\n              <Copy className={`h-3.5 w-3.5`} />\n            </button>\n            {src || ''}\n          </span>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/markdown/VirtualMessage.tsx",
    "content": "import React, { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  useConversationActions,\n  useMessageActions,\n  useWorking,\n} from \"@/store/chat.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { DialogClose } from \"@radix-ui/react-dialog\";\nimport {\n  Eye,\n  EyeOff,\n  Loader2,\n  Maximize,\n  RefreshCcwDot,\n  Wand2,\n} from \"lucide-react\";\n\nfunction getVirtualIcon(command: string) {\n  command = command.toLowerCase();\n\n  if (\n    command.includes(\"variant\") ||\n    (command.includes(\"variation\") &&\n      !(\n        command.includes(\"high_variation\") || command.includes(\"low_variation\")\n      ))\n  ) {\n    return <Wand2 className=\"h-4 w-4 inline-block mr-2\" />;\n  } else if (command.includes(\"upscale\") || command.includes(\"upsample\")) {\n    return <Maximize className=\"h-4 w-4 inline-block mr-2\" />;\n  } else if (command.includes(\"reroll\")) {\n    return <RefreshCcwDot className=\"h-4 w-4 inline-block mr-2\" />;\n  }\n}\n\nfunction getVirualPrompt(command: string) {\n  command = command.toLowerCase();\n\n  if (command.includes(\"variant\") || command.includes(\"variation\")) {\n    if (command.includes(\"low_variation\")) return \"chat.actions.subtle-vary\";\n    if (command.includes(\"high_variation\")) return \"chat.actions.strong-vary\";\n\n    return \"chat.actions.variant\";\n  } else if (command.includes(\"upscale\") || command.includes(\"upsample\")) {\n    if (command.includes(\"subtle\")) return \"chat.actions.subtle-upscale\";\n    if (command.includes(\"creative\")) return \"chat.actions.creative-upscale\";\n\n    return \"chat.actions.upscale\";\n  } else if (command.includes(\"reroll\")) {\n    return \"chat.actions.reroll\";\n  } else if (command.includes(\"inpaint\")) {\n    return \"chat.actions.region-vary\";\n  } else if (command.includes(\"outpaint\") || command.includes(\"customzoom\")) {\n    if (command.includes(\"custom\")) return \"chat.actions.zoom-custom\";\n\n    if (command.includes(\"50\")) return \"chat.actions.zoom-2x\";\n    if (command.includes(\"75\")) return \"chat.actions.zoom-1.5x\";\n\n    return \"chat.actions.zoom\";\n  } else if (command.includes(\"bookmark\")) {\n    return \"chat.actions.bookmark\";\n  } else if (command.includes(\"pan_\")) {\n    if (command.includes(\"pan_left\")) return \"chat.actions.pan-left\";\n    if (command.includes(\"pan_right\")) return \"chat.actions.pan-right\";\n    if (command.includes(\"pan_up\")) return \"chat.actions.pan-up\";\n    if (command.includes(\"pan_down\")) return \"chat.actions.pan-down\";\n  }\n}\n\nfunction GetI18nPrompt({ command }: { command: string }) {\n  const { t } = useTranslation();\n  \n  const prompt = getVirualPrompt(command);\n  if (!prompt) return null;\n  \n  return <>{t(prompt)}</>;\n}\n\ntype VirtualPromptProps = {\n  message: string;\n  children: React.ReactNode;\n};\n\nfunction VirtualPrompt({ message, children }: VirtualPromptProps) {\n  const [raw, setRaw] = useState<boolean>(false);\n  const toggle = (e: React.MouseEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setRaw(!raw);\n  };\n\n  const Comp = () => (\n    <>\n      {getVirtualIcon(message)}\n      {children} <GetI18nPrompt command={message} />\n    </>\n  );\n\n  return (\n    <div\n      className={`virtual-prompt flex flex-row items-center justify-center select-none`}\n    >\n      {raw ? message : <Comp />}\n\n      {!raw ? (\n        <Eye\n          className={`h-4 w-4 mx-2 cursor-pointer shrink-0`}\n          onClick={toggle}\n        />\n      ) : (\n        <EyeOff\n          className={`h-4 w-4 mx-2 cursor-pointer shrink-0`}\n          onClick={toggle}\n        />\n      )}\n    </div>\n  );\n}\n\ntype VirtualMessageProps = {\n  message: string;\n  children: React.ReactNode;\n};\n\nfunction parseMessage(message: string): { prompt: string; model: string } {\n  const segments = message.split(\"::\");\n  const model = segments.length > 1 ? segments[segments.length - 1] : \"\";\n  const prompt = decodeURIComponent(\n    segments.slice(0, segments.length - 1).join(\"::\"),\n  );\n\n  return { prompt, model };\n}\n\nexport function VirtualMessage({ message, children }: VirtualMessageProps) {\n  const { t } = useTranslation();\n  const { selected } = useConversationActions();\n  const { send: sendAction } = useMessageActions();\n  const working = useWorking();\n  const [isHovered, setIsHovered] = useState(false);\n\n  const { prompt, model } = parseMessage(message);\n  \n  if (prompt === \"reference\") {\n    const handleClick = (e: React.MouseEvent) => {\n      e.preventDefault();\n      let targetUrl = decodeURIComponent(model);\n      if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {\n        targetUrl = 'https://' + targetUrl;\n      }\n      window.open(targetUrl, \"_blank\", \"noopener,noreferrer\");\n    };\n    \n    return (\n      <span \n        className={`inline-flex items-center justify-center h-[18px] px-[6px] py-0\n                  text-[12px] font-normal \n                  ${isHovered ? 'bg-[#d0d0d0] text-[#202020]' : 'bg-[#e5e5e5] text-[#404040]'}\n                  rounded-[9px] cursor-pointer relative -top-[2px] ml-1\n                  font-variant-numeric tabular-nums align-middle flex-shrink-0\n                  transition-colors duration-150`}\n        onClick={handleClick}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n      >\n        {children}\n      </span>\n    );\n  }\n\n  return (\n    <Dialog>\n      <DialogTrigger asChild>\n        <Button\n          variant={`outline`}\n          className={`flex flex-row items-center virtual-action mx-1 my-0.5 min-w-[4rem]`}\n        >\n          {getVirtualIcon(message)}\n          {children}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"chat.send-message\")}</DialogTitle>\n          <DialogDescription className={`pb-2`}>\n            {t(\"chat.send-message-desc\")}\n          </DialogDescription>\n          <VirtualPrompt message={prompt}>{children}</VirtualPrompt>\n        </DialogHeader>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button variant={`outline`}>{t(\"cancel\")}</Button>\n          </DialogClose>\n          <DialogClose\n            disabled={working}\n            onClick={async () => {\n              selected(model);\n              await sendAction(prompt, model);\n            }}\n            asChild\n          >\n            <Button unClickable variant={`default`}>\n              {working && <Loader2 className={`h-4 w-4 mr-1.5 animate-spin`} />}\n              {t(\"confirm\")}\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/plugins/file.tsx",
    "content": "import { Download, Eye, Paperclip } from \"lucide-react\";\nimport { saveAsFile, saveBlobAsFile, saveImageAsFile } from \"@/utils/dom.ts\";\nimport React, { useMemo } from \"react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport FileViewer from \"@/components/FileViewer.tsx\";\n\n/**\n * file format:\n * ```file\n * [[<filename>]]\n * <file content>\n * ```\n */\n\ntype MarkdownFileProps = {\n  children: React.ReactNode;\n  acceptDownload?: boolean;\n};\n\nexport function MarkdownFile({ children, acceptDownload }: MarkdownFileProps) {\n  const data = children?.toString() || \"\";\n  const filename = data.split(\"\\n\")[0].replace(\"[[\", \"\").replace(\"]]\", \"\");\n  const content = data.replace(`[[${filename}]]\\n`, \"\");\n\n  // const suffix = useMemo(() => {\n  //   // get file extension from filename (like: .png, .jpg, .jpeg, .gif)\n  //   return filename.split(\".\").pop() || \"\";\n  // }, [filename]);\n\n  const image = useMemo(() => {\n    // get image url from content (like: https://i.imgur.com/xxxxx.png)\n    const match = content.match(/(https?:\\/\\/.*\\.(?:png|jpg|jpeg|gif))/);\n    return match ? match[0] : \"\";\n  }, [filename, content]);\n\n  const b64image = useMemo(() => {\n    // get base64 image from content (like: data:image/png;base64,xxxxx)\n    const match = content.match(\n      /data:image\\/([^;]+);base64,([a-zA-Z0-9+/=]+)/g,\n    );\n    return match ? match[0] : \"\";\n  }, [filename, content]);\n\n  return (\n    <div\n      className={`file-instance`}\n      onClick={(e) => {\n        if (!acceptDownload) return;\n        e.preventDefault();\n        e.stopPropagation();\n\n        saveAsFile(filename, content);\n      }}\n    >\n      <div className={`file-content px-1`}>\n        <Paperclip className={`mr-1 !bg-transparent`} />\n        <span className={`name mr-2`}>{filename}</span>\n        <div className={`grow`} />\n        {image || b64image ? (\n          <Button\n            variant={`ghost`}\n            size={`icon`}\n            className={`download-action p-0 h-4 w-4`}\n            onClick={async () => {\n              if (b64image) {\n                saveImageAsFile(filename, b64image);\n                return;\n              }\n              const res = await fetch(image);\n              saveBlobAsFile(filename, await res.blob());\n            }}\n          >\n            <Download className={`cursor-pointer !bg-transparent`} />\n          </Button>\n        ) : (\n          <FileViewer filename={filename} content={content} asChild>\n            <div className=\"w-fit h-fit cursor-pointer\">\n              <Eye className={`!bg-transparent`} />\n            </div>\n          </FileViewer>\n        )}\n      </div>\n      {image && <img src={image} className={`file-image`} alt={\"\"} />}\n      {b64image && <img src={b64image} className={`file-image`} alt={\"\"} />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/plugins/mermaid.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport mermaid from \"mermaid\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Check, Copy, Loader2 } from \"lucide-react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { copyClipboard } from \"@/utils/dom.ts\";\n\nmermaid.initialize({\n  theme: \"dark\",\n  themeCSS: `\n     .node rect {\n        stroke: #fff;\n      }\n  `,\n  fontFamily:\n    'Andika,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\"',\n});\n\ntype MermaidProps = {\n  children: string | React.ReactNode;\n};\n\nexport function Mermaid({ children }: MermaidProps) {\n  const chart = React.useRef<HTMLDivElement>(null);\n  const [error, setError] = React.useState<boolean>(false);\n  const [loading, setLoading] = React.useState<boolean>(false);\n  const [copied, setCopied] = React.useState<boolean>(false);\n\n  const createRenderTask = useDebouncedCallback(() => {\n    if (!chart.current) return;\n\n    // preflight check syntax is valid, if not, suppress the error message\n\n    console.debug(`[mermaid] create render task`);\n    mermaid\n      .run({\n        nodes: [chart.current],\n        suppressErrors: true, // suppresses the error message\n      })\n      .catch((e) => {\n        setError(true);\n        console.warn(`[mermaid] render failed: ${e}`);\n      });\n\n    setLoading(false);\n  }, 500);\n\n  useEffect(() => {\n    createRenderTask();\n    setLoading(true);\n  }, [children]);\n\n  return (\n    <div className={`whitespace-pre-wrap markdown-syntax`}>\n      <div\n        className={`markdown-syntax-header`}\n        onClick={async () => {\n          const text = children?.toString() || \"\";\n          await copyClipboard(text);\n          setCopied(true);\n        }}\n      >\n        {copied ? (\n          <Check className={`h-3 w-3`} />\n        ) : (\n          <Copy className={`h-3 w-3`} />\n        )}\n        <p>mermaid</p>\n      </div>\n      <div\n        className={cn(\n          \"flex flex-row items-center justify-center text-primary select-none mb-4\",\n          !loading && \"hidden\",\n        )}\n      >\n        <Loader2 className={`h-4 w-4 mr-2 animate-spin shrink-0`} />\n        {error ? \"Failed to render\" : \"Rendering...\"}\n      </div>\n      <div className={cn(\"mermaid\", loading && \"mt-2\")} ref={chart}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function MarkdownMermaid({ children }: { children: React.ReactNode }) {\n  return <Mermaid children={children} />;\n}\n"
  },
  {
    "path": "app/src/components/plugins/progress.tsx",
    "content": "import React, { useMemo } from \"react\";\nimport { parseNumber } from \"@/utils/base.ts\";\nimport { Check, Loader2 } from \"lucide-react\";\n\ntype MarkdownProgressbarProps = {\n  children: React.ReactNode;\n};\n\nexport function MarkdownProgressbar({ children }: MarkdownProgressbarProps) {\n  const data = children?.toString() || \"\";\n  const progress = useMemo(() => {\n    const arr = data.split(\"\\n\").filter((line) => line.trim().length > 0);\n    if (arr.length === 0) return 0;\n    const result = arr[arr.length - 1];\n    return parseNumber(result);\n  }, [data]);\n\n  return (\n    <div className={`progress`}>\n      <p\n        className={`flex flex-row items-center justify-center text-primary select-none text-center text-white px-6`}\n      >\n        {progress < 100 ? (\n          <Loader2\n            className={`h-4 w-4 mr-2 inline-block animate-spin shrink-0`}\n          />\n        ) : (\n          <Check className={`h-4 w-4 mr-2 inline-block animate-out shrink-0`} />\n        )}\n        Generating: {progress < 0 ? 0 : progress.toFixed()}%\n      </p>\n      {progress > 0 && (\n        <div\n          className={`progressbar relative h-4 w-full overflow-hidden rounded-full bg-muted min-w-[20vw] bg-white`}\n          role={`progressbar`}\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={progress}\n          data-max={100}\n        >\n          <p\n            className={`h-full w-full flex-1 bg-primary transition-all duration-300`}\n            style={{ transform: `translateX(-${100 - progress}%)` }}\n            data-max={100}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/accordion.tsx",
    "content": "import * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item\n    ref={ref}\n    className={cn(\"border-b\", className)}\n    {...props}\n  />\n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        \"flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className=\"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n    {...props}\n  >\n    <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n  </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "app/src/components/ui/alert-dialog.tsx",
    "content": "import * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"./lib/utils\";\nimport { buttonVariants } from \"./button\";\nimport Emoji from \"../Emoji\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = ({\n  ...props\n}: AlertDialogPrimitive.AlertDialogPortalProps) => (\n  <AlertDialogPrimitive.Portal {...props} />\n);\nAlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, children, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full\",\n        className,\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n  className,\n  notTextCentered,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & { notTextCentered?: boolean }) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 sm:text-left\",\n      !notTextCentered && \"text-center\",\n      className,\n    )}\n    {...props}\n  />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\ntype AlertDialogEmojiProps = React.HTMLAttributes<HTMLDivElement> & {\n  emoji: string;\n};\nconst AlertDialogEmoji = ({\n  emoji,\n  className,\n  ...props\n}: AlertDialogEmojiProps) => (\n  <Emoji\n    {...props}\n    emoji={emoji}\n    className={cn(\"w-12 h-12 mx-auto sm:mx-0 mb-2\", className)}\n  />\n);\nAlertDialogEmoji.displayName = \"AlertDialogEmoji\";\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold\", className)}\n    {...props}\n  />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"mt-2 sm:mt-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogEmoji,\n};\n"
  },
  {
    "path": "app/src/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n));\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n));\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "app/src/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-muted text-secondary-foreground hover:bg-muted/90\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline: \"text-foreground\",\n        full_outline:\n          \"text-foreground bg-muted hover:bg-muted/90 border-secondary\",\n        gold: \"border-transparent bg-amber-500/10 hover:bg-amber-500/20 text-amber-500\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nexport type BadgeVariants = keyof ReturnType<typeof badgeVariants>;\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "app/src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./lib/utils\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { Check, Loader2 } from \"lucide-react\";\nimport { useTemporaryState } from \"@/utils/hook.ts\";\nimport Clickable from \"@/components/ui/clickable.tsx\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        \"light-destructive\":\n          \"bg-destructive/10 text-destructive hover:bg-destructive/15 text-destructive/80 hover:text-destructive\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-10 rounded-md px-4\",\n        thin: \"h-9 rounded-md px-3.5 text-sm\",\n        xs: \"h-8 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n        \"icon-md\": \"h-9 w-9\",\n        \"icon-sm\": \"h-8 w-8\",\n        \"flex-icon-sm\": \"h-8 w-8\",\n        \"icon-xs\": \"h-7 w-7\",\n        \"p-xs\": \"p-1\",\n        \"default-sm\": \"h-8 px-3\",\n        \"default-lg\": \"h-9 px-6\",\n        \"default-xs\": \"h-7 px-2 text-xs\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n  loading?: boolean;\n  unClickable?: boolean;\n  classNameWrapper?: string;\n  tapScale?: number;\n  onLoadingChange?: (loading: boolean) => void;\n}\n\nimport { motion, AnimatePresence } from \"framer-motion\";\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      onClick,\n      disabled,\n      children,\n      asChild = false,\n      loading = false,\n      unClickable,\n      classNameWrapper,\n      tapScale,\n      onLoadingChange,\n      ...props\n    },\n    ref,\n  ) => {\n    const Comp = asChild ? Slot : \"button\";\n    const [working, setWorking] = useState<boolean>(false);\n    const [animating, setAnimating] = useState<boolean>(false);\n    const onTrigger =\n      loading && onClick\n        ? async (e: React.MouseEvent<HTMLButtonElement>) => {\n            if (disabled) return;\n            e.preventDefault();\n            e.stopPropagation();\n\n            if (working) return;\n            setWorking(true);\n            setAnimating(true);\n\n            try {\n              // execute the onClick handler\n              await onClick(e);\n            } finally {\n              setWorking(false);\n              // Keep animating for a minimum of 500ms\n              setTimeout(() => setAnimating(false), 500);\n            }\n          }\n        : onClick;\n\n    useEffect(() => {\n      onLoadingChange?.(working);\n    }, [working, onLoadingChange]);\n\n    const child = useMemo(() => {\n      if (asChild) return children;\n      if (size === \"icon\" || size === \"icon-sm\") {\n        if (loading && animating) {\n          return <Loader2 className={`animate-spin w-4 h-4`} />;\n        }\n      }\n\n      return (\n        <>\n          <AnimatePresence>\n            {loading && animating && (\n              <motion.div\n                initial={{ width: 0, opacity: 0 }}\n                animate={{ width: \"auto\", opacity: 1 }}\n                exit={{ width: 0, opacity: 0 }}\n                transition={{ duration: 0.2 }}\n                className=\"mr-2\"\n              >\n                <Loader2 className={`animate-spin w-4 h-4`} />\n              </motion.div>\n            )}\n          </AnimatePresence>\n          {children}\n        </>\n      );\n    }, [asChild, children, loading, animating, size]);\n\n    const comp = (\n      <Comp\n        className={cn(\n          \"button-wrapper\",\n          animating && \"is-loading\",\n          buttonVariants({ variant, size, className }),\n        )}\n        ref={ref}\n        onClick={onTrigger}\n        disabled={disabled || working}\n        {...props}\n      >\n        {child}\n      </Comp>\n    );\n\n    return unClickable ? (\n      comp\n    ) : (\n      <Clickable className={classNameWrapper} tapScale={tapScale}>\n        {comp}\n      </Clickable>\n    );\n  },\n);\nButton.displayName = \"Button\";\n\ntype TemporaryButtonProps = ButtonProps & {\n  interval?: number;\n};\n\nconst TemporaryButton = React.forwardRef<\n  HTMLButtonElement,\n  TemporaryButtonProps\n>(({ interval, children, onClick, ...props }, ref) => {\n  const { state, triggerState } = useTemporaryState(interval);\n\n  const event = (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (onClick) onClick(e);\n    triggerState();\n  };\n\n  return (\n    <Button ref={ref} onClick={event} {...props}>\n      {state ? <Check className={`h-4 w-4`} /> : children}\n    </Button>\n  );\n});\n\ntype UploadFileButtonProps = ButtonProps & {\n  onFileChange: (file: File) => Promise<void>;\n  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;\n  stayAfterUpload?: boolean;\n};\n\nconst UploadFileButton = React.forwardRef<\n  HTMLButtonElement,\n  UploadFileButtonProps\n>(({ onFileChange, inputProps, stayAfterUpload, ...props }, ref) => {\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const onFileChangeHandler = async (\n    e: React.ChangeEvent<HTMLInputElement>,\n  ) => {\n    const file = e.target.files?.[0];\n    if (file) {\n      await onFileChange(file);\n    }\n\n    !stayAfterUpload && inputRef.current && (e.target.value = \"\");\n  };\n\n  return (\n    <>\n      <Button ref={ref} onClick={() => inputRef.current?.click()} {...props} />\n      <input\n        ref={inputRef}\n        type=\"file\"\n        className=\"hidden\"\n        onChange={onFileChangeHandler}\n        {...inputProps}\n      />\n    </>\n  );\n});\n\nexport { Button, TemporaryButton, UploadFileButton, buttonVariants };\n"
  },
  {
    "path": "app/src/components/ui/calendar.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { DayPicker } from \"react-day-picker\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker>;\n\nfunction Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  ...props\n}: CalendarProps) {\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn(\"p-3\", className)}\n      classNames={{\n        months: \"flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0\",\n        month: \"space-y-4\",\n        caption: \"flex justify-center pt-1 relative items-center\",\n        caption_label: \"text-sm font-medium\",\n        nav: \"space-x-1 flex items-center\",\n        nav_button: cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"h-6 w-6 bg-transparent p-1 opacity-50 hover:opacity-100\",\n        ),\n        nav_button_previous: \"absolute left-1\",\n        nav_button_next: \"absolute right-1\",\n        table: \"w-full border-collapse space-y-1\",\n        head_row: \"flex\",\n        head_cell:\n          \"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]\",\n        row: \"flex w-full mt-2\",\n        cell: \"h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20\",\n        day: cn(\n          buttonVariants({ variant: \"ghost\" }),\n          \"h-9 w-9 p-0 font-normal aria-selected:opacity-100\",\n        ),\n        day_range_end: \"day-range-end\",\n        day_selected:\n          \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground\",\n        day_today: \"bg-accent text-accent-foreground\",\n        day_outside:\n          \"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30\",\n        day_disabled: \"text-muted-foreground opacity-50\",\n        day_range_middle:\n          \"aria-selected:bg-accent aria-selected:text-accent-foreground\",\n        day_hidden: \"invisible\",\n        ...classNames,\n      }}\n      components={{\n        IconLeft: ({ ...props }) => (\n          <ChevronLeft className=\"h-4 w-4\" {...props} />\n        ),\n        IconRight: ({ ...props }) => (\n          <ChevronRight className=\"h-4 w-4\" {...props} />\n        ),\n      }}\n      formatters={{\n        formatCaption: (date) => `${date.getFullYear()}/${date.getMonth() + 1}`,\n        formatWeekdayName: (date) =>\n          date.toLocaleDateString(undefined, { weekday: \"short\" }),\n      }}\n      {...props}\n    />\n  );\n}\nCalendar.displayName = \"Calendar\";\n\nexport { Calendar };\n"
  },
  {
    "path": "app/src/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className,\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-xl md:text-2xl font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "app/src/components/ui/checkbox.tsx",
    "content": "import * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <motion.div\n    whileTap={{ scale: 0.9 }}\n    className=\"w-fit h-fit flex items-center\"\n  >\n    <CheckboxPrimitive.Root\n      ref={ref}\n      className={cn(\n        \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <AnimatePresence>\n        {props.checked && (\n          <CheckboxPrimitive.Indicator\n            className={cn(\"flex items-center justify-center text-current\")}\n            asChild\n          >\n            <motion.div\n              initial={{ scale: 0 }}\n              animate={{ scale: 1 }}\n              exit={{ scale: 0 }}\n              transition={{ duration: 0.2 }}\n            >\n              <Check className=\"h-3 w-3\" />\n            </motion.div>\n          </CheckboxPrimitive.Indicator>\n        )}\n      </AnimatePresence>\n    </CheckboxPrimitive.Root>\n  </motion.div>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
  },
  {
    "path": "app/src/components/ui/clickable.tsx",
    "content": "import React from \"react\";\nimport { motion } from \"framer-motion\";\nimport { cn } from \"./lib/utils\";\n\nexport interface ClickableProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    React.PropsWithChildren<{}> {\n  tapScale?: number;\n  tapDuration?: number;\n  hoverScale?: number;\n}\n\nconst Clickable: React.FC<ClickableProps> = ({\n  children,\n  className,\n  tapScale = 0.95,\n  tapDuration = 0.1,\n  hoverScale,\n  onClick,\n}) => {\n  return (\n    <motion.div\n      className={cn(\"cursor-pointer\", className)}\n      whileTap={{\n        scale: tapScale,\n        transition: { duration: tapDuration },\n      }}\n      whileHover={hoverScale ? { scale: hoverScale } : {}}\n      whileFocus={hoverScale ? { scale: hoverScale } : {}}\n      onClick={onClick}\n    >\n      {children}\n    </motion.div>\n  );\n};\n\nClickable.displayName = \"Clickable\";\nexport default Clickable;\n"
  },
  {
    "path": "app/src/components/ui/combo-box.tsx",
    "content": "import React from \"react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\ntype ComboBoxProps = {\n  value: string;\n  onChange: (value: string) => void;\n  list: string[];\n  listTranslated?: string;\n  listFormatter?: (value: string) => any;\n  placeholder?: string;\n  defaultOpen?: boolean;\n  className?: string;\n  classNameContent?: string;\n  align?: \"start\" | \"end\" | \"center\" | undefined;\n  hideSearchBar?: boolean;\n  icon?: React.ReactNode;\n};\n\nexport function Combobox({\n  value,\n  onChange,\n  list,\n  listTranslated,\n  listFormatter,\n  placeholder,\n  defaultOpen,\n  className,\n  classNameContent,\n  align,\n  hideSearchBar,\n  icon,\n}: ComboBoxProps) {\n  const { t } = useTranslation();\n  const [open, setOpen] = React.useState(defaultOpen ?? false);\n  const valueList = React.useMemo((): string[] => {\n    // list set (if some element in current value is not in list, it will be added)\n    const seq = [...list, value ?? \"\"].filter((v) => v);\n    const set = new Set(seq);\n    return [...set];\n  }, [list]);\n\n  const formatter = React.useMemo(() => {\n    if (listFormatter) {\n      return listFormatter;\n    }\n\n    return (value: string) =>\n      listTranslated ? t(`${listTranslated}.${value}`) : value;\n  }, [listFormatter, listTranslated]);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          unClickable\n          className={cn(\"w-[320px] max-w-[60vw] justify-between\", className)}\n        >\n          {icon}\n          {(value ? formatter(value) : placeholder) ?? \"\"}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        className={cn(\"w-[320px] max-w-[60vw] p-0\", classNameContent)}\n        align={align}\n      >\n        <Command>\n          {!hideSearchBar && <CommandInput placeholder={placeholder} />}\n          <CommandEmpty>{t(\"admin.empty\")}</CommandEmpty>\n          <CommandList>\n            {valueList.map((key) => (\n              <CommandItem\n                key={key}\n                value={key}\n                onSelect={() => {\n                  if (key === value) return setOpen(false);\n\n                  onChange(key);\n                  setOpen(false);\n                }}\n              >\n                <Check\n                  className={cn(\n                    \"mr-2 h-4 w-4\",\n                    key === value ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                />\n                {formatter(key)}\n              </CommandItem>\n            ))}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/command.tsx",
    "content": "import * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Search } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ninterface CommandDialogProps extends DialogProps {}\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-none\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nconst ScrollCommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <ScrollArea className={cn(\"max-h-[300px] overflow-y-auto\", className)}>\n    <CommandList ref={ref} {...props} />\n  </ScrollArea>\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\nScrollCommandList.displayName = CommandList.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n  ScrollCommandList,\n};\n"
  },
  {
    "path": "app/src/components/ui/context-menu.tsx",
    "content": "import * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"./lib/utils\";\n\nconst ContextMenu = ContextMenuPrimitive.Root;\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group;\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal;\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n));\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n));\nContextMenuCheckboxItem.displayName =\n  ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold text-foreground\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n"
  },
  {
    "path": "app/src/components/ui/date-picker.tsx",
    "content": "import * as React from \"react\";\nimport { format } from \"date-fns\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar, CalendarProps } from \"@/components/ui/calendar\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { CalendarIcon, Eraser, Minus, Plus } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\ntype DatePickerProps = CalendarProps & {\n  classNameTrigger?: string;\n  value?: string;\n  onValueChange?: (value: string) => void;\n};\n\nfunction parseDate(value?: string, init?: boolean): Date | undefined {\n  try {\n    if (!value) return init ? undefined : new Date();\n    if (value.includes(\" \")) value = value.split(\" \")[0]; // Remove time\n    const [year, month, day] = value.split(\"-\").map(Number);\n    return new Date(year, month - 1, day);\n  } catch (e) {\n    console.warn(\"Invalid date format\", value, e);\n    return new Date();\n  }\n}\n\nconst DatePicker = ({\n  value,\n  onValueChange,\n  classNameTrigger,\n  ...props\n}: DatePickerProps) => {\n  const { t } = useTranslation();\n  const [date, setDate] = React.useState<Date | undefined>(\n    parseDate(value, true),\n  );\n\n  React.useEffect(() => {\n    const v = date ? format(date, \"yyyy-MM-dd\") : \"\";\n    onValueChange && onValueChange(v);\n    console.debug(`[calendar] value changed: [${v}]`);\n  }, [date]);\n\n  React.useEffect(() => {\n    const date = parseDate(value);\n    if (!date || date.getTime() === date.getTime()) return;\n    setDate(date);\n  }, [value]);\n\n  const addYear = () => {\n    const current = date || new Date();\n    setDate(new Date(current.setFullYear(current.getFullYear() + 1)));\n  };\n\n  const subYear = () => {\n    const current = date || new Date();\n    setDate(new Date(current.setFullYear(current.getFullYear() - 1)));\n  };\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          unClickable\n          variant={\"outline\"}\n          className={cn(\n            \"w-[240px] justify-start text-left font-normal\",\n            !date && \"text-muted-foreground\",\n            classNameTrigger,\n          )}\n        >\n          <CalendarIcon className=\"mr-2 h-4 w-4\" />\n          {date ? (\n            `${format(date, \"yyyy/MM/dd\")}`\n          ) : (\n            <span>{t(\"date.pick\")}</span>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n        <Calendar\n          mode=\"single\"\n          selected={date} //@ts-ignore\n          onSelect={(date) => date && setDate(date)}\n          initialFocus\n          {...props}\n        />\n        <div className={cn(\"p-3 pt-0 flex flex-row items-center\")}>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className={`mr-2`}\n            onClick={addYear}\n          >\n            <Plus className=\"h-4 w-4\" />\n          </Button>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className={`mr-2`}\n            onClick={subYear}\n          >\n            <Minus className=\"h-4 w-4\" />\n          </Button>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className={`ml-auto`}\n            onClick={() => setDate(undefined)}\n          >\n            <Eraser className=\"h-4 w-4\" />\n          </Button>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport default DatePicker;\n"
  },
  {
    "path": "app/src/components/ui/dialog.tsx",
    "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { Maximize, Minimize, X } from \"lucide-react\";\n\nimport { cn } from \"./lib/utils\";\nimport { Button, ButtonProps } from \"@/components/ui/button.tsx\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => (\n  <DialogPrimitive.Portal {...props} />\n);\nDialogPortal.displayName = DialogPrimitive.Portal.displayName;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\ntype DialogContentProps = {\n  hideClose?: boolean;\n  couldFullScreen?: boolean;\n  leftSideFullScreen?: boolean;\n};\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content> & DialogContentProps,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n    DialogContentProps\n>(\n  (\n    {\n      className,\n      children,\n      hideClose,\n      couldFullScreen,\n      leftSideFullScreen,\n      ...props\n    },\n    ref,\n  ) => {\n    const [maximized, setMaximized] = React.useState(false);\n\n    return (\n      <DialogPortal>\n        <DialogOverlay />\n        <DialogPrimitive.Content\n          ref={ref}\n          className={cn(\n            \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-[90vw] md:max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border rounded-md bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full\",\n            maximized && \"full-screen\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n          {couldFullScreen && (\n            <div\n              className={cn(\n                \"absolute top-4 rounded-sm opacity-70 cursor-pointer ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\",\n                leftSideFullScreen ? \"left-4\" : \"right-12\",\n              )}\n              onClick={() => setMaximized(!maximized)}\n            >\n              {maximized ? (\n                <Minimize className=\"h-4 w-4\" />\n              ) : (\n                <Maximize className=\"h-4 w-4\" />\n              )}\n            </div>\n          )}\n          {!hideClose && (\n            <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n              <X className=\"h-4 w-4\" />\n              <span className=\"sr-only\">Close</span>\n            </DialogPrimitive.Close>\n          )}\n        </DialogPrimitive.Content>\n      </DialogPortal>\n    );\n  },\n);\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  notTextCentered,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & { notTextCentered?: boolean }) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 sm:text-left\",\n      !notTextCentered && \"text-center\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nconst DialogCancel = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, ...props }, ref) => (\n    <DialogPrimitive.Close asChild>\n      <Button\n        ref={ref}\n        className={className}\n        variant={variant ?? \"outline\"}\n        {...props}\n      />\n    </DialogPrimitive.Close>\n  ),\n);\n\nDialogCancel.displayName = \"DialogCancel\";\n\nconst DialogAction = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, ...props }, ref) => (\n    <Button\n      ref={ref}\n      className={cn(\"mb-2 md:mb-0\", className)}\n      variant={variant ?? \"default\"}\n      {...props}\n    />\n  ),\n);\nDialogAction.displayName = \"DialogAction\";\n\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n  DialogCancel,\n  DialogAction,\n};\n"
  },
  {
    "path": "app/src/components/ui/drawer.tsx",
    "content": "import * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Drawer = ({\n  shouldScaleBackground = true,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n  <DrawerPrimitive.Root\n    shouldScaleBackground={shouldScaleBackground}\n    {...props}\n  />\n);\nDrawer.displayName = \"Drawer\";\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    ref={ref}\n    className={cn(\"fixed inset-0 z-50 bg-black/80\", className)}\n    {...props}\n  />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background outline-none\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n      {children}\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = \"DrawerContent\";\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"grid gap-1.5 p-4 text-center sm:text-left\", className)}\n    {...props}\n  />\n);\nDrawerHeader.displayName = \"DrawerHeader\";\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n    {...props}\n  />\n);\nDrawerFooter.displayName = \"DrawerFooter\";\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "app/src/components/ui/dropdown-menu.tsx",
    "content": "import * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"./lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    onClick={(e) => {\n      e.stopPropagation();\n      props.onClick?.(e);\n    }}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "app/src/components/ui/icons/Github.tsx",
    "content": "import React from \"react\";\n\nfunction Github(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 438.549 438.549\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z\"\n      ></path>\n    </svg>\n  );\n}\n\nexport default Github;\n"
  },
  {
    "path": "app/src/components/ui/icons/Send.tsx",
    "content": "import React from \"react\";\n\nfunction Send(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      className=\"h-4 w-4 mr-1 send-icon\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"m21.426 11.095-17-8A1 1 0 0 0 3.03 4.242l1.212 4.849L12 12l-7.758 2.909-1.212 4.849a.998.998 0 0 0 1.396 1.147l17-8a1 1 0 0 0 0-1.81z\"></path>\n    </svg>\n  );\n}\n\nexport default Send;\n"
  },
  {
    "path": "app/src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"./lib/utils\";\nimport { Eye, EyeOff } from \"lucide-react\";\nimport Icon from \"@/components/utils/Icon.tsx\";\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {\n  smallSize?: boolean;\n  classNameWrapper?: string;\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, smallSize, classNameWrapper, ...props }, ref) => {\n    const [show, setShow] = React.useState(false);\n\n    return !(type === \"password\") ? (\n      <input\n        type={type}\n        className={cn(\n          \"ui-input flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 focus:border-input autofill:bg-background\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    ) : (\n      <div className={cn(\"relative\", classNameWrapper)}>\n        <input\n          type={show ? \"text\" : \"password\"}\n          className={cn(\n            \"ui-input flex w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 focus:border-input pr-10 autofill:bg-background\",\n            className,\n          )}\n          ref={ref}\n          {...props}\n        />\n        <button\n          type=\"button\"\n          className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n          onClick={() => setShow(!show)}\n        >\n          <Icon\n            icon={show ? <EyeOff /> : <Eye />}\n            className={smallSize ? \"h-3.5 w-3.5\" : \"h-4 w-4\"}\n          />\n        </button>\n      </div>\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "app/src/components/ui/label.tsx",
    "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./lib/utils\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "app/src/components/ui/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "app/src/components/ui/multi-combobox.tsx",
    "content": "import React from \"react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\ntype MultiComboBoxProps = {\n  value: string[];\n  onChange: (value: string[]) => void;\n  list: string[];\n  listTranslate?: string;\n  listTranslateFormatter?: (\n    key: string,\n  ) => string | React.ReactNode | undefined;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  defaultOpen?: boolean;\n  className?: string;\n  align?: \"start\" | \"end\" | \"center\" | undefined;\n  disabled?: boolean;\n  disabledSearch?: boolean;\n  classNameWrapper?: string;\n  children?: React.ReactNode;\n};\n\nexport function MultiCombobox({\n  value,\n  onChange,\n  list,\n  listTranslate,\n  listTranslateFormatter,\n  placeholder,\n  searchPlaceholder,\n  defaultOpen,\n  className,\n  align,\n  disabled,\n  disabledSearch,\n  classNameWrapper,\n  children,\n}: MultiComboBoxProps) {\n  const { t } = useTranslation();\n  const [open, setOpen] = React.useState(defaultOpen ?? false);\n  const valueList = React.useMemo((): string[] => {\n    // list set (if some element in current value is not in list, it will be added)\n    const seq = [...list, ...(value ?? [])].filter((v) => v);\n    const set = new Set(seq);\n    return [...set];\n  }, [list]);\n\n  const v = value ?? [];\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        {children ?? (\n          <Button\n            unClickable\n            variant={`outline`}\n            role={`combobox`}\n            aria-expanded={open}\n            className={cn(\"w-[320px] max-w-[60vw] justify-between\", className)}\n            classNameWrapper={classNameWrapper}\n            disabled={disabled}\n          >\n            <Check className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n            {placeholder ?? `${v.length} Items Selected`}\n            <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n          </Button>\n        )}\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[320px] max-w-[60vw] p-0\" align={align}>\n        <Command>\n          {!disabledSearch && <CommandInput placeholder={searchPlaceholder} />}\n          <CommandEmpty>{t(\"admin.empty\")}</CommandEmpty>\n          <CommandList className={`thin-scrollbar`}>\n            {valueList.map((key) => (\n              <CommandItem\n                key={key}\n                value={key}\n                onSelect={(current) => {\n                  // keep original case\n                  const originalItem = valueList.find(item => item.toLowerCase() === current.toLowerCase());\n                  if (!originalItem) return;\n\n                  const existingIndex = v.findIndex(item => item.toLowerCase() === current.toLowerCase());\n                  if (existingIndex !== -1) {\n                    onChange(v.filter((_, index) => index !== existingIndex));\n                  } else {\n                    onChange([...v, originalItem]);\n                  }\n                }}\n              >\n                <Check\n                  className={cn(\n                    \"mr-2 h-4 w-4\",\n                    v.some(item => item.toLowerCase() === key.toLowerCase()) ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                />\n                {listTranslateFormatter\n                  ? listTranslateFormatter(key)\n                  : listTranslate\n                  ? t(`${listTranslate}.${key}`)\n                  : key}\n              </CommandItem>\n            ))}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/number-input.tsx",
    "content": "import * as React from \"react\";\nimport { Input, InputProps } from \"@/components/ui/input.tsx\";\nimport { getNumber } from \"@/utils/base.ts\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\nexport interface NumberInputProps extends InputProps {\n  value: number;\n  max?: number;\n  min?: number;\n  onValueChange: (value: number) => void;\n  acceptNegative?: boolean;\n  acceptNaN?: boolean;\n}\n\nconst NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n  ({ className, type, ...props }, ref) => {\n    const [value, setValue] = useState(props.value.toString());\n    useEffect(() => {\n      // fix life cycle: update value when props.value changed\n      if (getValue(value.toString()) !== props.value) {\n        setValue(props.value.toString());\n      }\n    }, [props.value]);\n\n    const getValue = (v: string) => {\n      const raw = getNumber(v, props.acceptNegative);\n      let val = parseFloat(raw);\n      if (isNaN(val) && !props.acceptNaN) val = 0;\n      if (props.max !== undefined && val > props.max) val = props.max;\n      else if (props.min !== undefined && val < props.min) val = props.min;\n      return val;\n    };\n\n    const formatValue = (v: string) => {\n      if (v.trim().length === 0) return v.trim();\n\n      if (!/^[-+]?(?:[0-9]*(?:\\.[0-9]*)?)?$/.test(v)) {\n        const exp = /[-+]?[0-9]+(\\.[0-9]+)?/g;\n        return v.match(exp)?.join(\"\") || \"\";\n      }\n\n      if (v === \"-\" && props.acceptNegative) return v;\n\n      // replace -0124.5 to -124.5, 0043 to 43, 2.000 to 2.000\n      const exp = /^[-+]?0+(?=[0-9]+(\\.[0-9]+)?$)/;\n      v = v.replace(exp, \"\");\n\n      const raw = getNumber(v, props.acceptNegative);\n      let val = parseFloat(raw);\n      if (isNaN(val) && !props.acceptNaN) return (props.min ?? 0).toString();\n      if (props.max !== undefined && val > props.max)\n        return props.max.toString();\n      else if (props.min !== undefined && val < props.min)\n        return props.min.toString();\n\n      return v;\n    };\n\n    const isValid = useMemo((): boolean => {\n      if (!/^[-+]?[0-9]+(\\.[0-9]+)?$/.test(value)) return false;\n      const val = getValue(value);\n      if (props.max !== undefined && val > props.max) return false;\n      else if (props.min !== undefined && val < props.min) return false;\n      return true;\n    }, [value]);\n\n    return (\n      <Input\n        {...props}\n        ref={ref}\n        className={cn(\n          \"number-input transition\",\n          className,\n          !isValid && \"border-red-600 focus:border-red-700\",\n        )}\n        id={props.id}\n        value={value}\n        onChange={(e) => {\n          setValue(formatValue(e.target.value));\n          props.onValueChange(getValue(e.target.value));\n        }}\n        min={props.min}\n        max={props.max}\n        onWheel={(e) => {\n          e.stopPropagation();\n        }}\n      />\n    );\n  },\n);\nNumberInput.displayName = \"NumberInput\";\n\nexport { NumberInput };\n"
  },
  {
    "path": "app/src/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { ButtonProps, buttonVariants } from \"@/components/ui/button\";\n\nconst Pagination = ({ ...props }: React.ComponentProps<\"nav\">) => (\n  <nav role=\"navigation\" aria-label=\"pagination\" {...props} />\n);\nPagination.displayName = \"Pagination\";\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn(\"flex flex-row items-center gap-1\", className)}\n    {...props}\n  />\n));\nPaginationContent.displayName = \"PaginationContent\";\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n  <li\n    ref={ref}\n    className={cn(\"cursor-pointer select-none\", className)}\n    {...props}\n  />\n));\nPaginationItem.displayName = \"PaginationItem\";\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? \"page\" : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? \"outline\" : \"ghost\",\n        size,\n      }),\n      className,\n    )}\n    {...props}\n  />\n);\nPaginationLink.displayName = \"PaginationLink\";\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"icon\"\n    className={cn(\"gap-1\", className)}\n    {...props}\n  >\n    <ChevronLeft className=\"h-4 w-4\" />\n  </PaginationLink>\n);\nPaginationPrevious.displayName = \"PaginationPrevious\";\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"icon\"\n    className={cn(\"gap-1\", className)}\n    {...props}\n  >\n    <ChevronRight className=\"h-4 w-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = \"PaginationNext\";\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) => (\n  <span\n    aria-hidden\n    className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n    {...props}\n  >\n    <MoreHorizontal className=\"h-4 w-4\" />\n  </span>\n);\nPaginationEllipsis.displayName = \"PaginationEllipsis\";\n\ntype PaginationActionProps = React.ComponentProps<\"div\"> & {\n  current: number;\n  total: number;\n  offset?: boolean;\n  onPageChange: (page: number) => void;\n  notCentered?: boolean;\n};\n\nconst PaginationAction = ({\n  current,\n  total,\n  offset = false,\n  className,\n  onPageChange,\n  children,\n  notCentered,\n  ...props\n}: PaginationActionProps) => {\n  const real = current + (offset ? 1 : 0);\n  const diff = total - real;\n\n  const hasPrev = current > 0;\n  const hasNext = diff >= 1;\n\n  const hasStepPrev = current > 1 && !hasNext;\n  const hasStepNext = diff >= 2 && !hasPrev;\n\n  const showRightEllipsis = diff > 2;\n  const showLeftEllipsis = real > 2 && !showRightEllipsis;\n\n  return (\n    <Pagination\n      className={cn(\n        \"py-4\",\n        !notCentered && \"mx-auto flex w-full justify-center\",\n        className,\n      )}\n      {...props}\n    >\n      <PaginationContent>\n        <PaginationItem onClick={() => hasPrev && onPageChange(current - 1)}>\n          <PaginationPrevious />\n        </PaginationItem>\n\n        {showLeftEllipsis && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n\n        {hasStepPrev && (\n          <PaginationItem onClick={() => onPageChange(current - 2)}>\n            <PaginationLink>{real - 2}</PaginationLink>\n          </PaginationItem>\n        )}\n\n        {hasPrev && (\n          <PaginationItem onClick={() => onPageChange(current - 1)}>\n            <PaginationLink>{real - 1}</PaginationLink>\n          </PaginationItem>\n        )}\n\n        <PaginationItem>\n          <PaginationLink isActive>{real}</PaginationLink>\n        </PaginationItem>\n\n        {hasNext && (\n          <PaginationItem onClick={() => onPageChange(current + 1)}>\n            <PaginationLink>{real + 1}</PaginationLink>\n          </PaginationItem>\n        )}\n\n        {hasStepNext && (\n          <PaginationItem onClick={() => onPageChange(current + 2)}>\n            <PaginationLink>{real + 2}</PaginationLink>\n          </PaginationItem>\n        )}\n\n        {showRightEllipsis && (\n          <PaginationItem>\n            <PaginationEllipsis />\n          </PaginationItem>\n        )}\n\n        <PaginationItem onClick={() => hasNext && onPageChange(current + 1)}>\n          <PaginationNext />\n        </PaginationItem>\n      </PaginationContent>\n    </Pagination>\n  );\n};\nPaginationAction.displayName = \"PaginationAction\";\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n  PaginationAction,\n};\n"
  },
  {
    "path": "app/src/components/ui/popover.tsx",
    "content": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "app/src/components/ui/progress.tsx",
    "content": "import * as React from \"react\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {\n    classNameIndicator?: string;\n  }\n>(({ className, classNameIndicator, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative h-4 w-full overflow-hidden rounded-full bg-muted\",\n      className,\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className={cn(\n        \"h-full w-full flex-1 bg-primary transition-all rounded-full\",\n        classNameIndicator,\n        value === 0 && \"opacity-0\",\n      )}\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nconst ValuableProgress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {\n    value: number;\n    max: number;\n    classNameIndicator?: string;\n  }\n>(({ value, max, ...props }, ref) => {\n  const progress = value / max;\n  return <Progress ref={ref} value={progress * 100} {...props} />;\n});\n\nexport { Progress, ValuableProgress };\n"
  },
  {
    "path": "app/src/components/ui/radio-box.tsx",
    "content": "import React from \"react\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\nexport type RadioBoxItemProps = {\n  id: string;\n  value: string;\n};\n\nexport type RadioBoxProps = {\n  title: string;\n  icon?: React.ReactElement;\n  prefix?: string;\n  defaultValue?: string;\n  items: RadioBoxItemProps[];\n\n  value?: string;\n  onValueChange?: (value: string) => void;\n  colLayout?: boolean;\n  className?: string;\n};\n\nexport const RadioBox = React.forwardRef<\n  React.ElementRef<typeof RadioGroup>,\n  RadioBoxProps\n>(\n  (\n    {\n      title,\n      icon,\n      prefix,\n      items,\n      colLayout,\n      defaultValue,\n      value,\n      onValueChange,\n      className,\n    },\n    ref,\n  ) => {\n    return (\n      <div\n        className={cn(\n          `inline-flex flex-row text-sm items-center mb-2`,\n          colLayout && \"items-start\",\n          className,\n        )}\n        ref={ref}\n      >\n        <div className=\"flex flex-row items-center select-none mr-2\">\n          {icon && <Icon icon={icon} className={`w-3.5 h-3.5 mr-1`} />}\n          <p>{title}</p>\n        </div>\n        <RadioGroup\n          defaultValue={defaultValue}\n          className={cn(\n            colLayout ? \"grid grid-cols-2 gap-2\" : \"flex flex-row space-x-1\",\n          )}\n          value={value}\n          onValueChange={onValueChange}\n        >\n          {items.map((item, index) => {\n            return (\n              <div\n                key={index}\n                className=\"flex items-center space-x-1 cursor-pointer\"\n                onClick={() => onValueChange && onValueChange(item.id)}\n              >\n                <RadioGroupItem value={item.id} id={`${prefix}-${item.id}`} />\n                <Label\n                  className={`cursor-pointer`}\n                  htmlFor={`${prefix}-${item.id}`}\n                >\n                  {item.value}\n                </Label>\n              </div>\n            );\n          })}\n        </RadioGroup>\n      </div>\n    );\n  },\n);\nRadioBox.displayName = \"RadioBox\";\n"
  },
  {
    "path": "app/src/components/ui/radio-group.tsx",
    "content": "import * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-5 w-5 rounded-full border-[1.5px] bg-primary border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 scale-90 cursor-pointer\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Check className=\"h-3 w-3 stroke-[4] animate-fade-in text-background\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "app/src/components/ui/scroll-area.tsx",
    "content": "import * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"./lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {\n    classNameViewport?: string;\n    asChildViewport?: boolean;\n  }\n>(\n  (\n    { className, classNameViewport, asChildViewport, children, ...props },\n    ref,\n  ) => (\n    <ScrollAreaPrimitive.Root\n      className={cn(\"relative overflow-hidden\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={ref}\n        className={cn(\n          \"scrollarea-viewport h-full w-full rounded-[inherit]\",\n          classNameViewport,\n        )}\n        asChild={asChildViewport}\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  ),\n);\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"horizontal-scrollbar h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb\n      className={cn(\n        \"relative rounded-full bg-border\",\n        orientation === \"vertical\" && \"flex-1\",\n      )}\n    />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "app/src/components/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n    classNameDown?: string;\n  }\n>(({ className, classNameDown, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className={cn(\"h-4 w-4 opacity-50\", classNameDown)} />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst NativeSelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ children, ...props }, ref) => (\n  <SelectPrimitive.Trigger ref={ref} {...props}>\n    {children}\n  </SelectPrimitive.Trigger>\n));\nNativeSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }) => {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        ref={(ref) => {\n          if (!ref) return;\n          ref.ontouchend = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n          };\n        }}\n        className={cn(\n          \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n});\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  NativeSelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "app/src/components/ui/separator.tsx",
    "content": "import * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"./lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref,\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "app/src/components/ui/sheet.tsx",
    "content": "import * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n  {\n    variants: {\n      side: {\n        top: \"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n        bottom:\n          \"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n        left: \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n        right:\n          \"inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  },\n);\n\ninterface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  SheetContentProps\n>(({ side = \"right\", className, children, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(sheetVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </SheetPrimitive.Close>\n    </SheetPrimitive.Content>\n  </SheetPortal>\n));\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold text-foreground\", className)}\n    {...props}\n  />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n  Sheet,\n  SheetPortal,\n  SheetOverlay,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "app/src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/components/ui/lib/utils\";\nimport React from \"react\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "app/src/components/ui/slider.tsx",
    "content": "import * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\ntype SliderProps = {\n  classNameThumb?: string;\n};\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root> & SliderProps,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & SliderProps\n>(({ className, classNameThumb, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex w-full touch-none select-none items-center\",\n      className,\n    )}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-2 w-full cursor-pointer grow overflow-hidden rounded-full bg-accent\">\n      <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n    </SliderPrimitive.Track>\n    <SliderPrimitive.Thumb\n      className={cn(\n        \"block h-5 w-5 cursor-pointer rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        classNameThumb,\n      )}\n    />\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "app/src/components/ui/sonner.tsx",
    "content": "import { useTheme } from \"next-themes\";\nimport { Toaster as Sonner } from \"sonner\";\nimport React from \"react\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner //@ts-ignore\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group toast border group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n          description: \"group-[.toast]:text-muted-foreground\",\n          actionButton:\n            \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton:\n            \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n        },\n      }}\n      position=\"top-right\"\n      // closeButton\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "app/src/components/ui/step.tsx",
    "content": "import React from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { motion } from \"framer-motion\";\n\nexport type StepProps = {\n  step: number;\n  label?: string;\n  children?: React.ReactNode;\n  className?: string;\n};\n\nfunction Step({ step, label, children, className, ...props }: StepProps) {\n  const variants = {\n    hidden: { opacity: 0, y: 20 },\n    visible: { opacity: 1, y: 0 },\n  };\n\n  return (\n    <motion.div\n      className={cn(\n        `step step-${step} group flex flex-row items-center cursor-pointer text-center`,\n        className,\n      )}\n      variants={variants}\n      initial=\"hidden\"\n      animate=\"visible\"\n      transition={{ duration: 0.3, delay: step * 0.2 }}\n      {...props}\n    >\n      <motion.div\n        className={cn(\n          \"step-number h-6 w-6 flex items-center justify-center shrink-0\",\n          \"bg-primary text-background rounded-full\",\n          \"text-sm font-bold border shadow mr-1.5\",\n          \"cursor-pointer select-none\",\n          \"transition duration-300 ease-in-out\",\n          \"group-hover:bg-background group-hover:text-primary\",\n        )}\n        whileHover={{ scale: 1.1 }}\n        whileTap={{ scale: 0.9 }}\n      >\n        {step}\n      </motion.div>\n      <motion.div\n        className={\"step-label text-primary text-sm\"}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ duration: 0.3, delay: step * 0.2 + 0.2 }}\n      >\n        {label}\n      </motion.div>\n      {children}\n    </motion.div>\n  );\n}\n\nexport default Step;\n"
  },
  {
    "path": "app/src/components/ui/switch.tsx",
    "content": "import * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"./lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "app/src/components/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"./lib/utils.ts\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area.tsx\";\nimport { useMemo } from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { Check, Filter } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\ntype TableProps = React.HTMLAttributes<HTMLTableElement> & {\n  classNameWrapper?: string;\n  classNameArea?: string;\n};\n\nconst Table = React.forwardRef<HTMLTableElement, TableProps>(\n  ({ className, classNameWrapper, classNameArea, ...props }, ref) => (\n    <ScrollArea type=\"always\" className={classNameArea}>\n      <div className={cn(\"relative w-full mb-2\", classNameWrapper)}>\n        <table\n          ref={ref}\n          className={cn(\"w-full caption-bottom text-sm\", className)}\n          {...props}\n        />\n      </div>\n      <ScrollBar className=\"cursor-pointer\" orientation=\"horizontal\" />\n    </ScrollArea>\n  ),\n);\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\"bg-primary font-medium text-primary-foreground\", className)}\n    {...props}\n  />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      \"p-4 py-1.5 md:py-2 align-middle [&:has([role=checkbox])]:pr-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nTableCaption.displayName = \"TableCaption\";\n\nexport type Visibility = {\n  [key: string]: boolean;\n};\n\nexport type VisibilityOptions = {\n  translatePrefix?: string;\n};\n\nexport type Column = { key: string; name: string; value: boolean };\n\nexport const useColumnsVisibility = (\n  initial: Visibility,\n  options?: VisibilityOptions,\n) => {\n  const [visibility, setVisibility] = React.useState(initial);\n  const bar = useMemo(\n    (): Column[] =>\n      Object.entries(visibility).map(([name, value]) => ({\n        key: name,\n        name: options?.translatePrefix\n          ? `${options.translatePrefix}.${name}`\n          : name,\n        value,\n      })),\n    [visibility, options],\n  );\n\n  const toggle = (key: string) => {\n    setVisibility((prev) => ({ ...prev, [key]: !prev[key] }));\n  };\n\n  const show = (key: string) => {\n    setVisibility((prev) => ({ ...prev, [key]: true }));\n  };\n\n  const hide = (key: string) => {\n    setVisibility((prev) => ({ ...prev, [key]: false }));\n  };\n\n  const merge = (key: string, ...args: string[]) => {\n    return cn(...args, !visibility[key] && \"hidden\");\n  };\n\n  return { visibility, bar, options, toggle, show, hide, merge };\n};\n\ntype ColumnsVisibilityBarProps = {\n  bar: Column[];\n  toggle: (key: string) => void;\n};\n\nexport const ColumnsVisibilityBar = ({\n  bar,\n  toggle,\n}: ColumnsVisibilityBarProps) => {\n  const { t } = useTranslation();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button className={`ml-2 shrink-0`} variant={`outline`} size={`icon`}>\n          <Filter className={`h-4 w-4`} />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align={`end`}>\n        {bar.map(({ key, name, value }) => (\n          <DropdownMenuItem\n            key={key}\n            onClick={(e) => {\n              e.preventDefault();\n              toggle(key);\n            }}\n          >\n            <Check\n              className={cn(\"h-4 w-4 mr-1 opacity-0\", value && \"opacity-100\")}\n            />\n            {t(name)}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "app/src/components/ui/tabs.tsx",
    "content": "import * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-10 items-center justify-center rounded-md bg-background-container border border-input p-1 text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-sm py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm px-4 md:px-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "app/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"./lib/utils\";\nimport { useMemo } from \"react\";\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = \"Textarea\";\n\n// FlexibleTextarea is a flexible rows textarea (current lines + 1)\nexport interface FlexibleTextareaProps extends TextareaProps {\n  rows?: number;\n  minRows?: number;\n  maxRows?: number;\n}\n\nconst FlexibleTextarea = React.forwardRef<\n  HTMLTextAreaElement,\n  FlexibleTextareaProps\n>(({ rows = 1, minRows, maxRows, className, ...props }, ref) => {\n  const lines = useMemo(() => {\n    const value = props.value?.toString() || \"\";\n    const count = value.split(\"\\n\").length + 1;\n    const res = Math.max(rows, count, minRows || 1);\n\n    if (maxRows) return Math.min(res, maxRows);\n    return res;\n  }, [props.value, rows, minRows]);\n\n  return (\n    <Textarea\n      className={cn(\"resize-none no-scrollbar\", className)}\n      ref={ref}\n      rows={lines}\n      {...props}\n    />\n  );\n});\n\nFlexibleTextarea.displayName = \"FlexibleTextarea\";\n\nexport { Textarea, FlexibleTextarea };\n"
  },
  {
    "path": "app/src/components/ui/toast.tsx",
    "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"./lib/utils\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 hover:text-foreground focus:outline-none focus:ring-2 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90 whitespace-pre-wrap\", className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "app/src/components/ui/toaster.tsx",
    "content": "import {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"./toast\";\nimport { useToast } from \"./use-toast\";\n\nexport function Toaster() {\n  const { toasts } = useToast();\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription\n                  className={`max-h-[90vh] no-scrollbar overflow-y-auto touch-pan-y w-full h-full`}\n                >\n                  {description}\n                </ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/toggle-group.tsx",
    "content": "import * as React from \"react\";\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\";\nimport { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/components/ui/lib/utils\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants>\n>({\n  size: \"default\",\n  variant: \"default\",\n});\n\nconst ToggleGroup = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root\n    ref={ref}\n    className={cn(\"flex items-center justify-center gap-1\", className)}\n    {...props}\n  >\n    <ToggleGroupContext.Provider value={{ variant, size }}>\n      {children}\n    </ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n));\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &\n    VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, ...props }, ref) => {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        toggleVariants({\n          variant: variant || context.variant,\n          size: size || context.size,\n        }),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "app/src/components/ui/toggle.tsx",
    "content": "import * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./lib/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-10 px-3\",\n        sm: \"h-9 px-2.5\",\n        lg: \"h-11 px-5\",\n        col: \"h-7 px-3 rounded-full mr-1 mb-1.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "app/src/components/ui/tooltip.tsx",
    "content": "import * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"./lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      \"max-w-[100vw]\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "app/src/components/ui/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"./toast\";\n\nconst TOAST_LIMIT = 3;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n  created?: number;\n  duration?: number;\n};\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_VALUE;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const appendToast = (\n  state: ToasterToast[],\n  toast: ToasterToast,\n): ToasterToast[] => {\n  for (const t of state) {\n    if (\n      t.title === toast.title &&\n      t.description === toast.description &&\n      t.created &&\n      toast.created &&\n      Math.abs(t.created - toast.created) < 1000\n    ) {\n      return state;\n    }\n  }\n\n  return [toast, ...state].slice(0, TOAST_LIMIT);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: appendToast(state.toasts, action.toast),\n      };\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t,\n        ),\n      };\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, \"id\">;\n\nfunction toast({ ...props }: Toast, timeout?: number) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n  setTimeout(dismiss, timeout ?? 5000);\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      created: Date.now(),\n      open: true,\n      duration: timeout,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "app/src/components/utils/Icon.tsx",
    "content": "import React from \"react\";\n\ntype Icon = {\n  icon: React.ReactElement | JSX.Element | React.ReactNode;\n  className?: string;\n  id?: string;\n} & React.SVGProps<SVGSVGElement>;\n\nfunction Icon({ icon, className, id, ...props }: Icon) {\n  return React.cloneElement(icon as React.ReactElement, {\n    className: className,\n    id: id,\n    ...props,\n  });\n}\n\nexport default Icon;\n"
  },
  {
    "path": "app/src/conf/api.ts",
    "content": "import axios from \"axios\";\nimport { getMemory } from \"@/utils/memory.ts\";\n\ntype AxiosConfig = {\n  endpoint: string;\n  token: string;\n};\n\nexport function setAxiosConfig(config: AxiosConfig) {\n  axios.defaults.baseURL = config.endpoint;\n  axios.defaults.headers.post[\"Content-Type\"] = \"application/json\";\n  axios.defaults.headers.common[\"Authorization\"] = getMemory(config.token);\n}\n"
  },
  {
    "path": "app/src/conf/bootstrap.ts",
    "content": "import {\n  getDev,\n  getRestApi,\n  getTokenField,\n  getWebsocketApi,\n} from \"@/conf/env.ts\";\nimport { syncSiteInfo } from \"@/admin/api/info.ts\";\nimport { setAxiosConfig } from \"@/conf/api.ts\";\nimport { version as _version } from \"./version.json\";\n\nexport const version: string = _version; // version of the current build\nexport const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin)\nexport const deploy: boolean = true; // is production environment (for api endpoint)\nexport const tokenField = getTokenField(deploy); // token field name for storing token\n\nexport let apiEndpoint: string = getRestApi(deploy); // api endpoint for rest api calls\nexport let websocketEndpoint: string = getWebsocketApi(deploy); // api endpoint for websocket calls\n\nsetAxiosConfig({\n  endpoint: apiEndpoint,\n  token: tokenField,\n});\n\nsyncSiteInfo();\n"
  },
  {
    "path": "app/src/conf/deeptrain.tsx",
    "content": "import {\n  deeptrainAppName,\n  deeptrainEndpoint,\n  useDeeptrain,\n} from \"@/conf/env.ts\";\nimport { dev } from \"@/conf/bootstrap.ts\";\nimport React from \"react\";\nimport { getQueryParam } from \"@/utils/path.ts\";\n\nexport function goDeepLogin() {\n  const appParam = dev ? \"dev\" : deeptrainAppName;\n  const aff = getQueryParam(\"aff\").trim();\n  const affQuery = aff ? `&aff=${encodeURIComponent(aff)}` : \"\";\n  location.href = `${deeptrainEndpoint}/login?app=${encodeURIComponent(appParam)}${affQuery}`;\n}\n\nexport function DeeptrainOnly({ children }: { children: React.ReactNode }) {\n  return useDeeptrain ? <>{children}</> : null;\n}\n"
  },
  {
    "path": "app/src/conf/env.ts",
    "content": "import { updateDocumentTitle, updateFavicon } from \"@/utils/dom.ts\";\nimport { setMemory } from \"@/utils/memory.ts\";\n\nexport let appName =\n  localStorage.getItem(\"app_name\") || import.meta.env.VITE_APP_NAME || \"CoAI\";\nexport let appLogo =\n  localStorage.getItem(\"app_logo\") ||\n  import.meta.env.VITE_APP_LOGO ||\n  \"/favicon.ico\";\nexport let blobEndpoint =\n  localStorage.getItem(\"blob_endpoint\") ||\n  import.meta.env.VITE_BLOB_ENDPOINT ||\n  \"https://blob.coai.dev\";\nexport let docsEndpoint =\n  localStorage.getItem(\"docs_url\") ||\n  import.meta.env.VITE_DOCS_ENDPOINT ||\n  \"https://coai.dev\";\nexport let buyLink =\n  localStorage.getItem(\"buy_link\") || import.meta.env.VITE_BUY_LINK || \"\";\n\nexport const useDeeptrain = !!import.meta.env.VITE_USE_DEEPTRAIN;\nexport const backendEndpoint = import.meta.env.VITE_BACKEND_ENDPOINT || \"/api\";\nexport const deeptrainEndpoint =\n  import.meta.env.VITE_DEEPTRAIN_ENDPOINT || \"https://deeptrain.net\";\nexport const deeptrainAppName = import.meta.env.VITE_DEEPTRAIN_APP || \"coai\";\nexport const deeptrainApiEndpoint =\n  import.meta.env.VITE_DEEPTRAIN_API_ENDPOINT || \"https://api.deeptrain.net\";\n\nupdateDocumentTitle(import.meta.env.VITE_APP_NAME);\nupdateFavicon(import.meta.env.VITE_APP_LOGO);\n\nexport function getDev(): boolean {\n  /**\n   * return if the current environment is development\n   */\n  return window.location.hostname === \"localhost\";\n}\n\nexport function getRestApi(deploy: boolean): string {\n  /**\n   * return the REST API address\n   */\n  return !deploy ? \"http://localhost:8094\" : backendEndpoint;\n}\n\nexport function getWebsocketApi(deploy: boolean): string {\n  /**\n   * return the WebSocket API address\n   */\n  if (!deploy) return \"ws://localhost:8094\";\n\n  if (backendEndpoint.startsWith(\"http://\"))\n    return `ws://${backendEndpoint.slice(7)}`;\n  if (backendEndpoint.startsWith(\"https://\"))\n    return `wss://${backendEndpoint.slice(8)}`;\n  if (backendEndpoint.startsWith(\"/\"))\n    return location.protocol === \"https:\"\n      ? `wss://${location.host}${backendEndpoint}`\n      : `ws://${location.host}${backendEndpoint}`;\n  return backendEndpoint;\n}\n\nexport function getTokenField(deploy: boolean): string {\n  /**\n   * return the token field name in localStorage\n   */\n  return deploy ? \"token\" : \"token-dev\";\n}\n\nexport function setAppName(name: string): void {\n  /**\n   * set the app name in localStorage\n   */\n  name = name.trim() || \"CoAI\";\n  setMemory(\"app_name\", name);\n  appName = name;\n\n  updateDocumentTitle(name);\n}\n\nexport function setAppLogo(logo: string): void {\n  /**\n   * set the app logo in localStorage\n   */\n  logo = logo.trim() || \"/favicon.ico\";\n  setMemory(\"app_logo\", logo);\n  appLogo = logo;\n\n  updateFavicon(logo);\n}\n\nexport function setDocsUrl(url: string): void {\n  /**\n   * set the docs url in localStorage\n   */\n  url = url.trim() || \"https://coai.dev\";\n  setMemory(\"docs_url\", url);\n  docsEndpoint = url;\n}\n\nexport function setBlobEndpoint(endpoint: string): void {\n  /**\n   * set the blob endpoint in localStorage\n   */\n  endpoint = endpoint.trim() || \"https://blob.coai.dev\";\n  setMemory(\"blob_endpoint\", endpoint);\n  blobEndpoint = endpoint;\n}\n\nexport function setBuyLink(link: string): void {\n  /**\n   * set the buy link in localStorage\n   */\n  link = link.trim() || \"\";\n  setMemory(\"buy_link\", link);\n  buyLink = link;\n}\n\n"
  },
  {
    "path": "app/src/conf/model.ts",
    "content": "import { Model } from \"@/api/types.tsx\";\n\nexport function getModelFromId(market: Model[], id: string): Model | undefined {\n  return market.find((model) => model.id === id);\n}\n\nexport function isHighContextModel(market: Model[], id: string): boolean {\n  const model = getModelFromId(market, id);\n  return !!model && model.high_context;\n}\n"
  },
  {
    "path": "app/src/conf/storage.ts",
    "content": "import { getMemory, setMemory } from \"@/utils/memory.ts\";\nimport { Model, Plan } from \"@/api/types.tsx\";\n\nexport function savePreferenceModels(models: Model[]): void {\n  setMemory(\"model_preference\", models.map((item) => item.id).join(\",\"));\n}\n\nexport function getPreferenceModels(): string[] {\n  const memory = getMemory(\"model_preference\");\n  return memory.length ? memory.split(\",\") : [];\n}\n\nexport function loadPreferenceModels(models: Model[]): Model[] {\n  models = models.filter((item) => item.id.length > 0 && item.name.length > 0);\n\n  // // sort by preference\n  // const preference = getPreferenceModels();\n\n  // return models.sort((a, b) => {\n  //   const aIndex = preference.indexOf(a.id);\n  //   const bIndex = preference.indexOf(b.id);\n\n  //   if (aIndex === -1 && bIndex === -1) return 0;\n  //   if (aIndex === -1) return 1;\n  //   if (bIndex === -1) return -1;\n\n  //   return aIndex - bIndex;\n  // });\n\n  return models;\n}\n\nexport function setOfflineModels(models: Model[]): void {\n  setMemory(\"model_offline\", JSON.stringify(models));\n}\n\nexport function parseOfflineModels(models: string): Model[] {\n  try {\n    const parsed = JSON.parse(models);\n    if (!Array.isArray(parsed)) return [];\n    return parsed\n      .map((item): Model | null => {\n        if (!item || typeof item !== \"object\") {\n          return null;\n        }\n\n        if (!item.id || !item.name) {\n          return null;\n        }\n\n        return {\n          id: item.id || \"\",\n          name: item.name || \"\",\n          description: item.description || \"\",\n          free: item.free || false,\n          auth: item.auth || false,\n          default: item.default || false,\n          high_context: item.high_context || false,\n          avatar: item.avatar || \"\",\n          tag: item.tag || [],\n          price: item.price,\n        } as Model;\n      })\n      .filter((item): item is Model => item !== null);\n  } catch {\n    return [];\n  }\n}\n\nexport function getOfflineModels(): Model[] {\n  const memory = getMemory(\"model_offline\");\n  return memory && memory.length ? parseOfflineModels(memory) : [];\n}\n\nexport function setOfflinePlans(plans: Plan[]): void {\n  setMemory(\"plan_offline\", JSON.stringify(plans));\n}\n\nexport function parseOfflinePlans(plans: string): Plan[] {\n  try {\n    const parsed = JSON.parse(plans);\n    if (!Array.isArray(parsed)) return [];\n    return parsed.filter((item) => typeof item === \"object\");\n  } catch {\n    return [];\n  }\n}\n\nexport function getOfflinePlans(): Plan[] {\n  const memory = getMemory(\"plan_offline\");\n  return memory && memory.length ? parseOfflinePlans(memory) : [];\n}\n"
  },
  {
    "path": "app/src/conf/subscription.tsx",
    "content": "import { Plan, Plans } from \"@/api/types.tsx\";\n\nexport const subscriptionType: Record<number, string> = {\n  1: \"basic\",\n  2: \"standard\",\n  3: \"pro\",\n};\n\nexport function getPlan(data: Plans, level: number): Plan {\n  const raw = data.filter((item) => item.level === level);\n  return raw.length > 0 ? raw[0] : { level: 0, price: 0, items: [] };\n}\n\nexport function getPlanModels(data: Plans, level: number): string[] {\n  return getPlan(data, level).items.flatMap((item) => item.models);\n}\n\nexport function includingModelFromPlan(\n  data: Plans,\n  level: number,\n  model: string,\n): boolean {\n  return getPlanModels(data, level).includes(model);\n}\n\nexport function getPlanPrice(data: Plans, level: number): number {\n  return getPlan(data, level).price;\n}\n\nexport function getPlanName(level: number): string {\n  return subscriptionType[level] || \"none\";\n}\n"
  },
  {
    "path": "app/src/conf/version.json",
    "content": "{\n  \"version\": \"4.25.0\"\n}\n"
  },
  {
    "path": "app/src/dialogs/SettingsDialog.tsx",
    "content": "import \"@/assets/pages/settings.less\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport * as settings from \"@/store/settings.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog.tsx\";\nimport { Checkbox } from \"@/components/ui/checkbox.tsx\";\nimport { useEffect, useState } from \"react\";\nimport { getMemoryPerformance } from \"@/utils/app.ts\";\nimport { version } from \"@/conf/bootstrap.ts\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select.tsx\";\nimport { langsProps, setLanguage } from \"@/i18n.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Slider } from \"@/components/ui/slider.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport Github from \"@/components/ui/icons/Github.tsx\";\nimport { isTauri } from \"@/utils/desktop.ts\";\nimport { useDeeptrain } from \"@/conf/env.ts\";\nimport ThemeToggle from \"@/components/ThemeProvider.tsx\";\n\nfunction SettingsDialog() {\n  const { t, i18n } = useTranslation();\n  const dispatch = useDispatch();\n\n  const open = useSelector(settings.dialogSelector);\n\n  const align = useSelector(settings.alignSelector);\n  const hideToolbar = useSelector(settings.hideToolbarSelector);\n  const hideToolbarText = useSelector(settings.hideToolbarTextSelector);\n  const context = useSelector(settings.contextSelector);\n  const sender = useSelector(settings.senderSelector);\n  const history = useSelector(settings.historySelector);\n\n  const temperature = useSelector(settings.temperatureSelector);\n  const maxTokens = useSelector(settings.maxTokensSelector);\n  const topP = useSelector(settings.topPSelector);\n  const topK = useSelector(settings.topKSelector);\n  const presencePenalty = useSelector(settings.presencePenaltySelector);\n  const frequencyPenalty = useSelector(settings.frequencyPenaltySelector);\n  const repetitionPenalty = useSelector(settings.repetitionPenaltySelector);\n\n  const [memorySize, setMemorySize] = useState(getMemoryPerformance());\n\n  const desktop = isTauri();\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setMemorySize(getMemoryPerformance());\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(open) => dispatch(settings.setDialog(open))}\n    >\n      <DialogContent className={`flex-dialog settings-dialog`} couldFullScreen>\n        <DialogHeader>\n          <DialogTitle>{t(\"settings.title\")}</DialogTitle>\n          <DialogDescription asChild>\n            <div className={`settings-container`}>\n              <div className={`settings-wrapper`}>\n                <div className={`settings-segment`}>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.version\")}</div>\n                    <div className={`grow`} />\n                    <div className={`value`}>\n                      v{version}\n                      <Badge className={`ml-1`}>Community</Badge>\n                    </div>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.theme\")}</div>\n                    <div className={`grow`} />\n                    <div className={`value`}>\n                      <ThemeToggle />\n                    </div>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.language\")}</div>\n                    <div className={`grow`} />\n                    <div className={`value`}>\n                      <Select\n                        value={i18n.language}\n                        onValueChange={(value: string) =>\n                          setLanguage(i18n, value)\n                        }\n                      >\n                        <SelectTrigger className={`select`}>\n                          <SelectValue\n                            placeholder={langsProps[i18n.language]}\n                          />\n                        </SelectTrigger>\n                        <SelectContent>\n                          {Object.entries(langsProps).map(\n                            ([key, value], idx) => (\n                              <SelectItem key={idx} value={key}>\n                                {value}\n                              </SelectItem>\n                            ),\n                          )}\n                        </SelectContent>\n                      </Select>\n                    </div>\n                  </div>\n                </div>\n                <div className={`settings-segment`}>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.sender\")}</div>\n                    <div className={`grow`} />\n                    <div className={`value`}>\n                      <Select\n                        value={sender ? \"true\" : \"false\"}\n                        onValueChange={(value: string) =>\n                          dispatch(settings.setSender(value === \"true\"))\n                        }\n                      >\n                        <SelectTrigger className={`select`}>\n                          <SelectValue\n                            placeholder={settings.sendKeys[sender ? 1 : 0]}\n                          />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value={\"false\"}>\n                            {settings.sendKeys[0]}\n                          </SelectItem>\n                          <SelectItem value={\"true\"}>\n                            {settings.sendKeys[1]}\n                          </SelectItem>\n                        </SelectContent>\n                      </Select>\n                    </div>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.align\")}</div>\n                    <div className={`grow`} />\n                    <Checkbox\n                      className={`value`}\n                      checked={align}\n                      onCheckedChange={(state: boolean) => {\n                        dispatch(settings.setAlign(state));\n                      }}\n                    />\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.hide-toolbar\")}</div>\n                    <div className={`grow`} />\n                    <Checkbox\n                      className={`value`}\n                      checked={hideToolbar}\n                      onCheckedChange={(state: boolean) => {\n                        dispatch(settings.setHideToolbar(state));\n                      }}\n                    />\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.hide-toolbar-text\")}\n                    </div>\n                    <div className={`grow`} />\n                    <Checkbox\n                      className={`value`}\n                      checked={hideToolbarText}\n                      onCheckedChange={(state: boolean) => {\n                        dispatch(settings.setHideToolbarText(state));\n                      }}\n                    />\n                  </div>\n                </div>\n                <div className={`settings-segment`}>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.context\")}</div>\n                    <div className={`grow`} />\n                    <Checkbox\n                      className={`value`}\n                      checked={context}\n                      onCheckedChange={(state: boolean) => {\n                        dispatch(settings.setContext(state));\n                      }}\n                    />\n                  </div>\n                  {context && (\n                    <div className={`item`}>\n                      <div className={`name`}>{t(\"settings.history\")}</div>\n                      <div className={`grow`} />\n                      <NumberInput\n                        className={cn(\n                          `value`,\n                          history === 0 && `text-destructive`,\n                        )}\n                        value={history}\n                        acceptNaN={false}\n                        min={0}\n                        max={999}\n                        onValueChange={(value: number) => {\n                          dispatch(settings.setHistory(value));\n                        }}\n                      />\n                    </div>\n                  )}\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.max-tokens\")}\n                      <Tips content={t(\"settings.max-tokens-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <NumberInput\n                      className={`value large-value`}\n                      value={maxTokens}\n                      acceptNaN={false}\n                      min={1}\n                      max={100000}\n                      onValueChange={(value: number) => {\n                        dispatch(settings.setMaxTokens(value));\n                      }}\n                    />\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.temperature\")}\n                      <Tips content={t(\"settings.temperature-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[temperature * 10]}\n                      min={0}\n                      max={10}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setTemperature(value[0] / 10));\n                      }}\n                    />\n                    <p className={`slider-value`}>{temperature.toFixed(1)}</p>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.presence-penalty\")}\n                      <Tips content={t(\"settings.presence-penalty-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[presencePenalty * 10]}\n                      min={-20}\n                      max={20}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setPresencePenalty(value[0] / 10));\n                      }}\n                    />\n                    <p className={`slider-value`}>\n                      {presencePenalty.toFixed(1)}\n                    </p>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.frequency-penalty\")}\n                      <Tips content={t(\"settings.frequency-penalty-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[frequencyPenalty * 10]}\n                      min={-20}\n                      max={20}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setFrequencyPenalty(value[0] / 10));\n                      }}\n                    />\n                    <p className={`slider-value`}>\n                      {frequencyPenalty.toFixed(1)}\n                    </p>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.repetition-penalty\")}\n                      <Tips content={t(\"settings.repetition-penalty-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[repetitionPenalty * 10]}\n                      min={0}\n                      max={20}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setRepetitionPenalty(value[0] / 10));\n                      }}\n                    />\n                    <p className={`slider-value`}>\n                      {repetitionPenalty.toFixed(1)}\n                    </p>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.top-p\")}\n                      <Tips content={t(\"settings.top-p-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[topP * 10]}\n                      min={0}\n                      max={10}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setTopP(value[0] / 10));\n                      }}\n                    />\n                    <p className={`slider-value`}>{topP.toFixed(1)}</p>\n                  </div>\n                  <div className={`item`}>\n                    <div className={`name`}>\n                      {t(\"settings.top-k\")}\n                      <Tips content={t(\"settings.top-k-tip\")} />\n                    </div>\n                    <div className={`grow`} />\n                    <Slider\n                      value={[topK]}\n                      min={0}\n                      max={20}\n                      step={1}\n                      className={`value ml-2 max-w-[10rem] mr-2`}\n                      classNameThumb={`h-4 w-4`}\n                      onValueChange={(value: number[]) => {\n                        dispatch(settings.setTopK(value[0]));\n                      }}\n                    />\n                    <p className={`slider-value`}>{topK.toFixed()}</p>\n                  </div>\n                </div>\n                <div className={`settings-segment`}>\n                  <div className={`item`}>\n                    <div className={`name`}>{t(\"settings.reset-settings\")}</div>\n                    <div className={`grow`} />\n                    <AlertDialog>\n                      <AlertDialogTrigger asChild>\n                        <Button\n                          size={`sm`}\n                          variant={`destructive`}\n                          className={`set-action`}\n                        >\n                          {t(\"reset\")}\n                        </Button>\n                      </AlertDialogTrigger>\n                      <AlertDialogContent>\n                        <AlertDialogHeader>\n                          <AlertDialogTitle>\n                            {t(\"settings.reset-settings\")}\n                          </AlertDialogTitle>\n                          <AlertDialogDescription>\n                            {t(\"settings.reset-settings-description\")}\n                          </AlertDialogDescription>\n                          <AlertDialogFooter>\n                            <AlertDialogCancel>{t(\"cancel\")}</AlertDialogCancel>\n                            <AlertDialogAction\n                              onClick={() => {\n                                dispatch(settings.resetSettings());\n                              }}\n                            >\n                              {t(\"confirm\")}\n                            </AlertDialogAction>\n                          </AlertDialogFooter>\n                        </AlertDialogHeader>\n                      </AlertDialogContent>\n                    </AlertDialog>\n                  </div>\n                </div>\n              </div>\n              <div className={`grow`} />\n              <div className={`info-box`}>\n                <p>\n                  {t(\"settings.memory\")}\n                  &nbsp;\n                  {!isNaN(memorySize)\n                    ? memorySize.toFixed(2) + \" MB\"\n                    : t(\"unknown\")}\n                </p>\n                <a\n                  className={cn(\n                    \"flex flex-row items-center\",\n                    !useDeeptrain && \"hidden\",\n                  )}\n                  href={`https://github.com/coaidev/coai`}\n                >\n                  <Github className={`inline-block h-4 w-4 mr-1.5`} />\n                  CoAI v{version}\n                  {desktop && <Badge className={`ml-1`}>App</Badge>}\n                </a>\n              </div>\n            </div>\n          </DialogDescription>\n        </DialogHeader>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default SettingsDialog;\n"
  },
  {
    "path": "app/src/dialogs/index.tsx",
    "content": "import { Toaster } from \"@/components/ui/toaster.tsx\";\nimport SettingsDialog from \"@/dialogs/SettingsDialog.tsx\";\n\nfunction DialogManager() {\n  return (\n    <>\n      <Toaster />\n      <SettingsDialog />\n    </>\n  );\n}\n\nexport default DialogManager;\n"
  },
  {
    "path": "app/src/events/blob.ts",
    "content": "import { EventCommitter } from \"@/events/struct.ts\";\n\nexport const blobEvent = new EventCommitter<File | File[]>({\n  name: \"blob\",\n});\n"
  },
  {
    "path": "app/src/events/info.ts",
    "content": "import { EventCommitter } from \"@/events/struct.ts\";\nimport { SiteInfo } from \"@/admin/api/info.ts\";\n\nexport const infoEvent = new EventCommitter<SiteInfo>({\n  name: \"info\",\n});\n"
  },
  {
    "path": "app/src/events/model.ts",
    "content": "import { EventCommitter } from \"./struct.ts\";\n\nexport const modelEvent = new EventCommitter<string>({\n  name: \"model\",\n});\n"
  },
  {
    "path": "app/src/events/spinner.ts",
    "content": "import { EventCommitter } from \"@/events/struct.ts\";\n\nexport type SpinnerEvent = {\n  id: number;\n  type: boolean;\n};\n\nexport const openSpinnerType = true;\nexport const closeSpinnerType = false;\n\nexport const spinnerEvent = new EventCommitter<SpinnerEvent>({\n  name: \"spinner\",\n});\n"
  },
  {
    "path": "app/src/events/struct.ts",
    "content": "export type EventCommitterProps = {\n  name: string;\n  destroyedAfterTrigger?: boolean;\n};\n\nexport class EventCommitter<T> {\n  name: string;\n  trigger: ((data: T) => void) | undefined;\n  listeners: ((data: T) => void)[] = [];\n  destroyedAfterTrigger: boolean;\n\n  constructor({ name, destroyedAfterTrigger = false }: EventCommitterProps) {\n    this.name = name;\n    this.destroyedAfterTrigger = destroyedAfterTrigger;\n  }\n\n  protected setTrigger(trigger: (data: T) => void) {\n    this.trigger = trigger;\n  }\n\n  protected clearTrigger() {\n    this.trigger = undefined;\n  }\n\n  protected triggerEvent(data: T) {\n    this.trigger && this.trigger(data);\n    if (this.destroyedAfterTrigger) this.clearTrigger();\n\n    this.listeners.forEach((listener) => listener(data));\n  }\n\n  public emit(data: T) {\n    this.triggerEvent(data);\n  }\n\n  public bind(trigger: (data: T) => void) {\n    this.setTrigger(trigger);\n  }\n\n  public addEventListener(listener: (data: T) => void) {\n    this.listeners.push(listener);\n  }\n\n  public removeEventListener(listener: (data: T) => void) {\n    this.listeners = this.listeners.filter((item) => item !== listener);\n  }\n\n  public clearEventListener() {\n    this.listeners = [];\n  }\n}\n"
  },
  {
    "path": "app/src/events/theme.ts",
    "content": "import { EventCommitter } from \"@/events/struct.ts\";\n\nexport const themeEvent = new EventCommitter<string>({\n  name: \"theme\",\n});\n"
  },
  {
    "path": "app/src/i18n.ts",
    "content": "import i18n from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\nimport { getMemory, setMemory } from \"@/utils/memory.ts\";\nimport cn from \"@/resources/i18n/cn.json\";\nimport en from \"@/resources/i18n/en.json\";\nimport ru from \"@/resources/i18n/ru.json\";\nimport ja from \"@/resources/i18n/ja.json\";\nimport tw from \"@/resources/i18n/tw.json\";\n\n// the translations\n// (tip move them in a JSON file and import them,\n// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files)\n\nconst resources = {\n  cn: { translation: cn },\n  en: { translation: en },\n  ru: { translation: ru },\n  ja: { translation: ja },\n  tw: { translation: tw },\n};\n\nexport const langsProps: Record<string, string> = {\n  cn: \"简体中文\",\n  tw: \"正體中文\",\n  en: \"English\",\n  ru: \"Русский\",\n  ja: \"日本語\",\n};\n\nexport const supportedLanguages = Object.keys(resources);\nexport const defaultLanguage = \"cn\";\n\ni18n\n  .use(initReactI18next)\n  .init({\n    resources,\n    lng: getLanguage(),\n    fallbackLng: \"en\",\n    interpolation: {\n      escapeValue: false, // react already safes from xss\n    },\n  })\n  .then(() => console.debug(`[i18n] initialized (language: ${i18n.language})`));\n\nexport default i18n;\n\nexport function getLanguage(): string {\n  const storage = getMemory(\"language\");\n  if (storage && supportedLanguages.includes(storage)) {\n    return storage;\n  }\n  // get browser language\n  const lang = navigator.language.split(\"-\")[0];\n  if (supportedLanguages.includes(lang)) {\n    return lang;\n  }\n  return defaultLanguage;\n}\n\nexport function setLanguage(i18n: any, lang: string): void {\n  if (supportedLanguages.includes(lang)) {\n    i18n\n      .changeLanguage(lang)\n      .then(() =>\n        console.debug(`[i18n] language changed (language: ${i18n.language})`),\n      );\n    setMemory(\"language\", lang);\n    return;\n  }\n  console.warn(`[i18n] language ${lang} is not supported`);\n}\n"
  },
  {
    "path": "app/src/main.tsx",
    "content": "import ReactDOM from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./conf/bootstrap.ts\";\nimport \"./i18n.ts\";\nimport \"./assets/main.less\";\nimport \"./assets/globals.less\";\nimport \"./conf/bootstrap.ts\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(<App />);\n"
  },
  {
    "path": "app/src/masks/prompts.ts",
    "content": "import { Mask } from \"@/masks/types.ts\";\nimport axios from \"axios\";\n\nlet MASKS: Mask[] = [];\nconst getMasks = async (): Promise<Mask[]> => {\n  try {\n    const response = await axios.get(\"/v1/presets\");\n    return response.data.content as Mask[];\n  } catch (e) {\n    console.warn(\"[presets] failed to get info from server\", e);\n    return [];\n  }\n};\n\nexport const initializeMasks = async () => {\n  MASKS = await getMasks();\n  return MASKS;\n};\n\ninitializeMasks().then(() => {\n  console.log(\"[presets] initialized:\", MASKS);\n});\n\nexport { MASKS };"
  },
  {
    "path": "app/src/masks/types.ts",
    "content": "import { UserRole } from \"@/api/types.tsx\";\n\nexport type MaskMessage = {\n  role: string;\n  content: string;\n};\n\nexport type Mask = {\n  avatar: string;\n  name: string;\n  description?: string;\n  tags?: string[];\n  lang?: string;\n  builtin?: boolean;\n  context: MaskMessage[];\n};\n\nexport type CustomMask = Mask & {\n  id: number;\n};\n\nexport const initialCustomMask: CustomMask = {\n  id: -1,\n  avatar: \"1f9d0\",\n  name: \"\",\n  context: [{ role: UserRole, content: \"\" }],\n};\n"
  },
  {
    "path": "app/src/payment/icons.tsx",
    "content": "import React from \"react\";\nimport { Button, ButtonProps } from \"@/components/ui/button.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport { ClassValue } from \"clsx\";\n\ntype IconProps = React.SVGProps<SVGSVGElement>;\n\nexport function Alipay(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"M21.4224 15.3576C17.5929 14.2048 15.3667 13.5176 14.744 13.296C15.312 12.32 15.768 11.2 16.064 9.976H12.8V8.872H16.8V8.192H12.8V6.344H11.264C10.984 6.344 10.952 6.592 10.952 6.592V8.184H7.2V8.864H10.952V9.968H7.88V10.584H14.104C13.88 11.36 13.576 12.096 13.216 12.76C11.808 12.296 11.024 11.976 9.304 11.816C6.048 11.504 5.296 13.296 5.176 14.392C5 16.064 6.48 17.424 8.688 17.424C10.896 17.424 12.368 16.4 13.768 14.704C14.9346 15.2619 17.1059 16.2293 20.2819 17.6062C18.4835 20.2577 15.4452 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 13.1778 21.7964 14.3081 21.4224 15.3576ZM8.432 16.368C6.096 16.368 5.728 14.888 5.848 14.272C5.968 13.656 6.648 12.856 7.952 12.856C9.448 12.856 10.784 13.24 12.392 14.016C11.256 15.496 9.872 16.368 8.432 16.368Z\"></path>\n    </svg>\n  );\n}\n\nexport function Wechat(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"M9.27099 14.6689C8.9532 14.8312 8.56403 14.7122 8.39132 14.4L8.3477 14.3054L6.53019 10.3069C6.52269 10.2588 6.52269 10.2097 6.53019 10.1615C6.53017 10.0735 6.56564 9.98916 6.62857 9.9276C6.6915 9.86603 6.7766 9.83243 6.86462 9.83438C6.93567 9.83269 7.00508 9.85582 7.06091 9.89981L9.24191 11.4265C9.40329 11.5346 9.59293 11.5928 9.78716 11.5937C9.90424 11.5945 10.0203 11.5723 10.1289 11.5283L20.176 7.02816C18.091 4.72544 15.1103 3.43931 12.0045 3.5022C6.4793 3.5022 2.00098 7.23172 2.00098 11.87C2.06681 14.4052 3.35646 16.7515 5.4615 18.1658C5.6878 18.3326 5.78402 18.6241 5.70141 18.8928L5.25067 20.594C5.22336 20.6714 5.20625 20.7521 5.19978 20.8339C5.19777 20.9232 5.23236 21.0094 5.29552 21.0726C5.35868 21.1358 5.44491 21.1703 5.5342 21.1684C5.60098 21.1645 5.66583 21.1445 5.72322 21.1102L7.90423 19.8452C8.06383 19.7467 8.2474 19.6939 8.43494 19.6925C8.53352 19.6923 8.63157 19.707 8.72574 19.7361C9.78781 20.0363 10.8863 20.188 11.99 20.1869C17.5152 20.1869 22.001 16.4574 22.001 11.8554C22.0108 10.4834 21.6301 9.13687 20.903 7.97326L9.35096 14.6253L9.27099 14.6689Z\"></path>\n    </svg>\n  );\n}\n\nexport function Paypal(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"M20.0673 8.47768C20.5591 9.35823 20.6237 10.4924 20.3676 11.8053C19.627 15.6107 17.0916 16.9253 13.8536 16.9253H13.3536C12.9583 16.9253 12.6216 17.214 12.5596 17.6047L12.519 17.8253L11.8896 21.818L11.857 21.988C11.795 22.3787 11.4583 22.6667 11.063 22.6667H7.72031C7.42365 22.6667 7.19698 22.402 7.24298 22.1093L7.41807 21H8.9367L9.88603 14.9793H11.2716C15.9496 14.9793 19.0209 12.7768 20.0673 8.47768ZM17.1066 3.38784C17.8693 4.25635 18.0908 5.19891 17.8597 6.67324C17.8405 6.79594 17.82 6.91391 17.7973 7.03253C17.0621 10.8057 14.7087 12.4793 10.8417 12.4793H8.95703C8.32647 12.4793 7.78368 12.8928 7.60372 13.4811L7.58913 13.4788L6.65969 19.3733H3.12169C3.08991 19.3733 3.06598 19.3454 3.07097 19.3136L5.66905 2.80233C5.74174 2.34036 6.13984 2 6.6075 2H12.583C14.7658 2 16.2998 2.46869 17.1066 3.38784Z\"></path>\n    </svg>\n  );\n}\n\nexport function Afdian(props: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 160 160\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path\n        fillRule=\"evenodd\"\n        d=\"M134.614 98.3714C133.294 97.5334 131.909 97.1697 130.563 97.02C133.724 89.3002 135.736 79.1949 128.887 69.1574C118.406 53.7998 103.38 45.8198 84.2346 45.4382C78.7809 45.3312 72.3517 45.5844 65.5487 45.8554C57.6493 46.1692 47.1369 46.5793 39.9921 45.9873C41.4161 45.2136 42.9326 44.4719 44.2462 43.8336C49.2728 41.384 53.2314 39.4763 51.9214 36.0925C51.2343 34.117 49.1874 33.0794 45.8233 33.0045C38.7426 32.8441 23.4421 36.9447 20.6903 43.8586C19.1418 47.7524 18.8854 55.2689 34.5668 61.9119C41.0174 64.6503 59.237 67.9879 66.2678 68.6867C68.2542 68.8793 69.7743 69.2822 70.9277 69.7101C69.3151 70.7727 67.6597 71.8888 65.9972 73.0298C63.1102 71.3824 58.3897 69.4391 54.8654 71.846C53.502 72.7695 52.7259 74.1316 52.6903 75.6827C52.6405 77.6117 53.8081 79.498 55.1217 81.017C49.9314 85.1639 45.7343 89.1825 44.2462 92.2811C42.5873 96.0893 41.9109 102.322 45.008 108.402C48.9382 116.118 57.6279 121.499 70.8423 124.394C88.1114 128.17 103.027 124.768 112.895 119.566C118.388 116.671 122.286 113.215 124.18 110.131C124.768 110.317 125.355 110.506 125.96 110.695C126.804 110.951 127.648 111.208 128.438 111.49C131.051 112.395 133.942 112.274 136.167 111.151C136.206 111.133 136.248 111.108 136.291 111.087C137.968 110.202 139.175 108.783 139.705 107.072C141.129 102.458 137.064 99.9082 134.614 98.3714ZM64.9999 90.6681C63.4307 90.6681 62.1621 91.9382 62.1621 93.5091C62.1621 95.0836 63.4307 96.3537 64.9999 96.3537C66.5691 96.3537 67.8378 95.0836 67.8378 93.5091C67.8378 91.9382 66.5691 90.6681 64.9999 90.6681ZM91.7568 99.1965C90.1876 99.1965 88.9189 100.467 88.9189 102.038C88.9189 103.612 90.1876 104.882 91.7568 104.882C93.326 104.882 94.5946 103.612 94.5946 102.038C94.5946 100.467 93.326 99.1965 91.7568 99.1965Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function Stripe(props: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 640 512\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"M165 144.7l-43.3 9.2-.2 142.4c0 26.3 19.8 43.3 46.1 43.3 14.6 0 25.3-2.7 31.2-5.9v-33.8c-5.7 2.3-33.7 10.5-33.7-15.7V221h33.7v-37.8h-33.7zm89.1 51.6l-2.7-13.1H213v153.2h44.3V233.3c10.5-13.8 28.2-11.1 33.9-9.3v-40.8c-6-2.1-26.7-6-37.1 13.1zm92.3-72.3l-44.6 9.5v36.2l44.6-9.5zM44.9 228.3c0-6.9 5.8-9.6 15.1-9.7 13.5 0 30.7 4.1 44.2 11.4v-41.8c-14.7-5.8-29.4-8.1-44.1-8.1-36 0-60 18.8-60 50.2 0 49.2 67.5 41.2 67.5 62.4 0 8.2-7.1 10.9-17 10.9-14.7 0-33.7-6.1-48.6-14.2v40c16.5 7.1 33.2 10.1 48.5 10.1 36.9 0 62.3-15.8 62.3-47.8 0-52.9-67.9-43.4-67.9-63.4zM640 261.6c0-45.5-22-81.4-64.2-81.4s-67.9 35.9-67.9 81.1c0 53.5 30.3 78.2 73.5 78.2 21.2 0 37.1-4.8 49.2-11.5v-33.4c-12.1 6.1-26 9.8-43.6 9.8-17.3 0-32.5-6.1-34.5-26.9h86.9c.2-2.3.6-11.6.6-15.9zm-87.9-16.8c0-20 12.3-28.4 23.4-28.4 10.9 0 22.5 8.4 22.5 28.4zm-112.9-64.6c-17.4 0-28.6 8.2-34.8 13.9l-2.3-11H363v204.8l44.4-9.4.1-50.2c6.4 4.7 15.9 11.2 31.4 11.2 31.8 0 60.8-23.2 60.8-79.6.1-51.6-29.3-79.7-60.5-79.7zm-10.6 122.5c-10.4 0-16.6-3.8-20.9-8.4l-.3-66c4.6-5.1 11-8.8 21.2-8.8 16.2 0 27.4 18.2 27.4 41.4.1 23.9-10.9 41.8-27.4 41.8zm-126.7 33.7h44.6V183.2h-44.6z\" />\n    </svg>\n  );\n}\n\nexport function QQ(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      {...props}\n    >\n      <path d=\"M19.9139 14.529C19.7336 13.955 19.4877 13.2856 19.2385 12.643L18.3288 10.3969C18.3295 10.371 18.3408 9.92858 18.3408 9.70053C18.3408 5.8599 16.5082 2.00037 12.0009 2C7.49403 2.00037 5.66113 5.8599 5.66113 9.70053C5.66113 9.92858 5.67237 10.371 5.67312 10.3969L4.76379 12.643C4.51453 13.2856 4.26827 13.955 4.08798 14.529C3.2285 17.2657 3.507 18.3982 3.71915 18.4238C4.17419 18.4779 5.49021 16.3635 5.49021 16.3635C5.49021 17.5879 6.12741 19.1858 7.5064 20.3398C6.99064 20.4971 6.35868 20.7388 5.95237 21.0355C5.58729 21.3025 5.63302 21.5743 5.69861 21.6841C5.9876 22.1661 10.6542 21.9918 12.0017 21.8417C13.3488 21.9918 18.0158 22.1661 18.3044 21.6841C18.3704 21.5743 18.4157 21.3025 18.0507 21.0355C17.6443 20.7388 17.012 20.4971 16.4959 20.3395C17.8745 19.1858 18.5117 17.5879 18.5117 16.3635C18.5117 16.3635 19.8281 18.4779 20.2831 18.4238C20.4949 18.3982 20.7734 17.2657 19.9139 14.529Z\"></path>\n    </svg>\n  );\n}\n\nexport const PaymentIcons: Record<string, React.ComponentType<IconProps>> = {\n  alipay: Alipay,\n  wxpay: Wechat,\n  qqpay: QQ,\n  paypal: Paypal,\n  stripe: Stripe,\n  afdian: Afdian,\n  \"xunhupay-wechat\": Wechat,\n  \"xunhupay-alipay\": Alipay,\n};\n\nexport const PaymentColorClasses: Record<string, ClassValue> = {\n  alipay: \"text-alipay-foreground bg-alipay hover:bg-alipay/90\",\n  wechatpay: \"text-wechatpay-foreground bg-wechatpay hover:bg-wechatpay/90\",\n  qqpay: \"text-qqpay-foreground bg-qqpay hover:bg-qqpay/90\",\n  paypal: \"text-paypal-foreground bg-paypal hover:bg-paypal/90\",\n  stripe: \"text-stripe-foreground bg-stripe hover:bg-stripe/90\",\n  afdian: \"text-afdian-foreground bg-afdian hover:bg-afdian/90\",\n  \"xunhupay-wechat\": \"text-black bg-white hover:bg-white/90\",\n  \"xunhupay-alipay\": \"text-alipay-foreground bg-alipay hover:bg-alipay/90\",\n};\n\nexport const PaymentIconTypes = Object.keys(PaymentIcons);\n\nexport function PaymentIcon({ type, ...props }: { type: string } & IconProps) {\n  const Icon = PaymentIcons[type] || Alipay;\n  return <Icon {...props} />;\n}\n\nexport function PaymentButton({\n  method,\n  className,\n  ...props\n}: ButtonProps & { method: string }) {\n  const { t } = useTranslation();\n  return (\n    <Button\n      {...props}\n      className={cn(\n        \"flex items-center border border-input\",\n        className,\n        PaymentColorClasses[method],\n      )}\n    >\n      <PaymentIcon\n        type={method}\n        className=\"h-5 w-5 mr-1 translate-y-[0.5px] loading-hidden\"\n      />\n      <p>{t(`payment.${method}`)}</p>\n    </Button>\n  );\n}\n"
  },
  {
    "path": "app/src/payment/request.ts",
    "content": "import { CommonResponse, withNotify } from \"@/api/common.ts\";\nimport axios from \"axios\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { getDeviceType, getDomain } from \"@/payment/utils.ts\";\nimport { appName } from \"@/conf/env.ts\";\nimport { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport type PaymentResponse = CommonResponse & {\n  data?: {\n    url: string;\n    params: Record<string, string>;\n  };\n};\n\nexport type PaymentStatusResponse = CommonResponse & {\n  order_state: boolean;\n  remaining_time?: number;\n};\n\nexport type PaymentOrder = {\n  user_id: number;\n  type: string;\n  service: string;\n  amount: number;\n  order_id: string;\n  name: string;\n  device: string;\n  state: boolean;\n  username: string;\n  created_at: string;\n  updated_at: string;\n};\n\nexport type PaymentListResponse = CommonResponse & {\n  data: PaymentOrder[];\n  total: number;\n};\n\nexport type RecheckOrderResponse = CommonResponse & {\n  order_state?: boolean;\n  is_changed?: boolean;\n};\n\nexport async function createPaymentOrder(\n  type: string,\n  quota: number,\n  name: string,\n): Promise<PaymentResponse> {\n  try {\n    const response = await axios.post<PaymentResponse>(\"/payment/create\", {\n      type,\n      quota,\n      domain: getDomain(),\n      name: `${appName} - ${name}`,\n      device: getDeviceType(),\n    });\n    return response.data;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e) };\n  }\n}\n\nexport async function getPaymentOrderStatus(\n  order: string,\n): Promise<PaymentStatusResponse> {\n  try {\n    const response = await axios.get<PaymentStatusResponse>(\n      `/payment/check/${order}`,\n    );\n    return response.data;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), order_state: false };\n  }\n}\n\nexport function usePaymentState(order: string): boolean {\n  const { t } = useTranslation();\n  const [state, setState] = useState(false);\n\n  useEffect(() => {\n    const interval = setInterval(async () => {\n      const response = await getPaymentOrderStatus(order);\n      withNotify(t, response);\n      if (response.status && response.order_state) {\n        setState(true);\n        clearInterval(interval);\n      }\n    }, 2000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return state;\n}\n\nexport async function getPaymentOrders(\n  page: number,\n  search: string,\n): Promise<PaymentListResponse> {\n  try {\n    const response = await axios.get<PaymentListResponse>(\n      \"/admin/payment/view\",\n      {\n        params: { page, search },\n      },\n    );\n    return response.data;\n  } catch (e) {\n    return { status: false, error: getErrorMessage(e), data: [], total: 0 };\n  }\n}\n\nexport async function recheckOrderStatus(\n  order: string,\n  service: string,\n): Promise<RecheckOrderResponse> {\n  try {\n    const response = await axios.get<RecheckOrderResponse>(\n      \"/admin/payment/recheck\",\n      {\n        params: { order, service },\n      },\n    );\n    return response.data;\n  } catch (e) {\n    return {\n      status: false,\n      error: getErrorMessage(e),\n      order_state: false,\n      is_changed: false,\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/payment/utils.ts",
    "content": "import { isMobile } from \"@/utils/device.ts\";\n\nexport function getDomain() {\n  // get the param `return_url` in epay\n  // get the domain from the window.location.origin property, e.g. https://example.com\n  return window.location.origin;\n}\n\nexport function getDeviceType() {\n  const ua = navigator.userAgent;\n  if (ua.match(/MicroMessenger/i) || ua.match(/Wechat/i)) {\n    return \"wechat\";\n  }\n\n  if (ua.match(/Alipay/i) || ua.match(/AliApp/i) || ua.match(/AlipayClient/i)) {\n    return \"alipay\";\n  }\n\n  if (ua.match(/QQ/i) || ua.match(/QQBrowser/i)) {\n    return \"qq\";\n  }\n\n  return isMobile() ? \"mobile\" : \"pc\";\n}\n"
  },
  {
    "path": "app/src/plugin/types.ts",
    "content": "export type PluginEditorState = {\n  id?: number;\n  avatar: string;\n  name: string;\n  description: string;\n  server_url: string;\n};\n\nexport type TestResult = {\n  status: 'idle' | 'testing' | 'success' | 'error';\n  tools?: Array<{\n    name: string;\n    description: string;\n    inputSchema: Record<string, unknown>;\n  }>;\n  error?: string;\n};\n\nexport type PluginEditorAction = \n  | { type: \"update-avatar\"; payload: string }\n  | { type: \"update-name\"; payload: string }\n  | { type: \"update-description\"; payload: string }\n  | { type: \"update-server-url\"; payload: string }\n  | { type: \"reset\" }\n  | { type: \"set-plugin\"; payload: PluginEditorState }\n  | { type: \"import-plugin\"; payload: PluginEditorState };\n\nexport const initialPluginState: PluginEditorState = {\n  avatar: \"1f9e9\",\n  name: \"\",\n  description: \"\",\n  server_url: \"\",\n};\n\nexport function pluginEditorReducer(\n  state: PluginEditorState, \n  action: PluginEditorAction\n): PluginEditorState {\n  switch (action.type) {\n    case \"update-avatar\":\n      return { ...state, avatar: action.payload };\n    case \"update-name\":\n      return { ...state, name: action.payload };\n    case \"update-description\":\n      return { ...state, description: action.payload };\n    case \"update-server-url\":\n      return { ...state, server_url: action.payload };\n    case \"reset\":\n      return { ...initialPluginState };\n    case \"set-plugin\":\n      return { ...action.payload };\n    case \"import-plugin\":\n      return {\n        ...action.payload,\n        id: -1,\n      };\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "app/src/resources/i18n/cn.json",
    "content": "{\n  \"end\": \"\",\n  \"add\": \"添加\",\n  \"not-found\": \"页面未找到\",\n  \"send\": \"发送\",\n  \"stop\": \"停止\",\n  \"home\": \"首页\",\n  \"new-chat\": \"新建对话\",\n  \"login\": \"登录\",\n  \"not-login\": \"未登录\",\n  \"login-action\": \"登录以享受更多功能\",\n  \"register\": \"注册\",\n  \"reset\": \"重置\",\n  \"anonymous\": \"未登录\",\n  \"login-require\": \"您需要登录才能使用此功能\",\n  \"coming-soon\": \"此功能开发中, 敬请期待!\",\n  \"logout\": \"登出\",\n  \"quota\": \"点数\",\n  \"starred\": \"收藏模型\",\n  \"unstarred\": \"常用模型\",\n  \"download\": \"下载\",\n  \"offline\": \"应用离线\",\n  \"loading\": \"加载中...\",\n  \"try-again\": \"重试\",\n  \"manage\": \"管理\",\n  \"invalid-token\": \"无效的令牌\",\n  \"invalid-token-prompt\": \"请重试。\",\n  \"login-failed\": \"登录失败\",\n  \"login-failed-prompt\": \"登录失败！原因: {{reason}}\",\n  \"login-success\": \"登录成功\",\n  \"login-success-prompt\": \"您已成功登录。\",\n  \"server-error\": \"登录错误\",\n  \"server-error-prompt\": \"登录出错，请重试。\",\n  \"error\": \"请求失败\",\n  \"request-failed\": \"请求失败，请检查您的网络并重试。\",\n  \"success\": \"请求成功\",\n  \"request-success\": \"您的操作已成功执行。\",\n  \"enter\": \"换行\",\n  \"assistant-suggest\": \"预设推荐\",\n  \"change-suggest\": \"换一组\",\n  \"new-announcement\": \"公告通知\",\n  \"no-announcement\": \"暂无公告\",\n  \"none\": \"无\",\n  \"readed\": \"已读\",\n  \"description\": \"描述\",\n  \"only-one-step\": \"还差一步\",\n  \"are-you-sure\": \"是否确认？\",\n  \"this-action-cannot-be-undone\": \"此操作无法撤消。\",\n  \"verify-email-description\": \"请输入您的邮箱以完成验证\",\n  \"filter\": {\n    \"filter\": \"筛选\",\n    \"conds\": \"已筛选 {{count}} 项条件\",\n    \"plan\": \"是否订阅\",\n    \"all\": \"全部\",\n    \"subscribed\": \"已订阅\",\n    \"unsubscribed\": \"未订阅\",\n    \"admin\": \"管理员\",\n    \"not-admin\": \"非管理员\",\n    \"ban\": \"是否封禁\",\n    \"banned\": \"已封禁\",\n    \"not-banned\": \"未封禁\",\n    \"sorts\": {\n      \"sort\": \"排序方式\",\n      \"id-desc\": \"ID 降序\",\n      \"id-asc\": \"ID 升序\",\n      \"quota-desc\": \"点数 降序\",\n      \"quota-asc\": \"点数 升序\",\n      \"used-quota-desc\": \"已用点数 降序\",\n      \"used-quota-asc\": \"已用点数 升序\",\n      \"plan-desc\": \"订阅到期时间 降序\",\n      \"plan-asc\": \"订阅到期时间 升序\"\n    }\n  },\n  \"learn-more\": \"了解更多\",\n  \"close\": \"关闭\",\n  \"connect\": \"绑定\",\n  \"disconnect\": \"解绑\",\n  \"edit\": \"编辑\",\n  \"editor\": \"编辑\",\n  \"pricing\": \"更多计费详情参见模型定价表\",\n  \"true\": \"是\",\n  \"false\": \"否\",\n  \"unknown\": \"未知\",\n  \"update\": \"更新\",\n  \"scroll-down\": \"滚至最新\",\n  \"broadcast\": \"公告\",\n  \"fatal\": \"应用崩溃\",\n  \"download-fatal-log\": \"下载错误日志\",\n  \"fatal-tips\": \"请您先检查您的网络，浏览器兼容性，尝试清除浏览器缓存并刷新页面。如果问题仍然存在，请下载日志并提供完整复现步骤以给开发者以便我们排查问题。\",\n  \"request-error\": \"请求失败，原因：{{reason}}\",\n  \"delete\": \"删除\",\n  \"remove\": \"移除\",\n  \"upward\": \"上移\",\n  \"downward\": \"下移\",\n  \"save\": \"保存\",\n  \"submit\": \"提交\",\n  \"announcement\": \"站点公告\",\n  \"notify\": \"通知\",\n  \"new-notify\": \"新通知\",\n  \"view-all\": \"查看全部\",\n  \"i-know\": \"我已知晓\",\n  \"empty\": \"空空如也\",\n  \"exit\": \"离开\",\n  \"model\": \"模型\",\n  \"min-quota\": \"最低余额\",\n  \"your-quota\": \"您的余额\",\n  \"title\": \"标题\",\n  \"my-account\": \"我的账户\",\n  \"back-home\": \"返回首页\",\n  \"tip\": \"温馨提示\",\n  \"get\": \"获取\",\n  \"authenticating\": \"认证中\",\n  \"authentication-failed\": \"认证失败\",\n  \"authenticating-prompt\": \"稍等片刻，我们正在认证您的账户...\",\n  \"oops-quota-exceeded\": \"糟糕，余额不足\",\n  \"oops-quota-exceeded-tip\": \"您的余额不足，请前往购买点数或订阅计划以继续\",\n  \"auth\": {\n    \"username\": \"用户名\",\n    \"username-placeholder\": \"请输入用户名\",\n    \"password\": \"密码\",\n    \"password-placeholder\": \"请输入密码\",\n    \"check-password\": \"确认密码\",\n    \"check-password-placeholder\": \"请再次输入密码\",\n    \"email\": \"邮箱\",\n    \"email-placeholder\": \"请输入邮箱\",\n    \"username-or-email\": \"用户名或邮箱\",\n    \"username-or-email-placeholder\": \"请输入用户名或邮箱\",\n    \"code\": \"验证码\",\n    \"code-placeholder\": \"请输入验证码\",\n    \"code-disabled-placeholder\": \"无需进行邮箱验证\",\n    \"wechat\": \"微信\",\n    \"send-code\": \"发送\",\n    \"incorrect-info\": \"填错信息？\",\n    \"fall-back\": \"回退一步\",\n    \"forgot-password\": \"忘记密码？\",\n    \"reset-password\": \"重置密码\",\n    \"no-account\": \"没有账号？\",\n    \"register\": \"注册一个\",\n    \"have-account\": \"已有账号？\",\n    \"login\": \"现在登录\",\n    \"next-step\": \"下一步\",\n    \"verify\": \"验证\",\n    \"length-range\": \"应为 {{min}} ~ {{max}} 位\",\n    \"same-rule\": \"两次输入不一致\",\n    \"invalid-email\": \"邮箱格式错误\",\n    \"reset-success\": \"重置成功\",\n    \"reset-success-prompt\": \"您的密码已重置，请使用新密码登录。\",\n    \"send-code-success\": \"发送成功\",\n    \"send-code-success-prompt\": \"验证码已发送至您的邮箱，请注意查收。\",\n    \"send-code-failed\": \"发送失败\",\n    \"send-code-failed-prompt\": \"验证码发送失败，原因：{{reason}}\",\n    \"register-success\": \"注册成功\",\n    \"register-success-prompt\": \"您已成功注册，欢迎你的到来！\",\n    \"disabled-mail\": \"当前站点的邮箱已被禁用，请联系管理员开启发件功能。\",\n    \"connected\": \"绑定成功\",\n    \"connected-prompt\": \"您已成功绑定账号！\",\n    \"providers\": {\n      \"baidu\": \"百度\",\n      \"huawei\": \"华为\",\n      \"weibo\": \"微博\",\n      \"sina\": \"微博\",\n      \"wx\": \"微信\",\n      \"qq\": \"QQ\",\n      \"xiaomi\": \"小米\",\n      \"douyin\": \"抖音\",\n      \"dingtalk\": \"钉钉\",\n      \"alipay\": \"支付宝\",\n      \"microsoft\": \"微软\"\n    }\n  },\n  \"tag\": {\n    \"free\": \"免费\",\n    \"official\": \"官方\",\n    \"unstable\": \"不稳定\",\n    \"web\": \"联网\",\n    \"high-quality\": \"高质量\",\n    \"high-context\": \"高上下文\",\n    \"high-price\": \"高定价\",\n    \"open-source\": \"开源\",\n    \"image-generation\": \"绘图\",\n    \"multi-modal\": \"多模态\",\n    \"fast\": \"快速\",\n    \"english-model\": \"英文模型\",\n    \"badges\": {\n      \"non-billing\": \"免费\",\n      \"times-billing\": \"{{price}} / 次\",\n      \"token-billing\": \"输入 {{input}} / 1k tokens 输出 {{output}} / 1k tokens\",\n      \"add\": \"添加工作台\",\n      \"remove\": \"移除工作台\",\n      \"plan-included\": \"订阅包含\",\n      \"plan-included-tip\": \"您的订阅已包含此模型，将优先使用订阅内额度\"\n    }\n  },\n  \"market\": {\n    \"title\": \"模型市场\",\n    \"list\": \"模型列表\",\n    \"model\": \"探索更多模型\",\n    \"go\": \"前往模型市场\",\n    \"explore\": \"探索模型\",\n    \"search\": \"搜索模型名称或简介...\",\n    \"model-api\": \"API 请求的模型 ID 名称\",\n    \"show-pricing\": \"显示价格\",\n    \"show-1m-pricing\": \"1M TOKENS\",\n    \"switch-model\": \"切换模型\",\n    \"switch-model-desc\": \"已切换至模型\",\n    \"switch-bookmark\": \"工作台\",\n    \"remove-bookmark\": \"已将模型从菜单栏移除\",\n    \"add-bookmark\": \"已将模型添加至菜单栏\"\n  },\n  \"conversation\": {\n    \"title\": \"对话\",\n    \"search\": \"搜索对话...\",\n    \"empty\": \"空空如也\",\n    \"empty-anonymous\": \"您当前处于匿名模式，对话将不会被保存。\",\n    \"refresh-failed\": \"刷新失败\",\n    \"refresh-failed-prompt\": \"请求出错，请重试。\",\n    \"remove-title\": \"是否确定？\",\n    \"remove-description\": \"此操作无法撤消。这将永久删除对话 \",\n    \"remove-all-title\": \"清除历史\",\n    \"remove-all-description\": \"此操作无法撤消。这将永久删除所有对话，是否继续？\",\n    \"cancel\": \"取消\",\n    \"delete\": \"删除\",\n    \"edit-title\": \"编辑标题\",\n    \"delete-conversation\": \"删除对话\",\n    \"delete-success\": \"对话已删除\",\n    \"delete-success-prompt\": \"对话已删除。\",\n    \"delete-failed\": \"删除失败\",\n    \"delete-failed-prompt\": \"删除对话失败，请检查您的网络并重试。\"\n  },\n  \"chat\": {\n    \"web\": \"联网\",\n    \"web-search\": \"联网搜索\",\n    \"web-page-summary\": \"逐页总结\",\n    \"web-depth\": \"搜索深度\",\n    \"web-quick-search\": \"快速搜索\",\n    \"web-detailed-search\": \"详细搜索\",\n    \"web-aria\": \"切换网络搜索功能\",\n    \"web-enable-toast\": \"已开启联网搜索\",\n    \"web-enable-tip\": \"联网搜索可能会消耗更多 Token\",\n    \"web-disable-toast\": \"已关闭联网搜索\",\n    \"web-enable-page-summary-toast\": \"已开启逐页总结\",\n    \"web-enable-page-summary-tip\": \"逐页总结可能会消耗更多 Token 并使输出变慢\",\n    \"web-disable-page-summary-toast\": \"已关闭逐页总结\",\n    \"web-search-quick-toast\": \"已将搜索深度切换至快速搜索\",\n    \"web-search-detailed-toast\": \"已将搜索深度切换至详细搜索\",\n    \"web-search-results\": \"已搜索到 {{count}} 条结果\",\n    \"web-search-results-hide\": \"收起搜索结果\",\n    \"web-search-results-query\": \"搜索关键词\",\n    \"web-search-results-visit-source\": \"访问源网站\",\n    \"web-search-no-results\": \"暂无搜索结果\",\n    \"deep-thinking\": \"深度思考\",\n    \"deep-thinking-enable-toast\": \"已开启深度思考\",\n    \"deep-thinking-enable-tip\": \"深度思考可能会导致输出较慢\",\n    \"deep-thinking-disable-toast\": \"已关闭深度思考\",\n    \"model-not-support-thinking-desc\": \"当前模型不支持开启深度思考\",\n    \"plugin\": \"插件\",\n    \"voice\": \"语音识别\",\n    \"placeholder\": \"输入聊天内容...\",\n    \"placeholder-raw\": \"写点什么...\",\n    \"recall\": \"历史复原\",\n    \"recall-desc\": \"检测到您上次有未发送的消息，已经为您恢复。\",\n    \"recall-cancel\": \"取消\",\n    \"send-message\": \"发送消息\",\n    \"send-message-desc\": \"是否确认发送此消息？\",\n    \"empty-preview\": \"输入的内容将会被渲染在此处 (支持 Markdown 语法) ~\",\n    \"actions\": {\n      \"upscale\": \"放大\",\n      \"subtle-upscale\": \"细微放大\",\n      \"creative-upscale\": \"创意放大\",\n      \"subtle-vary\": \"细微变化\",\n      \"strong-vary\": \"强烈变化\",\n      \"region-vary\": \"局部重绘\",\n      \"zoom\": \"缩放\",\n      \"zoom-1.5x\": \"缩放 1.5 倍\",\n      \"zoom-2x\": \"缩放 2 倍\",\n      \"zoom-custom\": \"自定义缩放\",\n      \"variant\": \"变化\",\n      \"reroll\": \"重绘\",\n      \"pan-left\": \"向左移动\",\n      \"pan-right\": \"向右移动\",\n      \"pan-up\": \"向上移动\",\n      \"pan-down\": \"向下移动\",\n      \"bookmark\": \"点赞\"\n    }\n  },\n  \"plugin\": {\n    \"title\": \"插件管理\",\n    \"add\": \"添加插件\",\n    \"create\": \"创建插件\",\n    \"save\": \"保存插件\",\n    \"cancel\": \"取消\",\n    \"enable\": \"启用\",\n    \"disable\": \"禁用\",\n    \"enabled\": \"已启用 {{name}}\",\n    \"disabled\": \"已禁用 {{name}}\",\n    \"enabled-badge\": \"已启用\",\n    \"import\": \"导入配置\",\n    \"quick-import\": \"快速导入 MCP 配置\",\n    \"import-json-config\": \"导入 JSON 配置\",\n    \"import-http-only-tip\": \"当前版本仅支持 HTTP 类型的 MCP 服务器\",\n    \"import-confirm\": \"确认导入\",\n    \"name\": \"插件名称\",\n    \"name-placeholder\": \"请输入插件名称\",\n    \"avatar\": \"插件头像\",\n    \"avatar-placeholder\": \"请输入头像链接\",\n    \"description\": \"插件描述\",\n    \"description-placeholder\": \"请输入插件描述\",\n    \"server-url\": \"服务器地址\",\n    \"server-url-placeholder\": \"请输入 MCP 服务器地址\",\n    \"server-url-required\": \"请输入服务器地址\",\n    \"loading\": \"加载中...\",\n    \"no-plugins\": \"暂无插件\",\n    \"save-success\": \"保存成功\",\n    \"save-error\": \"保存失败\",\n    \"delete-success\": \"删除成功\",\n    \"delete-error\": \"删除失败\",\n    \"load-error\": \"加载失败\",\n    \"refresh\": \"刷新\",\n    \"refresh-success\": \"刷新成功\",\n    \"test\": \"测试连接\",\n    \"testing\": \"连接中...\",\n    \"test-success\": \"连接成功\",\n    \"test-success-desc\": \"发现 {{count}} 个可用工具\",\n    \"test-error\": \"连接失败\",\n    \"test-required\": \"需要测试\",\n    \"test-description\": \"点击测试按钮验证插件连接\",\n    \"available-tools\": \"可用工具\",\n    \"connection-test\": \"连接测试\",\n    \"test-required-error\": \"创建新插件前需要先测试连接\",\n    \"test-required-hint\": \"新插件需要先测试连接才能创建\",\n    \"form-error\": \"请填写必填字段\",\n    \"import-success\": \"导入成功\",\n    \"import-error\": {\n      \"empty\": \"请输入配置内容\",\n      \"invalid-json\": \"无效的JSON格式\",\n      \"invalid-format\": \"配置格式不正确，请检查是否包含mcpServers字段\",\n      \"no-servers\": \"配置中未找到MCP服务器\",\n      \"stdio-not-supported\": \"当前版本仅支持HTTP类型的MCP服务器，不支持STDIO类型\",\n      \"unknown\": \"导入失败，请检查配置格式\"\n    },\n    \"mcp\": {\n      \"tool-call\": \"工具调用\",\n      \"tool-calling\": \"正在调用工具: {{name}}\",\n      \"tool-executing\": \"正在执行工具: {{name}}\",\n      \"tool-success\": \"工具执行成功: {{name}}\",\n      \"tool-error\": \"工具执行失败: {{name}}\",\n      \"arguments\": \"工具参数\",\n      \"result\": \"执行结果\",\n      \"error\": \"执行错误\",\n      \"status\": \"执行状态\",\n      \"status-start\": \"准备执行工具...\",\n      \"status-executing\": \"正在执行工具...\",\n      \"hide-details\": \"隐藏工具调用详情\",\n      \"show-details\": \"显示工具调用详情\",\n      \"plugin-name\": \"MCP插件\",\n      \"copy-param-value\": \"复制参数值\",\n      \"save\": \"保存\",\n      \"edit\": \"编辑\",\n      \"raw-arguments\": \"原始参数 (JSON)\",\n      \"no-arguments\": \"无参数\",\n      \"parsed-result\": \"解析后的结果\",\n      \"error-info\": \"错误信息\",\n      \"status-prepare\": \"准备调用工具...\",\n      \"status-success\": \"工具执行成功\",\n      \"status-error\": \"工具执行失败\",\n      \"status-calling\": \"工具调用中...\",\n      \"hide-debug\": \"隐藏调试信息\",\n      \"show-debug\": \"显示调试信息\",\n      \"tool-arguments\": \"工具参数\",\n      \"no-arguments-needed\": \"该工具无需参数\"\n    }\n  },\n  \"message\": {\n    \"copy\": \"复制消息\",\n    \"save\": \"保存为文件\",\n    \"save-image\": \"保存图片\",\n    \"use\": \"使用消息\",\n    \"edit\": \"编辑消息\",\n    \"stop\": \"停止回答\",\n    \"remove\": \"删除消息\",\n    \"restart\": \"重新回答\",\n    \"copy-area\": \"复制选中区域\",\n    \"thinking-process\": \"思考过程\",\n    \"saving-image-prompt\": \"图片生成中\",\n    \"saving-image-prompt-desc\": \"正在生成图片中，请稍等...\",\n    \"saving-image-failed\": \"图片生成失败\",\n    \"saving-image-failed-prompt\": \"图片生成失败，原因：{{reason}}\",\n    \"saving-image-success\": \"图片生成成功\",\n    \"saving-image-success-prompt\": \"图片已成功保存。\",\n    \"sharing\": {\n      \"title\": \"标题\",\n      \"time\": \"时间\",\n      \"message\": \"消息\"\n    }\n  },\n  \"quota-description\": \"消息的点数支出\",\n  \"buy\": {\n    \"not-config-link\": \"后台未配置购买链接\",\n    \"choose\": \"选择一个金额\",\n    \"title\": \"我的点数\",\n    \"other\": \"其他\",\n    \"other-desc\": \"多少点数？\",\n    \"buy\": \"购买 {{amount}} 点数\",\n    \"dalle\": \"DALL·E AI 绘图\",\n    \"dalle-free\": \"DALL·E 2 绘图永久免费\",\n    \"flex\": \"灵活计费\",\n    \"input\": \"输入\",\n    \"output\": \"输出\",\n    \"learn-more\": \"了解更多\",\n    \"buy-link\": \"前往购买\",\n    \"dialog-title\": \"购买点数\",\n    \"buy-description\": \"请选择您要购买的点数\",\n    \"dialog-desc\": \"您确定要购买 {{amount}} 点数吗？\",\n    \"dialog-cancel\": \"取消\",\n    \"dialog-buy\": \"购买\",\n    \"success\": \"购买成功\",\n    \"success-prompt\": \"您已成功购买 {{amount}} 点数。\",\n    \"redeem\": \"兑换\",\n    \"redeem-title\": \"领取兑换码\",\n    \"redeem-description\": \"请输入您的兑换码领取点数\",\n    \"redeem-placeholder\": \"请输入兑换码\",\n    \"deeptrain-tip\": \"提示：在 Deeptrain 充值至钱包后，请返回此处，点击购买相应点数\",\n    \"exchange-success\": \"兑换成功\",\n    \"exchange-success-prompt\": \"您已成功兑换 {{amount}} 点数。\",\n    \"failed\": \"购买失败\",\n    \"failed-prompt\": \"购买点数失败，请确保您有足够的余额。\",\n    \"exchange-failed\": \"兑换失败\",\n    \"exchange-failed-prompt\": \"兑换失败，原因：{{reason}}\",\n    \"gpt4-tip\": \"提示：web 联网版功能可能会带来更多的输入点数消耗\",\n    \"quota-info\": \"点数可以使用本站全部模型, 随用随付, 适合弹性计费选择\",\n    \"plan-info\": \"订阅可以按周期以固定价格使用常用模型, 适合固定长期使用选择\",\n    \"deeptrain-step-1\": \"选择点数并点击购买\",\n    \"deeptrain-step-2\": \"跳转 Deeptrain 钱包充值\",\n    \"deeptrain-step-3\": \"充值成功后返回此处再次购买\",\n    \"deeptrain-step-4\": \"(如果钱包已有足够余额 购买后会自动充值)\",\n    \"go\": \"前往\"\n  },\n  \"pkg\": {\n    \"title\": \"礼包\",\n    \"manage\": \"我的礼包\",\n    \"go\": \"前往实名认证\",\n    \"cert\": \"实名认证礼包\",\n    \"cert-desc\": \"实名认证后可获得 50 点数 （价值 5 元）\",\n    \"teen\": \"学生福利\",\n    \"teen-desc\": \"实名认证后未成年人（18 周岁及以下）可额外获得 150 点数 （价值 15 元）\",\n    \"close\": \"关闭\",\n    \"state\": {\n      \"true\": \"已领取\",\n      \"false\": \"无法领取\"\n    }\n  },\n  \"sub\": {\n    \"title\": \"订阅\",\n    \"disable\": \"本站订阅功能已被关闭\",\n    \"quota-link\": \"寻求弹性计费？购买点数\",\n    \"plan-not-support-relay\": \"站点订阅配额不涵盖中转 API , 中转 API 请使用弹性计费点数\",\n    \"subscription-link\": \"寻求固定计费？订阅计划\",\n    \"dialog-title\": \"订阅计划\",\n    \"month\": \"月\",\n    \"year\": \"年\",\n    \"new\": \"新计划\",\n    \"month-plan\": \"月度计划\",\n    \"year-plan\": \"年度计划\",\n    \"best-choice\": \"最佳选择\",\n    \"including-model\": \"涵盖模型\",\n    \"including-model-tip\": \"包含在本订阅中的可用模型使用额度\",\n    \"select-duration\": \"选择订阅时长\",\n    \"price-summary\": \"价格汇总\",\n    \"total-price\": \"总价\",\n    \"free\": \"免费版\",\n    \"free-price\": \"永久免费\",\n    \"basic\": \"基础版\",\n    \"standard\": \"标准版\",\n    \"pro\": \"专业版\",\n    \"none\": \"未订阅\",\n    \"plan-price\": \"{{money}} 元/月\",\n    \"include-tax\": \"含税\",\n    \"plan-usage\": \"{{name}} 每月使用 {{times}} 次\",\n    \"plan-unlimited-usage\": \"{{name}} 无限次使用\",\n    \"plan-item-usage\": \"{{times}}次\",\n    \"plan-item-unlimited-usage\": \"无限\",\n    \"year-earn-tip\": \"年度计划省 {{percent}}\",\n    \"plan-tip\": \"可调用模型\",\n    \"enterprise\": \"企业版\",\n    \"enterprise-service\": \"优先技术支持\",\n    \"enterprise-sla\": \"SLA 保障\",\n    \"enterprise-speed\": \"TPM 速率提升\",\n    \"enterprise-security\": \"SOC-2 标准数据安全保障\",\n    \"enterprise-data\": \"异地数据容灾备份\",\n    \"enterprise-deploy\": \"支持私有化部署\",\n    \"contact-sale\": \"联系销售\",\n    \"current\": \"当前计划\",\n    \"subscribe\": \"订阅\",\n    \"upgrade\": \"升级\",\n    \"downgrade\": \"降级\",\n    \"renew\": \"续费\",\n    \"cannot-select\": \"无法选择\",\n    \"select-time\": \"选择订阅时间\",\n    \"migrate-plan\": \"变更订阅计划\",\n    \"migrate-plan-desc\": \"变更订阅后，您的订阅时间将会根据剩余天数价格计算，重新计算订阅时间。（如降级会时间翻倍，升级会补齐差价）\",\n    \"price\": \"价格 {{price}} 元\",\n    \"price-tax\": \"含税 {{price}} 元\",\n    \"upgrade-price\": \"升级费用 {{price}} 元 (仅供参考)\",\n    \"upgrade-price-label\": \"升级费用\",\n    \"upgrade-price-notice\": \"仅供参考\",\n    \"upgrade-price-notice-tip\": \"升级费用仅供参考，实际价格以服务器精准计算为准\",\n    \"expired\": \"订阅剩余天数\",\n    \"quota-manage\": \"订阅配额\",\n    \"expired-days\": \"您的订阅将于 {{days}} 天后到期\",\n    \"refresh-days\": \"您的配额将于 {{refresh_days}} 天后刷新\",\n    \"get-refresh-days\": \"开始使用配额以获取刷新日期\",\n    \"time\": {\n      \"1\": \"1个月\",\n      \"3\": \"3个月\",\n      \"6\": \"半年\",\n      \"12\": \"1年\",\n      \"36\": \"3年\"\n    },\n    \"success\": \"订阅成功\",\n    \"success-prompt\": \"您已成功订阅 {{month}} 月订阅。\",\n    \"migrate-success\": \"变更成功\",\n    \"migrate-success-prompt\": \"您已成功变更订阅计划。\",\n    \"failed\": \"订阅失败\",\n    \"failed-prompt\": \"订阅失败，请确保您有足够的余额。\",\n    \"failed-quota-prompt\": \"订阅失败，您的余额不足 ({{quota}} 点数)\",\n    \"migrate-failed\": \"变更失败\",\n    \"sub-migrate-failed-prompt\": \"您的订阅变更失败，原因：{{reason}}\"\n  },\n  \"cancel\": \"取消\",\n  \"confirm\": \"确认\",\n  \"percent\": \"{{cent}}折\",\n  \"file\": {\n    \"file\": \"文件\",\n    \"upload\": \"上传\",\n    \"type\": \"支持 pdf, docx, pptx, xlsx, 图像, 文本等格式\",\n    \"drop\": \"拖拽文件到此处或点击上传\",\n    \"parse-error\": \"解析失败\",\n    \"parse-error-prompt\": \"文件解析失败：{{reason}}\",\n    \"parse-success-prompt\": \"文件解析成功: {{file}}\",\n    \"max-length\": \"内容过长\",\n    \"max-length-prompt\": \"由于上下文长度限制，内容已被截取\",\n    \"over-size\": \"文件过大\",\n    \"over-size-prompt\": \"单个附件大小不能超过 {{size}} MB\",\n    \"uploading\": \"文件上传中...\",\n    \"uploading-prompt\": \"正在上传文件中，请耐心等待\",\n    \"large-file\": \"大文件解析\",\n    \"large-file-prompt\": \"正在上传并解析大文件中，请耐心等待\",\n    \"large-file-success\": \"解析成功\",\n    \"large-file-success-prompt\": \"大文件解析成功，共耗时 {{time}} 秒\",\n    \"number\": \"{{number}} 个文件\",\n    \"zipper\": \"{{filename}} 和其他 {{number}} 个文件\",\n    \"empty-file\": \"无内容文件\",\n    \"empty-file-prompt\": \"文件内容为空，已自动忽略\"\n  },\n  \"generate\": {\n    \"title\": \"AI 项目生成器\",\n    \"input-placeholder\": \"生成一个python小游戏\",\n    \"failed\": \"生成失败\",\n    \"reason\": \"原因：\",\n    \"success\": \"生成成功\",\n    \"success-prompt\": \"成功生成项目！请选择下载格式。\",\n    \"empty\": \"生成中...\",\n    \"download\": \"下载 {{name}} 格式\"\n  },\n  \"api\": {\n    \"title\": \"API 设置\",\n    \"copied\": \"复制成功\",\n    \"copied-description\": \"API 密钥已复制到剪贴板\",\n    \"learn-more\": \"了解更多\",\n    \"reset\": \"重置密钥\",\n    \"reset-description\": \"是否确定？此操作无法撤消。这将永久重置 API 密钥，已有 API 密钥将会失效。\"\n  },\n  \"service\": {\n    \"title\": \"发现新版本\",\n    \"version\": \"版本\",\n    \"description\": \"发现新版本，是否立即更新？\",\n    \"update\": \"更新\",\n    \"offline-title\": \"离线模式\",\n    \"offline\": \"应用当前处于离线状态。\",\n    \"update-success\": \"更新成功\",\n    \"update-success-prompt\": \"您已更新至最新版本。\"\n  },\n  \"share\": {\n    \"title\": \"分享\",\n    \"share-conversation\": \"分享对话\",\n    \"description\": \"将此对话与他人分享：\",\n    \"copy-link\": \"复制链接\",\n    \"view\": \"查看\",\n    \"success\": \"分享成功\",\n    \"failed\": \"分享失败\",\n    \"copied\": \"复制成功\",\n    \"copied-description\": \"链接已复制到剪贴板\",\n    \"not-found\": \"对话未找到\",\n    \"not-found-description\": \"对话未找到，请检查链接是否正确或对话是否已被删除\",\n    \"manage\": \"分享管理\",\n    \"sync-error\": \"同步失败\",\n    \"name\": \"对话标题\",\n    \"time\": \"时间\",\n    \"action\": \"操作\",\n    \"empty\": \"还没有分享记录，赶快分享吧！\",\n    \"share-tip\": \"前往对话栏，点击分享按钮分享对话\"\n  },\n  \"record\": {\n    \"user\": \"用户\",\n    \"channel\": \"渠道\",\n    \"query\": \"查询记录\",\n    \"title\": \"使用记录\",\n    \"created-at\": \"时间\",\n    \"type\": \"类型\",\n    \"model\": \"模型\",\n    \"token\": \"令牌\",\n    \"input-tokens\": \"输入\",\n    \"output-tokens\": \"输出\",\n    \"quota\": \"点数\",\n    \"duration\": \"用时\",\n    \"detail\": \"备注\",\n    \"rpm-tips\": \"当前 RPM (每分钟请求数)\",\n    \"tpm-tips\": \"当前 TPM (每分钟 Token 数)\",\n    \"types\": {\n      \"system\": \"系统\",\n      \"consume\": \"消费\",\n      \"topup\": \"充值\",\n      \"all\": \"全部\"\n    },\n    \"detail-info\": {\n      \"input\": \"输入价格\",\n      \"output\": \"输出价格\",\n      \"percent\": \"分组倍率\",\n      \"times\": \"按次价格\",\n      \"no-cost\": \"不计费\",\n      \"cached\": \"击中缓存\",\n      \"plan\": \"订阅计费\",\n      \"empty\": \"无响应\",\n      \"error\": \"请求错误\"\n    },\n    \"billing-today\": \"今日消费\",\n    \"billing-month\": \"本月消费\",\n    \"request-today\": \"今日请求\",\n    \"request-month\": \"本月请求\",\n    \"cond\": {\n      \"model\": \"指定模型\",\n      \"type\": \"指定类型\",\n      \"model-placeholder\": \"请输入模型名\",\n      \"token-name\": \"指定令牌\",\n      \"token-name-placeholder\": \"请输入令牌名\",\n      \"start_time\": \"开始时间\",\n      \"end_time\": \"结束时间\",\n      \"username\": \"指定用户名\",\n      \"username-placeholder\": \"请输入用户名\"\n    }\n  },\n  \"docs\": {\n    \"title\": \"开放文档\"\n  },\n  \"invitation\": {\n    \"title\": \"兑换码\",\n    \"invitation\": \"礼品码\",\n    \"input-placeholder\": \"请输入礼品码\",\n    \"cancel\": \"取消\",\n    \"check\": \"验证\",\n    \"check-success\": \"兑换成功\",\n    \"check-success-description\": \"兑换成功！您已获得 {{amount}} 点数，开始您的 AI 之旅吧！\",\n    \"check-failed\": \"兑换失败\"\n  },\n  \"contact\": {\n    \"title\": \"联系我们\",\n    \"community\": \"加入社区\"\n  },\n  \"settings\": {\n    \"title\": \"设置\",\n    \"description\": \"设置\",\n    \"version\": \"版本\",\n    \"theme\": \"主题\",\n    \"light\": \"亮色\",\n    \"dark\": \"暗色\",\n    \"language\": \"语言\",\n    \"sender\": \"发送键\",\n    \"context\": \"保留上下文\",\n    \"history\": \"最大历史会话数\",\n    \"align\": \"聊天框居中\",\n    \"hide-model\": \"隐藏模型选择区\",\n    \"hide-toolbar\": \"默认隐藏工具栏\",\n    \"hide-toolbar-text\": \"隐藏工具栏文字\",\n    \"memory\": \"内存占用\",\n    \"max-tokens\": \"最大回复 Token 数\",\n    \"max-tokens-tip\": \"最大回复 Token 数，超过此数值将会被截断（过高的数值可能会导致超过模型的最大 Token 导致请求失败）\",\n    \"temperature\": \"温度\",\n    \"temperature-tip\": \"随机采样的比例，高温度会产生更多的随机性，低温度会产生较集中和确定性的文本\",\n    \"top-p\": \"核采样概率阈值\",\n    \"top-p-tip\": \"(TopP) 概率取值越大，生成的随机性越高；取值越低，生成的确定性越高\",\n    \"top-k\": \"采样候选集大小\",\n    \"top-k-tip\": \"(TopK) 候选集大小，越大生成的随机性越高，越小生成的确定性越高\",\n    \"presence-penalty\": \"存在惩罚\",\n    \"presence-penalty-tip\": \"(PresencePenalty) 存在惩罚，控制模型生成的新话题的可能性，提高此值可以增加谈论新话题的可能性\",\n    \"frequency-penalty\": \"频率惩罚\",\n    \"frequency-penalty-tip\": \"(FrequencyPenalty) 频率惩罚，控制模型生成字词的重复程度，提高此值可以降低重复字词出现频率的可能性\",\n    \"repetition-penalty\": \"重复惩罚\",\n    \"repetition-penalty-tip\": \"(RepetitionPenalty) 控制模型生成的重复程度，提高此值可以减少重复，但是可能会导致模型生成不连贯的文本（与 FrequencyPenalty 相似）\",\n    \"reset-settings\": \"重置全部设置\",\n    \"reset-settings-description\": \"是否确定？此操作无法撤消。这将永久重置全部设置。\"\n  },\n  \"article\": {\n    \"title\": \"批量生成文章\",\n    \"input-placeholder\": \"请输入文章标题（一行一个）\",\n    \"prompt-placeholder\": \"请输入预设（帮助 AI 生成文章，如：学术论文格式，800 字）\",\n    \"web-checkbox\": \"是否开启联网搜索功能\",\n    \"generate\": \"生成\",\n    \"progress-title\": \"生成中 （总共 {{total}} 篇， {{current}} 篇已生成）\",\n    \"generate-success\": \"生成成功\",\n    \"generate-success-prompt\": \"文章生成成功！请选择下载格式。\",\n    \"generate-failed\": \"生成失败\",\n    \"generate-failed-prompt\": \"文章生成失败，请检查您的网络并重试。\",\n    \"download-format\": \"下载 {{name}} 格式\"\n  },\n  \"admin\": {\n    \"dashboard\": \"数据分析\",\n    \"users\": \"后台管理\",\n    \"user\": \"用户管理\",\n    \"broadcast\": \"通知管理\",\n    \"channel\": \"渠道设置\",\n    \"settings\": \"系统设置\",\n    \"notifications\": \"推送中心\",\n    \"prize\": \"价格设定\",\n    \"payment\": \"支付订单\",\n    \"subscription\": \"订阅管理\",\n    \"exit\": \"退出后台\",\n    \"billing\": \"收入\",\n    \"billing-today\": \"今日入账\",\n    \"billing-month\": \"本月入账\",\n    \"subscription-users\": \"订阅用户\",\n    \"online-chats\": \"在线对话数\",\n    \"seat\": \"位\",\n    \"view\": \"查看\",\n    \"model-chart\": \"模型使用统计\",\n    \"model-chart-tip\": \"Token 用量\",\n    \"model-usage-chart\": \"模型使用占比\",\n    \"user-type-chart\": \"用户类型占比\",\n    \"user-type-chart-tip\": \"其他付费用户：指订阅过期用户或点数超过当前初始点数的用户（使用礼品码等操作也会被算作点数增加的变更）\",\n    \"user-type-chart-info\": \"总共 {{total}} 用户\",\n    \"request-chart\": \"请求量统计\",\n    \"billing-chart\": \"收入统计\",\n    \"error-chart\": \"错误统计\",\n    \"requests\": \"请求量\",\n    \"times\": \"异常次数\",\n    \"empty\": \"无数据\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"确认\",\n    \"invitation\": \"兑换码管理\",\n    \"code\": \"兑换码\",\n    \"invitation-code\": \"礼品码\",\n    \"invitation-manage\": \"礼品码管理\",\n    \"invitation-tips\": \"礼品码用于兑换点数，每一类礼品码一个用户只能使用一次（可作宣传使用）\",\n    \"redeem-tips\": \"兑换码用于兑换点数，可用于支付发卡等\",\n    \"quota\": \"点数\",\n    \"type\": \"类型\",\n    \"used\": \"状态\",\n    \"number\": \"数量\",\n    \"username\": \"用户名\",\n    \"email\": \"邮箱\",\n    \"month\": \"月数\",\n    \"poster\": \"发布者\",\n    \"post-at\": \"发布时间\",\n    \"broadcast-content\": \"公告内容\",\n    \"create-broadcast\": \"发布公告\",\n    \"broadcast-placeholder\": \"请输入通知内容 (支持 Markdown / HTML)\",\n    \"notify-all\": \"通知全体用户\",\n    \"broadcast-tip\": \"通知仅会显示最新一条，并且只会通知一次。系统设置中可设置站点公告，首次接收将会弹窗显示于首页并支持后续查看。\",\n    \"post\": \"发布\",\n    \"post-success\": \"发布成功\",\n    \"post-success-prompt\": \"公告发布成功。\",\n    \"post-failed\": \"发布失败\",\n    \"post-failed-prompt\": \"发布失败，原因：{{reason}}\",\n    \"level\": \"用户等级\",\n    \"is-admin\": \"管理员\",\n    \"is-banned\": \"封禁\",\n    \"used-quota\": \"已用点数\",\n    \"is-subscribed\": \"是否订阅\",\n    \"is-subscribed-tips\": \"是否订阅评判逻辑: 有订阅等级且订阅时间未过期\",\n    \"total-month\": \"总计订阅月数\",\n    \"expired-at\": \"订阅到期时间\",\n    \"enterprise\": \"企业版\",\n    \"action\": \"操作\",\n    \"search-username\": \"搜索用户名\",\n    \"password-action\": \"修改密码\",\n    \"password-action-desc\": \"请输入用户的新密码\",\n    \"set-admin-action\": \"设为管理员\",\n    \"set-admin-action-desc\": \"确定将该用户设为管理员？\",\n    \"cancel-admin-action\": \"取消管理员\",\n    \"cancel-admin-action-desc\": \"确定取消该用户的管理员权限？\",\n    \"ban-action\": \"封禁用户\",\n    \"ban-action-desc\": \"确定将该用户封禁？\",\n    \"unban-action\": \"解封用户\",\n    \"unban-action-desc\": \"确定将该用户解封？\",\n    \"email-action\": \"修改邮箱\",\n    \"email-action-desc\": \"请输入用户的新邮箱\",\n    \"group\": \"分组\",\n    \"group-setting\": \"分组设置\",\n    \"custom-group\": \"自定义分组\",\n    \"custom-group-action\": \"设置自定义分组\",\n    \"custom-group-action-desc\": \"请输入自定义分组名称\",\n    \"quota-action\": \"点数变更\",\n    \"quota-action-desc\": \"请输入点数变更值（正数为增加，负数为减少）\",\n    \"quota-set-action\": \"点数设置\",\n    \"quota-set-action-desc\": \"设置用户的点数\",\n    \"subscription-action\": \"订阅时间管理\",\n    \"subscription-action-desc\": \"请设置用户 {{username}} 的订阅到期时间\",\n    \"release-subscription-action\": \"释放订阅用量\",\n    \"release-subscription-action-desc\": \"是否释放用户的订阅用量？\",\n    \"subscription-level\": \"设置订阅等级\",\n    \"subscription-level-desc\": \"设置用户的订阅等级\",\n    \"operate-success\": \"操作成功\",\n    \"operate-success-prompt\": \"您的操作已成功执行。\",\n    \"operate-failed\": \"操作失败\",\n    \"operate-failed-prompt\": \"操作失败，原因：{{reason}}\",\n    \"created-at\": \"创建时间\",\n    \"updated-at\": \"更新时间\",\n    \"used-at\": \"领取时间\",\n    \"used-username\": \"领取用户\",\n    \"used-true\": \"已使用\",\n    \"used-false\": \"未使用\",\n    \"generate\": \"批量生成\",\n    \"generate-result\": \"生成结果\",\n    \"error\": \"请求失败\",\n    \"default-password\": \"密码修改提示\",\n    \"default-password-prompt\": \"您的管理员密码为默认密码，为了您的账号安全，请尽快修改密码。（前往后台管理 - 系统设置 - 修改 Root 密码）\",\n    \"coai-format-only\": \"此格式为 CoAI 独有格式\",\n    \"identity\": {\n      \"normal\": \"普通用户\",\n      \"api_paid\": \"其他付费用户\",\n      \"basic_plan\": \"基础版订阅用户\",\n      \"standard_plan\": \"标准版订阅用户\",\n      \"pro_plan\": \"专业版订阅用户\"\n    },\n    \"delete-broadcast\": \"删除通知\",\n    \"delete-broadcast-desc\": \"是否确定？此操作无法撤消。这将永久删除通知。\",\n    \"pay\": {\n      \"epay\": \"易支付\",\n      \"wechatpay\": \"微信支付\",\n      \"stripe\": \"Stripe\",\n      \"afdian\": \"爱发电\",\n      \"order\": \"订单号\",\n      \"amount\": \"金额\",\n      \"status\": \"支付状态\",\n      \"service\": \"支付渠道\",\n      \"type\": \"支付类型\",\n      \"device\": \"设备\",\n      \"username\": \"用户名\",\n      \"status-true\": \"已支付\",\n      \"status-false\": \"未支付\",\n      \"created-at\": \"创建时间\",\n      \"updated-at\": \"更新时间\",\n      \"action\": \"操作\",\n      \"copy-order\": \"复制订单号\",\n      \"check-order\": \"检查订单状态\",\n      \"check-result-same\": \"订单状态一致\",\n      \"check-result-diff\": \"订单状态更新\",\n      \"check-result-same-prompt\": \"订单状态一致，无需更新\",\n      \"check-result-diff-prompt\": \"订单状态已更新，已完成支付\",\n      \"search\": \"搜索订单号或用户名\"\n    },\n    \"market\": {\n      \"title\": \"模型市场\",\n      \"model-name\": \"模型名称\",\n      \"not-use\": \"部分模型未使用\",\n      \"import-all\": \"导入全部\",\n      \"new-model\": \"新建模型\",\n      \"model-name-placeholder\": \"请输入模型昵称 （如：GPT-4）\",\n      \"model-id\": \"模型 ID\",\n      \"model-id-placeholder\": \"请输入模型 ID （如：gpt-4-0613）\",\n      \"model-description\": \"模型简介\",\n      \"model-description-placeholder\": \"请输入模型简介\",\n      \"model-context\": \"高上下文\",\n      \"model-context-tip\": \"模型是否为高上下文模型（高上下文模型文件解析时不会被长内容截断）\",\n      \"function-calling\": \"函数调用\",\n      \"function-calling-tip\": \"模型是否支持 Function Calling 函数调用 (一些模型和逆向工程不支持函数调用)\",\n      \"vision-model\": \"识图模型\",\n      \"vision-model-tip\": \"模型是否为识图模型（识图模型支持图片输入，如 GPT-4 Turbo）\",\n      \"thinking-model\": \"思考模型\",\n      \"thinking-model-tip\": \"模型是否支持深度思考（深度思考模型会在输出内容时一并输出思考链，如 Claude 3.7 Sonnet）\",\n      \"ocr-model\": \"OCR 辅助\",\n      \"ocr-model-tip\": \"如果模型本身不支持图片输入，可以开启 OCR 文字识别以在一定程度上使模型具有视觉能力作为补充 (提示：文件解析服务必须支持 OCR 服务)\",\n      \"reverse-model\": \"逆向模型\",\n      \"reverse-model-tip\": \"如果逆向工程的模型通过 URL 支持全文件 (如 PDF, WORD) 解析，可以开启此选项，所有类型的文件解析将由上游提供，减少 Token 消耗。默认不开启则由本项目解析，适用于大部分模型。请保证文件解析服务已配置外部 URL 存储方案 (如 S3 / R2 / MinIO 等), 且模型上游支持外部 URL 解析文件。\",\n      \"model-is-default\": \"默认模型\",\n      \"model-is-default-tip\": \"模型是否添加至默认模型列表（未添加至默认模型列表的模型默认不会出现在首页模型列表中）\",\n      \"model-tag\": \"模型标签\",\n      \"model-image\": \"模型图片\",\n      \"custom-image\": \"自定义图片\",\n      \"custom-image-placeholder\": \"请输入自定义图片 URL 链接 （如：https://example.com/image.jpg）\",\n      \"update\": \"更新\",\n      \"migrate\": \"提交\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步选项\",\n      \"sync-site\": \"上游地址\",\n      \"sync-tip\": \"同步上游模型市场\",\n      \"sync-placeholder\": \"请输入上游 CoAI 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-failed\": \"同步失败\",\n      \"sync-failed-prompt\": \"地址无法请求或者模型市场模型为空\\n（端点：{{endpoint}}）\",\n      \"sync-success\": \"同步成功\",\n      \"sync-success-prompt\": \"已从上游同步，添加 {{length}} 个模型，请检查后点击提交方可生效，否则将不会保存\",\n      \"sync-items\": \"共发现 {{length}} 个模型，已有模型 {{exist}} 个（不会覆盖），新增模型 {{new}} 个（同步全部），本站点渠道已支持模型 {{support}} 个（同步已支持模型）\",\n      \"sync-all\": \"同步全部 ({{length}} 个)\",\n      \"sync-self\": \"同步已支持模型 ({{length}} 个)\",\n      \"update-success\": \"更新成功\",\n      \"update-success-prompt\": \"模型市场已成功更新（刷新浏览器即可立即应用）\",\n      \"update-failed\": \"更新失败\",\n      \"update-failed-prompt\": \"更新请求失败，原因：{{reason}}\"\n    },\n    \"redeem\": {\n      \"quota\": \"点数\",\n      \"used\": \"已用个数\",\n      \"total\": \"总个数\",\n      \"code\": \"兑换码\"\n    },\n    \"plan\": {\n      \"enable\": \"启用订阅\",\n      \"price\": \"价格\",\n      \"price-tip\": \"一月订阅价格 (单位：元)\",\n      \"item-id\": \"ID\",\n      \"item-id-placeholder\": \"请输入实体 ID (Item ID 不能多次使用，如：gpt-4)\",\n      \"item-name\": \"名称\",\n      \"item-name-placeholder\": \"请输入实体名称 (Item Name 用于显示在订阅列表中的实体名，如：GPT-4)\",\n      \"item-value\": \"配额\",\n      \"item-value-tip\": \"每月配额 (单位：次)\",\n      \"item-icon\": \"图标\",\n      \"item-icon-tip\": \"实体图标 (Item Icon 用于显示在订阅列表中的图标)\",\n      \"item-models\": \"模型\",\n      \"item-models-tip\": \"实体涵盖的模型 (Item Models 用于显示在订阅列表中的模型)\",\n      \"item-models-search-placeholder\": \"搜索模型 ID\",\n      \"item-models-placeholder\": \"已选 {{length}} 个模型\",\n      \"add-item\": \"添加\",\n      \"import-item\": \"导入\",\n      \"discounts\": \"折扣设置\",\n      \"discounts-tip\": \"折扣设置 (不启用即为默认设置，半年期订阅默认 90% 一年期订阅 80%)\",\n      \"discount-value\": \"折扣值\",\n      \"discount-off\": \"折扣\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步选项\",\n      \"sync-site\": \"上游地址\",\n      \"sync-placeholder\": \"请输入上游 CoAI 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-result\": \"发现上游订阅规则数 {{length}} 个，涵盖模型 {{models}} 个, 是否覆盖本站点订阅规则？\"\n    },\n    \"channels\": {\n      \"loading\": \"正在加载...\",\n      \"id\": \"渠道 ID\",\n      \"name\": \"名称\",\n      \"name-tip\": \"渠道名称，用于标识渠道\",\n      \"name-placeholder\": \"请输入渠道名称\",\n      \"search-channel\": \"搜索渠道名, 模型, 密钥...\",\n      \"type\": \"类型\",\n      \"priority\": \"优先级\",\n      \"priority-tip\": \"多渠道时，根据优先级顺序请求，越大优先级越高\",\n      \"weight\": \"权重\",\n      \"weight-tip\": \"同优先级时，根据权重比例进行均衡负载调用\",\n      \"retry\": \"最大重试次数\",\n      \"retry-name\": \"重试\",\n      \"retry-tip\": \"当渠道请求失败时，最多重试的次数\",\n      \"model\": \"模型\",\n      \"secret\": \"密钥\",\n      \"secret-number\": \"密钥数\",\n      \"secret-placeholder\": \"请输入密钥，格式：{{format}} (<>不用填)\\n多个密钥时，一行一个，请求时随机选取负载\",\n      \"endpoint\": \"接入点\",\n      \"endpoint-placeholder\": \"请输入接入点（即代理）\",\n      \"mapper\": \"模型映射\",\n      \"mapper-tip\": \"模型名转换，实现非对称的模型请求\",\n      \"mapper-placeholder\": \"请输入模型映射，一行一个，格式： model>model\\n前者为请求的模型，后者为映射的模型（需要在模型中存在），中间用 > 分隔\\n格式前加!表示原模型不包含在此渠道的可用范围内，如： !gpt-4-slow>gpt-4，那么 gpt-4 将不会被涵盖在此渠道的可请求模型中\",\n      \"group\": \"用户分组\",\n      \"advanced\": \"高级设置\",\n      \"group-tip\": \"用户分组，未包含的分组将不包含在此渠道的可用范围内 （分组为空时，所有用户都可以使用此渠道）\",\n      \"state\": \"状态\",\n      \"action\": \"操作\",\n      \"edit\": \"编辑渠道\",\n      \"enable\": \"启用渠道\",\n      \"disable\": \"禁用渠道\",\n      \"delete\": \"删除渠道\",\n      \"create\": \"创建渠道\",\n      \"new\": \"新建渠道\",\n      \"import\": \"导入已有渠道\",\n      \"joint\": \"对接上游\",\n      \"joint-endpoint\": \"上游地址\",\n      \"joint-endpoint-placeholder\": \"请输入上游 CoAI 的 API 地址，如：https://api.chatnio.net\",\n      \"upstream-endpoint-placeholder\": \"请输入上游 OpenAI 地址，如：https://api.openai.com\",\n      \"sync-secret-placeholder\": \"请输入上游渠道的 API 密钥\",\n      \"joint-secret\": \"API 秘钥\",\n      \"joint-secret-placeholder\": \"请输入上游 CoAI 的 API 秘钥\",\n      \"sync-failed\": \"同步失败\",\n      \"sync-failed-prompt\": \"地址无法请求或者模型市场模型为空\\n（端点：{{endpoint}}）\",\n      \"sync-success\": \"同步成功\",\n      \"sync-success-prompt\": \"已从上游同步添加 {{length}} 个模型。\",\n      \"search-model\": \"搜索模型\",\n      \"fill-template-models\": \"填入模板模型 ({{number}} 个)\",\n      \"add-custom-model\": \"添加自定义模型 （多个模型用空格分隔）\",\n      \"add-model\": \"添加模型\",\n      \"clear-models\": \"清空全部模型\",\n      \"group-placeholder\": \"已选 {{length}} 个分组\",\n      \"group-desc\": \"用户类型分组，未包含的分组将不包含在此渠道的可用范围内 （分组为空时，所有用户都可以使用此渠道），非特殊情况无需设置分组\",\n      \"groups\": {\n        \"anonymous\": \"匿名用户\",\n        \"normal\": \"普通用户\",\n        \"basic\": \"基础版订阅用户\",\n        \"standard\": \"标准版订阅用户\",\n        \"pro\": \"专业版订阅用户\",\n        \"admin\": \"管理员用户\",\n        \"custom\": \"自定义分组\"\n      },\n      \"proxy-type\": \"代理类型\",\n      \"proxy-endpoint\": \"代理地址\",\n      \"proxy-endpoint-placeholder\": \"请输入正向代理地址，如：socks5://example.com:1080\",\n      \"proxy-username\": \"代理用户名\",\n      \"proxy-username-placeholder\": \"请输入代理的鉴权用户名 (可选)\",\n      \"proxy-password\": \"代理密码\",\n      \"proxy-password-placeholder\": \"请输入代理的鉴权密码 (可选)\",\n      \"proxy-desc\": \"正向代理，支持 HTTP/HTTPS/SOCKS5 代理 (反向代理请填写接入点, 非特殊情况无需设置正向代理)\",\n      \"first-message-as-user\": \"将第一条消息默认转换为用户消息\",\n      \"first-message-as-user-tip\": \"如果开启，当第一条消息为 assistant 角色时，将被转换为 user 角色\",\n      \"first-message-as-user-desc\": \"某些模型（如 DeepSeek）不支持第一条消息为 assistant 角色，开启此选项可以将第一条 assistant 角色的消息转换为 user 角色。\",\n      \"merge-consecutive-user-messages\": \"合并连续用户消息\",\n      \"merge-consecutive-user-messages-tip\": \"如果开启，当连续两条消息均为用户消息时，将被合并为一条消息\",\n      \"merge-consecutive-user-messages-desc\": \"某些模型（如 DeepSeek）不支持连续两条用户消息，开启此选项可以将连续两条用户消息合并为一条消息。\"\n    },\n    \"charge\": {\n      \"id\": \"ID\",\n      \"type\": \"类型\",\n      \"model\": \"模型\",\n      \"quota\": \"点数\",\n      \"action\": \"操作\",\n      \"input\": \"输入\",\n      \"output\": \"输出\",\n      \"support-anonymous\": \"支持匿名\",\n      \"non-billing\": \"不计费\",\n      \"times-billing\": \"按次计费\",\n      \"token-billing\": \"按 Token 计费\",\n      \"anonymous\": \"支持匿名调用\",\n      \"time-count\": \"单次请求点数\",\n      \"input-count\": \"输入点数\",\n      \"output-count\": \"输出点数\",\n      \"add-rule\": \"添加规则\",\n      \"update-rule\": \"更新规则\",\n      \"unused-model\": \"部分模型计费规则未设置\",\n      \"unused-model-tip\": \"计费规则未设置的模型为避免损失，普通用户将无法请求\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步选项\",\n      \"sync-site\": \"上游地址\",\n      \"sync-tip\": \"同步上游计费规则\",\n      \"sync-placeholder\": \"请输入上游 CoAI 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-failed\": \"同步失败\",\n      \"sync-failed-prompt\": \"地址无法请求或者计费规则为空\\n（端点：{{endpoint}}）\",\n      \"sync-prompt\": \"已从上游获取 {{length}} 个模型的规则，将影响当前 {{influence}} 个模型的规则，是否继续？\",\n      \"sync-overwrite\": \"覆盖已有规则\",\n      \"sync-confirm\": \"确认同步\",\n      \"sync-builtin\": \"应用内置价格\",\n      \"group-pricing\": \"用户组定价比例\",\n      \"new-group\": \"用户组 ID\",\n      \"new-group-price\": \"价格\",\n      \"add-group\": \"添加用户组\",\n      \"update-group\": \"更新用户组\",\n      \"usd-currency\": \"美元兑人民币汇率\",\n      \"group-pricing-description\": \"用户组定价比例可用于区分不同用户组的计费价格，基准比例为 1，即用户价格 = 模型价格 * 比例\",\n      \"group-pricing-sample\": \"例：模型扣费 0.2 点数，用户组比例为 0.8，则实际扣费 0.2 * 0.8 = 0.16 点数\",\n      \"group-pricing-tip\": \"倍率可用于区分不同用户组的计费价格，**基准倍率为 1**，即**用户价格 = 模型价格 * 倍率**\\n\\n例：模型扣费 0.2 点数，用户组比例为 0.8，则实际扣费 0.2 x 0.8 = 0.16 点数\\n\\n- 购买倍率：用户购买点数时，扣费价格倍率\\n- 消费倍率：用户消费点数时，扣费价格倍率\",\n      \"default-price\": \"默认价格\",\n      \"custom-price\": \"自定义价格\",\n      \"add-new-group\": \"添加新用户组\",\n      \"new-group-buy-price\": \"购买倍率\",\n      \"new-group-consume-price\": \"消费倍率\",\n      \"new-group-description\": \"描述\"\n    },\n    \"system\": {\n      \"general\": \"常规设置\",\n      \"search\": \"联网搜索\",\n      \"site\": \"站点设置\",\n      \"operation\": \"运营设置\",\n      \"chat\": \"聊天设置\",\n      \"security\": \"安全设置\",\n      \"payment\": \"支付设置\",\n      \"mail\": \"SMTP 发件设置\",\n      \"common\": \"通用设置\",\n      \"save\": \"保存\",\n      \"update\": \"更新\",\n      \"group\": \"分组\",\n      \"group-price\": \"分组倍率\",\n      \"buy-price\": \"购买倍率\",\n      \"consume-price\": \"消费倍率\",\n      \"token-group\": \"令牌分组\",\n      \"token-group-tip\": \"令牌分组，勾选后当前分组将支持令牌分组，此分组将显示在所有用户可选的令牌分组中（所有内置分组无法开启令牌分组）\",\n      \"edit\": \"编辑\",\n      \"delete\": \"删除\",\n      \"type\": \"类型\",\n      \"actions\": \"操作\",\n      \"updateRoot\": \"修改 Root 密码\",\n      \"updateRootTip\": \"请谨慎操作，修改 Root 密码后，您需要重新登录。\",\n      \"updateRootPlaceholder\": \"请输入新的 Root 密码\",\n      \"updateRootRepeatPlaceholder\": \"请再次输入新的 Root 密码\",\n      \"test\": \"测试发件\",\n      \"title\": \"网站名称\",\n      \"description\": \"网站描述\",\n      \"titleTip\": \"网站名称，用于显示在网站标题，留空默认\",\n      \"descriptionTip\": \"网站描述，用于 SEO 搜索引擎优化中的描述，留空默认\",\n      \"logo\": \"网站 Logo\",\n      \"docs\": \"文档链接\",\n      \"docsTip\": \"文档链接，留空默认 https://coai.dev\",\n      \"logoTip\": \"网站 Logo 的链接，用于显示在网站标题，留空默认 (如 {{logo}})\",\n      \"file\": \"文件解析服务\",\n      \"filePlaceholder\": \"文件解析服务，留空默认 https://blob.coai.dev (不保证稳定性)\",\n      \"fileTip\": \"文件解析服务，请参考 [coai-blob-service](https://github.com/zmh-program/blob-service) 项目进行搭建\",\n      \"backend\": \"后端域名\",\n      \"backendTip\": \"后端域名（docker 安装默认路径为 /api），用于接收回调和存储等，默认为空\\n示例：{{backend}}\",\n      \"backendPlaceholder\": \"后端回调域名，默认为空，接受回调必填\",\n      \"realtime\": {\n        \"title\": \"WebSocket 实时流配置\",\n        \"wsBufferSize\": \"WS 缓冲大小\",\n        \"wsBufferSizeTip\": \"控制服务端向前端下行分片的队列长度。较小(如 1)可降低上游结束后的尾部等待；较大(如 24)兼容旧行为但可能出现更长尾拖。\",\n        \"wsAggregate\": \"WS 分片聚合\",\n        \"wsAggregateTip\": \"启用后按时间窗聚合多个小分片再下发，减少前端重渲染频次、提升流畅度。关闭则每个分片立即下发（旧行为）。\",\n        \"wsAggregateWindow\": \"WS 聚合时间窗（毫秒）\",\n        \"wsAggregateWindowTip\": \"分片聚合的时间窗口，建议 15–33ms。数值越大，合并越多、刷新更平滑，但首段可能稍晚。\"\n      },\n      \"debugMode\": \"调试模式\",\n      \"debugModeTip\": \"调试模式，开启后日志将输出详细的请求参数等的日志，用于排查问题\",\n      \"gravatar\": \"Gravatar 头像\",\n      \"gravatarPlaceholder\": \"Gravatar 代理地址，留空默认不开启 Gravatar 头像\",\n      \"mailHost\": \"发件域名\",\n      \"mailProtocol\": \"发件协议\",\n      \"mailPort\": \"SMTP 端口\",\n      \"mailUser\": \"用户名\",\n      \"mailPass\": \"密码\",\n      \"mailFrom\": \"发件人\",\n      \"mailEnableWhitelist\": \"启用域名后缀白名单\",\n      \"mailConfNotValid\": \"SMTP 发件参数未正确配置，已禁用邮箱验证\",\n      \"mailWhitelist\": \"域名后缀白名单\",\n      \"mailWhitelistSelected\": \"已选 {{length}} 个域名邮箱\",\n      \"mailWhitelistSearchPlaceholder\": \"搜索域名后缀\",\n      \"customWhitelistPlaceholder\": \"请输入自定义域名后缀列表（输入后将出现在选项列表中可供选择），使用英文逗号分隔，如：example.com,example.net\",\n      \"searchEndpoint\": \"搜索接入点\",\n      \"searchQuery\": \"最大搜索结果数\",\n      \"searchQueryTip\": \"最大搜索结果数，默认为 5\",\n      \"searchCrop\": \"开启结果截断\",\n      \"searchCropTip\": \"开启结果截断，开启后搜索结果内容的字符数如果超过最大结果字符数，则内容后面会被截断\",\n      \"searchCropLen\": \"最大结果字符数\",\n      \"searchEngines\": \"搜索引擎设置\",\n      \"searchEnginesPlaceholder\": \"已选 {{length}} 个搜索引擎\",\n      \"searchEnginesSearchPlaceholder\": \"请输入搜索引擎名称，如：Google\",\n      \"searchEnginesEmptyTip\": \"设置搜索引擎为空时，默认使用 SearXNG 内默认配置的搜索引擎\",\n      \"searchTest\": \"搜索测试\",\n      \"searchTestTip\": \"搜索测试，输入查询内容进行搜索测试\",\n      \"searchSafeSearch\": \"安全搜索模式\",\n      \"searchLLMExtract\": \"启用 LLM 关键词提取\",\n      \"searchLLMExtractTip\": \"使用 LLM 模型智能提取搜索关键词，可以提高搜索准确度\",\n      \"searchLLMModel\": \"关键词提取模型\",\n      \"searchLLMModelPlaceholder\": \"选择用于提取关键词的模型\",\n      \"searchSafeSearchModes\": {\n        \"none\": \"关闭\",\n        \"moderation\": \"中等\",\n        \"strict\": \"严格\"\n      },\n      \"searchImageProxy\": \"开启图片代理\",\n      \"searchImageProxyTip\": \"图片代理，开启后搜索引擎返回的图片将会通过 SearXNG 服务节点代理加载\",\n      \"searchTip\": \"[SearXNG](https://github.com/searxng/searxng) 开源搜索引擎提供联网搜索能力。SearXNG Docker 私有化部署示例：[SearXNG Docker](https://github.com/zmh-program/searxng)\",\n      \"searchPlaceholder\": \"SearXNG 服务接入点 (例如 http://ip:7980)\",\n      \"closeRegistration\": \"暂停注册\",\n      \"displayCurrency\": \"显示货币\",\n      \"displayCurrencyTip\": \"网站显示货币单位\",\n      \"closeRegistrationTip\": \"暂停注册，关闭后新用户将无法注册\",\n      \"closeRelay\": \"关闭中转 API\",\n      \"closeRelayTip\": \"关闭中转 API，关闭后中转 API 将无法使用\",\n      \"relayPlan\": \"订阅配额支持中转 API\",\n      \"relayPlanTip\": \"订阅配额支持中转 API，开启后中转 API 计费会优先考虑使用用户订阅配额\\n（提示：订阅为次数配额，对 Token 计费的模型可能会影响成本）\",\n      \"preDeductQuota\": \"启用预扣费\",\n      \"preDeductQuotaTip\": \"开启后将在请求开始时预扣费用，关闭后将在请求结束时扣费\",\n      \"quota\": \"用户初始点数\",\n      \"quotaTip\": \"用户注册后赠送的点数\",\n      \"buyLink\": \"购买链接\",\n      \"buyLinkPlaceholder\": \"请输入卡密的购买链接，留空不显示购买按钮\",\n      \"announcement\": \"站点公告\",\n      \"announcementPlaceholder\": \"请输入站点公告 (支持 Markdown / HTML 格式)\",\n      \"contact\": \"联系信息\",\n      \"contactPlaceholder\": \"请输入联系信息 (支持 Markdown / HTML 格式)\",\n      \"footer\": \"页脚信息\",\n      \"footerPlaceholder\": \"请输入页脚信息 (支持 Markdown / HTML 格式)\",\n      \"authFooter\": \"登录后隐藏页脚\",\n      \"hideKeyDocs\": \"隐藏密钥页面对接指南\",\n      \"article\": \"批量文章生成功能分组\",\n      \"articleTip\": \"批量文章生成功能分组，勾选后当前用户组可使用批量文章生成功能\",\n      \"generate\": \"AI 项目生成器分组\",\n      \"generateTip\": \"AI 项目生成器分组，勾选后当前用户组可使用 AI 项目生成器\",\n      \"groupPlaceholder\": \"已选 {{length}} 个分组\",\n      \"cache\": \"可缓存的模型\",\n      \"cacheTip\": \"可缓存的模型，勾选后当前模型可被缓存并击中缓存\",\n      \"cachePlaceholder\": \"已选 {{length}} 个模型\",\n      \"prompt_store\": \"Prompt 记录存储\",\n      \"prompt_storeTip\": \"Prompt 记录存储，开启后用户的 Prompt 记录将会存储在服务端\",\n      \"image_store\": \"图片存储\",\n      \"image_storeTip\": \"OpenAI 渠道 DALL-E 生成的图片将存储于服务端以防止图片失效\",\n      \"image_storeNoBackend\": \"未配置后端域名，无法启用图片存储\",\n      \"cacheAll\": \"设为全部可缓存\",\n      \"cacheFree\": \"设为免费模型可缓存\",\n      \"cacheNone\": \"设为全部不缓存\",\n      \"cacheExpired\": \"缓存过期时间\",\n      \"cacheExpiredTip\": \"缓存过期时间（单位：秒），默认 1 小时\",\n      \"cacheSize\": \"最大缓存可能性区大小\",\n      \"cacheSizeTip\": \"最大缓存可能性大小，即同一类型入参的最大缓存可能性大小，若参数为 1, 则最大缓存的内容为 1 个，后请求的内容会被直接击中，若参数为 4, 则有 4 种返回的内容，后请求的内容会被击中其中一个\",\n      \"epayTitle\": \"易支付\",\n      \"epayEnabled\": \"启用易支付\",\n      \"epayDomain\": \"易支付域名\",\n      \"epayDomainPlaceholder\": \"请输入易支付域名，如：https://pay.example.com\",\n      \"epayMethods\": \"支付方式\",\n      \"epayMethodsPlaceholder\": \"勾选启用的支付方式 (已选 {{length}} 种)\",\n      \"epayTip\": \"易支付是市面上一种**通用**的第三方聚合支付协议，**并非单独某家支付或软件**，您可以根据情况自行选择平台，**我们不做任何推荐和责任担保**。\\n如果您有资质可以自建易支付平台，或者直接接入别人的易支付平台：易支付平台一般包含**易支付**(企业/个体户收款 相对稳定 收入月结)类型和**码支付**(个人收款码 实时到账 费率较低)两种平台。\\n易支付设置，请注意一定要点击开启 **启用易支付** 选项后，才会启用易支付功能\\n易支付需要配置回调域名, 请在 **常规设置** 中配置 **后端域名** 后才可正常异步回调\",\n      \"epayBusinessId\": \"商户 ID\",\n      \"epayBusinessIdPlaceholder\": \"请输入易支付商户 ID\",\n      \"epayBusinessKey\": \"商户密钥\",\n      \"epayBusinessKeyPlaceholder\": \"请输入易支付商户密钥\",\n      \"epayAggregation\": \"聚合支付模式\",\n      \"epayAggregationTip\": \"聚合支付模式，开启后点击将不会选择支付方式 **直接跳转至聚合支付页面**\\n请确保您的易支付支持聚合支付模式\",\n      \"stripeTitle\": \"Stripe\",\n      \"stripeTip\":\"Stripe 是一种广泛使用的国际在线支付系统，支持多种支付方式，包括信用卡、借记卡、Apple Pay、Google Pay等。它为用户提供了安全、便捷的支付体验，特别适合需要处理国际支付的业务。\",\n      \"stripeEnabled\": \"启用 Stripe\",\n      \"stripeSecretKey\": \"Stripe Secret Key\",\n      \"stripeSecretKeyPlaceholder\": \"请输入 Stripe Secret Key\",\n      \"stripeWebhookSecret\": \"Stripe Webhook Secret\",\n      \"stripeWebhookSecretPlaceholder\": \"请输入 Stripe Webhook Secret\",\n      \"wechatPayTitle\": \"微信支付\",\n      \"wechatPayTip\": \"微信支付一直致力于为用户和企业提供安全、便捷、专业的在线支付服务。以“微信支付，不止支付”为核心理念，为个人用户创造了多种便民服务和应用场景，为各类企业以及小微商户提供专业的收款能力，运营能力，资金结算解决方案，以及安全保障。企业、商品、门店、用户已经通过微信连在了一起，让智慧生活，变成了现实。\",\n      \"wechatPayEnabled\": \"启用微信支付\",\n      \"wechatPayAppId\": \"微信支付 App ID\",\n      \"wechatPayAppIdPlaceholder\": \"请输入微信支付 App ID\",\n      \"wechatPayMchId\": \"微信支付商户号\",\n      \"wechatPayMchIdPlaceholder\": \"请输入微信支付商户号\",\n      \"wechatPayKey\": \"微信支付 API v3 密钥\",\n      \"wechatPayKeyPlaceholder\": \"请输入微信支付 API v3 密钥\",\n      \"wechatPaySerialNo\": \"微信支付平台证书序列号\",\n      \"wechatPaySerialNoPlaceholder\": \"请输入微信支付平台证书序列号\",\n      \"wechatPayCertificate\": \"微信支付平台证书\",\n      \"wechatPayCertificatePlaceholder\": \"请在此粘贴您的微信支付平台证书\",\n      \"wechatPayCertificateTip\": \"商户接收到 API v3 接口的返回内容，需要使用该证书公钥进行验签，另外某些敏感信息参数(如姓名、身份证号码)也需要使用该证书公钥加密后传输，详见[微信支付平台证书](https://pay.weixin.qq.com/doc/v3/merchant/4012068814)\",\n      \"xunhupayTitle\": \"虎皮椒支付\",\n      \"xunhupayTip\": \"虎皮椒是一个聚合支付平台，支持微信、支付宝等多种支付方式。配置后用户可通过虎皮椒进行充值。微信和支付宝需要分别配置不同的 APP ID 和 APP Secret。\",\n      \"xunhupayWechatEnabled\": \"启用虎皮椒微信\",\n      \"xunhupayAlipayEnabled\": \"启用虎皮椒支付宝\",\n      \"xunhupayWechatAppId\": \"虎皮椒微信 APP ID\",\n      \"xunhupayWechatAppIdPlaceholder\": \"请输入虎皮椒微信支付的 APP ID\",\n      \"xunhupayWechatAppSecret\": \"虎皮椒微信 APP Secret\",\n      \"xunhupayWechatAppSecretPlaceholder\": \"请输入虎皮椒微信支付的 APP Secret (密钥)\",\n      \"xunhupayAlipayAppId\": \"虎皮椒支付宝 APP ID\",\n      \"xunhupayAlipayAppIdPlaceholder\": \"请输入虎皮椒支付宝的 APP ID\",\n      \"xunhupayAlipayAppSecret\": \"虎皮椒支付宝 APP Secret\",\n      \"xunhupayAlipayAppSecretPlaceholder\": \"请输入虎皮椒支付宝的 APP Secret (密钥)\",\n      \"xunhupayEndpoint\": \"虎皮椒接口地址\",\n      \"xunhupayEndpointPlaceholder\": \"https://api.xunhupay.com 或 https://api.dpweixin.com\",\n      \"securityCheckType\": \"审核模式\",\n      \"securityCheckTypePlaceholder\": \"请选择审核类型\",\n      \"securityTextDatabase\": \"黑名单词库\",\n      \"securityTextDatabasePlaceholder\": \"请输入黑名单词库，词中间使用空格分隔, 比如：敏感词1 敏感词2\",\n      \"securityRegexDatabase\": \"正则黑名单表达式\",\n      \"securityRegexDatabasePlaceholder\": \"请输入正则黑名单表达式，表达式中间使用换行分隔, 比如：\\n^敏感词1$\\n^敏感词2$\",\n      \"securityBaiduApiKey\": \"百度云审核 API Key\",\n      \"securityBaiduApiKeyPlaceholder\": \"请输入百度云审核 API Key\",\n      \"securityBaiduSecretKey\": \"百度云审核 Secret Key\",\n      \"securityBaiduSecretKeyPlaceholder\": \"请输入百度云审核 Secret Key\",\n      \"securityCheckModels\": \"特定审核模型\",\n      \"securityCheckModelsPlaceholder\": \"已选 {{length}} 个特定审核模型\",\n      \"securityCheckModelsTip\": \"特定模型审核，勾选后当前模型可被特定审核模型审核，**默认情况下所有模型都会根据审核模式进行审核**，如果特定审核模型，则**只会根据特定审核模型进行审核**，**其他模型不会进行审核**\",\n      \"securityBaiduTip\": \"百度云审核模式，需填写百度云审核 **API Key** 和 **Secret Key** \\n 详情信息和配置审核策略粒度请参考 [百度云审核快速入门](https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy) \\n 违禁词汇审核策略请根据上方百度云文档在百度云控制台配置策略\",\n      \"securityCustomEndpoint\": \"自定义审核接入点\",\n      \"securityCustomEndpointPlaceholder\": \"请输入自定义审核接入点\",\n      \"securityCustomToken\": \"自定义审核 Token\",\n      \"securityCustomTokenPlaceholder\": \"请输入自定义审核 Token\",\n      \"securityCustomTip\": \"自定义审核模式，需填写自定义审核 **Token** 和 **接入点** \\n 请求与返回格式与百度云审核一致，可前往 [百度云审核文本审核请求说明](https://cloud.baidu.com/doc/ANTIPORN/s/Rk3h6xb3i) 进行参考适配\",\n      \"securityTypes\": {\n        \"none\": \"无审核模式\",\n        \"dict\": \"文本词库审核模式\",\n        \"regex\": \"文本正则审核模式\",\n        \"baidu\": \"百度云审核模式\",\n        \"custom\": \"自定义后端审核模式\"\n      },\n      \"securityBlacklistIPs\": \"黑名单 IP\",\n      \"securityBlacklistIPsPlaceholder\": \"请输入黑名单 IP\",\n      \"securityWhitelistIPs\": \"白名单 IP\",\n      \"securityWhitelistIPsPlaceholder\": \"请输入白名单 IP\",\n      \"securityWhitelistIPsTip\": \"黑白名单 IP **仅对 API 请求的速率限制中间件生效** ，如需限制其他请求或前端访问请使用 WAF 等安全防护服务\",\n      \"securityAddIPAddress\": \"添加 IP 地址\",\n      \"securityRemoveIPAddress\": \"删除 IP 地址\",\n      \"autoTitle\": {\n        \"title\": \"自动会话标题\",\n        \"tip\": \"使用 LLM 在首轮对话后自动总结并设置会话标题。设置自定义提示词为空时，默认使用 CoAI 内默认配置的提示词\",\n        \"enabled\": \"启用自动标题\",\n        \"model\": \"生成模型\",\n        \"modelPlaceholder\": \"留空表示使用当前会话模型\",\n        \"maxLen\": \"标题最大长度\",\n        \"minMsgs\": \"抽取消息条数\",\n        \"overwrite\": \"覆盖已有标题\",\n        \"prompt\": \"自定义提示词\",\n        \"promptPlaceholder\": \"可使用 {max_len} 作为占位符\"\n      },\n      \"oauth\": {\n        \"title\": \"第三方登录设置\",\n        \"wechat\": \"微信登录\",\n        \"google\": \"Google 登录\",\n        \"github\": \"Github 登录\",\n        \"telegram\": \"Telegram 登录\",\n        \"rainbow\": \"彩虹聚合登录\",\n        \"enable\": \"启用\",\n        \"disabled\": \"禁用\",\n        \"methods\": \"登录方式\",\n        \"methods_placeholder\": \"请输入登录方式，使用英文逗号分隔，如：qq,wx,baidu,douyin\",\n        \"client_id\": \"客户端 ID\",\n        \"client_id_placeholder\": \"请输入客户端 ID\",\n        \"client_secret\": \"客户端密钥\",\n        \"client_secret_placeholder\": \"请输入客户端密钥\",\n        \"redirect_uri\": \"重定向 URI\",\n        \"redirect_uri_placeholder\": \"请输入重定向 URI\",\n        \"base_url\": \"彩虹聚合登录域名\",\n        \"base_url_placeholder\": \"请输入彩虹聚合登录域名，留空则默认为彩虹聚合登录官方域名 https://u.cccyun.cc\",\n        \"require_email\": \"启用邮箱验证\",\n        \"scope\": \"授权范围\",\n        \"scope_placeholder\": \"请输入授权范围\",\n        \"auth_url\": \"授权 URL\",\n        \"auth_url_placeholder\": \"请输入授权 URL\",\n        \"token_url\": \"令牌 URL\",\n        \"token_url_placeholder\": \"请输入令牌 URL\",\n        \"user_info_url\": \"用户信息 URL\",\n        \"user_info_url_placeholder\": \"请输入用户信息 URL\",\n        \"bot_token\": \"机器人 Token\",\n        \"bot_token_placeholder\": \"请输入机器人 Token\",\n        \"bot_name\": \"机器人名称\",\n        \"bot_name_placeholder\": \"请输入机器人名称\"\n      },\n      \"custom\": \"主题设置\",\n      \"customJs\": \"自定义 JS\",\n      \"customJsPlaceholder\": \"请输入自定义 JS\",\n      \"customCss\": \"自定义 CSS\",\n      \"customCssPlaceholder\": \"请输入自定义 CSS\",\n      \"customHtml\": \"自定义 HTML\",\n      \"customHtmlPlaceholder\": \"请输入自定义 HTML\",\n      \"gaTrackingId\": \"Google Analytics 服务\",\n      \"gaTrackingIdPlaceholder\": \"请输入 Google Analytics ID\",\n      \"customThemeAlert\": \"注意：如果您使用了 WAF (如: Cloudflare WAF、长亭、1 Panel WAF、 宝塔 WAF) 等安全防护服务，您的自定义主题可能会被 WAF 误认为是恶意代码而被拦截显示错误码如 403 Forbidden，请注意检查您的 WAF 配置或者关闭 WAF 配置。\",\n      \"uploadFaviconSuccess\": \"Logo 上传成功! 请记得保存以应用新的 Logo\",\n      \"affiliateTitle\": \"联盟式行销设置\",\n      \"affiliateEnabled\": \"启用联盟式行销\",\n      \"affiliateCommissionRate\": \"佣金率\",\n      \"affiliateMinWithdraw\": \"最低提现金额\",\n      \"affiliateAllowExistingBind\": \"允许已注册用户绑定行销码\"\n    },\n    \"logger\": {\n      \"title\": \"服务日志\",\n      \"console\": \"控制台\",\n      \"consoleLength\": \"日志条数\"\n    },\n    \"cdn\": {\n      \"warmup\": \"资源预热\",\n      \"copy-data\": \"复制预热 URL 资源列表\",\n      \"warm-tip\": \"> 如果您在使用 CDN 服务，可以通过此功能预热 CSS / JS 等资源。\\n**每次更新后，您可以进行一次资源刷新以保证资源稳定性并提升加载速度。**\\nCDN (内容分发网络) 资源预热，预热后资源将会被缓存到 CDN 节点，加速访问。\\n通过预热功能，您可以在业务高峰前预先将热门资源缓存到CDN节点，降低源站压力提升用户体验。\\n提示：**预热执行会从CDN到源站拉取大量的数据，请关注源站宽带负载情况。**\"\n    },\n    \"license\": {\n      \"title\": \"授权管理\",\n      \"description\": \"CoAI Pro 版本授权管理\",\n      \"domain\": \"授权域名\",\n      \"digest\": \"签名摘要\",\n      \"module\": \"模块管理\",\n      \"info\": \"授权信息\",\n      \"purchase\": \"购买授权\",\n      \"pro-required\": \"该功能为 CoAI Pro 专属，请在授权管理页面购买 CoAI Pro 授权以使用该功能\",\n      \"modules\": {\n        \"bought\": \"已购买\",\n        \"not-bought\": \"购买\",\n        \"buy-tip\": \"请联系您的销售代表以购买此模块\",\n        \"contact-for-price\": \"访问文档以获取报价\",\n        \"coai-pro\": {\n          \"title\": \"CoAI Pro\",\n          \"description\": \"CoAI Pro 商业版授权，解锁对接多种支付渠道、用户（组）倍率控制、内容审核、会话日志等全部商业功能\"\n        },\n        \"multiKey\": { \n          \"title\": \"多令牌管理\",\n          \"description\": \"多 API KEY 管理, 支持一单元内多令牌分发管理, 支持设置可调用模型，余额限制，调用日志，状态管理，对接指南等高级功能\"\n        },\n        \"stripe\": {\n          \"title\": \"Stripe 支付\",\n            \"description\": \"Stripe Hosted Checkout 高级支付模块，支持 Stripe 银行卡/Bank/Link/微信/Alipay+ 等几十余种支付对接，支持多种货币支付\"\n        },\n        \"paypal\": {\n          \"title\": \"PayPal 支付\",\n          \"description\": \"PayPal 高级支付模块，支持 PayPal 银行卡支付等多货币支付功能\"\n        },\n        \"afdian\": {\n          \"title\": \"爱发电\",\n          \"description\": \"爱发电支付 Webhook 模块，支持爱发电余额购买\"\n        },\n        \"bot\": {\n          \"title\": \"机器人\",\n          \"description\": \"微信/飞书/Telegram/Discord 机器人 SaaS 模块\"\n        },\n        \"digital\": {\n          \"title\": \"数字人\",\n          \"description\": \"数字人视频生成模块定制，采用高级动态语音技术，支持语音和面部克隆，支持私有化部署推理与多种引擎，全行业场景支持，支持高维度定制\"\n        }\n      }\n    }\n  },\n  \"mask\": {\n    \"title\": \"预设\",\n    \"market\": \"预设市场\",\n    \"search\": \"搜索预设名称或描述...\",\n    \"system\": \"系统预设\",\n    \"custom\": \"我的预设\",\n    \"edit\": \"编辑预设\",\n    \"create\": \"新建预设\",\n    \"context\": \"包含 {{length}} 条上下文\",\n    \"avatar\": \"预设头像\",\n    \"conversation\": \"预设对话\",\n    \"name\": \"预设标题\",\n    \"name-placeholder\": \"请输入预设标题\",\n    \"description\": \"预设简介\",\n    \"description-placeholder\": \"请输入预设简介\",\n    \"search-emoji\": \"搜索 Emoji\",\n    \"actions\": {\n      \"clone\": \"克隆预设\",\n      \"use\": \"使用预设\",\n      \"edit\": \"编辑预设\",\n      \"delete\": \"删除预设\"\n    },\n    \"switch-preset\": \"切换预设\",\n    \"switch-preset-desc\": \"已开始新对话并切换至预设\"\n  },\n  \"payment\": {\n    \"wechat\": \"微信支付\",\n    \"wxpay\": \"微信支付\",\n    \"wechatpay\": \"微信支付\",\n    \"alipay\": \"支付宝\",\n    \"paypal\": \"PayPal\",\n    \"stripe\": \"Stripe\",\n    \"afdian\": \"爱发电\",\n    \"qqpay\": \"QQ 钱包\",\n    \"xunhupay-wechat\": \"微信支付\",\n    \"xunhupay-alipay\": \"支付宝\",\n    \"order\": {\n      \"quota\": \"{{quota}} 点数\"\n    },\n    \"dialog-wechatpay\": {\n      \"title\": \"微信支付\",\n      \"description\": \"请使用微信扫描下方二维码进行支付\",\n      \"success\": \"支付成功\",\n      \"loading\": \"加载中...\",\n      \"remaining-time\": \"剩余支付时间\"\n    },\n    \"dialog-xunhupay\": {\n      \"title\": \"支付\",\n      \"description\": \"请使用微信或支付宝扫描下方二维码进行支付\",\n      \"success\": \"支付成功\",\n      \"remaining-time\": \"剩余支付时间\"\n    },\n    \"notify-stripe\": {\n      \"success\": \"支付成功\",\n      \"canceled\": \"支付取消\",\n      \"processing\": \"支付处理中...\"\n    }\n  },\n  \"copied\": {\n    \"prompt\": \"复制\",\n    \"success\": \"复制成功\",\n    \"success-description\": \"内容已复制到剪贴板\",\n    \"failed\": \"复制失败\",\n    \"failed-description\": \"复制失败，原因：{{reason}}\"\n  },\n  \"date\": {\n    \"pick\": \"选择一个日期\",\n    \"today\": \"今天\",\n    \"clean\": \"归零\",\n    \"add-day\": \"增加一天\",\n    \"sub-day\": \"减少一天\",\n    \"add-month\": \"增加一个月\",\n    \"sub-month\": \"减少一个月\",\n    \"add-year\": \"增加一年\",\n    \"sub-year\": \"减少一年\"\n  },\n  \"renderer\": {\n    \"viewImage\": \"查看图片\",\n    \"imageLoadFailed\": \"图片 {{src}} 加载失败\",\n    \"base64Image\": \"展开图片 Base64\",\n    \"base64ImageCollapse\": \"收起图片 Base64\",\n    \"viewVideo\": \"查看视频\",\n    \"videoLoadFailed\": \"视频 {{src}} 加载失败\"\n  },\n  \"bar\": {\n    \"chat\": \"对话\",\n    \"chat-full\": \"开始对话\",\n    \"model\": \"模型\",\n    \"model-full\": \"探索模型\",\n    \"preset\": \"预设\",\n    \"preset-full\": \"预设市场\",\n    \"wallet\": \"钱包\",\n    \"key\": \"密钥\",\n    \"key-full\": \"令牌管理\",\n    \"wallet-full\": \"我的钱包\",\n    \"log\": \"日志\",\n    \"log-full\": \"使用记录\",\n    \"account\": \"账户\",\n    \"account-full\": \"账户管理\",\n    \"admin\": \"后台\",\n    \"admin-full\": \"后台管理\"\n  },\n  \"account\": {\n    \"title\": \"账号管理\",\n    \"deeptrain\": \"DeepTrain 统一账号管理\",\n    \"my-account\": \"我的账号\",\n    \"my-account-description\": \"您的账号信息、第三方账号绑定信息等\",\n    \"api-description\": \"您的全局账号 API 信息 \",\n    \"deeptrain-description\": \"您的 DeepTrain 统一账号绑定信息\",\n    \"registerDays\": \"注册 {{days}} 天\",\n    \"current-quota\": \"当前点数\",\n    \"used-quota\": \"已用点数\",\n    \"plan-total-month\": \"总计订阅月数\",\n    \"plan-total-month-tips\": \"订阅级别升级、降级导致的月数变动不会计入此统计\",\n    \"share-description\": \"查看、管理您的历史对话分享记录\",\n    \"share-delete\": \"删除分享\",\n    \"share-delete-description\": \"确定要删除此分享吗？\",\n    \"oauth\": \"第三方登录\",\n    \"oauth-description\": \"绑定、管理您的第三方登录信息\",\n    \"notification\": {\n      \"title\": \"通知中心\",\n      \"method\": \"通知方式\",\n      \"event\": \"订阅事件\",\n      \"description\": \"管理您的通知方式\",\n      \"fetchError\": \"获取通知配置失败\",\n      \"fetchErrorDesc\": \"获取通知配置失败，请检查网络并重试。\",\n      \"updateSuccess\": \"更新通知配置成功\",\n      \"updateSuccessDesc\": \"通知配置已成功更新（刷新浏览器即可立即应用）\",\n      \"save\": \"保存\",\n      \"updateError\": \"更新通知配置失败\",\n      \"updateErrorDesc\": \"更新通知配置失败，请检查网络并重试。\",\n      \"testSuccess\": \"测试通知配置成功\",\n      \"testSuccessDesc\": \"通知配置已成功测试\",\n      \"testError\": \"测试通知配置失败\",\n      \"testErrorDesc\": \"测试通知配置失败，请检查网络并重试。\",\n      \"enabled\": \"启用\",\n      \"disabled\": \"禁用\",\n      \"appToken\": \"应用 Token\",\n      \"topicId\": \"主题 ID\",\n      \"userId\": \"用户 UID\",\n      \"webhookUrl\": \"Webhook URL\",\n      \"botToken\": \"Bot Token\",\n      \"chatId\": \"聊天 ID\",\n      \"url\": \"URL\",\n      \"test\": \"测试\",\n      \"testDesc\": \"测试通知配置\",\n      \"alertTitle\": \"推送中心\",\n      \"alertDescription\": \"支持微信 (WxPusher)、Discord、Telegram、飞书推送\",\n      \"type\": {\n        \"email\": \"邮件\",\n        \"wxpusher\": \"微信 (WxPusher)\",\n        \"discord\": \"Discord\",\n        \"telegram\": \"Telegram\",\n        \"feishu\": \"飞书\",\n        \"webhook\": \"Webhook\"\n      },\n      \"events\": {\n        \"broadcast_event\": \"推送通知信息\",\n        \"payment_event\": \"支付、订阅通知\",\n        \"key_quota_not_enough_event\": \"密钥额度预警\",\n        \"account_quota_not_enough_event\": \"账户额度预警\"\n      },\n      \"tiplist\": {\n        \"email\": \"- 邮件推送为默认选项，通知将推送至您的注册邮箱\",\n        \"wxpusher\": \"- 微信推送使用 [WxPusher](https://wxpusher.zjiecode.com) 服务\\n- 在 WxPusher 官网注册并创建应用\\n- 获取应用的 AppToken 并填入配置\\n- 订阅后点击“我的 UID”获取用户 UID 填入配置 (多个 UID 请用逗号分隔)\\n- 扫描应用二维码关注以接收推送\",\n        \"discord\": \"- Discord 推送基于 Webhook 机制\\n- 在 Discord 服务器中选择目标频道\\n- 进入频道设置 > 集成 > 创建 Webhook\\n- 自定义 Webhook 名称和头像（可选）\\n- 复制生成的 Webhook URL 填入配置\",\n        \"telegram\": \"- Telegram 推送需要创建自己的 Bot\\n- 在 Telegram 中搜索 @BotFather 并开始对话\\n- 发送 /newbot 命令，按提示设置 Bot 名称和用户名\\n- 获取 Bot Token 并填入配置\\n- 将 Bot 加入目标群组或与其私聊\\n- 使用 @userinfobot 获取聊天的 Chat ID 并填入\",\n        \"feishu\": \"- 飞书推送使用群组自定义机器人\\n- 在目标飞书群组中添加自定义机器人\\n- 设置机器人名称、头像和描述\\n- 选择接收消息的群组\\n- 复制生成的 Webhook URL 填入配置\\n- 可设置关键词以增强安全性（可选）\",\n        \"webhook\": \"- 自定义 Webhook URL 推送\\n\\n```json\\nPOST ${WEBHOOK_URL}\\n{\\n  \\\"type\\\": \\\"string\\\",\\n  \\\"content\\\": \\\"string\\\",\\n  \\\"time\\\": \\\"number\\\",\\n  \\\"utc_time\\\": \\\"string\\\",\\n  \\\"account_id\\\": \\\"number\\\",\\n  \\\"additional_data\\\": {...}\\n}\\n```\"\n      }\n    }\n  },\n  \"key\": {\n    \"title\": \"我的令牌\",\n    \"description\": \"支持以 OpenAI API 标准格式调用本站全部 AI 大模型, 无需考虑 API 兼容性问题, 支持开发者/第三方工具无缝对接, 内置额度/时间/作用域/权限管理\",\n    \"name\": \"名称\",\n    \"noKey\": \"无密钥\",\n    \"apiBase\": \"API 接入点\",\n    \"apiBaseTip\": \"温馨提示：请在客户端中配置此 API Base 接入点，常见工具接入可直接查看下方接入指南中的接入方法，其他部分工具可能需要添加后缀 (例如 /v1), 请根据客户端要求填写\",\n    \"noKeyWarning\": \"无可用密钥\",\n    \"noKeyWarningTip\": \"请您先创建一个密钥后即可使用对接指南\",\n    \"namePlaceholder\": \"请输入名称\",\n    \"status\": \"状态\",\n    \"quota\": \"额度\",\n    \"quotaPlaceholder\": \"请输入可用额度\",\n    \"usedQuota\": \"已用额度\",\n    \"remainQuota\": \"剩余额度\",\n    \"infiniteQuota\": \"无限额度\",\n    \"createdAt\": \"创建时间\",\n    \"expiredAt\": \"过期时间\",\n    \"key\": \"密钥\",\n    \"default\": \"默认分组\",\n    \"unknown\": \"未知分组\",\n    \"createTip\": \"请勿将密钥泄露给他人（如推送至 Github 公共仓库），否则可能导致您的密钥余额被盗用，请妥善保管密钥！如果出现密钥泄露，请及时重置/删除令牌。\",\n    \"advanced\": \"高级设置\",\n    \"ipWhiteList\": \"IP 白名单\",\n    \"enableIpWhiteList\": \"启用 IP 白名单\",\n    \"enableIpWhiteListTip\": \"启用 IP 白名单，开启后只有白名单中的 IP 地址才能使用此密钥, 不填则允许全部 IP 使用 (非必要, 不建议启用)\",\n    \"ipWhiteListPlaceholder\": \"请输入 IP 地址或网段，格式： 127.0.0.1,192.168.0.0/16\",\n    \"modelWhiteList\": \"模型白名单\",\n    \"tokenGroup\": \"令牌分组\",\n    \"tokenGroupTip\": \"令牌自定义渠道分组\",\n    \"enableModelWhiteList\": \"启用模型白名单\",\n    \"enableModelWhiteListTip\": \"启用模型白名单，开启后只有白名单中的模型才能使用此密钥, 不填可使用全部模型 (非必要, 不建议启用)\",\n    \"modelWhiteListPlaceholder\": \"已勾选 {{length}} 个模型\",\n    \"create\": \"创建令牌\",\n    \"update\": \"更新令牌\",\n    \"nameEmpty\": \"名称不能为空\",\n    \"searchPlaceholder\": \"搜索密钥名称...\",\n    \"disabled\": \"已禁用\",\n    \"disable\": \"禁用\",\n    \"disableToken\": \"禁用令牌\",\n    \"active\": \"已启用\",\n    \"delete\": \"删除令牌\",\n    \"never\": \"永不过期\",\n    \"oneHour\": \"一小时\",\n    \"oneDay\": \"一天\",\n    \"oneWeek\": \"一周\",\n    \"oneMonth\": \"一个月\",\n    \"oneYear\": \"一年\",\n    \"docs\": \"对接指南\",\n    \"slogan\": \"“一键对接最前沿的 AI 产品！”\",\n    \"selectKey\": \"选择密钥\",\n    \"bindLobeChat\": \"绑定 Lobe Chat\",\n    \"bindLobeChatTip\": \"点击按钮后，将会跳转至 Lobe Chat 并自动绑定密钥等信息\",\n    \"bindNextChat\": \"绑定 Next Chat\",\n    \"bindNextChatTip\": \"点击按钮后，将会跳转至 Next Chat 并自动输入密钥等预设设置信息\",\n    \"bindOpenCat\": \"绑定 Open Cat\",\n    \"bindOpenCatTip\": \"点击按钮后，将会跳转至 Open Cat 并绑定预置参数 (请先在您的设备中安装 Open Cat)\",\n    \"bindOneAPIStep1\": \"进入渠道管理页，点击添加渠道\",\n    \"bindOneAPIStep2\": \"选择 OpenAI 类型并根据下方接入点、密钥填写对应信息\",\n    \"bindCoAIStep1\": \"进入后台的渠道管理页，点击对接上游\",\n    \"bindCoAIStep2\": \"根据下方接入点、密钥信息填写对应信息\"\n  },\n  \"aff\": {\n    \"title\": \"推广分成\",\n    \"bind-desc\": \"绑定推广码，开始获得被邀请用户购买后的返佣收益。\",\n    \"placeholder\": { \"code\": \"请输入推广码\" },\n    \"generate-code-first\": \"请先生成推广码\",\n    \"get\": \"获取推广码\",\n    \"get-placeholder\": \"点击获取推广码\",\n    \"get-success\": \"推广码已生成\",\n    \"bind-existing\": \"绑定推广码\",\n    \"bind-success\": \"绑定成功\",\n    \"bind-failed\": \"绑定失败\",\n    \"bind-failed-prompt\": \"绑定失败！原因: {{reason}}\",\n    \"withdraw\": \"提现\",\n    \"withdraw-all\": \"全部提现\",\n    \"withdraw-title\": \"收益提现\",\n    \"withdraw-desc\": \"将推广累计收益兑换为点数。输入金额留空为全部兑换。\",\n    \"withdraw-placeholder\": \"请输入提现金额（留空为全部）\",\n    \"withdraw-success\": \"提现成功\",\n    \"withdraw-success-prompt\": \"您已成功将 {{amount}} 金额兑换为 {{quota}} 点数。\",\n    \"withdraw-failed\": \"提现失败\",\n    \"withdraw-failed-prompt\": \"提现失败！原因: {{reason}}\",\n    \"invalid-amount\": \"无效金额\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"确定\",\n    \"stats\": {\n      \"referrals\": \"邀请人数\",\n      \"earnings\": \"累计收益\",\n      \"pending\": \"待结算\",\n      \"rate\": \"返佣率\"\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/resources/i18n/en.json",
    "content": "{\n  \"end\": \".\",\n  \"add\": \"Add\",\n  \"not-found\": \"Page not found\",\n  \"home\": \"Home\",\n  \"login\": \"Login\",\n  \"login-require\": \"You need to log in to use this feature\",\n  \"logout\": \"Logout\",\n  \"quota\": \"Credits\",\n  \"download\": \"Download\",\n  \"offline\": \"App offline\",\n  \"try-again\": \"Try again\",\n  \"invalid-token\": \"Invalid token\",\n  \"invalid-token-prompt\": \"Please try again.\",\n  \"login-failed\": \"Login failed\",\n  \"login-failed-prompt\": \"Login failed! Reason: {{reason}}\",\n  \"login-success\": \"Login successful\",\n  \"login-success-prompt\": \"You have been logged in successfully.\",\n  \"server-error\": \"Server error\",\n  \"server-error-prompt\": \"There was an error logging you in. Please try again.\",\n  \"error\": \"Request failed\",\n  \"request-failed\": \"Request failed. Please check your network and try again.\",\n  \"success\": \"Request successful\",\n  \"request-success\": \"Your operation has been successfully executed.\",\n  \"close\": \"Close\",\n  \"edit\": \"Edit\",\n  \"editor\": \"Edit\",\n  \"pricing\": \"See model pricing for more details\",\n  \"true\": \"Yes\",\n  \"false\": \"No\",\n  \"unknown\": \"Unknown\",\n  \"scroll-down\": \"Scroll to latest\",\n  \"broadcast\": \"Broadcast\",\n  \"fatal\": \"App crashed\",\n  \"download-fatal-log\": \"Download error log\",\n  \"fatal-tips\": \"Please check your internet connection and browser compatibility first. Try clearing your browser cache and refreshing the page. If the problem persists, please download the log and provide the complete reproduction steps to the developer so we can troubleshoot the issue.\",\n  \"tag\": {\n    \"free\": \"Free\",\n    \"official\": \"Official\",\n    \"unstable\": \"Unstable\",\n    \"web\": \"Web\",\n    \"high-quality\": \"High Quality\",\n    \"high-context\": \"High Context\",\n    \"high-price\": \"High Price\",\n    \"open-source\": \"Open Source\",\n    \"image-generation\": \"Image Generation\",\n    \"multi-modal\": \"Multi-Modal\",\n    \"fast\": \"Fast\",\n    \"english-model\": \"English Model\",\n    \"badges\": {\n      \"non-billing\": \"Free\",\n      \"times-billing\": \"{{price}} / time\",\n      \"token-billing\": \"{{input}} / 1k input tokens, {{output}} / 1k output tokens\",\n      \"add\": \"Add to Workbench\",\n      \"remove\": \"Remove from Workbench\",\n      \"plan-included\": \"Included in subscription\",\n      \"plan-included-tip\": \"Your subscription already includes this model. Subscription credits will be used first.\"\n    }\n  },\n  \"market\": {\n    \"title\": \"Model Marketplace\",\n    \"model\": \"Explore more models\",\n    \"explore\": \"Discover Models\",\n    \"search\": \"Search model name or description\",\n    \"model-api\": \"Model API ID\",\n    \"list\": \"Model List\",\n    \"go\": \"Go to the model marketplace\",\n    \"show-pricing\": \"SHOW PRICE\",\n    \"switch-model\": \"Switch Model\",\n    \"switch-model-desc\": \"Switched to model\",\n    \"switch-bookmark\": \"Workbench\",\n    \"remove-bookmark\": \"Model removed from menu bar\",\n    \"add-bookmark\": \"Model added to menu bar\",\n    \"show-1m-pricing\": \"1M tokens\"\n  },\n  \"conversation\": {\n    \"title\": \"Conversation\",\n    \"empty\": \"Empty\",\n    \"refresh-failed\": \"Refresh failed\",\n    \"refresh-failed-prompt\": \"There was an error during your request. Please try again.\",\n    \"remove-title\": \"Are you absolutely sure?\",\n    \"remove-description\": \"This action cannot be undone. This will permanently delete the conversation \",\n    \"remove-all-title\": \"Clear History\",\n    \"remove-all-description\": \"This action cannot be undone. This will permanently delete all conversations. Continue?\",\n    \"cancel\": \"Cancel\",\n    \"delete\": \"Delete\",\n    \"delete-conversation\": \"Delete Conversation\",\n    \"delete-success\": \"Conversation deleted\",\n    \"delete-success-prompt\": \"Conversation has been deleted.\",\n    \"delete-failed\": \"Delete failed\",\n    \"delete-failed-prompt\": \"Failed to delete conversation. Please check your network and try again.\",\n    \"edit-title\": \"Edit Title\",\n    \"empty-anonymous\": \"You are currently in anonymous mode. Conversations will not be saved.\",\n    \"search\": \"Search Conversations\"\n  },\n  \"chat\": {\n    \"web\": \"Web search\",\n    \"web-aria\": \"Toggle web searching feature\",\n    \"placeholder\": \"Type your message...\",\n    \"recall\": \"History Recall\",\n    \"recall-desc\": \"Detected unsent messages from last time. They have been restored for you.\",\n    \"recall-cancel\": \"Cancel\",\n    \"placeholder-enter\": \"Write something... (Enter to send, Shift + Enter for new line)\",\n    \"placeholder-raw\": \"Write something...\",\n    \"send-message\": \"Send Message\",\n    \"send-message-desc\": \"Are you sure you want to send this message?\",\n    \"actions\": {\n      \"upscale\": \"Zoom in\",\n      \"variant\": \"Change\",\n      \"reroll\": \"Redraw\",\n      \"subtle-upscale\": \"Subtle Zoom In\",\n      \"creative-upscale\": \"Creative Zoom\",\n      \"subtle-vary\": \"Subtle changes\",\n      \"strong-vary\": \"Strong change\",\n      \"region-vary\": \"Partial Redraw\",\n      \"zoom\": \"Scale\",\n      \"zoom-1\": {},\n      \"zoom-2x\": \"Zoom 2x\",\n      \"zoom-custom\": \"Custom Scaling\",\n      \"pan-left\": \"Left\",\n      \"pan-right\": \"Right\",\n      \"pan-up\": \"Upward\",\n      \"pan-down\": \"Downward\",\n      \"bookmark\": \"Like\"\n    },\n    \"empty-preview\": \"The input will be rendered here (Markdown syntax supported)\",\n    \"web-enable-toast\": \"Web search is enabled\",\n    \"web-disable-toast\": \"Web search is disabled\",\n    \"web-enable-tip\": \"Web search may consume more tokens\",\n    \"web-search\": \"Internet search\",\n    \"plugin\": \"Plugins\",\n    \"voice\": \"Speech Recognition\",\n    \"deep-thinking\": \"Deep Thinking\",\n    \"deep-thinking-enable-toast\": \"Deep Thinking Enabled\",\n    \"deep-thinking-enable-tip\": \"Deep thinking may result in slower output\",\n    \"deep-thinking-disable-toast\": \"Deep Thinking Closed\",\n    \"model-not-support-thinking-desc\": \"The current model does not support in-depth thinking\",\n    \"web-search-results\": \"{{count}} results found\",\n    \"web-search-results-hide\": \"Collapse search results\",\n    \"web-search-results-query\": \"Keywords Search\",\n    \"web-search-results-visit-source\": \"Visit source website\",\n    \"web-search-no-results\": \"No search results\",\n    \"web-page-summary\": \"Page-by-page summary\",\n    \"web-depth\": \"Search Depth\",\n    \"web-quick-search\": \"Instant Answers\",\n    \"web-detailed-search\": \"Detailed Search\",\n    \"web-enable-page-summary-toast\": \"Page-by-page summary enabled\",\n    \"web-enable-page-summary-tip\": \"A page-by-page summary may consume more tokens and slow down the output\",\n    \"web-disable-page-summary-toast\": \"Page-by-page summary closed\",\n    \"web-search-quick-toast\": \"Switched search depth to Quick Search\",\n    \"web-search-detailed-toast\": \"Switched search depth to detailed search\"\n  },\n  \"message\": {\n    \"copy\": \"Copy Message\",\n    \"save\": \"Save as File\",\n    \"use\": \"Use Message\",\n    \"stop\": \"Stop Answering\",\n    \"restart\": \"Restart Answer\",\n    \"copy-area\": \"Copy Selected Area\",\n    \"edit\": \"Edit messages\",\n    \"remove\": \"Delete a Message\",\n    \"save-image\": \"Save image\",\n    \"saving-image-prompt\": \"Image Generation in Progress\",\n    \"saving-image-prompt-desc\": \"Generating image, please wait...\",\n    \"saving-image-failed\": \"Image generation failed\",\n    \"saving-image-failed-prompt\": \"Image generation failed: {{reason}}\",\n    \"saving-image-success\": \"Image generated successfully\",\n    \"saving-image-success-prompt\": \"Image saved successfully.\",\n    \"sharing\": {\n      \"title\": \"Title\",\n      \"time\": \"Time\",\n      \"message\": \"Message\"\n    },\n    \"thinking-process\": \"Process of thinking\"\n  },\n  \"quota-description\": \"Spending quota for the message\",\n  \"buy\": {\n    \"choose\": \"Choose an amount\",\n    \"other\": \"Other\",\n    \"other-desc\": \"How many credits?\",\n    \"buy\": \"Buy {{amount}} credits\",\n    \"dalle\": \"DALL·E Image Generator\",\n    \"dalle-free\": \"DALL·E 2 Free Forever\",\n    \"flex\": \"Flexible Billing\",\n    \"input\": \"Input\",\n    \"output\": \"Output\",\n    \"learn-more\": \"Learn more\",\n    \"dialog-title\": \"Buy Credits\",\n    \"dialog-desc\": \"Are you sure you want to buy {{amount}} credits?\",\n    \"dialog-cancel\": \"Cancel\",\n    \"dialog-buy\": \"Buy\",\n    \"success\": \"Purchase successful\",\n    \"success-prompt\": \"You have successfully purchased {{amount}} credits.\",\n    \"failed\": \"Purchase failed\",\n    \"failed-prompt\": \"Failed to purchase credits. Please make sure you have enough balance.\",\n    \"gpt4-tip\": \"Tip: Web searching feature may consume more input credits\",\n    \"go\": \"Go\",\n    \"redeem\": \"Redeem\",\n    \"redeem-placeholder\": \"Please enter the redeem code\",\n    \"exchange-success\": \"Redeemed Successfully\",\n    \"exchange-success-prompt\": \"You have successfully redeemed {{amount}} credits.\",\n    \"exchange-failed\": \"Redemption Failed\",\n    \"exchange-failed-prompt\": \"Redemption failed: {{reason}}\",\n    \"buy-link\": \"Purchase\",\n    \"deeptrain-tip\": \"Tip: Once Deeptrain has reloaded to your wallet, come back here and click to buy the appropriate credits\",\n    \"not-config-link\": \"Purchase link is not configured in the backend\",\n    \"title\": \"My Credits\",\n    \"quota-info\": \"Credits can be used for all models on this platform. Pay as you go, suitable for flexible billing options.\",\n    \"deeptrain-step-1\": \"Select credits and click Buy\",\n    \"deeptrain-step-2\": \"Jump to Deeptrain Wallet Top-up\",\n    \"deeptrain-step-3\": \"After the top-up is successful, return here to buy again\",\n    \"deeptrain-step-4\": \"(If the wallet has enough balance, it will be automatically recharged after purchase)\",\n    \"plan-info\": \"Subscriptions allow use of in-subscription models at a fixed price per cycle, suitable for fixed long-term use options.\",\n    \"buy-description\": \"Please select the credits you want to buy\",\n    \"redeem-title\": \"Redeem Code\",\n    \"redeem-description\": \"Please enter your redemption code to claim your credits\"\n  },\n  \"pkg\": {\n    \"title\": \"Packages\",\n    \"go\": \"Go to Verify\",\n    \"cert\": \"Certification Package\",\n    \"cert-desc\": \"After real-name certification, you can get 50 credits (worth 5 CNY)\",\n    \"teen\": \"Student Package\",\n    \"teen-desc\": \"After real-name certification, teenagers (18 years old and under) can get an additional 150 credits (worth 15 CNY)\",\n    \"close\": \"Close\",\n    \"state\": {\n      \"true\": \"Received\",\n      \"false\": \"Not Received\"\n    },\n    \"manage\": \"My Packages\"\n  },\n  \"sub\": {\n    \"title\": \"Subscription\",\n    \"quota-link\": \"Looking for flexible billing? Buy credits\",\n    \"subscription-link\": \"Looking for fixed billing? Subscribe\",\n    \"dialog-title\": \"Subscription Plan\",\n    \"free\": \"Free\",\n    \"free-price\": \"Free Forever\",\n    \"basic\": \"Basic\",\n    \"standard\": \"Standard\",\n    \"pro\": \"Pro\",\n    \"plan-price\": \"{{money}} CNY/Month\",\n    \"include-tax\": \"Include Tax\",\n    \"enterprise\": \"Enterprise\",\n    \"enterprise-service\": \"Priority Service Support\",\n    \"enterprise-sla\": \"SLA Guarantee\",\n    \"enterprise-speed\": \"TPM Speed Increase\",\n    \"enterprise-security\": \"SOC-2 Standard Data Security Guarantee\",\n    \"enterprise-data\": \"Offsite Data Disaster Recovery Backup\",\n    \"enterprise-deploy\": \"Support Private Cloud Deployment\",\n    \"contact-sale\": \"Contact Sales\",\n    \"current\": \"Current Plan\",\n    \"subscribe\": \"Subscribe\",\n    \"upgrade\": \"Upgrade\",\n    \"downgrade\": \"Downgrade\",\n    \"renew\": \"Renew\",\n    \"cannot-select\": \"Cannot Select\",\n    \"select-time\": \"Select Subscription Time\",\n    \"migrate-plan\": \"Change Subscription Plan\",\n    \"migrate-plan-desc\": \"After changing the subscription, your subscription time will be recalculated based on the remaining days' price. (For example, downgrading will double the time, and upgrading will make up the difference)\",\n    \"price\": \"Price {{price}} CNY\",\n    \"price-tax\": \"Include Tax {{price}} CNY\",\n    \"upgrade-price\": \"Upgrade Fee {{price}} CNY (for reference only)\",\n    \"expired\": \"Subscription Remaining Days\",\n    \"time\": {\n      \"1\": \"1 Month\",\n      \"3\": \"3 Months\",\n      \"6\": \"6 Months\",\n      \"12\": \"1 Year\",\n      \"36\": \"3 Years\"\n    },\n    \"success\": \"Subscription successful\",\n    \"success-prompt\": \"You have successfully subscribed for {{month}} months.\",\n    \"migrate-success\": \"Plan change successful\",\n    \"migrate-success-prompt\": \"You have successfully changed your subscription plan.\",\n    \"failed\": \"Subscription failed\",\n    \"failed-prompt\": \"Failed to subscribe. Please make sure you have enough balance.\",\n    \"migrate-failed\": \"Plan change failed\",\n    \"migrate-failed-prompt\": \"Your subscription plan change failed.\",\n    \"plan-usage\": \"{{name}} uses {{times}} times per month\",\n    \"plan-tip\": \"Callable Model\",\n    \"disable\": \"This site's subscription feature has been turned off\",\n    \"plan-unlimited-usage\": \"{{name}} has unlimited uses\",\n    \"plan-not-support-relay\": \"Site subscription quota does not cover relay API. Please use flexible billing credits for relay API.\",\n    \"failed-quota-prompt\": \"Subscription failed. Your balance is insufficient ({{quota}} credits)\",\n    \"sub-migrate-failed-prompt\": \"Your subscription change failed: {{reason}}\",\n    \"month\": \"Month\",\n    \"year\": \"years\",\n    \"best-choice\": \"best choice\",\n    \"including-model\": \"Covered Models\",\n    \"including-model-tip\": \"Available model usage credits included in this subscription\",\n    \"none\": \"unsubscribed\",\n    \"plan-item-usage\": \"{{times}} Req\",\n    \"plan-item-unlimited-usage\": \"Infinite\",\n    \"year-earn-tip\": \"save {{percent}}\",\n    \"quota-manage\": \"Usage\",\n    \"expired-days\": \"Your subscription expires in {{days}} days\",\n    \"month-plan\": \"Monthly\",\n    \"year-plan\": \"Yearly\",\n    \"new\": \"New Scheme\",\n    \"select-duration\": \"Choose how long you want to subscribe\",\n    \"price-summary\": \"Price Summary\",\n    \"total-price\": \"Total Price\",\n    \"upgrade-price-label\": \"Upgrading Cost\",\n    \"upgrade-price-notice\": \"For  information only\",\n    \"upgrade-price-notice-tip\": \"The upgrade fee is for reference only, and the actual price is based on the exact calculation of the server.\",\n    \"refresh-days\": \"Your quota will refresh in {{refresh_days}} days\",\n    \"get-refresh-days\": \"Get started with quotas to get refresh dates\"\n  },\n  \"cancel\": \"Cancel\",\n  \"confirm\": \"Confirm\",\n  \"percent\": \"{{cent}}0%\",\n  \"file\": {\n    \"upload\": \"Upload\",\n    \"type\": \"Supports pdf, docx, pptx, xlsx, image, text and other formats\",\n    \"drop\": \"Drag and drop files here or click to upload\",\n    \"parse-error\": \"Parse Error\",\n    \"parse-error-prompt\": \"Parse Error: {{reason}}\",\n    \"max-length\": \"Content too long\",\n    \"max-length-prompt\": \"The content has been truncated due to the context length limit\",\n    \"over-size\": \"File too large\",\n    \"over-size-prompt\": \"The size of a single attachment cannot exceed {{size}} MB\",\n    \"large-file\": \"Large File Parsing\",\n    \"large-file-prompt\": \"Uploading and parsing large files, please wait patiently\",\n    \"number\": \"{{number}} files\",\n    \"zipper\": \"{{filename}} and {{number}} more ...\",\n    \"empty-file\": \"Empty File\",\n    \"empty-file-prompt\": \"File content is empty, has been automatically ignored\",\n    \"large-file-success\": \"Parsing Successful\",\n    \"large-file-success-prompt\": \"Large file parsed successfully in {{time}} seconds\",\n    \"file\": \"Files\",\n    \"parse-success-prompt\": \"File parsed successfully: {{file}}\",\n    \"uploading\": \"File upload on the way...\",\n    \"uploading-prompt\": \"Uploading file, please be patient\"\n  },\n  \"generate\": {\n    \"title\": \"AI Project Generator\",\n    \"input-placeholder\": \"Generate a Python game\",\n    \"failed\": \"Generation failed\",\n    \"reason\": \"Reason: \",\n    \"success\": \"Generation successful\",\n    \"success-prompt\": \"Project generated successfully! Please select the download format.\",\n    \"empty\": \"Generating...\",\n    \"download\": \"Download {{name}} format\"\n  },\n  \"api\": {\n    \"title\": \"API Key\",\n    \"copied\": \"Copied\",\n    \"copied-description\": \"API key has been copied to clipboard\",\n    \"learn-more\": \"Learn more\",\n    \"reset\": \"Reset Secret Key\",\n    \"reset-description\": \"Are you sure? This action cannot be undone. This will permanently reset the API key and the existing API key will expire.\"\n  },\n  \"service\": {\n    \"title\": \"New Version Available\",\n    \"version\": \"Version\",\n    \"description\": \"A new version is available. Do you want to update now?\",\n    \"update\": \"Update\",\n    \"offline-title\": \"Offline Mode\",\n    \"offline\": \"App is currently offline.\",\n    \"update-success\": \"Update Successful\",\n    \"update-success-prompt\": \"You have been updated to the latest version.\"\n  },\n  \"share\": {\n    \"title\": \"Share\",\n    \"share-conversation\": \"Share Conversation\",\n    \"description\": \"Share this conversation with others: \",\n    \"copy-link\": \"Copy Link\",\n    \"view\": \"View\",\n    \"success\": \"Share successful\",\n    \"failed\": \"Share failed\",\n    \"copied\": \"Copied\",\n    \"copied-description\": \"Link has been copied to clipboard\",\n    \"not-found\": \"Conversation not found\",\n    \"not-found-description\": \"Conversation not found. Please check if the link is correct or if the conversation has been deleted.\",\n    \"manage\": \"Sharing\",\n    \"sync-error\": \"Sync Error\",\n    \"name\": \"Conversation Title\",\n    \"time\": \"Time\",\n    \"action\": \"Action\",\n    \"empty\": \"You haven't shared any records yet. Share them now!\",\n    \"share-tip\": \"Go to the conversation bar and click the Share button to share the conversation\"\n  },\n  \"docs\": {\n    \"title\": \"Open Docs\"\n  },\n  \"invitation\": {\n    \"title\": \"Redeem Code\",\n    \"input-placeholder\": \"Please enter the redeem code\",\n    \"cancel\": \"Cancel\",\n    \"check\": \"Check\",\n    \"check-success\": \"Redeem Successful\",\n    \"check-success-description\": \"Redeem Successful! You have received {{amount}} credits. Start your AI journey!\",\n    \"check-failed\": \"Redeem Failed\",\n    \"invitation\": \"Gift\"\n  },\n  \"contact\": {\n    \"title\": \"Contact Us\",\n    \"community\": \"Join the community \"\n  },\n  \"settings\": {\n    \"title\": \"Settings\",\n    \"description\": \"Settings\",\n    \"version\": \"Version\",\n    \"language\": \"Language\",\n    \"sender\": \"Send Key\",\n    \"context\": \"Keep Context\",\n    \"history\": \"Max History Conversations\",\n    \"align\": \"Input Box Centered\",\n    \"memory\": \"Memory Usage\",\n    \"temperature\": \"Temperature\",\n    \"temperature-tip\": \"Random sampling ratio. High temperature produces more randomness, low temperature produces more concentrated and deterministic text.\",\n    \"max-tokens\": \"Maximum number of response tokens\",\n    \"max-tokens-tip\": \"Maximum number of reply tokens. Exceeding this value will be truncated (too high a value may cause the request to fail due to exceeding the model's maximum token limit).\",\n    \"top-p\": \"Nucleus Sampling Probability Threshold\",\n    \"top-p-tip\": \"(TopP) The higher the probability value, the higher the randomness generated; the lower the value, the higher the certainty generated.\",\n    \"top-k\": \"Sample Candidate Set Size\",\n    \"top-k-tip\": \"(TopK) Candidate set size. Larger values increase randomness, smaller values increase certainty.\",\n    \"presence-penalty\": \"Presence Penalty\",\n    \"presence-penalty-tip\": \"(PresencePenalty) Controls the likelihood of new topics generated by the model. Increasing this value can increase the likelihood of talking about new topics.\",\n    \"frequency-penalty\": \"Frequency Penalty\",\n    \"frequency-penalty-tip\": \"(FrequencyPenalty) Controls the degree of word repetition generated by the model. Increasing this value can reduce the possibility of word repetition.\",\n    \"repetition-penalty\": \"Repetition Penalty\",\n    \"repetition-penalty-tip\": \"(RepetitionPenalty) Controls the degree of repetition generated by the model. Increasing this value can reduce repetition, but may cause the model to generate incoherent text (similar to FrequencyPenalty).\",\n    \"reset-settings\": \"Reset all settings\",\n    \"reset-settings-description\": \"Are you sure? This action cannot be undone. This will permanently reset all settings.\",\n    \"hide-model\": \"Hide Model Selection\",\n    \"hide-toolbar\": \"Hide toolbar by default\",\n    \"hide-toolbar-text\": \"Hide toolbar text\",\n    \"theme\": \"Themes\",\n    \"light\": \"Light\",\n    \"dark\": \"Dark\"\n  },\n  \"article\": {\n    \"title\": \"Batch Generate Articles\",\n    \"input-placeholder\": \"Please enter the article titles (one per line)\",\n    \"prompt-placeholder\": \"Please enter the preset (to help AI generate articles, e.g.: academic paper format, 800 words)\",\n    \"web-checkbox\": \"Enable web search function\",\n    \"generate\": \"Generate\",\n    \"progress-title\": \"Generating ({{current}}/{{total}})\",\n    \"generate-success\": \"Generation Successful\",\n    \"generate-success-prompt\": \"Articles generated successfully! Please select the download format.\",\n    \"generate-failed\": \"Generation Failed\",\n    \"generate-failed-prompt\": \"Failed to generate articles. Please check your network and try again.\",\n    \"download-format\": \"Download {{name}} format\"\n  },\n  \"admin\": {\n    \"dashboard\": \"Data analysis\",\n    \"users\": \"Admin\",\n    \"broadcast\": \"Broadcast\",\n    \"channel\": \"Channels\",\n    \"settings\": \"System\",\n    \"prize\": \"Billing\",\n    \"billing-today\": \"Billing Today\",\n    \"billing-month\": \"Billing This Month\",\n    \"subscription-users\": \"Subscription Users\",\n    \"seat\": \"Seat\",\n    \"model-chart\": \"Model Usage Statistics\",\n    \"request-chart\": \"Request Statistics\",\n    \"billing-chart\": \"Revenue Statistics\",\n    \"error-chart\": \"Error Statistics\",\n    \"requests\": \"Requests\",\n    \"times\": \"Times\",\n    \"empty\": \"Empty\",\n    \"cancel\": \"Cancel\",\n    \"confirm\": \"Confirm\",\n    \"invitation\": \"Invitation Code Management\",\n    \"code\": \"Code\",\n    \"quota\": \"Quota\",\n    \"type\": \"Type\",\n    \"used\": \"Status\",\n    \"number\": \"Number\",\n    \"username\": \"Username\",\n    \"month\": \"Month\",\n    \"poster\": \"Poster\",\n    \"post-at\": \"Posted At\",\n    \"broadcast-content\": \"Broadcast Content\",\n    \"create-broadcast\": \"Create Broadcast\",\n    \"broadcast-placeholder\": \"Please enter the broadcast content\",\n    \"post\": \"Post\",\n    \"post-success\": \"Post Successful\",\n    \"post-success-prompt\": \"Broadcast posted successfully.\",\n    \"post-failed\": \"Post Failed\",\n    \"post-failed-prompt\": \"Post failed. Reason: {{reason}}\",\n    \"level\": \"Level\",\n    \"is-admin\": \"Admin\",\n    \"used-quota\": \"Used Quota\",\n    \"is-subscribed\": \"Subscribed\",\n    \"total-month\": \"Total Months\",\n    \"enterprise\": \"Enterprise\",\n    \"action\": \"Action\",\n    \"search-username\": \"Search Username\",\n    \"quota-action\": \"Quota Change\",\n    \"quota-action-desc\": \"Please enter the quota change value (positive for increase, negative for decrease)\",\n    \"subscription-action\": \"Subscription Time Management\",\n    \"subscription-action-desc\": \"Please set a subscription expiration time for user {{username}}\",\n    \"operate-success\": \"Operation Successful\",\n    \"operate-success-prompt\": \"Your operation has been successfully executed.\",\n    \"operate-failed\": \"Operation Failed\",\n    \"operate-failed-prompt\": \"Operation failed. Reason: {{reason}}\",\n    \"updated-at\": \"Updated on \",\n    \"used-true\": \"Used\",\n    \"used-false\": \"Unused\",\n    \"generate\": \"Generate\",\n    \"generate-result\": \"Generate Result\",\n    \"error\": \"Request Failed\",\n    \"channels\": {\n      \"id\": \"Channel ID\",\n      \"name\": \"Name\",\n      \"name-tip\": \"Channel name, used to identify the channel\",\n      \"name-placeholder\": \"Please enter the channel name\",\n      \"type\": \"Type\",\n      \"priority\": \"Priority\",\n      \"priority-tip\": \"When there are multiple channels, requests are made according to priority order. Higher priority takes precedence.\",\n      \"weight\": \"Weight\",\n      \"weight-tip\": \"When the priority is the same, load balancing calls are performed according to the weight ratio\",\n      \"retry\": \"Max Retry\",\n      \"retry-tip\": \"Maximum number of retries when the channel request fails\",\n      \"model\": \"Model\",\n      \"secret\": \"Secret\",\n      \"secret-placeholder\": \"Please enter the secret, format: {{format}} (<> not filled)\\nWhen there are multiple secrets, one is randomly selected when requesting the load\",\n      \"endpoint\": \"Endpoint\",\n      \"endpoint-placeholder\": \"Please enter the endpoint (i.e. proxy)\",\n      \"mapper\": \"Model Mapper\",\n      \"mapper-tip\": \"Model name conversion to achieve asymmetric model requests\",\n      \"mapper-placeholder\": \"Please enter the model mapper, one per line, format: model>model\\nThe former is the requested model, and the latter is the mapped model (which needs to exist in the model), separated by > in the middle\\nPreceding with ! indicates that the original model is not included in the available range of this channel, e.g.: !gpt-4-slow>gpt-4, then gpt-4 will not be covered in the available models that can be requested in this channel\",\n      \"group\": \"User Group\",\n      \"group-tip\": \"User group. Groups that are not included will not be included in the available range of this channel (when the group is empty, all users can use this channel)\",\n      \"state\": \"State\",\n      \"action\": \"Action\",\n      \"edit\": \"Edit Channel\",\n      \"enable\": \"Enable Channel\",\n      \"disable\": \"Disable Channel\",\n      \"delete\": \"Delete Channel\",\n      \"create\": \"Create Channel\",\n      \"search-model\": \"Search Model\",\n      \"fill-template-models\": \"Fill Template Models ({{number}})\",\n      \"add-custom-model\": \"Add Custom Model (Multiple models are separated by spaces)\",\n      \"add-model\": \"Add Model\",\n      \"clear-models\": \"Clear All Models\",\n      \"advanced\": \"Advanced settings\",\n      \"group-placeholder\": \"{{length}} groups selected\",\n      \"group-desc\": \"User type grouping. Groups not included will not be included in the available scope of this channel (when the grouping is empty, all users can use this channel). No need to set grouping for non-special cases.\",\n      \"groups\": {\n        \"anonymous\": \"Anonymous user\",\n        \"normal\": \"Normal\",\n        \"basic\": \"Basic Subscribers\",\n        \"standard\": \"Standard Subscribers\",\n        \"pro\": \"Pro Subscribers\",\n        \"admin\": \"Admin user\",\n        \"custom\": \"пользовательская подгруппа\"\n      },\n      \"joint\": \"Connect upstream\",\n      \"joint-endpoint\": \"Upstream address\",\n      \"joint-endpoint-placeholder\": \"Please enter the API address of the upstream CoAI, for example: https://api.chatnio.net\",\n      \"joint-secret\": \"API keys\",\n      \"joint-secret-placeholder\": \"Please enter the API key for upstream CoAI\",\n      \"sync-failed\": \"Sync Failed\",\n      \"sync-failed-prompt\": \"Address could not be requested or model market model is empty\\n(Endpoint: {{endpoint}})\",\n      \"sync-success\": \"Sync successful.\",\n      \"sync-success-prompt\": \"{{length}} models were added from upstream synchronization.\",\n      \"upstream-endpoint-placeholder\": \"Please enter the upstream OpenAI address, e.g. https://api.openai.com\",\n      \"sync-secret-placeholder\": \"Please enter API key for upstream channel\",\n      \"proxy-type\": \"Proxy Type\",\n      \"proxy-endpoint\": \"Proxy address\",\n      \"proxy-endpoint-placeholder\": \"Please enter forward proxy address, e.g.: socks5://example.com:1080\",\n      \"proxy-desc\": \"Forward proxy, supports HTTP/HTTPS/SOCKS5 proxy (for reverse proxy, please fill in the access point; no need to set forward proxy in non-special cases)\",\n      \"proxy-username\": \"Proxy Username\",\n      \"proxy-username-placeholder\": \"Please enter the proxy's authentication username (optional)\",\n      \"proxy-password\": \"Proxy Password\",\n      \"proxy-password-placeholder\": \"Please enter the proxy's authentication password (optional)\",\n      \"search-channel\": \"Search channel name, model, key...\",\n      \"retry-name\": \"Try again\",\n      \"secret-number\": \"Number of keys\",\n      \"loading\": \"Loading...\",\n      \"new\": \"Add new channel\",\n      \"import\": \"Import existing channels\",\n      \"first-message-as-user\": \"Convert first message to user message by default\",\n      \"first-message-as-user-tip\": \"If on, will be converted to user role when first message is assistant role\",\n      \"first-message-as-user-desc\": \"Some models (such as DeepSeek) do not support the first message as an assistant role. Turn this option on to convert the first message of the assistant role into a user role.\",\n      \"merge-consecutive-user-messages\": \"Merge continuous user messages\",\n      \"merge-consecutive-user-messages-tip\": \"If on, will be merged into one message when both consecutive messages are user messages\",\n      \"merge-consecutive-user-messages-desc\": \"Some models, such as DeepSeek, do not support two consecutive user messages, enabling this option can combine two consecutive user messages into one message.\"\n    },\n    \"charge\": {\n      \"id\": \"ID\",\n      \"type\": \"Type\",\n      \"model\": \"Model\",\n      \"quota\": \"Quota\",\n      \"action\": \"Action\",\n      \"input\": \"Input\",\n      \"output\": \"Output\",\n      \"support-anonymous\": \"Support Anonymous\",\n      \"non-billing\": \"Non-Billing\",\n      \"times-billing\": \"Times Billing\",\n      \"token-billing\": \"Token Billing\",\n      \"anonymous\": \"Support Anonymous Call\",\n      \"time-count\": \"Single Request Quota\",\n      \"input-count\": \"Input Quota\",\n      \"output-count\": \"Output Quota\",\n      \"add-rule\": \"Add Rule\",\n      \"update-rule\": \"Update Rule\",\n      \"unused-model\": \"Some model billing rules are not set\",\n      \"unused-model-tip\": \"Models without billing rules set up will not be available for regular users to request to avoid losses\",\n      \"sync\": \"Sync upstream\",\n      \"sync-option\": \"Synchronization Options\",\n      \"sync-site\": \"Upstream address\",\n      \"sync-tip\": \"Synchronize upstream billing rules\",\n      \"sync-placeholder\": \"Please enter the API address of the upstream CoAI, for example: https://api.chatnio.net\",\n      \"sync-failed\": \"Sync Failed\",\n      \"sync-failed-prompt\": \"Address could not be requested or billing rule is empty\\n(Endpoint: {{endpoint}})\",\n      \"sync-prompt\": \"The rules for {{length}} models have been fetched from upstream and will affect the rules for the current {{influence}} models. Do you want to continue?\",\n      \"sync-overwrite\": \"Overwrite existing rules\",\n      \"sync-confirm\": \"Confirm Sync\",\n      \"sync-builtin\": \"Built-in price in the app\",\n      \"usd-currency\": \"USD to CNY exchange rate\",\n      \"group-pricing\": \"User Group Pricing Ratio\",\n      \"new-group\": \"Group ID\",\n      \"add-group\": \"Add new group\",\n      \"group-pricing-description\": \"The user group pricing ratio can be used to distinguish the billing price of different user groups, with a base ratio of 1. The user price = model price * ratio\",\n      \"group-pricing-sample\": \"Example: If the model charges 0.2 credits and the user group ratio is 0.8, the actual charge is 0.2 * 0.8 = 0.16 credits\",\n      \"default-price\": \"Default Prices\",\n      \"custom-price\": \"Override Price\",\n      \"group-pricing-tip\": \"Multiplier can be used to distinguish the billing price of different user groups, * * The base multiplier is 1 * *, that is, * * user price = model price * multiplier * *\\n\\nExample: If the model charges 0.2 points and the user group ratio is 0.8, the actual charge is 0.2 x 0.8 = 0.16 points\\n\\n- Purchase multiplier: when the user purchases credits, the deduction price multiplier\\n- Consumption multiplier: the deduction price multiplier when the user spends credits\",\n      \"new-group-price\": \"Price\",\n      \"add-new-group\": \"Add New User Group\",\n      \"new-group-buy-price\": \"Purchase Rate\",\n      \"new-group-consume-price\": \"Consumption ratio\",\n      \"new-group-description\": \"Description\",\n      \"update-group\": \"Update User\"\n    },\n    \"system\": {\n      \"general\": \"General Settings\",\n      \"search\": \"Web Search\",\n      \"mail\": \"SMTP Settings\",\n      \"save\": \"Save\",\n      \"backend\": \"Backend Domain\",\n      \"backendTip\": \"Backend domain name (docker installation default path is /api), used for receiving callbacks and storage, etc. Default is empty\\nExample: {{backend}}\",\n      \"mailHost\": \"Mail Host\",\n      \"mailPort\": \"SMTP Port\",\n      \"mailUser\": \"Username\",\n      \"mailPass\": \"Password\",\n      \"searchEndpoint\": \"Search Endpoint\",\n      \"searchQuery\": \"Max Search Results\",\n      \"searchTip\": \"[SearXNG](https://github.com/searxng/searxng) is an open-source search engine that provides networked search capabilities. SearXNG Docker Privatization Deployment Example: [SearXNG Docker](https://github.com/zmh-program/searxng)\",\n      \"mailFrom\": \"Sender\",\n      \"test\": \"Test outgoing\",\n      \"updateRoot\": \"Change Root Password\",\n      \"updateRootTip\": \"Please proceed with caution. After changing the root password, you will need to log in again.\",\n      \"updateRootPlaceholder\": \"Please enter a new root password\",\n      \"updateRootRepeatPlaceholder\": \"Please enter the new root password again\",\n      \"title\": \"Site name\",\n      \"titleTip\": \"Site name to display in the site title. Leave blank for default.\",\n      \"logo\": \"Site Logo\",\n      \"logoTip\": \"Link to the site logo to display in the site title, leave blank for default (e.g. {{logo}})\",\n      \"backendPlaceholder\": \"Backend callback domain name, empty by default, required for accepting callbacks\",\n      \"docs\": \"Document Link\",\n      \"docsTip\": \"Document link, leave blank for default https://coai.dev\",\n      \"file\": \"File Parsing Service\",\n      \"filePlaceholder\": \"File parsing service, leave blank for default https://blob.coai.dev (stability not guaranteed)\",\n      \"fileTip\": \"For file parsing services, please refer to the [coai-blob-service] (https://github.com/zmh-program/blob-service) project to build.\",\n      \"site\": \"Site Settings\",\n      \"quota\": \"User Initial Points\",\n      \"quotaTip\": \"Credits given after user registration\",\n      \"announcement\": \"Site Announcement\",\n      \"announcementPlaceholder\": \"Please enter a site announcement (Markdown/HTML format supported)\",\n      \"mailEnableWhitelist\": \"Enable domain suffix whitelist\",\n      \"mailWhitelist\": \"Domain Suffix Whitelist\",\n      \"mailWhitelistSelected\": \"{{length}} domain email selected\",\n      \"mailWhitelistSearchPlaceholder\": \"Search Domain Suffixes\",\n      \"customWhitelistPlaceholder\": \"Please enter a list of custom domain suffixes (which will appear in the list of options to choose from), separated by commas, e.g.: example.com, example.net\",\n      \"buyLink\": \"Buy Link\",\n      \"buyLinkPlaceholder\": \"Please enter the card secret purchase link, leave blank to not show the purchase button\",\n      \"mailConfNotValid\": \"SMTP send parameters are not configured correctly, mailbox verification is disabled\",\n      \"contact\": \"Contact Information\",\n      \"contactPlaceholder\": \"Please enter contact information (Markdown/HTML supported)\",\n      \"common\": \"General Settings\",\n      \"article\": \"Batch Post Generation Feature Grouping\",\n      \"articleTip\": \"Batch post generation function grouping, after checking the current user group can use batch post generation function\",\n      \"generate\": \"AI Project Builder Grouping\",\n      \"generateTip\": \"AI project generator grouping, after checking the current user group can use AI project generator\",\n      \"groupPlaceholder\": \"{{length}} groups selected\",\n      \"cache\": \"Cacheable Model\",\n      \"cacheTip\": \"Cacheable model, after checking the current model can be cached and hit the cache\",\n      \"cachePlaceholder\": \"{{length}} models selected\",\n      \"cacheAll\": \"Make All Cacheable\",\n      \"cacheFree\": \"Make free model cacheable\",\n      \"cacheNone\": \"Make All Uncached\",\n      \"cacheExpired\": \"Cache Expiration Time\",\n      \"cacheExpiredTip\": \"Cache expiration time (in seconds), default 1 hour\",\n      \"cacheSize\": \"Max Cache Likelihood Size\",\n      \"cacheSizeTip\": \"Maximum cache likelihood, that is, the maximum cache likelihood of the same type of input parameter. If the parameter is 1, the maximum cache content is 1, and the requested content will be directly hit. If the parameter is 4, there are 4 returned contents, and the requested content will be hit one of them.\",\n      \"closeRegistration\": \"Enrollment paused\",\n      \"closeRegistrationTip\": \"Registration is paused, new users will not be able to register after closing\",\n      \"footer\": \"Footer Infor\",\n      \"footerPlaceholder\": \"Please enter footer information (Markdown/HTML format supported)\",\n      \"authFooter\": \"Hide footer after login\",\n      \"relayPlan\": \"Subscription Quota Support Staging API\",\n      \"relayPlanTip\": \"Subscription quota supports the transit API, after opening the transit API billing will give priority to the use of user subscription quota\\n(Tip: Subscription is a quota of times, the model of billing for tokens may affect the cost)\",\n      \"searchQueryTip\": \"Maximum number of search results, default is 5\",\n      \"searchPlaceholder\": \"SearXNG Service Access Point (e.g. http://ip: 7980)\",\n      \"image_store\": \"Picture storage\",\n      \"image_storeTip\": \"Images generated by the OpenAI channel DALL-E will be stored on the server to prevent invalidation of the images\",\n      \"image_storeNoBackend\": \"No backend domain configured, cannot enable image storage\",\n      \"closeRelay\": \"Turn off Staging API\",\n      \"closeRelayTip\": \"Turn off the staging API, the staging API will not be available after turning off\",\n      \"debugMode\": \"debugging mode\",\n      \"debugModeTip\": \"Debug mode, after turning on, the log will output detailed request parameters and other logs for troubleshooting\",\n      \"operation\": \"Operational Settings\",\n      \"chat\": \"Chat Settings\",\n      \"payment\": \"Payment Settings\",\n      \"epayTitle\": \"Easy to pay\",\n      \"epayEnabled\": \"Enable EasyPay\",\n      \"epayDomain\": \"EasyPay Domain\",\n      \"epayDomainPlaceholder\": \"Please enter an EasyPay domain name, such as: https://pay.example.com\",\n      \"epayMethods\": \"Payment Method\",\n      \"epayMethodsPlaceholder\": \"Check enabled payment methods ({{length}} selected)\",\n      \"epayBusinessId\": \"Merchant ID\",\n      \"epayBusinessIdPlaceholder\": \"Please enter the EasyPay Merchant ID\",\n      \"epayBusinessKey\": \"Merchant Key\",\n      \"epayBusinessKeyPlaceholder\": \"Please enter the EasyPay merchant key\",\n      \"security\": \"Security Settings\",\n      \"securityCheckType\": \"audit mode\",\n      \"securityCheckTypePlaceholder\": \"Please select a review type\",\n      \"securityTextDatabase\": \"Blacklist Thesaurus\",\n      \"securityTextDatabasePlaceholder\": \"Please enter the blacklist vocabulary, separated by spaces in the middle of the words, such as: Sensitive word 1 Sensitive word 2\",\n      \"securityRegexDatabase\": \"Regular Blacklist Expression\",\n      \"securityRegexDatabasePlaceholder\": \"Please enter a regular blacklist expression, separated by line breaks in the middle of the expression, for example:\\n^ Sensitive Words 1$\\n^ Sensitive word 2$\",\n      \"securityBaiduApiKey\": \"Baidu Cloud Audit API Key\",\n      \"securityBaiduApiKeyPlaceholder\": \"Please enter Baidu Cloud Review API Key\",\n      \"securityBaiduSecretKey\": \"Baidu Cloud Audit Secret Key\",\n      \"securityBaiduSecretKeyPlaceholder\": \"Please enter Baidu Cloud Review Secret Key\",\n      \"securityCheckModels\": \"Specific Audit Model\",\n      \"securityCheckModelsPlaceholder\": \"{{length}} specific audit models selected\",\n      \"securityCheckModelsTip\": \"Specific model review. After checking, the current model can be reviewed by the specific audit model. * * By default, all models will be reviewed according to the audit mode * *. If the specific audit model is selected, * * will only be reviewed according to the specific audit model * *, * * other models will not be reviewed * *\",\n      \"securityBaiduTip\": \"Baidu Cloud Audit Mode, Baidu Cloud Audit * * API Key * * and * * Secret Key * * are required \\n For more information and to configure audit strategy granularity, please refer to [Baidu Cloud Audit Quick Start] (https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy) \\n Prohibited vocabulary review strategy Please configure the policy in the Baidu Cloud console according to the Baidu Cloud document above\",\n      \"securityTypes\": {\n        \"none\": \"No Audit Mode\",\n        \"dict\": \"Text Thesaurus Review Mode\",\n        \"regex\": \"Text regular review mode\",\n        \"baidu\": \"Baidu Cloud Audit Mode\",\n        \"custom\": \"Custom backend audit mode\"\n      },\n      \"epayTip\": \"EasyPay is a third-party aggregated payment agreement in the market * * Generic * *. * * It is not a separate payment or software * *. You can choose the platform according to the situation. * * We do not make any recommendations and liability guarantees * *.\\nIf you are qualified to build your own ePayment platform, or directly connect to someone else's ePayment platform: ePayment platforms generally include two platforms: * * EasyPay * * (business/self-employed collections are relatively stable income monthly settlement) and * * CodePayment * * (personal collection codes have low real-time arrival rates).\\nEasyPay settings, please note that you must click on the * * Enable EasyPay * * option to enable EasyPay\\nEasyPay needs to configure the callback domain name, please configure the * * back-end domain name * * in the * * General Settings * * before the normal asynchronous callback\",\n      \"epayAggregation\": \"Aggregated payment model\",\n      \"epayAggregationTip\": \"Aggregate payment mode, clicking on it will not select the payment method * * directly to the aggregate payment page * *. Please make sure your EasyPay supports aggregate payment model.\",\n      \"prompt_store\": \"Prompt Record Storage\",\n      \"prompt_storeTip\": \"Prompt record storage, after opening, the user's Prompt record will be stored on the server\",\n      \"customTitle\": \"custom theming\",\n      \"customJS\": \"Custom JS\",\n      \"customJSTip\": \"Please enter custom JS\",\n      \"customCSS\": \"Custom Project CSS\",\n      \"customCSSTip\": \"Please enter custom CSS\",\n      \"custom\": \"Theme Settings\",\n      \"customJs\": \"Custom JS\",\n      \"customJsPlaceholder\": \"Please enter custom JS\",\n      \"customCss\": \"Custom Project CSS\",\n      \"customCssPlaceholder\": \"Please enter custom CSS\",\n      \"searchCrop\": \"Turn on results truncation\",\n      \"searchCropTip\": \"Turn on result truncation, if the number of characters in the search result content exceeds the maximum number of characters, the content will be truncated\",\n      \"searchCropLen\": \"Maximum Result Characters\",\n      \"searchEngines\": \"Search Engine Settings\",\n      \"searchEnginesPlaceholder\": \"{{length}} search engines selected\",\n      \"searchEnginesSearchPlaceholder\": \"Please enter the search engine name, ex: Google\",\n      \"searchEnginesEmptyTip\": \"When the search engine is empty, the default search engine configured in SearXNG is used by default\",\n      \"searchSafeSearch\": \"SafeSearch Mode\",\n      \"searchSafeSearchModes\": {\n        \"none\": \"Turn off\",\n        \"moderation\": \"Medium\",\n        \"strict\": \"Demanding\"\n      },\n      \"searchImageProxy\": \"Turn on image proxy\",\n      \"searchImageProxyTip\": \"Image proxy, the image returned by the search engine after opening will be loaded through the SearXNG service node proxy\",\n      \"searchTest\": \"Search Quizzes\",\n      \"searchTestTip\": \"Search test, enter the query for search test\",\n      \"mailProtocol\": \"Shipping Agreement\",\n      \"description\": \"Website Description\",\n      \"descriptionTip\": \"Website description, used for description in SEO, leave blank for default\",\n      \"uploadFaviconSuccess\": \"Logo uploaded successfully! Remember to save to apply the new logo\",\n      \"customHtml\": \"Custom HTML\",\n      \"customHtmlPlaceholder\": \"Please enter custom HTML\",\n      \"customThemeAlert\": \"Note: If you use security protection services such as WAF (such as Cloudflare WAF, Long Pavilion, 1 Panel WAF, Pagoda WAF), your custom theme may be mistaken for malicious code by WAF and be blocked with an error code such as 403 Forbidden. Please check your WAF configuration or turn off the WAF configuration.\",\n      \"gaTrackingId\": \"Google Analytics v3\",\n      \"gaTrackingIdPlaceholder\": \"Google Analytics ID \",\n      \"displayCurrency\": \"Displayed Currency\",\n      \"displayCurrencyTip\": \"Website Display Currency Unit\",\n      \"update\": \"Updated\",\n      \"group\": \"Grouping\",\n      \"group-price\": \"Grouping Ratio\",\n      \"token-group\": \"Token grouping\",\n      \"token-group-tip\": \"Token grouping, if checked, the current grouping will support token grouping, this grouping will be displayed in all user-selectable token groups (all built-in groups cannot turn on token grouping)\",\n      \"edit\": \"Edit\",\n      \"delete\": \"Delete\",\n      \"type\": \"Type\",\n      \"actions\": \"Operation\",\n      \"buy-price\": \"Purchase Rate\",\n      \"consume-price\": \"Consumption ratio\",\n      \"gravatar\": \"Gravatar avatar\",\n      \"gravatarPlaceholder\": \"Gravatar proxy address, leave blank to disable Gravatar avatar by default\",\n      \"oauth\": {\n        \"title\": \"Open sign settings\",\n        \"wechat\": \"WeChat login\",\n        \"google\": \"Google Login\",\n        \"github\": \"Github Login\",\n        \"telegram\": \"Telegram Login\",\n        \"enable\": \"Enable\",\n        \"disabled\": \"Disable\",\n        \"client_id\": \"Client ID\",\n        \"client_id_placeholder\": \"Please enter Client ID\",\n        \"client_secret\": \"Client Secret\",\n        \"client_secret_placeholder\": \"Please enter the client secret\",\n        \"redirect_uri\": \"Redirect URIs\",\n        \"redirect_uri_placeholder\": \"Please enter the redirect uri\",\n        \"scope\": \"Authorization scope\",\n        \"scope_placeholder\": \"Please enter the scope of authorization\",\n        \"auth_url\": \"Authorisation URL\",\n        \"auth_url_placeholder\": \"Please enter the authorization URL\",\n        \"token_url\": \"Token URL\",\n        \"token_url_placeholder\": \"Please enter the token URL\",\n        \"user_info_url\": \"User Information URL\",\n        \"user_info_url_placeholder\": \"Please enter the user information URL\",\n        \"bot_token\": \"Bot Token\",\n        \"bot_token_placeholder\": \"Please enter the robot token\",\n        \"bot_name\": \"Bot name\",\n        \"bot_name_placeholder\": \"Please enter bot name\",\n        \"rainbow\": \"Rainbow Aggregation Login\",\n        \"methods\": \"Login method\",\n        \"methods_placeholder\": \"Please enter the login method, separated by commas, such as: qq, wx, baidu, douyin\",\n        \"base_url\": \"Rainbow Aggregation Login Domain\",\n        \"base_url_placeholder\": \"Please enter rainbow aggregation login domain name, leave blank to default to rainbow aggregation official domain name https://u.cccyun.cc\",\n        \"require_email\": \"Enable Email Verfication\"\n      },\n      \"stripeTitle\": \"Stripe\",\n      \"stripeTip\": \"Stripe is a widely used international online payment system that supports multiple payment methods, including credit cards, debit cards, Apple Pay, Google Pay, and more. It provides users with a secure and convenient payment experience, which is especially suitable for businesses that need to process international payments.\",\n      \"stripeEnabled\": \"Enable Stripe\",\n      \"stripeSecretKey\": \"Stripe Secret Key\",\n      \"stripeSecretKeyPlaceholder\": \"Stripe Secret Key\",\n      \"stripeWebhookSecret\": \"Stripe Webhook Secret Key\",\n      \"stripeWebhookSecretPlaceholder\": \"Please enter Stripe Webhook Secret\",\n      \"securityCustomEndpoint\": \"Custom Audit Access Point\",\n      \"securityCustomEndpointPlaceholder\": \"Please enter a custom audit access point\",\n      \"securityCustomToken\": \"Custom Audit Token\",\n      \"securityCustomTokenPlaceholder\": \"Please enter a custom audit token\",\n      \"securityCustomTip\": \"Custom Audit Mode with * * Token * * and * * Access Point * * \\n The request and return format are consistent with Baidu Cloud Review, you can go to [Baidu Cloud Review Text Review Request Instructions] (https://cloud.baidu.com/doc/ANTIPORN/s/Rk3h6xb3i) for reference adaptation\",\n      \"wechatPayTitle\": \"WeChat Payment\",\n      \"wechatPayTip\": \"WeChat Pay has always been committed to providing users and businesses with secure, convenient and professional online payment services. With the core concept of \\\"WeChat Payment, More Than Payment\\\", we have created a variety of convenient services and application scenarios for individual users, providing professional collection capabilities, operational capabilities, fund settlement solutions, and security for all kinds of enterprises and small and micro merchants. Enterprises, products, stores, and users have been connected through WeChat, making smart life a reality.\",\n      \"wechatPayEnabled\": \"Enable Weixinpay Payment\",\n      \"wechatPayAppId\": \"WeChat Pay App ID\",\n      \"wechatPayAppIdPlaceholder\": \"Please enter WeChat Pay App ID\",\n      \"wechatPayMchId\": \"WeChat Pay Merchant ID\",\n      \"wechatPayMchIdPlaceholder\": \"Please enter WeChat Pay merchant number\",\n      \"wechatPayKey\": \"WeChat Pay API v3 Key\",\n      \"wechatPayKeyPlaceholder\": \"Please enter WeChat Pay API v3 key\",\n      \"wechatPaySerialNo\": \"WeChat Payment Platform Certificate Serial Number\",\n      \"wechatPaySerialNoPlaceholder\": \"Please enter the WeChat Payment Platform certificate serial number\",\n      \"wechatPayCertificate\": \"WeChat Payment Platform Certificate\",\n      \"wechatPayCertificatePlaceholder\": \"Paste your WeChat Pay platform certificate here\",\n      \"wechatPayCertificateTip\": \"The merchant receives the returned content of the API v3 interface, and needs to use the public key of the certificate for verification. In addition, some sensitive information parameters (such as name, ID number) also need to be encrypted with the public key of the certificate for transmission. For details, see [WeChat Payment Platform Certificate] (https://pay.weixin.qq.com/doc/v3/merchant/4012068814)\",\n      \"searchLLMExtract\": \"Enable LLM keyword extraction\",\n      \"searchLLMExtractTip\": \"Using the LLM model to intelligently extract search keywords can improve search accuracy\",\n      \"searchLLMModel\": \"Keyword extraction model\",\n      \"searchLLMModelPlaceholder\": \"Select a model for extracting keywords\",\n      \"securityBlacklistIPs\": \"Blacklist IP\",\n      \"securityBlacklistIPsPlaceholder\": \"Please enter the blacklist IP\",\n      \"securityWhitelistIPs\": \"Whitelisted IP\",\n      \"securityWhitelistIPsPlaceholder\": \"Please enter the whitelist IP\",\n      \"securityWhitelistIPsTip\": \"Blacklisted IPs * * are only valid for rate-limiting middleware for API requests * *, if you want to limit other requests or front-end access, please use security services such as WAF\",\n      \"securityAddIPAddress\": \"Add IP address\",\n      \"securityRemoveIPAddress\": \"Delete this address\",\n      \"preDeductQuota\": \"Enable withholding\",\n      \"preDeductQuotaTip\": \"When turned on, fees will be withheld at the start of the request, and when turned off, fees will be deducted at the end of the request\",\n      \"affiliateTitle\": \"Affiliate Marketing Settings\",\n      \"affiliateEnabled\": \"Enable affiliate marketing\",\n      \"affiliateCommissionRate\": \"Commission Rates\",\n      \"affiliateMinWithdraw\": \"Minimum Withdrawal Amount ( \",\n      \"affiliateAllowExistingBind\": \"Allow registered users to bind marching codes\",\n      \"realtime\": {\n        \"title\": \"WebSocket Live Stream Configuration\",\n        \"wsBufferSize\": \"WS Buffer Size\",\n        \"wsBufferSizeTip\": \"Controls the queue length of the server downstream sharding to the front end. Smaller (e.g. 1) reduces the tail wait after the end of the upstream; larger (e.g. 24) is compatible with the old behavior but may have a longer tail.\",\n        \"wsAggregate\": \"WS Fragmentation Aggregation\",\n        \"wsAggregateTip\": \"After enabling, aggregate multiple small shards by time window and then issue them, reducing the frequency of front-end re-rendering and improving smoothness. If closed, each shard is issued immediately (old behavior).\",\n        \"wsAggregateWindow\": \"WS Aggregation Time Window (ms)\",\n        \"wsAggregateWindowTip\": \"Time window for fragment aggregation, 15-33ms recommended. The larger the value, the more you merge and the smoother the refresh, but the first paragraph may be a little later.\"\n      },\n      \"hideKeyDocs\": \"Hide key page docking guide\",\n      \"xunhupayTitle\": \"Tiger Pepper Payment\",\n      \"xunhupayTip\": \"Tiger Pepper is an aggregated payment platform that supports various payment methods such as WeChat and Alipay. After configuration, the user can top up with tiger pepper. WeChat and Alipay need to configure different app IDs and app Secrets respectively.\",\n      \"xunhupayWechatEnabled\": \"Enable Tiger Pepper WeChat\",\n      \"xunhupayAlipayEnabled\": \"Enable Tiger Pepper Alipay\",\n      \"xunhupayWechatAppId\": \"Tiger Pepper WeChat App ID\",\n      \"xunhupayWechatAppIdPlaceholder\": \"Please enter the app ID of Tiger Pepper WeChat Payment\",\n      \"xunhupayWechatAppSecret\": \"Tiger Pepper WeChat app Secret\",\n      \"xunhupayWechatAppSecretPlaceholder\": \"Please enter the app Secret of Tiger Pepper WeChat Payment (Key)\",\n      \"xunhupayAlipayAppId\": \"Tiger Pepper Alipay App ID\",\n      \"xunhupayAlipayAppIdPlaceholder\": \"Please enter the app ID of Tiger Pepper Alipay\",\n      \"xunhupayAlipayAppSecret\": \"Tiger Pepper Alipay App Secret\",\n      \"xunhupayAlipayAppSecretPlaceholder\": \"Please enter the app Secret of Tiger Pepper Alipay (key)\",\n      \"xunhupayEndpoint\": \"Tiger Pepper Interface Address\",\n      \"xunhupayEndpointPlaceholder\": \"https://api.xunhupay.com or https://api.dpweixin.com\",\n      \"autoTitle\": {\n        \"title\": \"Automatic session title\",\n        \"enabled\": \"Enable automatic titles\",\n        \"model\": \"Generate Model\",\n        \"modelPlaceholder\": \"Leave blank to use current session model\",\n        \"maxLen\": \"Maximum length of title\",\n        \"minMsgs\": \"Withdraw number of messages\",\n        \"overwrite\": \"Overwrite existing title\",\n        \"prompt\": \"Custom Prompt\",\n        \"promptPlaceholder\": \"{max_len} can be used as a placeholder\",\n        \"tip\": \"Use LLM to automatically summarize and set the session title after the first round of conversations. When the custom prompt word is set to empty, the default prompt word in the default configuration in CoAI is used.\"\n      }\n    },\n    \"user\": \"Users\",\n    \"invitation-code\": \"Invitation Code\",\n    \"invitation-manage\": \"Invitation code management\",\n    \"invitation-tips\": \"Invitation codes are used to redeem points. Each type of invitation code can only be used once by one user (can be used for publicity)\",\n    \"redeem-tips\": \"Redemption codes are used to redeem credits and can be used to pay for card issuance, etc.\",\n    \"redeem\": {\n      \"quota\": \"Credits\",\n      \"used\": \"Used Count\",\n      \"total\": \"Total\",\n      \"code\": \"Code\"\n    },\n    \"market\": {\n      \"title\": \"Marketplace\",\n      \"model-name\": \"model name\",\n      \"model-name-placeholder\": \"Please enter the model nickname (e.g. GPT-4)\",\n      \"model-id\": \"Former ID\",\n      \"model-id-placeholder\": \"Please enter the model ID (e.g. gpt-4-0613)\",\n      \"model-description\": \"Introduction to the Model\",\n      \"model-description-placeholder\": \"Please enter a model introduction\",\n      \"model-context\": \"High Context\",\n      \"model-context-tip\": \"Whether the model is a high context model (high context model files are not truncated by long content when parsed)\",\n      \"model-is-default\": \"Default Model\",\n      \"model-is-default-tip\": \"Whether the model is added to the default model list (models not added to the default model list will not appear in the home model list by default)\",\n      \"model-tag\": \"Model label\",\n      \"update-success\": \"Upgrade successful\",\n      \"update-success-prompt\": \"Model Marketplace updated successfully (refresh your browser to apply now)\",\n      \"update-failed\": \"Update failed\",\n      \"update-failed-prompt\": \"Update request failed for {{reason}}\",\n      \"model-image\": \"Model Picture\",\n      \"custom-image\": \"Custom Image\",\n      \"custom-image-placeholder\": \"Please enter a custom image URL (e.g. https://example.com/image.jpg)\",\n      \"update\": \"Update\",\n      \"new-model\": \"Create a new model\",\n      \"migrate\": \"Submit\",\n      \"sync\": \"Sync upstream\",\n      \"sync-tip\": \"Synchronize upstream model markets\",\n      \"sync-placeholder\": \"Please enter the API address of the upstream CoAI, for example: https://api.chatnio.net\",\n      \"sync-all\": \"Sync all ({{length}})\",\n      \"sync-self\": \"Sync supported models ({{length}})\",\n      \"sync-site\": \"Upstream address\",\n      \"sync-option\": \"Synchronization Options\",\n      \"sync-failed\": \"Sync Failed\",\n      \"sync-failed-prompt\": \"Address could not be requested or model market model is empty\\n(Endpoint: {{endpoint}})\",\n      \"sync-items\": \"A total of {{length}} models have been found, {{exist}} models have been found (will not be overwritten), {{new}} models have been added (all synchronized), {{support}} models have been supported by this site channel (synchronized supported models)\",\n      \"sync-success\": \"Sync successfully.\",\n      \"sync-success-prompt\": \"Synced from upstream, added {{length}} models, please check and click submit to take effect, otherwise it will not be saved\",\n      \"not-use\": \"Some models are not used\",\n      \"import-all\": \"Import Full\",\n      \"function-calling\": \"function call\",\n      \"function-calling-tip\": \"Whether the model supports Function Calling function calls (some models and reverse engineering do not support function calls)\",\n      \"vision-model\": \"Mapping Model\",\n      \"vision-model-tip\": \"Whether the model is a map model (map model supports picture input, such as GPT-4 Turbo)\",\n      \"ocr-model\": \"OCR Assist\",\n      \"ocr-model-tip\": \"If the model itself does not support image input, OCR text recognition can be turned on to complement the model's visual capabilities to some extent (hint: file parsing services must support OCR services)\",\n      \"reverse-model\": \"Reverse model\",\n      \"reverse-model-tip\": \"If the reverse engineering model supports full file parsing by URL (such as PDF, word), this option can be turned on, and all types of file parsing will be provided by the upstream, reducing token consumption. If it is not turned on by default, it will be resolved by this project, which is applicable to most models. Please ensure that the file parsing service has configured an external URL storage scheme (such as S3/R2/MinIO, etc.), and that external URL parsing files are supported upstream of the model.\",\n      \"thinking-model\": \"Thinking Models\",\n      \"thinking-model-tip\": \"Whether the model supports deep thinking (deep thinking models output a chain of thoughts when outputting content, such as Claude 3.7 Sonnet)\"\n    },\n    \"model-chart-tip\": \"Token usage\",\n    \"subscription\": \"Subscription\",\n    \"logger\": {\n      \"title\": \"Loggers\",\n      \"console\": \"Console\",\n      \"consoleLength\": \"Number of log entries\"\n    },\n    \"plan\": {\n      \"enable\": \"Enable subscriptions\",\n      \"price\": \"Price\",\n      \"price-tip\": \"January subscription price (unit: yuan)\",\n      \"item-id\": \"ID\",\n      \"item-id-placeholder\": \"Please enter Entity ID (Item ID cannot be used more than once, ex: gpt-4)\",\n      \"item-name\": \"Name\",\n      \"item-name-placeholder\": \"Please enter the entity name (Item Name is used to display the entity name in the subscription list, e.g. GPT-4)\",\n      \"item-value\": \"Quota\",\n      \"item-value-tip\": \"Monthly quota (unit: times)\",\n      \"item-icon\": \"Icon\",\n      \"item-icon-tip\": \"Entity icons (icons used by Item Icons to appear in the subscription list)\",\n      \"item-models\": \"Model\",\n      \"item-models-tip\": \"The models covered by the entity (Item Models are used to display the models in the subscription list)\",\n      \"item-models-search-placeholder\": \"Search Model ID\",\n      \"item-models-placeholder\": \"{{length}} models selected\",\n      \"add-item\": \"add\",\n      \"import-item\": \"Import\",\n      \"sync\": \"Sync upstream\",\n      \"sync-option\": \"Synchronization Options\",\n      \"sync-site\": \"Upstream address\",\n      \"sync-placeholder\": \"Please enter the API address of the upstream CoAI, for example: https://api.chatnio.net\",\n      \"sync-result\": \"The number of upstream subscription rules was found to be {{length}}, covering {{models}} models. Do you want to overwrite the subscription rules on this site?\",\n      \"discounts\": \"Discount Settings\",\n      \"discounts-tip\": \"Discount settings (default if not enabled, default is 90% for semi-annual subscriptions, 80% for one-year subscriptions)\",\n      \"discount-value\": \"Value of discount\",\n      \"discount-off\": \"Diskon\"\n    },\n    \"model-usage-chart\": \"Proportion of models used\",\n    \"user-type-chart\": \"Proportion of user types\",\n    \"identity\": {\n      \"normal\": \"Normal\",\n      \"api_paid\": \"Other paying users\",\n      \"basic_plan\": \"Basic Subscribers\",\n      \"standard_plan\": \"Standard Subscribers\",\n      \"pro_plan\": \"Pro Subscribers\"\n    },\n    \"user-type-chart-info\": \"Total {{total}} users\",\n    \"user-type-chart-tip\": \"Other paying users: refers to users who subscribe to expired users or users whose points exceed the current initial points (operations such as using invitation codes will also be counted as changes in points increase)\",\n    \"is-banned\": \"Ban\",\n    \"email\": \"Email\",\n    \"quota-set-action\": \"Point Settings\",\n    \"quota-set-action-desc\": \"Set user's credits\",\n    \"release-subscription-action\": \"Release subscription usage\",\n    \"release-subscription-action-desc\": \"Free up subscription usage for users?\",\n    \"subscription-level\": \"Set your pledge tier\",\n    \"subscription-level-desc\": \"Setting a subscription level for a user\",\n    \"password-action\": \"Change Password\",\n    \"password-action-desc\": \"Please enter the user's new password\",\n    \"email-action\": \"Modify email\",\n    \"email-action-desc\": \"Please enter the user's new email\",\n    \"default-password\": \"Password change prompt\",\n    \"default-password-prompt\": \"Your administrator password is the default password. For the security of your account, please change your password as soon as possible. (Go to Back Office - System Settings - Change Root Password)\",\n    \"set-admin-action\": \"Set as adminisitrator\",\n    \"set-admin-action-desc\": \"Are you sure you want to make this user an admin?\",\n    \"cancel-admin-action\": \"Cancel the admin\",\n    \"cancel-admin-action-desc\": \"Are you sure you want to remove admin access for this user?\",\n    \"ban-action\": \"Ban User\",\n    \"ban-action-desc\": \"Are you sure you want to ban this user?\",\n    \"unban-action\": \" unBlock User\",\n    \"unban-action-desc\": \"Are you sure you want to unblock this user?\",\n    \"billing\": \"Income\",\n    \"coai-format-only\": \"This format is unique to CoAI\",\n    \"exit\": \"Exit\",\n    \"view\": \"View\",\n    \"broadcast-tip\": \"Notifications will only show the most recent one and will only be notified once. Site announcements can be set in the system settings. The pop-up window will be displayed on the homepage for the first time and subsequent viewing will be supported.\",\n    \"created-at\": \"Creation Time\",\n    \"used-at\": \"Collection time\",\n    \"used-username\": \"Claim User\",\n    \"payment\": \"Orders\",\n    \"pay\": {\n      \"epay\": \"Easy to pay\",\n      \"afdian\": \"Love Power Generation\",\n      \"order\": \"Order No.\",\n      \"amount\": \"Amount\",\n      \"status\": \"Payment status\",\n      \"service\": \"Payment Rails\",\n      \"type\": \"Payment Type\",\n      \"device\": \"Equipment\",\n      \"username\": \"Username\",\n      \"status-true\": \"Paid\",\n      \"status-false\": \"Unpaid\",\n      \"created-at\": \"Creation Time\",\n      \"updated-at\": \"Updated on \",\n      \"action\": \"Operation\",\n      \"copy-order\": \"Duplicate order number\",\n      \"search\": \"Search for order number or username\",\n      \"check-order\": \"Checking order status\",\n      \"check-result-same\": \"Consistent order status\",\n      \"check-result-diff\": \"Order Status Update\",\n      \"check-result-same-prompt\": \"Consistent order status, no need to update\",\n      \"check-result-diff-prompt\": \"Order status updated and payment completed\",\n      \"wechatpay\": \"WeChat Payment\",\n      \"stripe\": \"Stripe\"\n    },\n    \"delete-broadcast\": \"Delete alert\",\n    \"delete-broadcast-desc\": \"Are you sure? This action cannot be undone. This will permanently delete the notification.\",\n    \"expired-at\": \"Subscription Expiration Time\",\n    \"is-subscribed-tips\": \"Subscription judgment logic: There is a subscription tier and the subscription period has not expired\",\n    \"online-chats\": \"Chat Count\",\n    \"cdn\": {\n      \"warmup\": \"CDN Warm\",\n      \"warm-tip\": \"> If you are using a CDN service, you can warm up resources with this feature.\\n* * After each update, you can perform a resource refresh to ensure the stability of the resource and increase the loading speed. * *\\nCDN (Content Delivery Network) resources are preheated, and after preheating, the resources will be cached to the CDN node to speed up access.\\nWith the warm-up function, you can cache popular resources to the CDN node before the peak of business, reducing the pressure on the source station and improving the user experience.\\nTip: * * Warm-up execution will pull a lot of data from the CDN to the source station, please pay attention to the source station's broadband load. * *\",\n      \"copy-data\": \"Copy Preheated URL Resource List\"\n    },\n    \"license\": {\n      \"title\": \"License\",\n      \"domain\": \"Authorized domains\",\n      \"digest\": \"Signature Digest: \",\n      \"module\": \"Package management\",\n      \"modules\": {\n        \"bought\": \"Already Purchased\",\n        \"not-bought\": \"Not purchased\",\n        \"multiKey\": {\n          \"title\": \"Multi-Token Management\",\n          \"description\": \"Multi-API key management, support for multi-token distribution management in one unit, support for setting callable models, balance limits, call logs, status management, integration guides and other advanced functions\"\n        },\n        \"stripe\": {\n          \"title\": \"Stripe Payment\",\n          \"description\": \"Stripe Hosted Checkout advanced payment module, supports Stripe card/Bank/Link/WeChat/Alipay+ and other dozens of payment docking, supports multi-currency payment\"\n        },\n        \"paypal\": {\n          \"title\": \"Paypal Payout\",\n          \"description\": \"PayPal advanced payment module, support PayPal card payment and other functions\"\n        },\n        \"afdian\": {\n          \"title\": \"Love Power Generation\",\n          \"description\": \"Love Power Generation Payment Webhook Module to Support Love Power Generation Balance Purchase\"\n        },\n        \"bot\": {\n          \"title\": \"BOT\",\n          \"description\": \"WeChat/Flybook/Telegram/Discord Robot SaaS Module\"\n        },\n        \"digital\": {\n          \"title\": \"Digital person\",\n          \"description\": \"Digital human video generation module customization, advanced dynamic voice technology, support for voice and face cloning, support for private deployment inference and multiple engines, industry-wide scene support, support for high-dimensional customization\"\n        },\n        \"buy-tip\": \"Please contact your sales representative to purchase this module\",\n        \"contact-for-price\": \"Access documentation to get a quote\",\n        \"coai-pro\": {\n          \"title\": \"CoAI Pro\",\n          \"description\": \"CoAI Pro Business Edition authorization to unlock all business functions such as docking multiple payment channels, user (group) magnification control, content review, session logs, etc.\"\n        }\n      },\n      \"info\": \"License Info:\",\n      \"description\": \"CoAI Pro Version License Management\",\n      \"purchase\": \"Purchase license\",\n      \"pro-required\": \"This feature is exclusive to CoAI Pro, please purchase a CoAI Pro license on the license management page to use this feature\"\n    },\n    \"group\": \"Grouping\",\n    \"custom-group\": \"пользовательская подгруппа\",\n    \"custom-group-action\": \"Set custom grouping\",\n    \"custom-group-action-desc\": \"Please enter a custom group name\",\n    \"group-setting\": \"Group settings\",\n    \"notifications\": \"Push to site\",\n    \"notify-all\": \"Notify all users\"\n  },\n  \"mask\": {\n    \"title\": \"Preset\",\n    \"search\": \"Search Mask Name\",\n    \"context\": \"Contains {{length}} context\",\n    \"system\": \"System Presets\",\n    \"custom\": \"My Presets\",\n    \"edit\": \"Edit Preset\",\n    \"create\": \"New Preset\",\n    \"avatar\": \"Preset Avatar\",\n    \"conversation\": \"Scheduled conversations\",\n    \"name\": \"Preset Title\",\n    \"name-placeholder\": \"Please Enter preset title\",\n    \"description\": \"Introduction to Presets\",\n    \"description-placeholder\": \"Please enter a preset profile\",\n    \"search-emoji\": \"Search Emoji\",\n    \"actions\": {\n      \"clone\": \"Clone Preset\",\n      \"use\": \"Use From Preset\",\n      \"edit\": \"Edit Preset\",\n      \"delete\": \"Delete Preset\"\n    },\n    \"market\": \"Preset Markets\",\n    \"switch-preset\": \"Toggle preset\",\n    \"switch-preset-desc\": \"Started a new conversation and switched to a preset\"\n  },\n  \"register\": \"Register\",\n  \"auth\": {\n    \"username\": \"Username\",\n    \"password\": \"Password\",\n    \"username-or-email\": \"Username or E-mail\",\n    \"username-or-email-placeholder\": \"Please enter your username or mailbox\",\n    \"password-placeholder\": \"Please enter the password\",\n    \"forgot-password\": \"Lost Password?\",\n    \"reset-password\": \"Password Reset\",\n    \"no-account\": \"No account?\",\n    \"register\": \"Sign up now\",\n    \"username-placeholder\": \"Please enter username\",\n    \"check-password\": \"Enter Password again\",\n    \"check-password-placeholder\": \"Please enter the password again\",\n    \"email\": \"Email\",\n    \"email-placeholder\": \"Enter email\",\n    \"have-account\": \"Already have an account? \",\n    \"login\": \"Login Now\",\n    \"next-step\": \"Next\",\n    \"verify\": \"Verification\",\n    \"code\": \"CAPTCHA\",\n    \"code-placeholder\": \"Please enter OTP code\",\n    \"send-code\": \"Post\",\n    \"incorrect-info\": \"Wrong information?\",\n    \"fall-back\": \"Go back one step\",\n    \"length-range\": \"Expected {{min}} ~ {{max}} digits\",\n    \"same-rule\": \"* Fields do not match\",\n    \"invalid-email\": \"The email doesn't look right !\",\n    \"reset-success\": \"Reset successful\",\n    \"reset-success-prompt\": \"Your password has been reset, please log in with your new password.\",\n    \"send-code-success\": \"Send success !\",\n    \"send-code-success-prompt\": \"The verification code has been sent to your email, please check it.\",\n    \"send-code-failed\": \"Send failed\",\n    \"send-code-failed-prompt\": \"Failed to send verification code, reason: {{reason}}\",\n    \"register-success\": \"Account created !\",\n    \"register-success-prompt\": \"You have successfully registered, welcome!\",\n    \"disabled-mail\": \"The mailbox of the current site has been disabled, please contact the administrator to enable the mailing function.\",\n    \"code-disabled-placeholder\": \"No email verification required\",\n    \"wechat\": \"WeChat\",\n    \"connected\": \"Binding success\",\n    \"connected-prompt\": \"You've successfully linked your accounts!\",\n    \"providers\": {\n      \"baidu\": \"Baidu\",\n      \"huawei\": \"Huawei\",\n      \"weibo\": \"Weibo\",\n      \"sina\": \"Weibo\",\n      \"wx\": \"WeChat\",\n      \"qq\": \"QQ\",\n      \"xiaomi\": \"Xiaomi\",\n      \"douyin\": \"Tik Tok:\",\n      \"dingtalk\": \"Dingtalk\",\n      \"alipay\": \"Alipay\",\n      \"microsoft\": \"Microsoft\"\n    }\n  },\n  \"reset\": \"Reset\",\n  \"request-error\": \"Request failed for {{reason}}\",\n  \"update\": \"Updated\",\n  \"delete\": \"Delete\",\n  \"remove\": \"remove\",\n  \"upward\": \"Top\",\n  \"downward\": \"Move down\",\n  \"save\": \"Save\",\n  \"announcement\": \"Site Announcement\",\n  \"i-know\": \"Yes, I understand.\",\n  \"submit\": \"Send\",\n  \"empty\": \"empty\",\n  \"exit\": \"Leave\",\n  \"model\": \"Model\",\n  \"min-quota\": \"Minimum Balance\",\n  \"your-quota\": \"Your balance\",\n  \"title\": \"Title\",\n  \"my-account\": \"My Account\",\n  \"payment\": {\n    \"wechat\": \"WeChat\",\n    \"wxpay\": \"WeChat Pay\",\n    \"alipay\": \"Alipay\",\n    \"paypal\": \"PayPal\",\n    \"stripe\": \"Stripe\",\n    \"afdian\": \"Love Power Generation\",\n    \"qqpay\": \"QQ Wallet\",\n    \"order\": {\n      \"quota\": \"{{quota}} credits\"\n    },\n    \"wechatpay\": \"WeChat Payment\",\n    \"dialog-wechatpay\": {\n      \"title\": \"WeChat Payment\",\n      \"description\": \"Please use WeChat to scan the QR code below to pay\",\n      \"success\": \"Payment Successful\",\n      \"loading\": \"In load...\",\n      \"remaining-time\": \"Remaining payment time\"\n    },\n    \"notify-stripe\": {\n      \"success\": \"Payment Successful\",\n      \"canceled\": \"Payment canceled\",\n      \"processing\": \"Processing Payment...\"\n    },\n    \"xunhupay-wechat\": \"Tiger Pepper WeChat\",\n    \"xunhupay-alipay\": \"Tiger Pepper Alipay\",\n    \"dialog-xunhupay\": {\n      \"title\": \"Tiger Pepper Payment\",\n      \"description\": \"Please use WeChat or Alipay to scan the QR code below to pay\",\n      \"success\": \"Payment Successful\",\n      \"remaining-time\": \"Remaining payment time\"\n    }\n  },\n  \"back-home\": \"Return to Home\",\n  \"copied\": {\n    \"prompt\": \"Duplicate\",\n    \"success\": \"\",\n    \"success-description\": \"Content copied to clipboard\",\n    \"failed\": \"Copy failed\",\n    \"failed-description\": \"Copy failed for {{reason}}\"\n  },\n  \"record\": {\n    \"user\": \"Users\",\n    \"title\": \"Usage\",\n    \"created-at\": \"Time\",\n    \"type\": \"Type\",\n    \"model\": \"Model\",\n    \"token\": \"Token\",\n    \"input-tokens\": \"Input\",\n    \"output-tokens\": \"Output\",\n    \"quota\": \"Credits\",\n    \"duration\": \"Used time\",\n    \"detail\": \"Remarks\",\n    \"types\": {\n      \"system\": \"System\",\n      \"consume\": \"Consume\",\n      \"topup\": \"Top Up\",\n      \"all\": \"All\"\n    },\n    \"billing-today\": \"Spend Today\",\n    \"billing-month\": \"Spend This Month\",\n    \"cond\": {\n      \"model\": \"Model\",\n      \"model-placeholder\": \"Please enter a model name\",\n      \"token-name\": \"Token name\",\n      \"token-name-placeholder\": \"Please enter a token name\",\n      \"start_time\": \"Start time\",\n      \"end_time\": \"End time\",\n      \"username\": \"Specify Username\",\n      \"username-placeholder\": \"Please enter username\",\n      \"type\": \"Type\"\n    },\n    \"request-today\": \"Requests Today\",\n    \"request-month\": \"Requests This Month\",\n    \"detail-info\": {\n      \"input\": \"Input Price\",\n      \"output\": \"Output Price\",\n      \"times\": \"Time Price\",\n      \"no-cost\": \"No Charge\",\n      \"cached\": \"Hit Cache\",\n      \"plan\": \"Subscription Billing\",\n      \"empty\": \"Null response\",\n      \"error\": \"Bad Request\",\n      \"percent\": \"Group Ratio\"\n    },\n    \"rpm-tips\": \"Current rpm (requests per minute)\",\n    \"tpm-tips\": \"Current TPM (tokens per minute)\",\n    \"query\": \"Query\",\n    \"channel\": \"Channel\"\n  },\n  \"date\": {\n    \"pick\": \"Select date\",\n    \"today\": \"Today\",\n    \"clean\": \"Return to zero\",\n    \"add-day\": \"Add one day\",\n    \"sub-day\": \"Decrease by one day\",\n    \"add-month\": \"Add one month\",\n    \"sub-month\": \"Decrease by one month\",\n    \"add-year\": \"Add one year\",\n    \"sub-year\": \"Decrease by one year\"\n  },\n  \"renderer\": {\n    \"viewImage\": \"Views image\",\n    \"imageLoadFailed\": \"Image {{src}} failed to load\",\n    \"base64Image\": \"Expand Base64 image\",\n    \"base64ImageCollapse\": \"Collapse Picture Base64\",\n    \"viewVideo\": \"View video\",\n    \"videoLoadFailed\": \"Video {{src}} failed to load\"\n  },\n  \"login-action\": \"Sign in to enjoy more features\",\n  \"anonymous\": \"please sign in here\",\n  \"bar\": {\n    \"chat\": \"Chat\",\n    \"model\": \"Model\",\n    \"wallet\": \"Wallet\",\n    \"log\": \"Log\",\n    \"admin\": \"Admin\",\n    \"preset\": \"Discover\",\n    \"account\": \"Accounts\",\n    \"chat-full\": \"Chat\",\n    \"model-full\": \"Model\",\n    \"preset-full\": \"Discover\",\n    \"wallet-full\": \"Wallet\",\n    \"log-full\": \"Usage\",\n    \"account-full\": \"Account\",\n    \"admin-full\": \"Dashboard\",\n    \"key\": \"key\",\n    \"key-full\": \"API\"\n  },\n  \"notify\": \"Notice\",\n  \"new-notify\": \"New notification\",\n  \"view-all\": \"See all\",\n  \"filter\": {\n    \"filter\": \"Filter\",\n    \"conds\": \"{{count}} criteria filtered\",\n    \"plan\": \"Is Subscription\",\n    \"all\": \"Overall Minimum Value\",\n    \"subscribed\": \"Subscribed\",\n    \"unsubscribed\": \"unsubscribed\",\n    \"admin\": \"Administrator\",\n    \"not-admin\": \"Non-Admins\",\n    \"ban\": \"Whether to ghost\",\n    \"banned\": \"Banned\",\n    \"not-banned\": \"Not Ghosted\",\n    \"sorts\": {\n      \"sort\": \"Sort By\",\n      \"id-desc\": \"ID descending\",\n      \"id-asc\": \"ID ASC\",\n      \"quota-desc\": \"Points Descending\",\n      \"quota-asc\": \"Points ascending\",\n      \"used-quota-desc\": \"Points Used Descending\",\n      \"used-quota-asc\": \"Points used ascending\",\n      \"plan-desc\": \"Subscription expiration time descending\",\n      \"plan-asc\": \"Subscription expires in ascending order\"\n    }\n  },\n  \"not-login\": \"please sign in here\",\n  \"account\": {\n    \"title\": \"Account Management\",\n    \"my-account\": \"My account\",\n    \"registerDays\": \"Sign up for {{days}} days\",\n    \"current-quota\": \"Balance\",\n    \"used-quota\": \"Used Credits\",\n    \"plan-total-month\": \"Total Subscription Months\",\n    \"plan-total-month-tips\": \"Changes in the number of months due to subscription level upgrades and downgrades do not count towards this statistic\",\n    \"deeptrain\": \"DeepTrain Unified Account Management\",\n    \"deeptrain-description\": \"CoAI is a product of DeepTrain. DeepTrain's unified account management system provides users with unified account management services. You can view your account information, third-party account linking, 2FA settings, wallet settings, authentication information, and more on this page. Deeptrain Unified Account Management is commonly used in CoAI, Fystart, LightNotes and other products.\",\n    \"my-account-description\": \"Your account information, third-party account linking information, etc.\",\n    \"api-description\": \"Your global account API information\",\n    \"share-description\": \"View and manage your history conversations\",\n    \"share-delete\": \"Remove share\",\n    \"share-delete-description\": \"Are you sure you want to delete this share?\",\n    \"notification\": {\n      \"title\": \"Notification centre\",\n      \"description\": \"Manage your notification methods\",\n      \"fetchError\": \"Failed to get notification configuration\",\n      \"fetchErrorDesc\": \"Failed to get notification configuration, please check the network and try again.\",\n      \"updateSuccess\": \"Update notification configured successfully\",\n      \"updateSuccessDesc\": \"Notification configuration updated successfully (refresh your browser to apply now)\",\n      \"save\": \"Save\",\n      \"updateError\": \"Failed to update notification configuration\",\n      \"updateErrorDesc\": \"Failed to update notification configuration, please check the network and try again.\",\n      \"testSuccess\": \"Test notification configured successfully\",\n      \"testSuccessDesc\": \"Notification configuration successfully tested\",\n      \"testError\": \"Test notification configuration failed\",\n      \"testErrorDesc\": \"Test notification configuration failed, please check the network and try again.\",\n      \"enabled\": \"Enable\",\n      \"disabled\": \"Disable\",\n      \"appToken\": \"App Token\",\n      \"topicId\": \"Theme ID\",\n      \"webhookUrl\": \"Webhook URL\",\n      \"botToken\": \"Bot Token\",\n      \"chatId\": \"Chat ID\",\n      \"test\": \"Test\",\n      \"testDesc\": \"Test Notification Configuration\",\n      \"alertTitle\": \"Push to site\",\n      \"alertDescription\": \"Supports WeChat (WxPusher), Discord, Telegram, Flybook push\",\n      \"type\": {\n        \"wxpusher\": \"WeChat (WxPusher)\",\n        \"discord\": \"Discord\",\n        \"telegram\": \"Telegram\",\n        \"feishu\": \"Feishu\",\n        \"email\": \"Email\",\n        \"webhook\": \"Webhook\"\n      },\n      \"tiplist\": {\n        \"wxpusher\": \"- WeChat Push using [WxPusher] (https://wxpusher.zjiecode.com) service\\n- Register and create an app on the WxPusher website\\n- Get the AppToken for the app and populate the configuration\\n- TopicId is optional for mass mailing\\n- Scan app QR code to follow to receive push\",\n        \"discord\": \"- Discord push based on webhook mechanism\\n- Select the target channel in the Discord server\\n- Go to Channel Settings > Integrations > Create Webhook\\n- Custom webhook name and avatar (optional)\\n- Copy the generated webhook URL population configuration\",\n        \"telegram\": \"- Telegram push needs to create its own bot\\n- Search for @ BotFather in Telegram and start a conversation\\n- Send the/newbot command and follow the prompts to set the bot name and username\\n- Get the Bot Token and populate the configuration\\n- Join or privately chat with a bot in a target group\\n- Use @ userinfobot to get the Chat ID of the chat and fill it in\",\n        \"feishu\": \"- Flybook push uses group custom bots\\n- Adding custom bots to the target flying book group\\n- Set bot name, avatar and description\\n- Select the group that will receive the message\\n- Copy the generated webhook URL population configuration\\n- Keywords can be set for added security (optional)\",\n        \"email\": \"- Email push is the default option and notifications will be pushed to your registered email address\",\n        \"webhook\": \"- Custom webhook URL push\\n\\n`` `json\\nPost ${WEBHOOK_URL}\\n{\\n  \\\"type\\\": \\\"string\\\",\\n  \\\"content\\\": \\\"string\\\",\\n  \\\"time\\\": \\\"number\\\",\\n  \\\"utc_time\\\": \\\"string\\\",\\n  \\\"account_id\\\": \\\"number\\\",\\n  \\\"additional_data\\\": {...}\\n}\\n```\"\n      },\n      \"method\": \"Inform the way\",\n      \"event\": \"Subscription Events\",\n      \"url\": \"URL\",\n      \"events\": {\n        \"broadcast_event\": \"Push notification information\",\n        \"payment_event\": \"Payments, subscription notifications\",\n        \"key_quota_not_enough_event\": \"Key Limit Alert\",\n        \"account_quota_not_enough_event\": \"Account Limit Warning\"\n      },\n      \"userId\": \"User UID\"\n    },\n    \"oauth\": \"The third party signin\",\n    \"oauth-description\": \"Bind and manage your third-party logins\"\n  },\n  \"manage\": \"Administration\",\n  \"loading\": \"In load...\",\n  \"send\": \"Post\",\n  \"stop\": \"Stop\",\n  \"new-chat\": \"New Conversation\",\n  \"enter\": \"Line feed\",\n  \"key\": {\n    \"title\": \"My Tokens\",\n    \"name\": \"Name\",\n    \"namePlaceholder\": \"Please enter a name\",\n    \"status\": \"Status\",\n    \"quota\": \"Amount\",\n    \"quotaPlaceholder\": \"Please enter available quota\",\n    \"usedQuota\": \"Used credit limit\",\n    \"remainQuota\": \"The remaining amount\",\n    \"infiniteQuota\": \"Unlimited credits\",\n    \"createdAt\": \"Creation Time\",\n    \"expiredAt\": \"Expiration time\",\n    \"key\": \"key\",\n    \"advanced\": \"Advanced settings\",\n    \"ipWhiteList\": \"IP Whitelist\",\n    \"enableIpWhiteList\": \"Enable IP Whitelisting\",\n    \"enableIpWhiteListTip\": \"Turn on the IP whitelist. Only the IP addresses in the whitelist can use this key, if not filled in, all IPs are allowed (not necessary, not recommended)\",\n    \"ipWhiteListPlaceholder\": \"Please enter the IP address or network segment in the format: 127.0.0.1,192.168.0.0/16\",\n    \"modelWhiteList\": \"Model whitelist\",\n    \"enableModelWhiteList\": \"Enable model whitelist\",\n    \"enableModelWhiteListTip\": \"Enable model whitelisting. Only models in the whitelist can use this key. Leave blank to use all models (optional, not recommended)\",\n    \"modelWhiteListPlaceholder\": \"{{length}} models checked\",\n    \"create\": \"create token\",\n    \"update\": \"Refresh Token\",\n    \"nameEmpty\": \"name can not be empty\",\n    \"searchPlaceholder\": \"Search for a key name...\",\n    \"disabled\": \"Disabled\",\n    \"active\": \"Enabled\",\n    \"delete\": \"Delete token\",\n    \"never\": \"It never expires\",\n    \"oneHour\": \"One hour\",\n    \"oneDay\": \"Yitian│one day\",\n    \"oneWeek\": \"One Week\",\n    \"oneMonth\": \"1 month\",\n    \"oneYear\": \"One year\",\n    \"disable\": \"Disable\",\n    \"disableToken\": \"Disable token\",\n    \"docs\": \"Docking Guide\",\n    \"slogan\": \"\\\"One-click docking of cutting-edge AI products!\\\"\",\n    \"selectKey\": \"Select Key\",\n    \"bindLobeChat\": \"Bind Lobe Chat\",\n    \"bindLobeChatTip\": \"After clicking the button, you will be redirected to Lobe Chat and automatically bind the key and other information\",\n    \"bindNextChat\": \"Bind Next Chat\",\n    \"bindNextChatTip\": \"After clicking the button, you will be redirected to Next Chat and automatically enter the preset information such as the key\",\n    \"bindOpenCat\": \"Bind Open Cat\",\n    \"bindOpenCatTip\": \"After clicking the button, you will be redirected to Open Cat and bound to the preset parameters (please install Open Cat in your device first)\",\n    \"bindOneAPIStep1\": \"Go to the Channel Management page and click Add Channel\",\n    \"bindOneAPIStep2\": \"Select the OpenAI type and fill in the corresponding information according to the access point, key below\",\n    \"bindCoAIStep1\": \"Go to the channel management page in the background and click Connect upstream\",\n    \"bindCoAIStep2\": \"Fill in the corresponding information according to the access point and key information below\",\n    \"description\": \"Supports calling all AI models of this site in the OpenAI API standard format, without considering API compatibility issues, supports seamless integration of developers/third-party tools, built-in quota/time/scope/permission management\",\n    \"noKey\": \"No Key! \",\n    \"apiBase\": \"API Access Points\",\n    \"apiBaseTip\": \"Reminder: Please configure this API Base access point in the client. Common tool access can directly view the access method in the access guide below. Some other tools may need to add a suffix (e.g./v1). Please fill in according to the client's requirements.\",\n    \"noKeyWarning\": \"none\",\n    \"noKeyWarningTip\": \"Please create a key before using the docking guide\",\n    \"default\": \"Default grouping\",\n    \"unknown\": \"Unknown grouping\",\n    \"createTip\": \"Do not disclose the key to others (such as pushing it to the Github public repository), otherwise your key balance may be stolen, please keep the key carefully! If a key leak occurs, reset/delete the token in a timely manner.\",\n    \"tokenGroup\": \"Token grouping\",\n    \"tokenGroupTip\": \"Token Custom Channel Grouping\"\n  },\n  \"coming-soon\": \"This feature is under development, stay tuned!\",\n  \"starred\": \"Favorite Models\",\n  \"unstarred\": \"Common models\",\n  \"assistant-suggest\": \"Preset Recommendations\",\n  \"change-suggest\": \"Change\",\n  \"new-announcement\": \"Announcements Notifications\",\n  \"no-announcement\": \"No announcements posted yet.\",\n  \"readed\": \"Have read\",\n  \"learn-more\": \"Learn More\",\n  \"none\": \"None\",\n  \"description\": \"Description\",\n  \"tip\": \"Reminder\",\n  \"get\": \"Get\",\n  \"authenticating\": \"Verifying\",\n  \"authenticating-prompt\": \"Just a moment while we verify your account...\",\n  \"authentication-failed\": \"Authentication failed\",\n  \"oops-quota-exceeded\": \"Oops, insufficient balance\",\n  \"oops-quota-exceeded-tip\": \"Your balance is insufficient, please go to Purchase Credits or Subscription Plan to continue\",\n  \"only-one-step\": \"Just one more step to go!\",\n  \"verify-email-description\": \"Please enter your email to complete verification\",\n  \"are-you-sure\": \"Confirm or not? \",\n  \"this-action-cannot-be-undone\": \"This action cannot be undone.\",\n  \"connect\": \"Bind\",\n  \"disconnect\": \"Unbind\",\n  \"plugin\": {\n    \"title\": \"MOD manager\",\n    \"add\": \"Add plugin\",\n    \"create\": \"Create Addons \",\n    \"save\": \"Save Plugin\",\n    \"cancel\": \"Cancel\",\n    \"enable\": \"Enable\",\n    \"disable\": \"Disable\",\n    \"enabled\": \"Enabled {{name}}\",\n    \"disabled\": \"{{name}} is disabled\",\n    \"enabled-badge\": \"Enabled\",\n    \"import\": \"Import configration\",\n    \"quick-import\": \"Quick Import MCP Configuration\",\n    \"name\": \"Addon Name\",\n    \"name-placeholder\": \"Please enter a plugin name\",\n    \"avatar\": \"Plugin avatar\",\n    \"avatar-placeholder\": \"Please enter an avatar link\",\n    \"description\": \"Plugins Description\",\n    \"description-placeholder\": \"Please enter the plugin description\",\n    \"server-url\": \"Server address\",\n    \"server-url-placeholder\": \"Please enter the MCP server address\",\n    \"loading\": \"Memuat...\",\n    \"no-plugins\": \"No plugins\",\n    \"save-success\": \"Save successful.\",\n    \"save-error\": \"save failed\",\n    \"delete-success\": \"Delete successful\",\n    \"delete-error\": \"Failed to delete\",\n    \"test\": \" Test Connection\",\n    \"testing\": \"Connecting...\",\n    \"test-success\": \"connected\",\n    \"test-error\": \"Connection failed\",\n    \"import-success\": \"The tapes for indexing are imported successfully.\",\n    \"import-error\": {\n      \"empty\": \"Please enter a configuration\",\n      \"invalid-json\": \"Invalid JSON format\",\n      \"invalid-format\": \"Incorrect configuration format, please check if mcpServers field is included\",\n      \"no-servers\": \"MCP server not found in configuration\",\n      \"stdio-not-supported\": \"Current version only supports MCP server of HTTP type, STDIO type is not supported\",\n      \"unknown\": \"Import failed, please check the configuration format\"\n    },\n    \"mcp\": {\n      \"tool-call\": \"Tool call\",\n      \"tool-calling\": \"Calling tool: {{name}}\",\n      \"tool-executing\": \"Executing tool: {{name}}\",\n      \"tool-success\": \"Tool executed successfully: {{name}}\",\n      \"tool-error\": \"Tool execution failed: {{name}}\",\n      \"arguments\": \"Tool Arguments\",\n      \"result\": \"Execution Status\",\n      \"error\": \"Execution fault\",\n      \"status\": \"execute state\",\n      \"status-start\": \"Preparing execution tools...\",\n      \"status-executing\": \"Executing tool...\",\n      \"hide-details\": \"Hide Tool Call Details\",\n      \"show-details\": \"Show tool call details\",\n      \"plugin-name\": \"MCP Plugin\",\n      \"copy-param-value\": \"Copy Parameter Value\",\n      \"save\": \"Save\",\n      \"edit\": \"Edit\",\n      \"raw-arguments\": \"Raw parameters (JSON)\",\n      \"no-arguments\": \"There is no parameter\",\n      \"parsed-result\": \"Result after parsing\",\n      \"error-info\": \"Error Message\",\n      \"status-prepare\": \"Preparing to call the tool...\",\n      \"status-success\": \"Tool Executed Successfully\",\n      \"status-error\": \"Tool execution failed\",\n      \"status-calling\": \"Calling tool...\",\n      \"hide-debug\": \"Hide debug info\",\n      \"show-debug\": \"display debug messages\",\n      \"tool-arguments\": \"Tool Arguments\",\n      \"no-arguments-needed\": \"The tool does not require parameters\"\n    },\n    \"load-error\": \"\",\n    \"refresh\": \"Refresh\",\n    \"refresh-success\": \"Refresh successful.\",\n    \"test-success-desc\": \"{{count}} tools found available\",\n    \"import-json-config\": \"Import JSON Configuration\",\n    \"import-http-only-tip\": \"The current version only supports MCP servers of HTTP type\",\n    \"import-confirm\": \"Confirm Demo Import\",\n    \"server-url-required\": \"Please Enter A Server Address\",\n    \"test-required\": \"Requires Testing\",\n    \"test-description\": \"Click the test button to verify the plugin connection\",\n    \"available-tools\": \"Available Widgets\",\n    \"connection-test\": \"Connectivity test\",\n    \"test-required-error\": \"You need to test the connection before creating a new plugin\",\n    \"test-required-hint\": \"A new plugin needs to test the connection before it can be created\",\n    \"form-error\": \"Please fill Required fields\"\n  },\n  \"aff\": {\n    \"title\": \"Affiliate\",\n    \"bind-desc\": \"Bind your referral code to start earning commission on purchases.\",\n    \"placeholder\": {\n      \"code\": \"Enter referral code\"\n    },\n    \"get\": \"Get Code\",\n    \"get-placeholder\": \"Click to get your referral code\",\n    \"get-success\": \"Referral code generated\",\n    \"bind-existing\": \"Bind Code\",\n    \"bind-success\": \"Bind successfully\",\n    \"bind-failed\": \"Bind failed\",\n    \"cancel\": \"Cancel\",\n    \"confirm\": \"Confirm\",\n    \"stats\": {\n      \"referrals\": \"Referrals\",\n      \"earnings\": \"Earnings\",\n      \"pending\": \"Pending\",\n      \"rate\": \"Rate\"\n    },\n    \"generate-code-first\": \"Please become a promotional code\",\n    \"bind-failed-prompt\": \"Binding failed! Reason: {{reason}}\",\n    \"withdraw\": \"Withdrawl\",\n    \"withdraw-all\": \"All Withdrawal\",\n    \"withdraw-title\": \"Earnings Withdrawal\",\n    \"withdraw-desc\": \"Redeem cumulative promotional earnings for points. Leave blank for full redemption.\",\n    \"withdraw-placeholder\": \"Please enter the withdrawal amount (leave blank for all)\",\n    \"withdraw-success\": \"Withdrawal was successful\",\n    \"withdraw-success-prompt\": \"You have successfully converted {{amount}} to {{quota}} credits.\",\n    \"withdraw-failed\": \"Withdraw failed.\",\n    \"withdraw-failed-prompt\": \"Withdrawal failed! Reason: {{reason}}\",\n    \"invalid-amount\": \"Invalid amount\"\n  }\n}"
  },
  {
    "path": "app/src/resources/i18n/ja.json",
    "content": "{\n  \"end\": \"\",\n  \"add\": \"登録\",\n  \"not-found\": \"ページが見つかりません\",\n  \"home\": \"トップページ\",\n  \"login\": \"ログイン\",\n  \"login-require\": \"この機能を使用するにはログインする必要があります\",\n  \"logout\": \"サインアウト\",\n  \"quota\": \"Whirlies\",\n  \"download\": \"ダウンロード\",\n  \"offline\": \"オフラインで適用\",\n  \"try-again\": \"再試行\",\n  \"invalid-token\": \"無効なトークン\",\n  \"invalid-token-prompt\": \"もう一度お試しください。\",\n  \"login-failed\": \"ログイン失敗\",\n  \"login-failed-prompt\": \"ログインに失敗しました！理由：{{reason}}\",\n  \"login-success\": \"ログイン OK\",\n  \"login-success-prompt\": \"ログインに成功しました。\",\n  \"server-error\": \"ログインエラー\",\n  \"server-error-prompt\": \"ログイン中にエラーが発生しました。もう一度お試しください。\",\n  \"error\": \"要求失敗\",\n  \"request-failed\": \"リクエストに失敗しました。ネットワークを確認して、もう一度お試しください。\",\n  \"success\": \"リクエストに成功しました\",\n  \"request-success\": \"アクションは正常に実行されました。\",\n  \"close\": \"閉じる\",\n  \"edit\": \"編集\",\n  \"editor\": \"編集\",\n  \"pricing\": \"請求の詳細については、モデル価格表を参照してください\",\n  \"true\": \"はい\",\n  \"false\": \"いいえ\",\n  \"unknown\": \"不明\",\n  \"scroll-down\": \"最新までスクロール\",\n  \"broadcast\": \"公告\",\n  \"fatal\": \"アプリのクラッシュ\",\n  \"download-fatal-log\": \"エラーログのダウンロード\",\n  \"fatal-tips\": \"まずウェブとブラウザの互換性を確認し、ブラウザのキャッシュをクリアしてページを更新してみてください。問題が解決しない場合は、ログをダウンロードして完全な再現手順を開発者に提供し、問題のトラブルシューティングを行います。\",\n  \"tag\": {\n    \"free\": \"無料\",\n    \"official\": \"広汽Hondaが\",\n    \"unstable\": \"ダンプ/復元\",\n    \"web\": \"ネットワーキング\",\n    \"high-quality\": \"高品質\",\n    \"high-context\": \"ハイコンテキスト\",\n    \"high-price\": \"高い料金設定\",\n    \"open-source\": \"オープンソース\",\n    \"image-generation\": \"画像生成\",\n    \"multi-modal\": \"マルチモーダル\",\n    \"fast\": \"高速\",\n    \"english-model\": \"英語モデル\",\n    \"badges\": {\n      \"non-billing\": \"無料\",\n      \"times-billing\": \"{{price }}/回\",\n      \"token-billing\": \"入力{{input }}/ 1 kトークン出力{{output }}/ 1 kトークン\",\n      \"add\": \"ワークベンチを追加\",\n      \"remove\": \"ワークベンチを削除\",\n      \"plan-included\": \"サブスクリプションに含まれるもの\",\n      \"plan-included-tip\": \"サブスクリプションにはすでにこのモデルが含まれています。サブスクリプションクレジットが最初に使用されます\"\n    }\n  },\n  \"market\": {\n    \"title\": \"モデルマーケット\",\n    \"model\": \"他のモデルを見る\",\n    \"explore\": \"モデルを見る\",\n    \"search\": \"モデル名またはイントロダクションを検索\",\n    \"model-api\": \"API要求のモデルID名\",\n    \"list\": \"モデルリスト\",\n    \"go\": \"モデルマーケットに行く\",\n    \"show-pricing\": \"料金を表示\",\n    \"switch-model\": \"モデルの切り替え\",\n    \"switch-model-desc\": \"モデルに切り替えました\",\n    \"switch-bookmark\": \"作業台\",\n    \"remove-bookmark\": \"メニューバーからモデルを削除しました\",\n    \"add-bookmark\": \"モデルがメニューバーに追加されました\",\n    \"show-1m-pricing\": \"100万トークン\"\n  },\n  \"conversation\": {\n    \"title\": \"ダイアログ\",\n    \"empty\": \"空\",\n    \"refresh-failed\": \"更新に失敗しました\",\n    \"refresh-failed-prompt\": \"リクエストにエラーが発生しました。もう一度お試しください。\",\n    \"remove-title\": \"本当によろしいですか？\",\n    \"remove-description\": \"この操作は元に戻せません。これにより、会話が完全に削除されます\",\n    \"remove-all-title\": \"履歴\",\n    \"remove-all-description\": \"この操作は元に戻せません。これによりすべての会話が完全に削除されます。続行しますか？\",\n    \"cancel\": \"キャンセル\",\n    \"delete\": \"削除\",\n    \"delete-conversation\": \"会話を削除\",\n    \"delete-success\": \"会話が削除されました\",\n    \"delete-success-prompt\": \"会話が削除されました。\",\n    \"delete-failed\": \"削除失敗\",\n    \"delete-failed-prompt\": \"会話を削除できませんでした。ネットワークを確認して、もう一度お試しください。\",\n    \"edit-title\": \"タイトルを編集...\",\n    \"empty-anonymous\": \"現在匿名モードになっているため、会話は保存されません。\",\n    \"search\": \"会話を検索...\"\n  },\n  \"chat\": {\n    \"web\": \"ネットワーキング\",\n    \"web-aria\": \"ウェブ検索を切り替える\",\n    \"placeholder\": \"チャットを入力してください...\",\n    \"recall\": \"履歴の回復\",\n    \"recall-desc\": \"最後に未送信のメッセージが検出され、復元されました。\",\n    \"recall-cancel\": \"キャンセル\",\n    \"placeholder-enter\": \"何か書いてください... （送信するにはEnter、ラップするにはShift + Enter ）\",\n    \"placeholder-raw\": \"何か書いてください...\",\n    \"send-message\": \"メッセージを送信\",\n    \"send-message-desc\": \"このメッセージを送信してもよろしいですか？\",\n    \"actions\": {\n      \"upscale\": \"拡大\",\n      \"variant\": \"変化\",\n      \"reroll\": \"再描画\",\n      \"subtle-upscale\": \"微妙なズームイン\",\n      \"creative-upscale\": \"クリエイティブZoom\",\n      \"subtle-vary\": \"わずかな変更\",\n      \"strong-vary\": \"大きな変化\",\n      \"region-vary\": \"部分的な再描画\",\n      \"zoom\": \"ズーム\",\n      \"zoom-1\": {},\n      \"zoom-2x\": \"ズーム2倍\",\n      \"zoom-custom\": \"カスタムズーム\",\n      \"pan-left\": \"左移動\",\n      \"pan-right\": \"右移動\",\n      \"pan-up\": \"上移動\",\n      \"pan-down\": \"下移動\",\n      \"bookmark\": \"いいね\"\n    },\n    \"empty-preview\": \"入力はここにレンダリングされます（ Markdown構文をサポート）〜\",\n    \"web-enable-toast\": \"ネットワーク検索がオンになっています\",\n    \"web-disable-toast\": \"ネットワーク検索がオフになっています\",\n    \"web-enable-tip\": \"ネットワーク検索はより多くのトークンを消費する可能性があります\",\n    \"web-search\": \"インターネット検索\",\n    \"plugin\": \"プラグイン\",\n    \"voice\": \"Speech Recognition\",\n    \"deep-thinking\": \"深い思考\",\n    \"deep-thinking-enable-toast\": \"ディープシンキングが有効になりました\",\n    \"deep-thinking-enable-tip\": \"深い思考は、アウトプットが遅くなる可能性があります\",\n    \"deep-thinking-disable-toast\": \"ディープ・シンキング・クローズ\",\n    \"model-not-support-thinking-desc\": \"現在のモデルは、詳細な思考をサポートしていません\",\n    \"web-search-results\": \"{{count}}件の結果が見つかりました\",\n    \"web-search-results-hide\": \"検索結果を折りたたむ\",\n    \"web-search-results-query\": \"検索キーワード\",\n    \"web-search-results-visit-source\": \"ソースウェブサイトにアクセス\",\n    \"web-search-no-results\": \"検索結果なし\",\n    \"web-page-summary\": \"ページごとの概要\",\n    \"web-depth\": \"検索深度\",\n    \"web-quick-search\": \"クイック検索\",\n    \"web-detailed-search\": \"詳細検索\",\n    \"web-enable-page-summary-toast\": \"ページごとのサマリーが有効になっています\",\n    \"web-enable-page-summary-tip\": \"ページごとのサマリーは、より多くのトークンを消費し、出力を遅くする可能性があります\",\n    \"web-disable-page-summary-toast\": \"ページごとの概要が閉じられました\",\n    \"web-search-quick-toast\": \"検索深度をクイック検索に切り替えました\",\n    \"web-search-detailed-toast\": \"検索深度を詳細検索に切り替えました\"\n  },\n  \"message\": {\n    \"copy\": \"メッセージをコピー\",\n    \"save\": \"ファイルとして保存\",\n    \"use\": \"メッセージを使用する\",\n    \"stop\": \"回答を停止\",\n    \"restart\": \"再回答\",\n    \"copy-area\": \"選択を複製\",\n    \"edit\": \"メッセージを編集\",\n    \"remove\": \"メッセージを削除\",\n    \"save-image\": \"写真を保存\",\n    \"saving-image-prompt\": \"画像生成中\",\n    \"saving-image-prompt-desc\": \"画像を生成しています。しばらくお待ちください...\",\n    \"saving-image-failed\": \"画像の生成に失敗しました\",\n    \"saving-image-failed-prompt\": \"{{reason}}のために画像生成に失敗しました\",\n    \"saving-image-success\": \"画像が正常に生成されました\",\n    \"saving-image-success-prompt\": \"画像が正常に保存されました。\",\n    \"sharing\": {\n      \"title\": \"タイトル\",\n      \"time\": \"期日\",\n      \"message\": \"メッセージ\"\n    },\n    \"thinking-process\": \"思考プロセス\"\n  },\n  \"quota-description\": \"メッセージのクレジット使用額\",\n  \"buy\": {\n    \"choose\": \"金額を選択してください\",\n    \"other\": \"その他\",\n    \"other-desc\": \"ポイントは何点ですか？\",\n    \"buy\": \"{{amount}}クレジットを購入\",\n    \"dalle\": \"ダル・E AIドローイング\",\n    \"dalle-free\": \"Dall・E 2 Drawing Free Forever\",\n    \"flex\": \"柔軟な請求\",\n    \"input\": \"インプット\",\n    \"output\": \"出力\",\n    \"learn-more\": \"詳細はこちら\",\n    \"dialog-title\": \"クレジットを購入する\",\n    \"dialog-desc\": \"本当に{{amount}}クレジットを購入しますか？\",\n    \"dialog-cancel\": \"キャンセル\",\n    \"dialog-buy\": \"購入\",\n    \"success\": \"購入が完了しました\",\n    \"success-prompt\": \"{{amount}}クレジットを正常に購入しました。\",\n    \"failed\": \"購入できませんでした\",\n    \"failed-prompt\": \"クレジットの購入に失敗しました。十分な残高があることを確認。\",\n    \"gpt4-tip\": \"ヒント：ウェブに接続された機能により、より多くの入力ポイントが消費される可能性があります\",\n    \"go\": \"行く\",\n    \"redeem\": \"交換する\",\n    \"redeem-placeholder\": \"引き換えコードを入力してください\",\n    \"exchange-success\": \"交換成功\",\n    \"exchange-success-prompt\": \"{{amount}}クレジットを正常に引き換えました。\",\n    \"exchange-failed\": \"引き換えに失敗しました\",\n    \"exchange-failed-prompt\": \"{{reason}}のため、引き換えに失敗しました\",\n    \"buy-link\": \"購入しに行く\",\n    \"deeptrain-tip\": \"ヒント： Deeptrainがウォレットにリロードされたら、ここに戻ってクリックして適切なクレジットを購入してください\",\n    \"not-config-link\": \"購入リンクがバックグラウンドで設定されていません\",\n    \"title\": \"マイポイント\",\n    \"quota-info\": \"ポイントは、このステーションのすべてのモデルを使用することができ、柔軟な請求オプションに適しています\",\n    \"deeptrain-step-1\": \"クレジットを選択し、購入をクリックします\",\n    \"deeptrain-step-2\": \"Deeptrainウォレットのチャージにジャンプ\",\n    \"deeptrain-step-3\": \"チャージが完了したら、ここに戻って再度購入してください\",\n    \"deeptrain-step-4\": \"（ウォレットに十分な残高がある場合、購入後に自動的にチャージされます）\",\n    \"plan-info\": \"サブスクリプションは、サイクルごとに固定された価格でサブスクリプションモデルを使用でき、固定された長期使用オプションに適しています。\",\n    \"buy-description\": \"購入したいクレジットを選択してください\",\n    \"redeem-title\": \"コードを利用する\",\n    \"redeem-description\": \"クレジットを受け取るには、引き換えコードを入力してください\"\n  },\n  \"pkg\": {\n    \"title\": \"パック\",\n    \"go\": \"実名認証に進む\",\n    \"cert\": \"実名認証パック\",\n    \"cert-desc\": \"実名認証後に50ポイント（ 5元相当）を獲得\",\n    \"teen\": \"学生の福利厚生\",\n    \"teen-desc\": \"未成年者（ 18歳以下）は、実名認証後に追加の150ポイント（ 15元相当）を得ることができます\",\n    \"close\": \"閉じる\",\n    \"state\": {\n      \"true\": \"受取済\",\n      \"false\": \"受領不可\"\n    },\n    \"manage\": \"マイパック\"\n  },\n  \"sub\": {\n    \"title\": \"購読\",\n    \"quota-link\": \"柔軟な請求方法をお探しですか？クレジットを購入\",\n    \"subscription-link\": \"固定請求をお探しですか？サブスクリプションプラン\",\n    \"dialog-title\": \"サブスクリプションプラン\",\n    \"free\": \"無料版\",\n    \"free-price\": \"永久に無料\",\n    \"basic\": \"ベーシック\",\n    \"standard\": \"標準\",\n    \"pro\": \"プロ\",\n    \"plan-price\": \"{{money}}元/月\",\n    \"include-tax\": \"(税込)\",\n    \"enterprise\": \"エンタープライズ版\",\n    \"enterprise-service\": \"優先技術サポート\",\n    \"enterprise-sla\": \"SLA保護\",\n    \"enterprise-speed\": \"TPMレートブースト\",\n    \"enterprise-security\": \"SOC -2標準データセキュリティ\",\n    \"enterprise-data\": \"オフサイトデータディザスタリカバリ\",\n    \"enterprise-deploy\": \"民営化されたデプロイメントのサポート\",\n    \"contact-sale\": \"セールスチームに問い合わせる\",\n    \"current\": \"現在のプラン\",\n    \"subscribe\": \"購読\",\n    \"upgrade\": \"レベルアップ\",\n    \"downgrade\": \"レベルダウン\",\n    \"renew\": \"VIP資格を延長する\",\n    \"cannot-select\": \"選択できません\",\n    \"select-time\": \"登録時期を選択してください\",\n    \"migrate-plan\": \"サブスクリプションプランを変更する\",\n    \"migrate-plan-desc\": \"サブスクリプションを変更した後、サブスクリプション期間は残りの日数の価格に基づいて再計算されます。（ダウングレードが時間を2倍にすると、アップグレードが差額を埋め合わせます）\",\n    \"price\": \"価格{{price}}元\",\n    \"price-tax\": \"税込{{price}}元\",\n    \"upgrade-price\": \"アップグレード費用{{price}}元（参考用）\",\n    \"expired\": \"残りのサブスクリプション日数\",\n    \"time\": {\n      \"1\": \"1ヶ月\",\n      \"3\": \"3ヶ月\",\n      \"6\": \"半期\",\n      \"12\": \"1年\",\n      \"36\": \"3年\"\n    },\n    \"success\": \"サブスクリプションが正常に終了しました\",\n    \"success-prompt\": \"{{month}}のサブスクリプションに成功しました。\",\n    \"migrate-success\": \"変更に成功しました\",\n    \"migrate-success-prompt\": \"サブスクリプションプランが正常に変更されました。\",\n    \"failed\": \"サブスクリプションに失敗しました\",\n    \"failed-prompt\": \"サブスクリプションに失敗しました。十分な残高があることを確認してください。\",\n    \"migrate-failed\": \"変更できませんでした\",\n    \"migrate-failed-prompt\": \"サブスクリプションの変更に失敗しました。\",\n    \"plan-usage\": \"{{name}}は月に{{times}}回使用\",\n    \"plan-tip\": \"呼び出し可能なモデル\",\n    \"disable\": \"このサイトのサブスクリプション機能はオフになっています\",\n    \"plan-unlimited-usage\": \"{{name}}は無制限に使用できます\",\n    \"plan-not-support-relay\": \"サイトサブスクリプションクォータはステージングAPIをカバーしていません。ステージングAPIに柔軟な請求クレジットを使用してください\",\n    \"failed-quota-prompt\": \"サブスクリプションに失敗しました。残高が不足しています（{{ quota}}クレジット）\",\n    \"sub-migrate-failed-prompt\": \"サブスクリプションの変更が{{reason}}で失敗しました\",\n    \"month\": \"月\",\n    \"year\": \"年\",\n    \"best-choice\": \"ベストチョイス\",\n    \"including-model\": \"対象モデル\",\n    \"including-model-tip\": \"このサブスクリプションに含まれる利用可能なモデル使用クレジット\",\n    \"none\": \"購読していません\",\n    \"plan-item-usage\": \"{{times}}回\",\n    \"plan-item-unlimited-usage\": \"無限\",\n    \"year-earn-tip\": \"年間プランで{{percent}}節約\",\n    \"quota-manage\": \"サブスクリプションクォータ\",\n    \"expired-days\": \"サブスクリプションの有効期限が{{days}}日後に切れます\",\n    \"month-plan\": \"月間プラン\",\n    \"year-plan\": \"年間プラン\",\n    \"new\": \"新規プラン\",\n    \"select-duration\": \"サブスクリプション期間を選択してください\",\n    \"price-summary\": \"料金の概要\",\n    \"total-price\": \"合計価格\",\n    \"upgrade-price-label\": \"アップグレード料金\",\n    \"upgrade-price-notice\": \"参考用\",\n    \"upgrade-price-notice-tip\": \"アップグレード料金は参考用であり、実際の価格はサーバーの正確な計算に基づいています。\",\n    \"refresh-days\": \"クォータは{{refresh_days}}日後に更新されます\",\n    \"get-refresh-days\": \"クォータを使用して日付を更新する\"\n  },\n  \"cancel\": \"キャンセル\",\n  \"confirm\": \"確認\",\n  \"percent\": \"{{cent}}オフ\",\n  \"file\": {\n    \"upload\": \"アップロード\",\n    \"type\": \"pdf、docx、pptx、xlsx、画像、テキスト、その他の形式をサポート\",\n    \"drop\": \"ここにファイルをドロップするか、クリックしてアップロードしてください\",\n    \"parse-error\": \"解析に失敗しました\",\n    \"parse-error-prompt\": \"解析に失敗しました: {{reason}}\",\n    \"max-length\": \"コンテンツが長すぎます\",\n    \"max-length-prompt\": \"コンテキストの長さの制限により、コンテンツが傍受されました\",\n    \"over-size\": \"ファイルが大きすぎます\",\n    \"over-size-prompt\": \"1つの添付ファイルのサイズは{{size}} MBを超えることはできません\",\n    \"large-file\": \"大きなファイル解析\",\n    \"large-file-prompt\": \"大きなファイルをアップロードして解析しています。しばらくお待ちください\",\n    \"number\": \"{{number}}ファイル\",\n    \"zipper\": \"{{filename}}とその他{{number}}件\",\n    \"empty-file\": \"コンテンツファイルがありません\",\n    \"empty-file-prompt\": \"ファイルの内容が空で、自動的に無視されます\",\n    \"large-file-success\": \"解析に成功しました\",\n    \"large-file-success-prompt\": \"{{time}}秒で大きなファイルが正常に解析されました\",\n    \"file\": \"ファイル\",\n    \"parse-success-prompt\": \"ファイルが正常に解析されました: {{file}}\",\n    \"uploading\": \"ファイルのアップロード中…\",\n    \"uploading-prompt\": \"ファイルをアップロードしています。しばらくお待ちください\"\n  },\n  \"generate\": {\n    \"title\": \"AIプロジェクトビルダー\",\n    \"input-placeholder\": \"Pythonミニゲームを生成する\",\n    \"failed\": \"生成に失敗しました\",\n    \"reason\": \"理由\",\n    \"success\": \"正常に生成されました\",\n    \"success-prompt\": \"プロジェクトが正常に生成されました！ダウンロード形式を選択してください。\",\n    \"empty\": \"生成しています...\",\n    \"download\": \"{{name}}形式をダウンロード\"\n  },\n  \"api\": {\n    \"title\": \"API設定\",\n    \"copied\": \"コピー成功\",\n    \"copied-description\": \"APIキーをクリップボードにコピーしました\",\n    \"learn-more\": \"詳細はこちら\",\n    \"reset\": \"キーをリセット\",\n    \"reset-description\": \"本当によろしいですか？この操作は元に戻せません。これにより、APIキーが完全にリセットされ、既存のAPIキーが期限切れになります。\"\n  },\n  \"service\": {\n    \"title\": \"新しいバージョンを発見する\",\n    \"version\": \"バージョン\",\n    \"description\": \"新しいバージョンが見つかりました。今すぐ更新しますか？\",\n    \"update\": \"更新\",\n    \"offline-title\": \"オフラインモード\",\n    \"offline\": \"アプリは現在オフラインです。\",\n    \"update-success\": \"正常に更新されました\",\n    \"update-success-prompt\": \"最新バージョンに更新しました。\"\n  },\n  \"share\": {\n    \"title\": \"シェアする\",\n    \"share-conversation\": \"会話を共有\",\n    \"description\": \"この会話を他の人と共有してください。\",\n    \"copy-link\": \"リンクをコピー\",\n    \"view\": \"確認\",\n    \"success\": \"成功の共有\",\n    \"failed\": \"共有に失敗しました\",\n    \"copied\": \"コピー成功\",\n    \"copied-description\": \"リンクをクリップボードにコピーしました\",\n    \"not-found\": \"会話が見つかりません\",\n    \"not-found-description\": \"会話が見つかりません。リンクが正しいかどうか、または会話が削除されたかどうかを確認してください\",\n    \"manage\": \"シェア管理\",\n    \"sync-error\": \"同期に失敗しました\",\n    \"name\": \"会話のタイトル\",\n    \"time\": \"日時\",\n    \"action\": \"操作\",\n    \"empty\": \"まだ記録を共有していません。今すぐ共有しましょう！\",\n    \"share-tip\": \"会話バーに移動し、[共有]ボタンをクリックして会話を共有します\"\n  },\n  \"docs\": {\n    \"title\": \"ドキュメントを開く\"\n  },\n  \"invitation\": {\n    \"title\": \"コードを利用する\",\n    \"input-placeholder\": \"引き換えコードを入力してください\",\n    \"cancel\": \"キャンセル\",\n    \"check\": \"検証\",\n    \"check-success\": \"交換成功\",\n    \"check-success-description\": \"正常に引き換えられました！ AIの旅を始めるために{{amount}}クレジットを獲得しました！\",\n    \"check-failed\": \"引き換えに失敗しました\",\n    \"invitation\": \"招待コード\"\n  },\n  \"contact\": {\n    \"title\": \"お問い合わせ\",\n    \"community\": \"コミュニティに参加する\"\n  },\n  \"settings\": {\n    \"title\": \"設定\",\n    \"description\": \"設定\",\n    \"version\": \"バージョン\",\n    \"language\": \"言語\",\n    \"sender\": \"キーを送信\",\n    \"context\": \"コンテキストを保持\",\n    \"history\": \"最大履歴セッション数\",\n    \"align\": \"チャットボックスを中央に配置\",\n    \"memory\": \"メモリ使用量\",\n    \"temperature\": \"温度\",\n    \"temperature-tip\": \"ランダムサンプリング比、高温はよりランダム性を生み、低温はより集中的で決定論的なテキストを生成します\",\n    \"max-tokens\": \"レスポンストークンの最大数\",\n    \"max-tokens-tip\": \"この値を超えるリプライトークンの最大数は切り捨てられます（値が高すぎると、モデルの最大トークンを超えるために要求が失敗する可能性があります）\",\n    \"top-p\": \"カーネルサンプリング確率閾値\",\n    \"top-p-tip\": \"（ TopP ）確率値が高いほど生成されるランダム性が高く、値が低いほど生成される確実性が高くなります\",\n    \"top-k\": \"サンプル候補セットサイズ\",\n    \"top-k-tip\": \"(TopK)候補セットサイズ、生成のランダム性が大きいほど生成が小さいほど確実性が高い\",\n    \"presence-penalty\": \"ペナルティの存在\",\n    \"presence-penalty-tip\": \"(PresencePenalty)モデルによって生成された新しいトピックの可能性を制御するためのペナルティがあります。この値を増やすと、新しいトピックについて話す可能性が高くなります\",\n    \"frequency-penalty\": \"フリークエンシー・パニュメント\",\n    \"frequency-penalty-tip\": \"（ FrequencyPenalty ）周波数ペナルティ、モデルによって生成された単語の繰り返しの度合いを制御し、この値を増やすことで単語の繰り返しの可能性を減らすことができます\",\n    \"repetition-penalty\": \"重複した処罰\",\n    \"repetition-penalty-tip\": \"(RepetitionPenalty)モデルによって生成される繰り返しの度合いを制御します。この値を大きくすると、繰り返しを減らすことができますが、モデルが不整合のテキストを生成する可能性があります(FrequencyPenaltyと同様)\",\n    \"reset-settings\": \"すべての設定をリセット\",\n    \"reset-settings-description\": \"本当によろしいですか？この操作は元に戻せません。これにより、すべての設定が完全にリセットされます。\",\n    \"hide-model\": \"モデルの選択を非表示にする\",\n    \"hide-toolbar\": \"デフォルトでツールバーを非表示にする\",\n    \"hide-toolbar-text\": \"ツールバーのテキストを非表示にする\",\n    \"theme\": \"テーマ\",\n    \"light\": \"明るい色:\",\n    \"dark\": \"グレー表示にする色\"\n  },\n  \"article\": {\n    \"title\": \"投稿の一括生成\",\n    \"input-placeholder\": \"投稿タイトルを入力してください（ 1行に1つ）\",\n    \"prompt-placeholder\": \"プリセットを入力してください（ AIが記事を生成するのに役立ちます。例：学術論文フォーマット、800ワード）\",\n    \"web-checkbox\": \"ネットワーク検索機能をオンにするかどうか\",\n    \"generate\": \"生成\",\n    \"progress-title\": \"生成中（{{current}}生成中の{{total}}）\",\n    \"generate-success\": \"正常に生成されました\",\n    \"generate-success-prompt\": \"投稿が正常に生成されました！ダウンロード形式を選択してください。\",\n    \"generate-failed\": \"生成に失敗しました\",\n    \"generate-failed-prompt\": \"投稿の生成に失敗しました。ネットワークを確認して、もう一度お試しください。\",\n    \"download-format\": \"{{name}}形式をダウンロード\"\n  },\n  \"admin\": {\n    \"dashboard\": \"データ分析\",\n    \"users\": \"ユーザー管理\",\n    \"broadcast\": \"お知らせ管理\",\n    \"channel\": \"チャンネル設定\",\n    \"settings\": \"システムの設定\",\n    \"prize\": \"料金設定\",\n    \"billing-today\": \"本日受領\",\n    \"billing-month\": \"今月受領\",\n    \"subscription-users\": \"購読者\",\n    \"seat\": \"人\",\n    \"model-chart\": \"モデル使用統計\",\n    \"request-chart\": \"ボリューム統計の要求\",\n    \"billing-chart\": \"収益統計\",\n    \"error-chart\": \"エラー統計\",\n    \"requests\": \"リクエストボリューム\",\n    \"times\": \"例外の数\",\n    \"empty\": \"データなし\",\n    \"cancel\": \"キャンセル\",\n    \"confirm\": \"確認\",\n    \"invitation\": \"引き換えコード管理\",\n    \"code\": \"コードを利用する\",\n    \"quota\": \"Whirlies\",\n    \"type\": \"種類\",\n    \"used\": \"状態\",\n    \"number\": \"数量\",\n    \"username\": \"ユーザー名\",\n    \"month\": \"月\",\n    \"poster\": \"提供者:\",\n    \"post-at\": \"出稿時期\",\n    \"broadcast-content\": \"お知らせ内容\",\n    \"create-broadcast\": \"プレス配信\",\n    \"broadcast-placeholder\": \"お知らせ内容を入力してください\",\n    \"post\": \"公開\",\n    \"post-success\": \"正常に公開されました\",\n    \"post-success-prompt\": \"お知らせが正常に公開されました。\",\n    \"post-failed\": \"公開に失敗しました\",\n    \"post-failed-prompt\": \"{{reason}}のために公開に失敗しました\",\n    \"level\": \"レベル\",\n    \"is-admin\": \"監督\",\n    \"used-quota\": \"クレジットが使用されました\",\n    \"is-subscribed\": \"登録するかどうか\",\n    \"total-month\": \"合計サブスクリプション月数\",\n    \"enterprise\": \"エンタープライズ版\",\n    \"action\": \"操作\",\n    \"search-username\": \"ユーザー名を検索\",\n    \"quota-action\": \"ポイントの変更\",\n    \"quota-action-desc\": \"ポイント変更値を入力してください（プラスは増加、マイナスは減少）\",\n    \"subscription-action\": \"サブスクリプション時間管理\",\n    \"subscription-action-desc\": \"ユーザー{{username}}のサブスクリプションの有効期限を設定してください\",\n    \"operate-success\": \"操作成功\",\n    \"operate-success-prompt\": \"アクションは正常に実行されました。\",\n    \"operate-failed\": \"操作に失敗しました\",\n    \"operate-failed-prompt\": \"{{reason}}の操作が失敗しました\",\n    \"updated-at\": \"アップデート時間\",\n    \"used-true\": \"使用済み\",\n    \"used-false\": \"活用していない\",\n    \"generate\": \"バッチ生成\",\n    \"generate-result\": \"結果を生成する\",\n    \"error\": \"要求失敗\",\n    \"channels\": {\n      \"id\": \"チャンネルID\",\n      \"name\": \"名称\",\n      \"name-tip\": \"チャンネル名、チャンネルを識別するために使用\",\n      \"name-placeholder\": \"チャンネル名を入力してください\",\n      \"type\": \"種類\",\n      \"priority\": \"優先度\",\n      \"priority-tip\": \"マルチチャネルの場合、優先順位に従って要求すると、優先順位が大きいほど優先順位が高くなります\",\n      \"weight\": \"重量\",\n      \"weight-tip\": \"同じ優先順位の場合、重量比に基づいて負荷コールのバランスをとる\",\n      \"retry\": \"最大再試行回数\",\n      \"retry-tip\": \"チャネルリクエストが失敗したときの最大再試行回数\",\n      \"model\": \"モデル\",\n      \"secret\": \"鍵\",\n      \"secret-placeholder\": \"キーを入力してください、フォーマット：{{format}}\\\\ n複数のキーが1行に1つある場合、リクエスト時にペイロードをランダムに選択してください\",\n      \"endpoint\": \"接続点\",\n      \"endpoint-placeholder\": \"アクセスポイント（プロキシなど）を入力してください\",\n      \"mapper\": \"モデルマッピング\",\n      \"mapper-tip\": \"非対称モデル要求のモデル名変換\",\n      \"mapper-placeholder\": \"モデルマッピングを入力してください。1行に1つ、形式：モデル>モデル\\\\ n前者は要求されたモデル、後者はマッピングされたモデル（モデルに存在する必要があります）、中央には>区切り\\\\ n形式が付いています！元のモデルがこのチャネルの利用可能なスコープに含まれていないことを示します。たとえば、! gpt -4 - slow > gpt -4の場合、gpt -4はこのチャネルの要求可能なモデルに含まれません。\",\n      \"group\": \"ユーザーのグループ化\",\n      \"group-tip\": \"ユーザーグループ化、含まれていないグループは、このチャネルの利用可能な範囲に含まれません（グループ化が空の場合、すべてのユーザーがこのチャネルを使用できます）\",\n      \"state\": \"状態\",\n      \"action\": \"操作\",\n      \"edit\": \"チャンネルを編集\",\n      \"enable\": \"チャンネルを有効にする\",\n      \"disable\": \"チャンネルを無効にする\",\n      \"delete\": \"チャンネルを削除\",\n      \"create\": \"チャンネルを作成する\",\n      \"search-model\": \"モデルを検索\",\n      \"fill-template-models\": \"テンプレートモデルを入力する({{number}})\",\n      \"add-custom-model\": \"カスタムモデルの追加（スペースで区切られた複数のモデル）\",\n      \"add-model\": \"モデルを追加\",\n      \"clear-models\": \"すべてのモデルをクリア\",\n      \"advanced\": \"詳細設定\",\n      \"group-placeholder\": \"{{length}}グループが選択されました\",\n      \"group-desc\": \"ユーザータイプのグループ化、含まれていないグループは、このチャネルの利用可能なスコープに含まれません（グループ化が空の場合、すべてのユーザーがこのチャネルを使用できます）。特別なケース以外のグループ化を設定する必要はありません\",\n      \"groups\": {\n        \"anonymous\": \"匿名のユーザー\",\n        \"normal\": \"一般ユーザー\",\n        \"basic\": \"ベーシックサブスクライバー\",\n        \"standard\": \"標準サブスクライバー\",\n        \"pro\": \"Pro Subscribers\",\n        \"admin\": \"管理者ユーザー\",\n        \"custom\": \"カスタムグループ化\"\n      },\n      \"joint\": \"上流にドッキング\",\n      \"joint-endpoint\": \"アップストリームアドレス\",\n      \"joint-endpoint-placeholder\": \"アップストリームのCoAIのAPIアドレスを入力してください。例： https://api.chatnio.net\",\n      \"joint-secret\": \"APIキー\",\n      \"joint-secret-placeholder\": \"アップストリームのチャットNioのAPIキーを入力してください\",\n      \"sync-failed\": \"同期に失敗しました\",\n      \"sync-failed-prompt\": \"住所をリクエストできなかったか、モデルマーケットモデルが空です\\n（エンドポイント：{{ endpoint }}）\",\n      \"sync-success\": \"同期成功\",\n      \"sync-success-prompt\": \"{{length}}モデルがアップストリーム同期から追加されました。\",\n      \"upstream-endpoint-placeholder\": \"上流のOpenAIアドレスを入力してください。例： https://api.openai.com\",\n      \"sync-secret-placeholder\": \"アップストリームチャネルのAPIキーを入力してください\",\n      \"proxy-type\": \"プロキシのタイプ\",\n      \"proxy-endpoint\": \"プロキシアドレス\",\n      \"proxy-endpoint-placeholder\": \"転送プロキシアドレスを入力してください。例： socks 5 :// example.com: 1080\",\n      \"proxy-desc\": \"フォワードプロキシ、HTTP/HTTPS/SOCKS 5プロキシをサポート（リバースプロキシはアクセスポイントに記入してください。特別な場合以外はフォワードプロキシを設定する必要はありません）\",\n      \"proxy-username\": \"プロキシユーザー名\",\n      \"proxy-username-placeholder\": \"エージェントの認証ユーザー名を入力してください（オプション）\",\n      \"proxy-password\": \"プロキシパスワード\",\n      \"proxy-password-placeholder\": \"エージェントの認証パスワードを入力してください（オプション）\",\n      \"search-channel\": \"チャンネル名、モデル、キーを検索...\",\n      \"retry-name\": \"リトライ\",\n      \"secret-number\": \"キーの数\",\n      \"loading\": \"アップデート中\",\n      \"new\": \"新しいチャンネルを作成する\",\n      \"import\": \"既存のチャンネルをインポート\",\n      \"first-message-as-user\": \"デフォルトで最初のメッセージをユーザーメッセージに変換する\",\n      \"first-message-as-user-tip\": \"オンの場合、最初のメッセージがアシスタントロールの場合、ユーザーロールに変換されます\",\n      \"first-message-as-user-desc\": \"一部のモデル(DeepSeekなど)は、アシスタントロールとしての最初のメッセージをサポートしていません。このオプションをオンにすると、アシスタントロールの最初のメッセージがユーザーロールに変換されます。\",\n      \"merge-consecutive-user-messages\": \"継続的なユーザーメッセージをマージする\",\n      \"merge-consecutive-user-messages-tip\": \"オンの場合、両方の連続したメッセージがユーザーメッセージである場合、は1つのメッセージにマージされます\",\n      \"merge-consecutive-user-messages-desc\": \"DeepSeekなどの一部のモデルでは、2つの連続したユーザーメッセージをサポートしていません。このオプションを有効にすると、2つの連続したユーザーメッセージを1つのメッセージに結合できます。\"\n    },\n    \"charge\": {\n      \"id\": \"ID\",\n      \"type\": \"種類\",\n      \"model\": \"モデル\",\n      \"quota\": \"Whirlies\",\n      \"action\": \"操作\",\n      \"input\": \"インプット\",\n      \"output\": \"出力\",\n      \"support-anonymous\": \"匿名性のサポート\",\n      \"non-billing\": \"請求なし\",\n      \"times-billing\": \"ペイパービュー\",\n      \"token-billing\": \"トークンとして請求済み\",\n      \"anonymous\": \"匿名通話のサポート\",\n      \"time-count\": \"シングルリクエストポイント\",\n      \"input-count\": \"ポイントを入力\",\n      \"output-count\": \"出力ポイント\",\n      \"add-rule\": \"規則の追加\",\n      \"update-rule\": \"ルールを更新\",\n      \"unused-model\": \"一部のモデルの請求ルールが設定されていません\",\n      \"unused-model-tip\": \"請求ルールで設定されていないモデル損失を回避するため、通常のユーザーはリクエストできません\",\n      \"sync\": \"アップストリームを同期\",\n      \"sync-option\": \"同期のオプション\",\n      \"sync-site\": \"アップストリームアドレス\",\n      \"sync-tip\": \"アップストリームの請求ルールを同期する\",\n      \"sync-placeholder\": \"アップストリームのCoAIのAPIアドレスを入力してください。例： https://api.chatnio.net\",\n      \"sync-failed\": \"同期に失敗しました\",\n      \"sync-failed-prompt\": \"住所をリクエストできなかったか、請求ルールが空です\\n（エンドポイント：{{ endpoint }}）\",\n      \"sync-prompt\": \"{{length}}モデルのルールはアップストリームから取得されており、現在の{{influence}}モデルのルールに影響します。続行しますか？\",\n      \"sync-overwrite\": \"既存のルールを上書きする\",\n      \"sync-confirm\": \"同期を確認\",\n      \"sync-builtin\": \"アプリに組み込まれている料金\",\n      \"usd-currency\": \"米ドル対人民元為替レート\",\n      \"group-pricing\": \"ユーザーグループの価格設定比率\",\n      \"new-group\": \"ユーザーグループID\",\n      \"add-group\": \"ユーザーグループを追加\",\n      \"group-pricing-description\": \"ユーザーグループプライシング比率は、異なるユーザーグループの請求価格を区別するために使用することができ、ベース比率は1、すなわちユーザー価格=モデル価格*比率\",\n      \"group-pricing-sample\": \"例：モデルが0.2ポイントを請求し、ユーザーグループ比率が0.8の場合、実際の料金は0.2 * 0.8 = 0.16ポイントです\",\n      \"default-price\": \"デフォルト価格\",\n      \"custom-price\": \"カスタム料金\",\n      \"group-pricing-tip\": \"乗数は、異なるユーザーグループの請求価格を区別するために使用できます。**基本乗数は1 **、つまり**ユーザー価格=モデル価格*乗数**です。\\n\\n例：モデルが0.2ポイントを請求し、ユーザーグループ比率が0.8の場合、実際の料金は0.2 x 0.8 = 0.16ポイントです\\n\\n-購入乗数：ユーザーがクレジットを購入すると、控除価格乗数\\n-消費乗数：ユーザーがクレジットを使用した場合の控除価格乗数\",\n      \"new-group-price\": \"価格\",\n      \"add-new-group\": \"新しいユーザーグループを追加\",\n      \"new-group-buy-price\": \"購入レート\",\n      \"new-group-consume-price\": \"消費比率\",\n      \"new-group-description\": \"説明\",\n      \"update-group\": \"ユーザーグループを更新する\"\n    },\n    \"system\": {\n      \"general\": \"全般設定\",\n      \"search\": \"インターネット検索\",\n      \"mail\": \"SMTP送信設定\",\n      \"save\": \"保存\",\n      \"backend\": \"バックエンドドメイン\",\n      \"backendTip\": \"バックエンドドメイン名（ dockerインストールのデフォルトパスは/api ）、コールバックやストレージなどを受信するために使用、デフォルトは空です\\n例：{{ backend}}\",\n      \"mailHost\": \"送信ドメイン名\",\n      \"mailPort\": \"SMTPポート\",\n      \"mailUser\": \"ユーザー名\",\n      \"mailPass\": \"パスワード\",\n      \"searchEndpoint\": \"アクセスポイントを検索\",\n      \"searchQuery\": \"検索結果の最大数\",\n      \"searchTip\": \"[SearXNG](https://github.com/searxng/searxng) ネットワーク検索機能を提供するオープンソースの検索エンジン。SearXNG Docker民営化の展開例： [SearXNG Docker](https://github.com/zmh-program/searxng)\",\n      \"mailFrom\": \"発信元\",\n      \"test\": \"テスト送信\",\n      \"updateRoot\": \"ルートパスワードの変更\",\n      \"updateRootTip\": \"ルートパスワードを変更した後、再度ログインする必要がありますので、慎重に進んでください。\",\n      \"updateRootPlaceholder\": \"新しいrootパスワードを入力してください\",\n      \"updateRootRepeatPlaceholder\": \"新しいrootパスワードをもう一度入力してください\",\n      \"title\": \"サイト名\",\n      \"titleTip\": \"サイトタイトルに表示するサイト名、デフォルトの場合は空白のままにします\",\n      \"logo\": \"サイトロゴ\",\n      \"logoTip\": \"サイトタイトルに表示するサイトロゴへのリンク、デフォルトの場合は空白のままにします（例：{{ logo }}）\",\n      \"backendPlaceholder\": \"バックエンドコールバックドメイン名、デフォルトで空、コールバックを受け入れるために必要\",\n      \"docs\": \"ドキュメントリンク\",\n      \"docsTip\": \"ドキュメントリンク、デフォルトの場合は空白のままhttps://coai.dev\",\n      \"file\": \"ファイル解析サービス\",\n      \"filePlaceholder\": \"ファイル解析サービス、デフォルトの場合は空白のままhttps://blob.coai.dev （安定性は保証されていません）\",\n      \"fileTip\": \"ファイル解析サービスについては、[coai-blob-service ]( https://github.com/zmh-program/blob-service)プロジェクトを参照して構築してください。\",\n      \"site\": \"サイト設定\",\n      \"quota\": \"ユーザーの初期ポイント\",\n      \"quotaTip\": \"ユーザー登録後に付与されたクレジット\",\n      \"announcement\": \"サイトのお知らせ\",\n      \"announcementPlaceholder\": \"サイトのお知らせを入力してください（ Markdown/HTML形式に対応）\",\n      \"mailEnableWhitelist\": \"ドメインサフィックスホワイトリストを有効にする\",\n      \"mailWhitelist\": \"ドメインサフィックスホワイトリスト\",\n      \"mailWhitelistSelected\": \"{{length}}ドメインメールが選択されました\",\n      \"mailWhitelistSearchPlaceholder\": \"ドメイン接尾辞を検索\",\n      \"customWhitelistPlaceholder\": \"カスタムドメインサフィックスのリスト（選択するオプションのリストに表示されます）をカンマで区切って入力してください。例： example.com、example.net\",\n      \"buyLink\": \"購入リンク\",\n      \"buyLinkPlaceholder\": \"カードシークレット購入リンクを入力してください。購入ボタンを表示しない場合は空白のままにしてください\",\n      \"mailConfNotValid\": \"SMTP送信パラメータが正しく設定されていません。メールボックスの検証が無効になっています\",\n      \"contact\": \"コンタクト＆インフォメーション\",\n      \"contactPlaceholder\": \"連絡先情報を入力してください（ Markdown/HTML対応）\",\n      \"common\": \"基本設定\",\n      \"article\": \"一括ポストジェネレーションフィーチャーグループ\",\n      \"articleTip\": \"バッチポストジェネレーション機能のグループ化、現在のユーザーグループを確認した後、バッチポストジェネレーション機能を使用することができます\",\n      \"generate\": \"AIプロジェクトビルダーグループ\",\n      \"generateTip\": \"AIプロジェクトジェネレータグループ、現在のユーザーグループを確認した後、AIプロジェクトジェネレータを使用することができます\",\n      \"groupPlaceholder\": \"{{length}}グループが選択されました\",\n      \"cache\": \"キャッシュ可能なモデル\",\n      \"cacheTip\": \"キャッシュ可能なモデル、現在のモデルをチェックした後、キャッシュしてキャッシュをヒットすることができます\",\n      \"cachePlaceholder\": \"{{length}}モデルが選択されました\",\n      \"cacheAll\": \"すべてのキャッシュ可能にする\",\n      \"cacheFree\": \"フリーモデルをキャッシュ可能にする\",\n      \"cacheNone\": \"キャッシュされていないものをすべて作成\",\n      \"cacheExpired\": \"キャッシュの有効期限\",\n      \"cacheExpiredTip\": \"キャッシュの有効期限（秒単位）、デフォルト1時間\",\n      \"cacheSize\": \"最大キャッシュ尤度サイズ\",\n      \"cacheSizeTip\": \"最大キャッシュ尤度、つまり、同じ種類の入力パラメーターの最大キャッシュ尤度。パラメーターが1の場合、最大キャッシュコンテンツは1で、要求されたコンテンツは直接ヒットします。パラメーターが4の場合、4つの返されたコンテンツがあり、要求されたコンテンツのいずれかがヒットします。\",\n      \"closeRegistration\": \"登録が一時停止されました\",\n      \"closeRegistrationTip\": \"登録が一時停止されています。新規ユーザーは閉じると登録できなくなります\",\n      \"footer\": \"フッター情報\",\n      \"footerPlaceholder\": \"フッター情報を入力してください（ Markdown/HTML形式に対応）\",\n      \"authFooter\": \"ログイン後にフッターを非表示にする\",\n      \"relayPlan\": \"サブスクリプションクォータサポートステージングAPI\",\n      \"relayPlanTip\": \"サブスクリプションクォータはトランジットAPIをサポートしています。トランジットAPI請求を開いた後、ユーザーサブスクリプションクォータの使用が優先されます\\n（ヒント：サブスクリプションは時間のクォータであり、トークンの請求モデルはコストに影響する可能性があります）\",\n      \"searchQueryTip\": \"検索結果の最大数、デフォルトは5です\",\n      \"searchPlaceholder\": \"SearXNGサービスアクセスポイント（例： http :// ip: 7980 ）\",\n      \"image_store\": \"画像ストレージ\",\n      \"image_storeTip\": \"OpenAIチャンネルDALL - Eによって生成された画像は、画像の無効化を防ぐためにサーバーに保存されます\",\n      \"image_storeNoBackend\": \"バックエンドドメインが設定されていません。画像ストレージを有効にできません\",\n      \"closeRelay\": \"ステージングAPIをオフにする\",\n      \"closeRelayTip\": \"ステージングAPIをオフにすると、オフにするとステージングAPIは使用できなくなります\",\n      \"debugMode\": \"試験調整モード\",\n      \"debugModeTip\": \"デバッグモード、オンにすると、ログは詳細な要求パラメータとトラブルシューティングのための他のログを出力します\",\n      \"operation\": \"操作設定\",\n      \"chat\": \"チャット設定\",\n      \"payment\": \"支払い設定\",\n      \"epayTitle\": \"お支払いが簡単\",\n      \"epayEnabled\": \"EasyPayを有効にする\",\n      \"epayDomain\": \"EasyPayドメイン\",\n      \"epayDomainPlaceholder\": \"次のようなEasyPayドメイン名を入力してください： https://pay.example.com\",\n      \"epayMethods\": \"支払い方法\",\n      \"epayMethodsPlaceholder\": \"有効な支払い方法を確認する（{{ length}}を選択）\",\n      \"epayBusinessId\": \"販売業者ID\",\n      \"epayBusinessIdPlaceholder\": \"EasyPayマーチャントIDを入力してください\",\n      \"epayBusinessKey\": \"商人の鍵\",\n      \"epayBusinessKeyPlaceholder\": \"EasyPayマーチャントキーを入力してください\",\n      \"security\": \"セキュリティ設定\",\n      \"securityCheckType\": \"レビューモード\",\n      \"securityCheckTypePlaceholder\": \"レビュータイプを選択してください\",\n      \"securityTextDatabase\": \"ブラックリストシソーラス\",\n      \"securityTextDatabasePlaceholder\": \"次のように、単語の真ん中にスペースで区切られたブラックリストの語彙を入力してください。センシティブワード1センシティブワード2\",\n      \"securityRegexDatabase\": \"正規のブラックリスト式\",\n      \"securityRegexDatabasePlaceholder\": \"式の中央に改行で区切られた通常のブラックリスト式を入力してください。例：\\n^慎重に扱うべき単語1 $\\n^慎重に扱うべき単語2 $\",\n      \"securityBaiduApiKey\": \"Baiduクラウド監査APIキー\",\n      \"securityBaiduApiKeyPlaceholder\": \"Baidu Cloud Review APIキーを入力してください\",\n      \"securityBaiduSecretKey\": \"Baiduクラウド監査シークレットキー\",\n      \"securityBaiduSecretKeyPlaceholder\": \"Baidu Cloudレビューシークレットキーを入力してください\",\n      \"securityCheckModels\": \"特定の監査モデル\",\n      \"securityCheckModelsPlaceholder\": \"{{length}}個の特定の監査モデルが選択されました\",\n      \"securityCheckModelsTip\": \"特定のモデルレビュー。確認後、現在のモデルは特定の監査モデルでレビューできます。**デフォルトでは、すべてのモデルは監査モードに従ってレビューされます**。特定の監査モデルが選択されている場合、**特定の監査モデルに従ってのみレビューされます**、**他のモデルはレビューされません**\",\n      \"securityBaiduTip\": \"バイドゥクラウド監査モード、バイドゥクラウド監査** APIキー**および**シークレットキー**が必要です \\n 詳細と監査戦略の細分性の設定については、[Baidu Cloud Audit Quick Start ]（ https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy ）を参照してください。 \\n 禁止されている語彙レビュー戦略上記のBaidu Cloudドキュメントに従って、Baidu Cloudコンソールでポリシーを設定してください\",\n      \"securityTypes\": {\n        \"none\": \"監査モードなし\",\n        \"dict\": \"テキスト類義語辞典レビューモード\",\n        \"regex\": \"テキストレギュラーレビューモード\",\n        \"baidu\": \"Baiduクラウド監査モード\",\n        \"custom\": \"カスタムバックエンド監査モード\"\n      },\n      \"epayTip\": \"EasyPayは、市場におけるサードパーティの集約支払い契約**ジェネリック**です。**別の支払いやソフトウェアではありません**。状況に応じてプラットフォームを選択できます。**私たちはいかなる推奨事項や責任保証も行いません**。\\n独自のePaymentプラットフォームを構築するか、他の誰かのePaymentプラットフォームに直接接続する資格がある場合： ePaymentプラットフォームには通常、** EasyPay **（ビジネス/自営業のコレクションは比較的安定した収入月間決済）と** CodePayment **（個人のコレクションコードはリアルタイムの到着率が低い）の2つのプラットフォームがあります。\\nEasyPayの設定、EasyPayを有効にするには** EasyPayを有効にする**オプションをクリックする必要があることに注意してください\\nEasyPayはコールバックドメイン名を設定する必要があります。通常の非同期コールバックの前に、**一般設定**で**バックエンドドメイン名**を設定してください\",\n      \"epayAggregation\": \"集約された支払いモデル\",\n      \"epayAggregationTip\": \"集計支払いモードをクリックしても、**集計支払いページ**に直接支払い方法が選択されることはありません。お使いのEasyPayが集計支払いモデルをサポートしていることを確認してください。\",\n      \"prompt_store\": \"プロンプトレコードストレージ\",\n      \"prompt_storeTip\": \"プロンプトレコードストレージ、開いた後、ユーザーのプロンプトレコードはサーバーに保存されます\",\n      \"searchCrop\": \"結果の切り捨てをオンにする\",\n      \"searchCropTip\": \"結果の切り捨てをオンにすると、検索結果コンテンツの文字数が最大文字数を超えた場合、コンテンツは切り捨てられます\",\n      \"searchCropLen\": \"最大結果文字数\",\n      \"searchEngines\": \"検索エンジン設定\",\n      \"searchEnginesPlaceholder\": \"{{length}}検索エンジンが選択されました\",\n      \"searchEnginesSearchPlaceholder\": \"検索エンジン名を入力してください（例： Google ）\",\n      \"searchEnginesEmptyTip\": \"検索エンジンが空の場合、SearXNGで設定されたデフォルトの検索エンジンがデフォルトで使用されます\",\n      \"searchSafeSearch\": \"セーフサーチモード\",\n      \"searchSafeSearchModes\": {\n        \"none\": \"閉じる\",\n        \"moderation\": \"ミディアム\",\n        \"strict\": \"厳格\"\n      },\n      \"searchImageProxy\": \"画像プロキシをオンにする\",\n      \"searchImageProxyTip\": \"画像プロキシ、開いた後に検索エンジンによって返される画像は、SearXNGサービスノードプロキシを介して読み込まれます\",\n      \"searchTest\": \"クイズを検索\",\n      \"searchTestTip\": \"検索テスト、検索テストのクエリを入力してください\",\n      \"customTitle\": \"テーマをカスタマイズ\",\n      \"customJS\": \"カスタムJS\",\n      \"customJSTip\": \"カスタムJSを入力してください\",\n      \"customCSS\": \"カスタムCSS\",\n      \"customCSSTip\": \"カスタムCSSを入力してください\",\n      \"custom\": \"テーマ設定\",\n      \"customJs\": \"カスタムJS\",\n      \"customJsPlaceholder\": \"カスタムJSを入力してください\",\n      \"customCss\": \"カスタムCSS\",\n      \"customCssPlaceholder\": \"カスタムCSSを入力してください\",\n      \"mailProtocol\": \"配送契約\",\n      \"description\": \"ウェブサイトの説明\",\n      \"descriptionTip\": \"SEOの説明に使用されるウェブサイトの説明は、デフォルトの場合は空白のままにします\",\n      \"uploadFaviconSuccess\": \"ロゴが正常にアップロードされました！保存して新しいロゴを適用することを忘れないでください\",\n      \"customHtml\": \"カスタムHTML\",\n      \"customHtmlPlaceholder\": \"カスタムHTMLを入力してください\",\n      \"customThemeAlert\": \"注： WAFなどのセキュリティ保護サービス（ Cloudflare WAF、Long Pavilion、1パネルWAF、Pagoda WAFなど）を使用している場合、カスタムテーマがWAFによって悪意のあるコードと誤認され、403 Forbiddenなどのエラーコードでブロックされる可能性があります。WAF構成を確認するか、WAF構成をオフにしてください。\",\n      \"gaTrackingId\": \"Googleアナリティクスサービス\",\n      \"gaTrackingIdPlaceholder\": \"GoogleアナリティクスIDを入力してください\",\n      \"displayCurrency\": \"通貨を表示\",\n      \"displayCurrencyTip\": \"ウェブサイト表示通貨単位\",\n      \"update\": \"アップデート\",\n      \"group\": \"グループ\",\n      \"group-price\": \"グループ化率\",\n      \"token-group\": \"トークンのグループ化\",\n      \"token-group-tip\": \"トークングループ化、チェックされている場合、現在のグループ化はトークングループ化をサポートし、このグループ化はすべてのユーザーが選択可能なトークングループに表示されます（すべての組み込みグループはトークングループ化を有効にすることはできません）\",\n      \"edit\": \"編集\",\n      \"delete\": \"削除\",\n      \"type\": \"種類\",\n      \"actions\": \"操作\",\n      \"buy-price\": \"購入レート\",\n      \"consume-price\": \"消費比率\",\n      \"gravatar\": \"グラバターアバター\",\n      \"gravatarPlaceholder\": \"Gravatarプロキシアドレス、デフォルトでGravatarアバターを無効にするには空白のままにします\",\n      \"oauth\": {\n        \"title\": \"サードパーティのログイン設定\",\n        \"wechat\": \"WeChatログイン\",\n        \"google\": \"Googleログイン\",\n        \"github\": \"GitHubログイン\",\n        \"telegram\": \"Telegramログイン\",\n        \"enable\": \"アクティブ\",\n        \"disabled\": \"無効にする\",\n        \"client_id\": \"クライアントID\",\n        \"client_id_placeholder\": \"クライアントIDを入力してください\",\n        \"client_secret\": \"クライアントシークレット\",\n        \"client_secret_placeholder\": \"クライアントシークレットを入力してください\",\n        \"redirect_uri\": \"リダイレクトURI\",\n        \"redirect_uri_placeholder\": \"リダイレクトURIを入力してください\",\n        \"scope\": \"承認の範囲\",\n        \"scope_placeholder\": \"承認の範囲を入力してください\",\n        \"auth_url\": \"承認URL\",\n        \"auth_url_placeholder\": \"認証URLを入力してください\",\n        \"token_url\": \"トークンURL\",\n        \"token_url_placeholder\": \"トークンURLを入力してください\",\n        \"user_info_url\": \"ユーザー情報URL\",\n        \"user_info_url_placeholder\": \"ユーザー情報URLを入力してください\",\n        \"bot_token\": \"ロボットトークン\",\n        \"bot_token_placeholder\": \"ロボットトークンを入力してください\",\n        \"bot_name\": \"ボット名\",\n        \"bot_name_placeholder\": \"ボット名を入力してください\",\n        \"rainbow\": \"レインボーアグリゲーションログイン\",\n        \"methods\": \"ログイン方法\",\n        \"methods_placeholder\": \"qq、wx、baidu、douyinなどのカンマで区切られたログイン方法を入力してください\",\n        \"base_url\": \"レインボーアグリゲーションログインドメイン\",\n        \"base_url_placeholder\": \"レインボーアグリゲーションログインドメイン名を入力してください。デフォルトでレインボーアグリゲーションオフィシャルドメイン名https://u.cccyun.ccにするには空白のままにしてください。\",\n        \"require_email\": \"メール認証を有効にする\"\n      },\n      \"stripeTitle\": \"Stripe\",\n      \"stripeTip\": \"Stripeは、クレジットカード、デビットカード、Apple Pay、Google Payなど、複数の支払い方法をサポートする、広く使用されている国際的なオンライン決済システムです。ユーザーに安全で便利な決済エクスペリエンスを提供します。これは、国際決済を処理する必要がある企業に特に適しています。\",\n      \"stripeEnabled\": \"Stripeを有効にする\",\n      \"stripeSecretKey\": \"Stripeシークレットキー\",\n      \"stripeSecretKeyPlaceholder\": \"Stripeシークレットキーを入力してください\",\n      \"stripeWebhookSecret\": \"Stripe Webhookシークレット\",\n      \"stripeWebhookSecretPlaceholder\": \"Stripe Webhookシークレットを入力してください\",\n      \"securityCustomEndpoint\": \"カスタム監査アクセスポイント\",\n      \"securityCustomEndpointPlaceholder\": \"カスタム監査アクセスポイントを入力してください\",\n      \"securityCustomToken\": \"カスタム監査トークン\",\n      \"securityCustomTokenPlaceholder\": \"カスタム監査トークンを入力してください\",\n      \"securityCustomTip\": \"**トークン**と**アクセスポイント**によるカスタム監査モード \\n リクエストと返品の形式はBaidu Cloud Reviewと一致しており、[Baidu Cloud Review Text Review Request Instructions ]（ https://cloud.baidu.com/doc/ANTIPORN/s/Rk3h6xb3i ）にアクセスして参照適応を行うことができます\",\n      \"wechatPayTitle\": \"WeChat Pay\",\n      \"wechatPayTip\": \"WeChat Payは、ユーザーと企業に安全で便利なプロフェッショナルなオンライン決済サービスを提供することに常にコミットしてきました。「WeChat Payment, More Than Payment」をコアコンセプトに、個人ユーザー向けにさまざまな便利なサービスとアプリケーションシナリオを作成し、あらゆる種類の企業や小規模およびマイクロマーチャントにプロフェッショナルな収集機能、運用機能、資金決済ソリューション、セキュリティを提供しています。企業、製品、店舗、ユーザーがWeChatを通じてつながり、スマートライフを実現しています。\",\n      \"wechatPayEnabled\": \"WeChat Payを有効にする\",\n      \"wechatPayAppId\": \"WeChat PayアプリID\",\n      \"wechatPayAppIdPlaceholder\": \"WeChat PayアプリIDを入力してください\",\n      \"wechatPayMchId\": \"WeChat Pay販売業者番号\",\n      \"wechatPayMchIdPlaceholder\": \"WeChat Pay販売業者番号を入力してください\",\n      \"wechatPayKey\": \"WeChat Pay API v 3キー\",\n      \"wechatPayKeyPlaceholder\": \"WeChat Pay API v 3キーを入力してください\",\n      \"wechatPaySerialNo\": \"WeChat決済プラットフォーム証明書シリアル番号\",\n      \"wechatPaySerialNoPlaceholder\": \"WeChat決済プラットフォーム証明書のシリアル番号を入力してください\",\n      \"wechatPayCertificate\": \"WeChat決済プラットフォーム証明書\",\n      \"wechatPayCertificatePlaceholder\": \"WeChat Payプラットフォーム証明書をここに貼り付けてください\",\n      \"wechatPayCertificateTip\": \"加盟店はAPI v 3インターフェースの返されたコンテンツを受け取り、検証のために証明書の公開鍵を使用する必要があります。また、送信のために一部の機密情報パラメータ（名前、ID番号など）も証明書の公開鍵で暗号化する必要があります。詳細については、[WeChat Payment Platform Certificate ]（ https://pay.weixin.qq.com/doc/v3/merchant/4012068814 ）を参照してください。\",\n      \"searchLLMExtract\": \"LLMキーワード抽出を有効にする\",\n      \"searchLLMExtractTip\": \"LLMモデルを使用して検索キーワードをインテリジェントに抽出することで、検索精度を向上させることができます\",\n      \"searchLLMModel\": \"キーワード抽出モデル\",\n      \"searchLLMModelPlaceholder\": \"キーワードを抽出するモデルを選択\",\n      \"securityBlacklistIPs\": \"ブラックリストに登録されたIP\",\n      \"securityBlacklistIPsPlaceholder\": \"ブラックリストIPを入力してください\",\n      \"securityWhitelistIPs\": \"ホワイトリストIP\",\n      \"securityWhitelistIPsPlaceholder\": \"ホワイトリストIPを入力してください\",\n      \"securityWhitelistIPsTip\": \"ブラックリストに登録されたIP **は、APIリクエストのレート制限ミドルウェア**にのみ有効です。他のリクエストやフロントエンドへのアクセスを制限する場合は、WAFなどのセキュリティサービスを使用してください\",\n      \"securityAddIPAddress\": \"IPアドレスを追加\",\n      \"securityRemoveIPAddress\": \"IPアドレスを削除\",\n      \"preDeductQuota\": \"源泉徴収を有効にする\",\n      \"preDeductQuotaTip\": \"オンにすると、リクエストの開始時に手数料が源泉徴収され、オフにすると、リクエストの終了時に手数料が差し引かれます\",\n      \"affiliateTitle\": \"アフィリエイトマーケティング設定\",\n      \"affiliateEnabled\": \"アフィリエイトマーケティングを有効にする\",\n      \"affiliateCommissionRate\": \"コミッション率\",\n      \"affiliateMinWithdraw\": \"最低出金額\",\n      \"affiliateAllowExistingBind\": \"登録ユーザーがマーチングコードをバインドできるようにする\",\n      \"realtime\": {\n        \"title\": \"WebSocketライブストリーム設定\",\n        \"wsBufferSize\": \"WSバッファサイズ\",\n        \"wsBufferSizeTip\": \"サーバーのダウンストリームシャーディングからフロントエンドまでのキューの長さを制御します。より小さい（例えば、１ ）は、上流の終了後のテール待ち時間を減少させる；より大きい（例えば、２ ４ ）は、古い挙動と互換性があるが、より長いテールを有し得る。\",\n        \"wsAggregate\": \"WSフラグメンテーションアグリゲーション\",\n        \"wsAggregateTip\": \"有効にした後、複数の小さなシャードをタイムウィンドウで集約してから発行し、フロントエンドの再レンダリングの頻度を減らし、滑らかさを向上させます。閉じると、各シャードはすぐに発行されます（古い動作）。\",\n        \"wsAggregateWindow\": \"WS集約時間枠（ミリ秒）\",\n        \"wsAggregateWindowTip\": \"フラグメントアグリゲーションの時間枠、15〜33ミリ秒を推奨します。値が大きいほど、マージが多くなり、リフレッシュがスムーズになりますが、最初の段落は少し遅れる場合があります。\"\n      },\n      \"hideKeyDocs\": \"キーページドッキングガイドを非表示にする\",\n      \"xunhupayTitle\": \"タイガーペッパーペイメント\",\n      \"xunhupayTip\": \"Tiger Pepperは、WeChatやAlipayなどのさまざまな支払い方法をサポートする集約型支払いプラットフォームです。設定後、ユーザーはタイガーペッパーを追加することができます。WeChatとAlipayは、それぞれ異なるアプリIDとアプリシークレットを設定する必要があります。\",\n      \"xunhupayWechatEnabled\": \"Tiger Pepper WeChatを有効にする\",\n      \"xunhupayAlipayEnabled\": \"Tiger Pepper Alipayを有効にする\",\n      \"xunhupayWechatAppId\": \"Tiger Pepper WeChatアプリID\",\n      \"xunhupayWechatAppIdPlaceholder\": \"Tiger Pepper WeChat PaymentのアプリIDを入力してください\",\n      \"xunhupayWechatAppSecret\": \"Tiger Pepper WeChatアプリシークレット\",\n      \"xunhupayWechatAppSecretPlaceholder\": \"アプリSecret of Tiger Pepper WeChat Payment (Key)を入力してください\",\n      \"xunhupayAlipayAppId\": \"Tiger Pepper AlipayアプリID\",\n      \"xunhupayAlipayAppIdPlaceholder\": \"Tiger Pepper AlipayのアプリIDを入力してください\",\n      \"xunhupayAlipayAppSecret\": \"Tiger Pepper Alipayアプリシークレット\",\n      \"xunhupayAlipayAppSecretPlaceholder\": \"アプリSecret of Tiger Pepper Alipay （キー）を入力してください\",\n      \"xunhupayEndpoint\": \"Tiger Pepperインターフェースアドレス\",\n      \"xunhupayEndpointPlaceholder\": \"https://api.xunhupay.comまたはhttps://api.dpweixin.com\",\n      \"autoTitle\": {\n        \"title\": \"自動セッションタイトル\",\n        \"enabled\": \"自動タイトルを有効にする\",\n        \"model\": \"モデルを生成\",\n        \"modelPlaceholder\": \"現在のセッションモデルを使用するには空白のままにします\",\n        \"maxLen\": \"タイトルの最大長\",\n        \"minMsgs\": \"メッセージの撤回数\",\n        \"overwrite\": \"既存のタイトルを上書きする\",\n        \"prompt\": \"カスタムプロンプト\",\n        \"promptPlaceholder\": \"{max_len}はプレースホルダーとして使用できます\",\n        \"tip\": \"LLMを使用して、最初のラウンドの会話の後、セッションのタイトルを自動的に要約して設定します。カスタムプロンプトワードが空に設定されている場合、CoAIのデフォルト設定のデフォルトプロンプトワードが使用されます。\"\n      }\n    },\n    \"user\": \"ユーザー管理\",\n    \"invitation-code\": \"招待コード\",\n    \"invitation-manage\": \"招待コードの管理\",\n    \"invitation-tips\": \"招待コードはポイントの引き換えに使用されます。各タイプの招待コードは1人のユーザーが1回のみ使用できます（宣伝に使用できます）\",\n    \"redeem-tips\": \"引き換えコードはクレジットの引き換えに使用され、カード発行などの支払いに使用できます。\",\n    \"redeem\": {\n      \"quota\": \"Whirlies\",\n      \"used\": \"使用数\",\n      \"total\": \"合計\",\n      \"code\": \"コードを利用する\"\n    },\n    \"market\": {\n      \"title\": \"モデルマーケット\",\n      \"model-name\": \"モデル名\",\n      \"model-name-placeholder\": \"モデルのニックネームを入力してください（例： GPT -4 ）\",\n      \"model-id\": \"モデルID\",\n      \"model-id-placeholder\": \"モデルIDを入力してください（例： gpt -4 -0613 ）\",\n      \"model-description\": \"モデルの紹介\",\n      \"model-description-placeholder\": \"モデル紹介を入力してください\",\n      \"model-context\": \"ハイコンテキスト\",\n      \"model-context-tip\": \"モデルがハイコンテキストモデルであるかどうか（ハイコンテキストモデルファイルは、解析時に長いコンテンツによって切り捨てられません）\",\n      \"model-is-default\": \"デフォルトモデル\",\n      \"model-is-default-tip\": \"モデルがデフォルトモデルリストに追加されるかどうか（デフォルトでは、デフォルトモデルリストに追加されていないモデルはホームモデルリストに表示されません）\",\n      \"model-tag\": \"モデルラベル\",\n      \"update-success\": \"正常に更新されました\",\n      \"update-success-prompt\": \"モデルマーケットプレイスが正常に更新されました（今すぐ適用するにはブラウザを更新してください）\",\n      \"update-failed\": \"更新に失敗\",\n      \"update-failed-prompt\": \"{{reason}}の更新リクエストが失敗しました\",\n      \"model-image\": \"モデル写真\",\n      \"custom-image\": \"カスタム画像\",\n      \"custom-image-placeholder\": \"カスタム画像URLを入力してください（例： https://example.com/image.jpg ）\",\n      \"update\": \"更新\",\n      \"new-model\": \"新しいモデル\",\n      \"migrate\": \"提出\",\n      \"sync\": \"アップストリームを同期\",\n      \"sync-tip\": \"アップストリームモデル市場の同期\",\n      \"sync-placeholder\": \"アップストリームのCoAIのAPIアドレスを入力してください。例： https://api.chatnio.net\",\n      \"sync-all\": \"すべて同期({{length}})\",\n      \"sync-self\": \"サポートされているモデルを同期（{{ length }}）\",\n      \"sync-site\": \"アップストリームアドレス\",\n      \"sync-option\": \"同期のオプション\",\n      \"sync-failed\": \"同期に失敗しました\",\n      \"sync-failed-prompt\": \"住所をリクエストできなかったか、モデルマーケットモデルが空です\\n（エンドポイント：{{ endpoint }}）\",\n      \"sync-items\": \"合計{{length}}個のモデルが見つかりました。{{exist}}個のモデルが見つかりました（上書きされません）。{{new}}個のモデルが追加されました（すべて同期済み）。{{support}}個のモデルがこのサイトチャネルでサポートされています（同期対応モデル）\",\n      \"sync-success\": \"同期成功\",\n      \"sync-success-prompt\": \"アップストリームから同期され、{{length}}モデルが追加されました。確認して送信をクリックして有効にしてください。有効にしないと保存されません\",\n      \"not-use\": \"一部のモデルは使用されていません\",\n      \"import-all\": \"すべてインポート...\",\n      \"function-calling\": \"関数呼び出し\",\n      \"function-calling-tip\": \"モデルが関数呼び出し関数呼び出しをサポートしているかどうか（一部のモデルとリバースエンジニアリングは関数呼び出しをサポートしていません）\",\n      \"vision-model\": \"マッピングモデル\",\n      \"vision-model-tip\": \"モデルが地図モデルかどうか（地図モデルはGPT -4 Turboなどの画像入力に対応）\",\n      \"ocr-model\": \"OCRアシスト\",\n      \"ocr-model-tip\": \"モデル自体が画像入力をサポートしていない場合、OCRテキスト認識をオンにして、モデルのビジュアル機能をある程度補完することができます（ヒント：ファイル解析サービスはOCRサービスをサポートする必要があります）\",\n      \"reverse-model\": \"リバースモデル\",\n      \"reverse-model-tip\": \"リバースエンジニアリングモデルがURLによる完全なファイル解析（ PDF、Wordなど）をサポートしている場合、このオプションをオンにすることができ、すべての種類のファイル解析がアップストリームによって提供され、トークンの消費が削減されます。デフォルトでオンになっていない場合、このプロジェクトによって解決されます。これはほとんどのモデルに適用されます。ファイル解析サービスが外部URLストレージスキーム（ S 3/R 2/MinIOなど）を構成しており、外部URL解析ファイルがモデルの上流でサポートされていることを確認してください。\",\n      \"thinking-model\": \"思考モデル\",\n      \"thinking-model-tip\": \"モデルがディープシンキングをサポートしているかどうか（ディープシンキングモデルは、クロード3.7ソネットなどのコンテンツを出力する際に一連の思考を出力します）\"\n    },\n    \"model-chart-tip\": \"トークンの使用状況\",\n    \"subscription\": \"サブスクリプション管理\",\n    \"logger\": {\n      \"title\": \"サービスログ\",\n      \"console\": \"コンソール\",\n      \"consoleLength\": \"ログエントリの数\"\n    },\n    \"plan\": {\n      \"enable\": \"サブスクリプションを有効にする\",\n      \"price\": \"価格\",\n      \"price-tip\": \"1月のサブスクリプション価格（単位：元）\",\n      \"item-id\": \"ID\",\n      \"item-id-placeholder\": \"エンティティIDを入力してください（アイテムIDは複数回使用することはできません。例： gpt -4 ）\",\n      \"item-name\": \"名称\",\n      \"item-name-placeholder\": \"エンティティ名を入力してください（アイテム名はサブスクリプションリストにエンティティ名を表示するために使用されます。例： GPT -4 ）\",\n      \"item-value\": \"クォータ\",\n      \"item-value-tip\": \"月間クォータ（単位：回）\",\n      \"item-icon\": \"を使って「\",\n      \"item-icon-tip\": \"エンティティアイコン（サブスクリプションリストに表示されるアイテムアイコンで使用されるアイコン）\",\n      \"item-models\": \"モデル\",\n      \"item-models-tip\": \"エンティティがカバーするモデル（アイテムモデルは、サブスクリプションリストにモデルを表示するために使用されます）\",\n      \"item-models-search-placeholder\": \"モデルIDを検索\",\n      \"item-models-placeholder\": \"{{length}}モデルが選択されました\",\n      \"add-item\": \"登録\",\n      \"import-item\": \"導入\",\n      \"sync\": \"アップストリームを同期\",\n      \"sync-option\": \"同期のオプション\",\n      \"sync-site\": \"アップストリームアドレス\",\n      \"sync-placeholder\": \"アップストリームのCoAIのAPIアドレスを入力してください。例： https://api.chatnio.net\",\n      \"sync-result\": \"アップストリームサブスクリプションルールの数は{{length}}で、{{models}}モデルをカバーしています。このサイトのサブスクリプションルールを上書きしますか？\",\n      \"discounts\": \"割引設定\",\n      \"discounts-tip\": \"割引設定（有効になっていない場合はデフォルト、半期サブスクリプションの場合は90%、1年間サブスクリプションの場合は80%）\",\n      \"discount-value\": \"割引額\",\n      \"discount-off\": \"オフ\"\n    },\n    \"model-usage-chart\": \"使用機種の割合\",\n    \"user-type-chart\": \"ユーザータイプの割合\",\n    \"identity\": {\n      \"normal\": \"一般ユーザー\",\n      \"api_paid\": \"その他の有料ユーザー\",\n      \"basic_plan\": \"ベーシックサブスクライバー\",\n      \"standard_plan\": \"標準サブスクライバー\",\n      \"pro_plan\": \"Pro Subscribers\"\n    },\n    \"user-type-chart-info\": \"合計{{total}}ユーザー\",\n    \"user-type-chart-tip\": \"他の有料ユーザー：有効期限切れのユーザーまたはポイントが現在の初期ポイントを超えるユーザーをサブスクライブするユーザーを指します（招待コードを使用するなどの操作もポイントの増加としてカウントされます）\",\n    \"is-banned\": \"禁止\",\n    \"email\": \"メール\",\n    \"quota-set-action\": \"ポイント設定\",\n    \"quota-set-action-desc\": \"ユーザーのクレジットを設定する\",\n    \"release-subscription-action\": \"サブスクリプションの使用をリリースする\",\n    \"release-subscription-action-desc\": \"ユーザーのサブスクリプション使用を無料にしますか？\",\n    \"subscription-level\": \"サポートランクを設定する\",\n    \"subscription-level-desc\": \"ユーザーのサブスクリプションレベルの設定\",\n    \"password-action\": \"パスワード変更\",\n    \"password-action-desc\": \"ユーザーの新しいパスワードを入力してください\",\n    \"email-action\": \"メールアドレスを変更する\",\n    \"email-action-desc\": \"ユーザーの新しいメールアドレスを入力してください\",\n    \"default-password\": \"パスワード変更プロンプト\",\n    \"default-password-prompt\": \"管理者パスワードはデフォルトのパスワードです。アカウントのセキュリティのため、できるだけ早くパスワードを変更してください。（バックオフィスに移動-システム設定-ルートパスワードの変更）\",\n    \"set-admin-action\": \"管理者にする\",\n    \"set-admin-action-desc\": \"このユーザーを管理者にしてもよろしいですか？\",\n    \"cancel-admin-action\": \"管理者をキャンセル\",\n    \"cancel-admin-action-desc\": \"このユーザーの管理者アクセスを削除してもよろしいですか？\",\n    \"ban-action\": \"ゴーストユーザー\",\n    \"ban-action-desc\": \"このユーザーを禁止してもよろしいですか？\",\n    \"unban-action\": \"ユーザーのブロックを解除する\",\n    \"unban-action-desc\": \"このユーザーのブロックを解除してもよろしいですか？\",\n    \"billing\": \"収入\",\n    \"coai-format-only\": \"このフォーマットはCoAIに固有です\",\n    \"exit\": \"バックグラウンドからログアウト\",\n    \"view\": \"確認\",\n    \"broadcast-tip\": \"通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。\",\n    \"created-at\": \"作成日時\",\n    \"used-at\": \"乗車時間\",\n    \"used-username\": \"ユーザーを請求する\",\n    \"payment\": \"注文代金の支払い\",\n    \"pay\": {\n      \"epay\": \"お支払いが簡単\",\n      \"afdian\": \"発電を愛する\",\n      \"order\": \"注文書番号\",\n      \"amount\": \"金額\",\n      \"status\": \"お支払い状況\",\n      \"service\": \"支払いチャネル\",\n      \"type\": \"支払いタイプ\",\n      \"device\": \"デバイス\",\n      \"username\": \"ユーザー名\",\n      \"status-true\": \"支払い済み\",\n      \"status-false\": \"未払い\",\n      \"created-at\": \"作成日時\",\n      \"updated-at\": \"アップデート時間\",\n      \"action\": \"操作\",\n      \"copy-order\": \"注文番号が重複しています\",\n      \"search\": \"注文番号またはユーザー名を検索\",\n      \"check-order\": \"注文状況を確認する\",\n      \"check-result-same\": \"一貫した注文状況\",\n      \"check-result-diff\": \"注文状況の更新\",\n      \"check-result-same-prompt\": \"注文状況が一貫しているため、更新する必要はありません\",\n      \"check-result-diff-prompt\": \"注文ステータスが更新され、支払いが完了しました\",\n      \"wechatpay\": \"WeChat Pay\",\n      \"stripe\": \"Stripe\"\n    },\n    \"delete-broadcast\": \"通知を削除\",\n    \"delete-broadcast-desc\": \"本当によろしいですか？この操作は元に戻せません。これにより、通知が完全に削除されます。\",\n    \"expired-at\": \"サブスクリプションの有効期限\",\n    \"is-subscribed-tips\": \"サブスクリプション判断ロジック：サブスクリプションティアがあり、サブスクリプション期間が満了していません\",\n    \"online-chats\": \"チャット数\",\n    \"cdn\": {\n      \"warmup\": \"リソースのウォームアップ\",\n      \"warm-tip\": \"> CDNサービスを使用している場合は、この機能を使用してリソースをウォームアップできます。\\n**各アップデートの後、リソースのリフレッシュを実行して、リソースの安定性を確保し、読み込み速度を向上させることができます。**\\nCDN （コンテンツ配信ネットワーク）リソースは予熱され、予熱後、リソースはCDNノードにキャッシュされてアクセスを高速化します。\\nウォームアップ機能を使用すると、ビジネスのピーク前に人気のあるリソースをCDNノードにキャッシュすることができ、ソースステーションへの圧力を軽減し、ユーザーエクスペリエンスを向上させることができます。\\nヒント：**ウォームアップ実行は、CDNから送信元ステーションに大量のデータをプルします。送信元ステーションのブロードバンド負荷に注意してください。**\",\n      \"copy-data\": \"事前に加熱されたURLリソースリストをコピー\"\n    },\n    \"license\": {\n      \"title\": \"授権管理\",\n      \"domain\": \"許可されたドメイン\",\n      \"digest\": \"署名の概要\",\n      \"module\": \"モジュール管理\",\n      \"modules\": {\n        \"bought\": \"売却済み\",\n        \"not-bought\": \"購入されていません\",\n        \"multiKey\": {\n          \"title\": \"マルチトークン管理\",\n          \"description\": \"マルチAPIキー管理、1つのユニットでのマルチトークン配布管理のサポート、コール可能なモデルの設定、残高制限、コールログ、ステータス管理、統合ガイド、その他の高度な機能のサポート\"\n        },\n        \"stripe\": {\n          \"title\": \"Stripe Payments\",\n          \"description\": \"Stripeアドバンスドペイメントモジュール、Stripeカード決済とAlipayHKドッキングをサポート、マルチ通貨決済をサポート\"\n        },\n        \"paypal\": {\n          \"title\": \"PayPal支払い\",\n          \"description\": \"PayPalアドバンスドペイメントモジュール、PayPalカード決済などの機能をサポート\"\n        },\n        \"afdian\": {\n          \"title\": \"発電を愛する\",\n          \"description\": \"LOVE発電支払いWebhookモジュールでLOVE発電残高購入をサポート\"\n        },\n        \"bot\": {\n          \"title\": \"ロボット\",\n          \"description\": \"WeChat/Flybook/Telegram/DiscordロボットSaaSモジュール\"\n        },\n        \"digital\": {\n          \"title\": \"デジタルヒューマン\",\n          \"description\": \"デジタルヒューマンビデオ生成モジュールのカスタマイズ、高度な動的音声技術、音声と顔のクローニングのサポート、プライベート展開の推論と複数のエンジンのサポート、業界全体のシーンサポート、高次元カスタマイズのサポート\"\n        },\n        \"buy-tip\": \"このモジュールを購入するには、営業担当者にお問い合わせください\",\n        \"contact-for-price\": \"文書にアクセスして見積もりを取得する\",\n        \"coai-pro\": {\n          \"title\": \"CoAI Pro\",\n          \"description\": \"複数の支払いチャネルのドッキング、ユーザー（グループ）の拡大制御、コンテンツレビュー、セッションログなど、すべてのビジネス機能のロックを解除するCoAI Pro Business Edition承認。\"\n        }\n      },\n      \"info\": \"承認情報\",\n      \"description\": \"CoAI Proバージョンライセンス管理\",\n      \"purchase\": \"購入承認\",\n      \"pro-required\": \"この機能はCoAI Pro専用です。この機能を使用するには、ライセンス管理ページでCoAI Proライセンスを購入してください\"\n    },\n    \"group\": \"グループ\",\n    \"custom-group\": \"カスタムグループ化\",\n    \"custom-group-action\": \"カスタムグループを設定する\",\n    \"custom-group-action-desc\": \"カスタムグループ名を入力してください\",\n    \"group-setting\": \"グループ設定\",\n    \"notifications\": \"プッシュセンター\",\n    \"notify-all\": \"すべてのユーザーに通知\"\n  },\n  \"mask\": {\n    \"title\": \"プリセット\",\n    \"search\": \"プリセット名を検索\",\n    \"context\": \"{{length}}のコンテキストが含まれています\",\n    \"system\": \"システムプリセット\",\n    \"custom\": \"マイプリセット\",\n    \"edit\": \"プリセットを編集\",\n    \"create\": \"新規プリセット\",\n    \"avatar\": \"プリセットアバター\",\n    \"conversation\": \"予定された会話\",\n    \"name\": \"プリセットタイトル\",\n    \"name-placeholder\": \"プリセットのタイトルを入力してください\",\n    \"description\": \"プリセットの紹介\",\n    \"description-placeholder\": \"プリセットプロファイルを入力してください\",\n    \"search-emoji\": \"絵文字を検索\",\n    \"actions\": {\n      \"clone\": \"プリセットを複製\",\n      \"use\": \"プリセットを使用\",\n      \"edit\": \"プリセットを編集\",\n      \"delete\": \"プリセットを削除します。\"\n    },\n    \"market\": \"プリセットマーケット\",\n    \"switch-preset\": \"プリセットを切り替える\",\n    \"switch-preset-desc\": \"新しい会話を開始し、プリセットに切り替えました\"\n  },\n  \"register\": \"登録\",\n  \"auth\": {\n    \"username\": \"ユーザー名\",\n    \"password\": \"パスワード\",\n    \"username-or-email\": \"ユーザー名またはメールアドレス\",\n    \"username-or-email-placeholder\": \"ユーザー名またはメールアドレスを入力してください\",\n    \"password-placeholder\": \"パスワードを入力してください\",\n    \"forgot-password\": \"パスワードを忘れた！\",\n    \"reset-password\": \"パスワードをリセット\",\n    \"no-account\": \"アカウントをお持ちではありませんか？\",\n    \"register\": \"にサインアップする\",\n    \"username-placeholder\": \"ここにあなたのユーザ名を入力\",\n    \"check-password\": \"パスワード確認\",\n    \"check-password-placeholder\": \"もう一度、パスワードを入力し直して下さい。\",\n    \"email\": \"メールアドレス\",\n    \"email-placeholder\": \"メールアドレスを入力してください\",\n    \"have-account\": \"すでにアカウントをお持ちですか？\",\n    \"login\": \"ログインしました。\",\n    \"next-step\": \"次へ\",\n    \"verify\": \"検証\",\n    \"code\": \"認証コード\",\n    \"code-placeholder\": \"認証コードを入力してください\",\n    \"send-code\": \"送信する\",\n    \"incorrect-info\": \"情報が間違っていますか？\",\n    \"fall-back\": \"1つ前のステップに戻る\",\n    \"length-range\": \"{{min }}〜{{ max}}桁が必要です\",\n    \"same-rule\": \"一貫性のない入力\",\n    \"invalid-email\": \"メールの形式が正しくありません\",\n    \"reset-success\": \"リセットに成功しました\",\n    \"reset-success-prompt\": \"パスワードがリセットされました。新しいパスワードでログインしてください。\",\n    \"send-code-success\": \"送信成功\",\n    \"send-code-success-prompt\": \"認証コードがメールアドレスに送信されました。ご確認ください。\",\n    \"send-code-failed\": \"送信失敗\",\n    \"send-code-failed-prompt\": \"認証コードの送信に失敗しました。理由：{{ reason}}\",\n    \"register-success\": \"登録に成功しました\",\n    \"register-success-prompt\": \"登録が完了しました。ようこそ！\",\n    \"disabled-mail\": \"現在のサイトのメールボックスは無効になっています。管理者に連絡して郵送機能を有効にしてください。\",\n    \"code-disabled-placeholder\": \"メールアドレスの認証は必要ありません\",\n    \"wechat\": \"WeChatPay\",\n    \"connected\": \"バインドに成功しました\",\n    \"connected-prompt\": \"アカウントのリンクが完了しました！\",\n    \"providers\": {\n      \"baidu\": \"百度\",\n      \"huawei\": \"ファーウェイ\",\n      \"weibo\": \"微博\",\n      \"sina\": \"微博\",\n      \"wx\": \"WeChatPay\",\n      \"qq\": \"QQ\",\n      \"xiaomi\": \"きび\",\n      \"douyin\": \"抖音\",\n      \"dingtalk\": \"ダボ\",\n      \"alipay\": \"Alipay\",\n      \"microsoft\": \"Microsoft\"\n    }\n  },\n  \"reset\": \"リセット\",\n  \"request-error\": \"{{reason}}のためにリクエストできませんでした\",\n  \"update\": \"更新\",\n  \"delete\": \"削除\",\n  \"remove\": \"追放\",\n  \"upward\": \"上へ移動\",\n  \"downward\": \"下へ移動\",\n  \"save\": \"保存\",\n  \"announcement\": \"サイトのお知らせ\",\n  \"i-know\": \"私は知っています\",\n  \"submit\": \"提出\",\n  \"empty\": \"空\",\n  \"exit\": \"離れる\",\n  \"model\": \"モデル\",\n  \"min-quota\": \"最低残高\",\n  \"your-quota\": \"残高\",\n  \"title\": \"タイトル\",\n  \"my-account\": \"マイアカウント\",\n  \"payment\": {\n    \"wechat\": \"WeChat Pay\",\n    \"wxpay\": \"WeChat Pay\",\n    \"alipay\": \"Alipay\",\n    \"paypal\": \"PayPal\",\n    \"stripe\": \"Stripe\",\n    \"afdian\": \"発電を愛する\",\n    \"qqpay\": \"QQウォレット\",\n    \"order\": {\n      \"quota\": \"{{quota}}クレジット\"\n    },\n    \"wechatpay\": \"WeChat Pay\",\n    \"dialog-wechatpay\": {\n      \"title\": \"WeChat Pay\",\n      \"description\": \"WeChatを使用して、以下のQRコードをスキャンしてお支払いください\",\n      \"success\": \"購入完了！\",\n      \"loading\": \"読み込み中…\",\n      \"remaining-time\": \"残りのお支払い時間\"\n    },\n    \"notify-stripe\": {\n      \"success\": \"購入完了！\",\n      \"canceled\": \"支払いキャンセル！\",\n      \"processing\": \"支払いを処理しています...\"\n    },\n    \"xunhupay-wechat\": \"タイガーペッパーWeChat\",\n    \"xunhupay-alipay\": \"タイガーペッパーアリペイ\",\n    \"dialog-xunhupay\": {\n      \"title\": \"タイガーペッパーペイメント\",\n      \"description\": \"WeChatまたはAlipayを使用して、以下のQRコードをスキャンしてお支払いください\",\n      \"success\": \"購入完了！\",\n      \"remaining-time\": \"残りのお支払い時間\"\n    }\n  },\n  \"back-home\": \"ホームページに戻る\",\n  \"copied\": {\n    \"prompt\": \"Copy\",\n    \"success\": \"コピー成功\",\n    \"success-description\": \"コンテンツをクリップボードにコピーしました\",\n    \"failed\": \"コピー失敗\",\n    \"failed-description\": \"{{reason}}のためにコピーできませんでした\"\n  },\n  \"record\": {\n    \"user\": \"顧客\",\n    \"title\": \"使用履歴\",\n    \"created-at\": \"期日\",\n    \"type\": \"種類\",\n    \"model\": \"モデル\",\n    \"token\": \"トークン\",\n    \"input-tokens\": \"入力\",\n    \"output-tokens\": \"出力\",\n    \"quota\": \"Whirlies\",\n    \"duration\": \"経過時間\",\n    \"detail\": \"備考\",\n    \"types\": {\n      \"system\": \"システム\",\n      \"consume\": \"消費\",\n      \"topup\": \"チャージ\",\n      \"all\": \"すべて\"\n    },\n    \"billing-today\": \"今すぐご利用ください\",\n    \"billing-month\": \"今月のご利用額\",\n    \"cond\": {\n      \"model\": \"モデル\",\n      \"model-placeholder\": \"モデル名を入力してください\",\n      \"token-name\": \"トークン名\",\n      \"token-name-placeholder\": \"トークン名を入力してください\",\n      \"start_time\": \"開始時間\",\n      \"end_time\": \"終了時間\",\n      \"username\": \"ユーザー名を指定してください\",\n      \"username-placeholder\": \"ここにあなたのユーザ名を入力\",\n      \"type\": \"タイプを指定してください\"\n    },\n    \"request-today\": \"本日のリクエスト\",\n    \"request-month\": \"今月のリクエスト\",\n    \"detail-info\": {\n      \"input\": \"価格を入力してください\",\n      \"output\": \"アウトプット価格\",\n      \"times\": \"1価格あたり\",\n      \"no-cost\": \"請求なし\",\n      \"cached\": \"ヒットキャッシュ\",\n      \"plan\": \"サブスクリプション請求\",\n      \"empty\": \"キャリア喪失\",\n      \"error\": \"リクエストエラー\",\n      \"percent\": \"グループ化率\"\n    },\n    \"rpm-tips\": \"現在のRPM （ 1分あたりのリクエスト数）\",\n    \"tpm-tips\": \"現在のTPM （ 1分あたりのトークン数）\",\n    \"query\": \"レコードのクエリ\",\n    \"channel\": \"チャネル\"\n  },\n  \"date\": {\n    \"pick\": \"日付を選択\",\n    \"today\": \"今日\",\n    \"clean\": \"ゼロに戻る\",\n    \"add-day\": \"1日追加\",\n    \"sub-day\": \"1日短縮\",\n    \"add-month\": \"1か月追加\",\n    \"sub-month\": \"1か月短縮\",\n    \"add-year\": \"1年追加\",\n    \"sub-year\": \"1年減\"\n  },\n  \"renderer\": {\n    \"viewImage\": \"画像を表示\",\n    \"imageLoadFailed\": \"画像{{src}}の読み込みに失敗しました\",\n    \"base64Image\": \"Base 64イメージを展開する\",\n    \"base64ImageCollapse\": \"画像ベース64を折りたたむ\",\n    \"viewVideo\": \"ビデオを見る\",\n    \"videoLoadFailed\": \"ビデオ{{src}}の読み込みに失敗しました\"\n  },\n  \"login-action\": \"サインインしてその他の機能をご利用ください\",\n  \"anonymous\": \"ログインしていません\",\n  \"bar\": {\n    \"chat\": \"ダイアログ\",\n    \"model\": \"モデル\",\n    \"wallet\": \"財布\",\n    \"log\": \"ログ\",\n    \"admin\": \"バックグラウンド\",\n    \"preset\": \"プリセット\",\n    \"account\": \"アカウント\",\n    \"chat-full\": \"会話を開始する\",\n    \"model-full\": \"モデルを見る\",\n    \"preset-full\": \"プリセットマーケット\",\n    \"wallet-full\": \"ウォレット\",\n    \"log-full\": \"使用履歴\",\n    \"account-full\": \"アカウント管理\",\n    \"admin-full\": \"管理者\",\n    \"key\": \"鍵\",\n    \"key-full\": \"トークン管理\"\n  },\n  \"notify\": \"お知らせ\",\n  \"new-notify\": \"新しい通知\",\n  \"view-all\": \"すべて見る\",\n  \"filter\": {\n    \"filter\": \"フィルタ \",\n    \"conds\": \"フィルタリングされた{{count}}条件\",\n    \"plan\": \"登録するかどうか\",\n    \"all\": \"すべて\",\n    \"subscribed\": \"登録済み\",\n    \"unsubscribed\": \"購読していません\",\n    \"admin\": \"監督\",\n    \"not-admin\": \"非管理者\",\n    \"ban\": \"ゴースト化するかどうか\",\n    \"banned\": \"ゴースト化されました\",\n    \"not-banned\": \"ゴースト化されていません\",\n    \"sorts\": {\n      \"sort\": \"領域で\",\n      \"id-desc\": \"ID降順\",\n      \"id-asc\": \"ID昇順\",\n      \"quota-desc\": \"ポイント降順\",\n      \"quota-asc\": \"ポイントの昇順\",\n      \"used-quota-desc\": \"使用したポイント（降順）\",\n      \"used-quota-asc\": \"昇順で使用されるポイント\",\n      \"plan-desc\": \"サブスクリプションの有効期限時間の降順\",\n      \"plan-asc\": \"サブスクリプションは昇順で期限切れになります\"\n    }\n  },\n  \"not-login\": \"ログインしていません\",\n  \"account\": {\n    \"title\": \"アカウント管理\",\n    \"my-account\": \"マイアカウント\",\n    \"registerDays\": \"{{days}}日間サインアップ\",\n    \"current-quota\": \"現在のポイント\",\n    \"used-quota\": \"クレジットが使用されました\",\n    \"plan-total-month\": \"合計サブスクリプション月数\",\n    \"plan-total-month-tips\": \"サブスクリプションレベルのアップグレードとダウングレードによる月数の変更は、この統計にはカウントされません\",\n    \"deeptrain\": \"DeepTrainユニファイドアカウント管理\",\n    \"deeptrain-description\": \"CoAIはDeepTrainの製品です。DeepTrainのユニファイドアカウント管理システムは、ユーザーにユニファイドアカウント管理サービスを提供します。このページでは、アカウント情報、サードパーティのアカウントリンク、2 FA設定、ウォレット設定、認証情報などを表示できます。Deeptrainユニファイドアカウント管理は、CoAI、Fystart、LightNotesなどの製品で一般的に使用されています。\",\n    \"my-account-description\": \"お客様のアカウント情報、第三者のアカウントリンク情報など\",\n    \"api-description\": \"グローバルアカウントAPI情報\",\n    \"share-description\": \"履歴の会話の表示と管理\",\n    \"share-delete\": \"シェアを削除\",\n    \"share-delete-description\": \"この共有を削除してもよろしいですか？\",\n    \"notification\": {\n      \"title\": \"通知センター\",\n      \"description\": \"通知方法を管理する\",\n      \"fetchError\": \"通知設定の取得に失敗しました\",\n      \"fetchErrorDesc\": \"通知設定の取得に失敗しました。ネットワークを確認して、もう一度お試しください。\",\n      \"updateSuccess\": \"更新通知が正常に構成されました\",\n      \"updateSuccessDesc\": \"通知設定が正常に更新されました（今すぐ適用するにはブラウザを更新してください）\",\n      \"save\": \"保存\",\n      \"updateError\": \"通知設定の更新に失敗しました\",\n      \"updateErrorDesc\": \"通知設定の更新に失敗しました。ネットワークを確認して、もう一度お試しください。\",\n      \"testSuccess\": \"テスト通知が正常に設定されました\",\n      \"testSuccessDesc\": \"通知設定が正常にテストされました\",\n      \"testError\": \"テスト通知の設定に失敗しました\",\n      \"testErrorDesc\": \"テスト通知の設定に失敗しました。ネットワークを確認して、もう一度やり直してください。\",\n      \"enabled\": \"アクティブ\",\n      \"disabled\": \"無効にする\",\n      \"appToken\": \"アプリトークン\",\n      \"topicId\": \"被験者ID\",\n      \"webhookUrl\": \"Webhook URL\",\n      \"botToken\": \"トークン\",\n      \"chatId\": \"チャットID\",\n      \"test\": \"テスト\",\n      \"testDesc\": \"テスト通知の設定\",\n      \"alertTitle\": \"プッシュセンター\",\n      \"alertDescription\": \"WeChat （ WxPusher ）、Discord、Telegram、Flybookプッシュに対応\",\n      \"type\": {\n        \"wxpusher\": \"WeChat （ WxPusher ）\",\n        \"discord\": \"Discord\",\n        \"telegram\": \"電報\",\n        \"feishu\": \"空飛ぶ本\",\n        \"email\": \"メール\",\n        \"webhook\": \"Webhooks\"\n      },\n      \"tiplist\": {\n        \"wxpusher\": \"- [WxPusher ]( https://wxpusher.zjiecode.com)サービスを使用したWeChatプッシュ\\n- WxPusher Webサイトでアプリを登録して作成する\\n-アプリのAppTokenを取得し、構成を入力する\\n-一括メーリングの場合、TopicIdはオプションです\\n-アプリのQRコードをスキャンしてフォローし、プッシュ通知を受け取る\",\n        \"discord\": \"- Webhookメカニズムに基づくDiscordプッシュ\\n- Discordサーバーでターゲットチャンネルを選択します\\n- [チャンネル設定] > [統合] > [Webhookの作成]に移動します\\n-カスタムWebhook名とアバター（オプション）\\n-生成されたWebhook URLポピュレーション構成をコピーする\",\n        \"telegram\": \"- Telegramプッシュは独自のボットを作成する必要があります\\n- Telegramで@ BotFatherを検索して会話を開始\\n-/newbotコマンドを送信し、プロンプトに従ってボット名とユーザー名を設定します\\n-ボットトークンを取得し、設定を入力します\\n-ターゲットグループのボットに参加するか、個人的にチャットする\\n- @ userinfobotを使用してチャットのチャットIDを取得し、入力します\",\n        \"feishu\": \"- Flybookプッシュはグループカスタムボットを使用します\\n-ターゲットのフライングブックグループにカスタムボットを追加する\\n-ボット名、アバター、説明を設定する\\n-メッセージを受信するグループを選択します\\n-生成されたWebhook URLポピュレーション構成をコピーする\\n-セキュリティを強化するためにキーワードを設定できます（オプション）\",\n        \"email\": \"-メールプッシュは既定のオプションであり、通知は登録済みのメールアドレスにプッシュされます\",\n        \"webhook\": \"-カスタムWebhook URLプッシュ\\n\\n''' json\\n${WEBHOOK_URL}を投稿\\n{\\n  \\\"type \\\":\\\" string \\\",\\n  \\\"content \\\":\\\" string \\\",\\n  \\\"time \\\":\\\" number \\\",\\n  \\\"utc_time \\\":\\\"文字列\\\",\\n  \\\"account_id \\\":\\\"番号\\\",\\n  \\\"additional_data \\\": {...}}\\n\\n```\"\n      },\n      \"method\": \"通知方法\",\n      \"event\": \"サブスクリプションイベント\",\n      \"url\": \"この URL\",\n      \"events\": {\n        \"broadcast_event\": \"プッシュ通知情報\",\n        \"payment_event\": \"支払い、サブスクリプション通知\",\n        \"key_quota_not_enough_event\": \"キーリミットアラート\",\n        \"account_quota_not_enough_event\": \"アカウント制限の警告\"\n      },\n      \"userId\": \"ユーザーUID\"\n    },\n    \"oauth\": \"サードパーティのログイン\",\n    \"oauth-description\": \"サードパーティのログインをバインドして管理する\"\n  },\n  \"manage\": \"管理\",\n  \"loading\": \"読み込み中…\",\n  \"send\": \"送信する\",\n  \"stop\": \"停止\",\n  \"new-chat\": \"新しい会話\",\n  \"enter\": \"ラインフィード\",\n  \"key\": {\n    \"title\": \"マイトークン\",\n    \"name\": \"名称\",\n    \"namePlaceholder\": \"名称を入力してください\",\n    \"status\": \"状態\",\n    \"quota\": \"制限\",\n    \"quotaPlaceholder\": \"利用可能な割り当てを入力してください\",\n    \"usedQuota\": \"使用済みクォータ\",\n    \"remainQuota\": \"残りのクレジット\",\n    \"infiniteQuota\": \"無制限のクレジット\",\n    \"createdAt\": \"作成日時\",\n    \"expiredAt\": \"失効時間\",\n    \"key\": \"鍵\",\n    \"advanced\": \"詳細設定\",\n    \"ipWhiteList\": \"IPホワイトリスト\",\n    \"enableIpWhiteList\": \"IPホワイトリストを有効にする\",\n    \"enableIpWhiteListTip\": \"IPホワイトリストをオンにします。ホワイトリスト内のIPアドレスのみがこのキーを使用できます。入力されていない場合、すべてのIPが許可されます（不要、推奨されません）\",\n    \"ipWhiteListPlaceholder\": \"IPアドレスまたはネットワークセグメントを127.0.0.1,192.168.0.0/16の形式で入力してください\",\n    \"modelWhiteList\": \"モデルホワイトリスト\",\n    \"enableModelWhiteList\": \"モデルのホワイトリストを有効にする\",\n    \"enableModelWhiteListTip\": \"モデルのホワイトリストを有効にします。ホワイトリスト内のモデルのみがこのキーを使用できます。すべてのモデルを使用するには空白のままにします（オプション、推奨されません）\",\n    \"modelWhiteListPlaceholder\": \"{{length}}モデルがチェックされました\",\n    \"create\": \"トークンを作成\",\n    \"update\": \"トークンを更新\",\n    \"nameEmpty\": \"名前は空にできません\",\n    \"searchPlaceholder\": \"キー名を検索...\",\n    \"disabled\": \"無効\",\n    \"active\": \"有効\",\n    \"delete\": \"トークンを削除\",\n    \"never\": \"有効期限なし\",\n    \"oneHour\": \"1時間\",\n    \"oneDay\": \"1日\",\n    \"oneWeek\": \"週\",\n    \"oneMonth\": \"1か月\",\n    \"oneYear\": \"1年\",\n    \"disable\": \"無効にする\",\n    \"disableToken\": \"トークンを無効にする\",\n    \"docs\": \"ドッキングガイド\",\n    \"slogan\": \"「最先端のAI製品をワンクリックでドッキング！」\",\n    \"selectKey\": \"キーを選択\",\n    \"bindLobeChat\": \"バインドローブチャット\",\n    \"bindLobeChatTip\": \"ボタンをクリックすると、Lobe Chatにリダイレクトされ、キーやその他の情報が自動的にバインドされます\",\n    \"bindNextChat\": \"次のチャットをバインド\",\n    \"bindNextChatTip\": \"ボタンをクリックすると、次のチャットにリダイレクトされ、キーなどのプリセット情報が自動的に入力されます\",\n    \"bindOpenCat\": \"開いた猫を縛る\",\n    \"bindOpenCatTip\": \"ボタンをクリックすると、Open Catにリダイレクトされ、プリセットパラメータにバインドされます（まずデバイスにOpen Catをインストールしてください）\",\n    \"bindOneAPIStep1\": \"[チャネル管理]ページに移動し、[チャネルを追加]をクリックします。\",\n    \"bindOneAPIStep2\": \"OpenAIタイプを選択し、アクセスポイントに従って対応する情報を入力します。以下のキー\",\n    \"bindCoAIStep1\": \"バックグラウンドでチャネル管理ページに移動し、「アップストリームに接続」をクリックします\",\n    \"bindCoAIStep2\": \"アクセスポイントと以下のキー情報に従って対応する情報を入力してください\",\n    \"description\": \"APIの互換性の問題を考慮せずに、このサイトのすべてのAIモデルをOpenAI API標準フォーマットで呼び出すことをサポートし、開発者/サードパーティツールのシームレスな統合、組み込みのクォータ/時間/スコープ/権限管理をサポートします\",\n    \"noKey\": \"キーがありません\",\n    \"apiBase\": \"APIアクセスポイント\",\n    \"apiBaseTip\": \"リマインダー：このAPIベースアクセスポイントをクライアントで設定してください。共通のツールアクセスは、以下のアクセスガイドでアクセス方法を直接表示できます。他の一部のツールでは、サフィックス（/ v 1など）を追加する必要がある場合があります。クライアントの要件に応じて記入してください。\",\n    \"noKeyWarning\": \"なし\",\n    \"noKeyWarningTip\": \"ドッキングガイドを使用する前にキーを作成してください\",\n    \"default\": \"デフォルトのグループ化\",\n    \"unknown\": \"不明なグループ化\",\n    \"createTip\": \"キーを他の人に開示しないでください（ Githubパブリックリポジトリへのプッシュなど）。そうしないと、キーの残高が盗まれる可能性があります。キーを慎重に保管してください！キーリークが発生した場合は、適時にトークンをリセット/削除します。\",\n    \"tokenGroup\": \"トークンのグループ化\",\n    \"tokenGroupTip\": \"トークンのカスタムチャネルグループ\"\n  },\n  \"coming-soon\": \"この機能は開発中です。お楽しみに！\",\n  \"starred\": \"お気に入りのモデル\",\n  \"unstarred\": \"一般的なモデル\",\n  \"assistant-suggest\": \"プリセットの推奨事項\",\n  \"change-suggest\": \"グループを変更\",\n  \"new-announcement\": \"お知らせ\",\n  \"no-announcement\": \"お知らせはありません\",\n  \"readed\": \"既読\",\n  \"learn-more\": \"詳細はこちら\",\n  \"none\": \"なし\",\n  \"description\": \"説明\",\n  \"tip\": \"チップ：\",\n  \"get\": \"ゲット\",\n  \"authenticating\": \"認証中\",\n  \"authenticating-prompt\": \"アカウントの認証が完了するまで少々お待ちください...\",\n  \"authentication-failed\": \"認証に失敗\",\n  \"oops-quota-exceeded\": \"残高が不足しています\",\n  \"oops-quota-exceeded-tip\": \"残高が不足しています。購入クレジットまたはサブスクリプションプランに移動して続行してください\",\n  \"only-one-step\": \"あと一歩\",\n  \"verify-email-description\": \"認証を完了するにはメールアドレスを入力してください\",\n  \"are-you-sure\": \"本当によろしいですか？\",\n  \"this-action-cannot-be-undone\": \"この操作は元に戻せません。\",\n  \"connect\": \"連携\",\n  \"disconnect\": \"バインドを解除\",\n  \"plugin\": {\n    \"title\": \"プラグイン管理\",\n    \"add\": \"プラグインを追加\",\n    \"create\": \"プラグインを作成\",\n    \"save\": \"プラグインを保存\",\n    \"cancel\": \"キャンセル\",\n    \"enable\": \"アクティブ\",\n    \"disable\": \"無効にする\",\n    \"enabled\": \"{{name}}を有効にしました\",\n    \"disabled\": \"{{name}}は無効です\",\n    \"enabled-badge\": \"有効\",\n    \"import\": \"プロファイルをインポート\",\n    \"quick-import\": \"クイックインポートMCP設定\",\n    \"name\": \"プラグイン名\",\n    \"name-placeholder\": \"プラグイン名を入力してください\",\n    \"avatar\": \"プラグインアバター\",\n    \"avatar-placeholder\": \"アバターリンクを入力してください\",\n    \"description\": \"プラグインの説明\",\n    \"description-placeholder\": \"プラグインの説明を入力してください\",\n    \"server-url\": \"サーバのアドレス:\",\n    \"server-url-placeholder\": \"MCPサーバーアドレスを入力してください\",\n    \"loading\": \"読み込み中…\",\n    \"no-plugins\": \"プラグインなし\",\n    \"save-success\": \"保存成功\",\n    \"save-error\": \"保存に失敗しました\",\n    \"delete-success\": \"削除成功\",\n    \"delete-error\": \"削除失敗\",\n    \"test\": \"テストクラス\",\n    \"testing\": \"接続中...\",\n    \"test-success\": \"接続に成功しました\",\n    \"test-error\": \"接続に失敗しました\",\n    \"import-success\": \"\\\"%1\\\" の書き込みに成功しました。\",\n    \"import-error\": {\n      \"empty\": \"設定を入力してください\",\n      \"invalid-json\": \"無効なJSON形式です\",\n      \"invalid-format\": \"設定フォーマットが正しくありません。mcpServersフィールドが含まれているか確認してください\",\n      \"no-servers\": \"MCPサーバーが設定に見つかりません\",\n      \"stdio-not-supported\": \"現在のバージョンはHTTPタイプのMCPサーバーのみをサポートし、STDIOタイプはサポートされていません\",\n      \"unknown\": \"インポートに失敗しました。設定形式を確認してください\"\n    },\n    \"mcp\": {\n      \"tool-call\": \"ツールコール\",\n      \"tool-calling\": \"通話ツール：{{ name}}\",\n      \"tool-executing\": \"実行ツール：{{ name}}\",\n      \"tool-success\": \"ツールが正常に実行されました: {{name}}\",\n      \"tool-error\": \"ツールの実行に失敗しました: {{name}}\",\n      \"arguments\": \"ツールパラメータ\",\n      \"result\": \"実行結果\",\n      \"error\": \"実行エラー\",\n      \"status\": \"実行ステータス\",\n      \"status-start\": \"実行ツールを準備しています...\",\n      \"status-executing\": \"ツールを実行しています...\",\n      \"hide-details\": \"ツール呼び出しの詳細を非表示にする\",\n      \"show-details\": \"ツールコールの詳細を表示\",\n      \"plugin-name\": \"MCPプラグイン\",\n      \"copy-param-value\": \"パラメータ値をコピー\",\n      \"save\": \"保存\",\n      \"edit\": \"編集\",\n      \"raw-arguments\": \"未加工パラメーター(JSON)\",\n      \"no-arguments\": \"パラメータなし\",\n      \"parsed-result\": \"解析後の結果\",\n      \"error-info\": \"エラーメッセージ\",\n      \"status-prepare\": \"ツールを呼び出す準備をしています...\",\n      \"status-success\": \"ツールが正常に実行されました\",\n      \"status-error\": \"ツールの実行に失敗しました\",\n      \"status-calling\": \"ツールを呼び出しています...\",\n      \"hide-debug\": \"デバッグ情報を非表示にする\",\n      \"show-debug\": \"デバッグ情報を表示\",\n      \"tool-arguments\": \"ツールパラメータ\",\n      \"no-arguments-needed\": \"ツールはパラメータを必要としません\"\n    },\n    \"load-error\": \"読み込み失敗\",\n    \"refresh\": \"更新\",\n    \"refresh-success\": \"成功したリロード\",\n    \"test-success-desc\": \"利用可能なツールが{{count}}件見つかりました\",\n    \"import-json-config\": \"JSON設定のインポート\",\n    \"import-http-only-tip\": \"現在のバージョンはHTTPタイプのMCPサーバーのみをサポートしています\",\n    \"import-confirm\": \"インポートの確認\",\n    \"server-url-required\": \"サーバーアドレスを入力してください\",\n    \"test-required\": \"テストが必要です\",\n    \"test-description\": \"テストボタンをクリックして、プラグインの接続を確認します\",\n    \"available-tools\": \"利用可能なツール\",\n    \"connection-test\": \"接続テスト\",\n    \"test-required-error\": \"新しいプラグインを作成する前に、接続をテストする必要があります\",\n    \"test-required-hint\": \"新しいプラグインは、作成する前に接続をテストする必要があります\",\n    \"form-error\": \"必須項目を入力してください\"\n  },\n  \"aff\": {\n    \"title\": \"プロモーションシェア\",\n    \"bind-desc\": \"紹介コードをバインドして、招待されたユーザーが購入した後にリベートを獲得しましょう。\",\n    \"placeholder\": {\n      \"code\": \"プロモーションコードを入力してください\"\n    },\n    \"generate-code-first\": \"プロモーションコードに登録してください\",\n    \"get\": \"コードを受け取る\",\n    \"get-placeholder\": \"クリックしてプロモーションコードを入手\",\n    \"get-success\": \"プロモーションコードが生成されました\",\n    \"bind-existing\": \"BINDプロモーションコード\",\n    \"bind-success\": \"バインドに成功しました\",\n    \"bind-failed\": \"バインドに失敗しました\",\n    \"bind-failed-prompt\": \"バインドに失敗しました！理由：{{ reason}}\",\n    \"withdraw\": \"出金\",\n    \"withdraw-all\": \"すべてを撤回する\",\n    \"withdraw-title\": \"売り上げの引き出し\",\n    \"withdraw-desc\": \"累積プロモーション売り上げをポイントと引き換えます。完全に引き換えるには空白のままにしてください。\",\n    \"withdraw-placeholder\": \"出金額を入力してください（すべての場合は空白のままにしてください）\",\n    \"withdraw-success\": \"引出しました\",\n    \"withdraw-success-prompt\": \"{{amount}}を{{quota}}クレジットに変換しました。\",\n    \"withdraw-failed\": \"引き出しに失敗しました\",\n    \"withdraw-failed-prompt\": \"引き出しに失敗しました！理由：{{ reason}}\",\n    \"invalid-amount\": \"無効な金額\",\n    \"cancel\": \"キャンセル\",\n    \"confirm\": \"確定\",\n    \"stats\": {\n      \"referrals\": \"招待者\",\n      \"earnings\": \"累計売り上げ\",\n      \"pending\": \"決済待ち\",\n      \"rate\": \"リベート率\"\n    }\n  }\n}"
  },
  {
    "path": "app/src/resources/i18n/ru.json",
    "content": "{\n  \"end\": \"\",\n  \"add\": \"Добавить\",\n  \"not-found\": \"Страница не найдена\",\n  \"home\": \"Главная\",\n  \"login\": \"Войти\",\n  \"login-require\": \"Вам нужно войти, чтобы использовать эту функцию\",\n  \"logout\": \"Выйти\",\n  \"quota\": \"Квота\",\n  \"download\": \"Скачать\",\n  \"offline\": \"Приложение оффлайн\",\n  \"try-again\": \"Попробуйте еще раз\",\n  \"invalid-token\": \"Неверный токен\",\n  \"invalid-token-prompt\": \"Пожалуйста, попробуйте еще раз.\",\n  \"login-failed\": \"Ошибка входа\",\n  \"login-failed-prompt\": \"Ошибка входа! Причина: {{reason}}\",\n  \"login-success\": \"Успешный вход\",\n  \"login-success-prompt\": \"Вы успешно вошли в систему.\",\n  \"server-error\": \"Ошибка сервера\",\n  \"server-error-prompt\": \"При входе произошла ошибка. Пожалуйста, попробуйте еще раз.\",\n  \"error\": \"Ошибка запроса\",\n  \"request-failed\": \"Ошибка запроса. Пожалуйста, проверьте свою сеть и попробуйте еще раз.\",\n  \"success\": \"Успешный запрос\",\n  \"request-success\": \"Ваша операция была успешно выполнена.\",\n  \"close\": \"Закрыть\",\n  \"edit\": \"Редактировать\",\n  \"editor\": \"Редактировать\",\n  \"pricing\": \"См. ценообразование моделей для получения дополнительной информации\",\n  \"true\": \"Да\",\n  \"false\": \"Нет\",\n  \"unknown\": \"Неизвестный\",\n  \"scroll-down\": \"Прокрутите вниз\",\n  \"broadcast\": \"Объявление\",\n  \"fatal\": \"Приложение вылетело\",\n  \"download-fatal-log\": \"Скачать журнал ошибок\",\n  \"fatal-tips\": \"Сначала проверьте совместимость веб-браузера, попробуйте очистить кэш браузера и обновить страницу. Если проблема не устранена, загрузите журнал и предоставьте разработчику полные инструкции по воспроизведению, чтобы мы могли устранить проблему.\",\n  \"tag\": {\n    \"free\": \"Бесплатно\",\n    \"official\": \"Официальный\",\n    \"unstable\": \"Нестабильный\",\n    \"web\": \"Веб\",\n    \"high-quality\": \"Высокое качество\",\n    \"high-context\": \"Высокий контекст\",\n    \"high-price\": \"Высокая цена\",\n    \"open-source\": \"Открытый исходный код\",\n    \"image-generation\": \"Генерация изображений\",\n    \"multi-modal\": \"Мульти Модальный\",\n    \"fast\": \"Быстрый\",\n    \"english-model\": \"Английская модель\",\n    \"badges\": {\n      \"non-billing\": \"Бесплатно\",\n      \"times-billing\": \"{{price}}/время\",\n      \"token-billing\": \"Ввод {{input}}/1k токенов Вывод {{output}}/1k токенов\",\n      \"add\": \"Добавить верстак\",\n      \"remove\": \"Удалить верстак\",\n      \"plan-included\": \"Подписка содержит\",\n      \"plan-included-tip\": \"Ваша подписка уже содержит эту модель, кредиты подписки будут использованы в первую очередь\"\n    }\n  },\n  \"market\": {\n    \"title\": \"Рынок моделей\",\n    \"model\": \"Исследуйте больше моделей\",\n    \"explore\": \"Ознакомьтесь с моделью\",\n    \"search\": \"Поиск по имени модели или описанию\",\n    \"model-api\": \"Идентификатор модели запроса API\",\n    \"list\": \"Перечень моделей\",\n    \"go\": \"Перейти на модельный рынок\",\n    \"show-pricing\": \"Показывать цены\",\n    \"switch-model\": \"Переключить модель\",\n    \"switch-model-desc\": \"Переключено на модель\",\n    \"switch-bookmark\": \"Верстак\",\n    \"remove-bookmark\": \"Модель удалена из строки меню\",\n    \"add-bookmark\": \"Модель добавлена в строку меню\",\n    \"show-1m-pricing\": \"1M токенов\"\n  },\n  \"conversation\": {\n    \"title\": \"Разговор\",\n    \"empty\": \"Пусто\",\n    \"refresh-failed\": \"Ошибка обновления\",\n    \"refresh-failed-prompt\": \"При выполнении запроса произошла ошибка. Пожалуйста, попробуйте еще раз.\",\n    \"remove-title\": \"Вы уверены?\",\n    \"remove-description\": \"Это действие нельзя отменить. Это навсегда удалит разговор \",\n    \"remove-all-title\": \"Очистить историю\",\n    \"remove-all-description\": \"Это действие нельзя отменить. Это навсегда удалит все разговоры, продолжить?\",\n    \"cancel\": \"Отмена\",\n    \"delete\": \"Удалить\",\n    \"delete-conversation\": \"Удалить разговор\",\n    \"delete-success\": \"Разговор удален\",\n    \"delete-success-prompt\": \"Разговор был удален.\",\n    \"delete-failed\": \"Ошибка удаления\",\n    \"delete-failed-prompt\": \"Не удалось удалить разговор. Пожалуйста, проверьте свою сеть и попробуйте еще раз.\",\n    \"edit-title\": \"Редактировать заголовок\",\n    \"empty-anonymous\": \"В настоящее время вы находитесь в анонимном режиме, и разговоры не будут сохранены.\",\n    \"search\": \"Поиск бесед...\"\n  },\n  \"chat\": {\n    \"web\": \"нетворкинг\",\n    \"web-aria\": \"Переключить веб-поиск\",\n    \"placeholder\": \"Введите свой чат...\",\n    \"recall\": \"История\",\n    \"recall-desc\": \"Обнаружено, что у вас есть неотправленные сообщения в прошлый раз, они были восстановлены для вас.\",\n    \"recall-cancel\": \"Отмена\",\n    \"placeholder-enter\": \"Напишите что-нибудь... (Enter для отправки, Shift + Enter для обтекания)\",\n    \"placeholder-raw\": \"Напишите что-нибудь...\",\n    \"send-message\": \"Отправить\",\n    \"send-message-desc\": \"Вы уверены, что хотите отправить это сообщение?\",\n    \"actions\": {\n      \"upscale\": \"Увел\",\n      \"variant\": \"перемена\",\n      \"reroll\": \"Перерисовать\",\n      \"subtle-upscale\": \"Незначительное увеличение\",\n      \"creative-upscale\": \"Creative Zoom\",\n      \"subtle-vary\": \"Незначительные изменения\",\n      \"strong-vary\": \"Сильные изменения\",\n      \"region-vary\": \"Частичная перерисовка\",\n      \"zoom\": \"Масштабирование\",\n      \"zoom-1\": {},\n      \"zoom-2x\": \"Масштаб 2x\",\n      \"zoom-custom\": \"Пользовательское масштабирование\",\n      \"pan-left\": \"Сдвинуть влево\",\n      \"pan-right\": \"Сдвинуть вправо\",\n      \"pan-up\": \"Вверх\",\n      \"pan-down\": \"Вверх\",\n      \"bookmark\": \"Лайк\"\n    },\n    \"empty-preview\": \"Входные данные будут отображаться здесь (поддерживается синтаксис Markdown) ~\",\n    \"web-enable-toast\": \"Поиск по сети включен\",\n    \"web-disable-toast\": \"Поиск по сети отключен\",\n    \"web-enable-tip\": \"Сетевой поиск может потреблять больше токенов\",\n    \"web-search\": \"Поиск в Интернете\",\n    \"plugin\": \"Плагины\",\n    \"voice\": \"Распознавание речи\",\n    \"deep-thinking\": \"Глубокое мышление\",\n    \"deep-thinking-enable-toast\": \"Глубокое мышление включено\",\n    \"deep-thinking-enable-tip\": \"Глубокое мышление может привести к более медленному выходу\",\n    \"deep-thinking-disable-toast\": \"Глубокое мышление закрыто\",\n    \"model-not-support-thinking-desc\": \"Текущая модель не поддерживает глубокое мышление\",\n    \"web-search-results\": \"Найдено результатов: {{count}}\",\n    \"web-search-results-hide\": \"Свернуть результаты поиска\",\n    \"web-search-results-query\": \"Поиск по ключевым словам\",\n    \"web-search-results-visit-source\": \"Перейти на исходный сайт\",\n    \"web-search-no-results\": \"Нет результатов поиска\",\n    \"web-page-summary\": \"Постраничная сводка\",\n    \"web-depth\": \"Глубина поиска\",\n    \"web-quick-search\": \"Найти\",\n    \"web-detailed-search\": \"Подробный поиск\",\n    \"web-enable-page-summary-toast\": \"Постраничная сводка включена\",\n    \"web-enable-page-summary-tip\": \"Постраничная сводка может потреблять больше токенов и замедлять вывод\",\n    \"web-disable-page-summary-toast\": \"Постраничная сводка закрыта\",\n    \"web-search-quick-toast\": \"Глубина поиска переключена на Быстрый поиск\",\n    \"web-search-detailed-toast\": \"Глубина поиска переключена на детальный поиск\"\n  },\n  \"message\": {\n    \"copy\": \"Копировать сообщение\",\n    \"save\": \"Сохранить как файл\",\n    \"use\": \"Использовать сообщение\",\n    \"stop\": \"Остановить ответ\",\n    \"restart\": \"Перезапустить ответ\",\n    \"copy-area\": \"Копировать выбранную область\",\n    \"edit\": \"Редактировать сообщение\",\n    \"remove\": \"Удалить сообщение\",\n    \"save-image\": \"Сохранить\",\n    \"saving-image-prompt\": \"Выполняется создание изображения\",\n    \"saving-image-prompt-desc\": \"Создание изображения, подождите...\",\n    \"saving-image-failed\": \"Не удалось создать изображение\",\n    \"saving-image-failed-prompt\": \"Не удалось создать изображение по {{reason}}\",\n    \"saving-image-success\": \"Изображение успешно сгенерировано\",\n    \"saving-image-success-prompt\": \"Изображение успешно сохранено.\",\n    \"sharing\": {\n      \"title\": \"заглавие\",\n      \"time\": \"Время\",\n      \"message\": \"Сообщения\"\n    },\n    \"thinking-process\": \"Процесс мышления\"\n  },\n  \"quota-description\": \"квота расходов на сообщение\",\n  \"buy\": {\n    \"choose\": \"Выберите сумму\",\n    \"other\": \"Другое\",\n    \"other-desc\": \"Сколько очков?\",\n    \"buy\": \"Купить {{amount}} очков\",\n    \"dalle\": \"Генератор изображений DALL·E\",\n    \"dalle-free\": \"DALL·E 2 бесплатно навсегда\",\n    \"flex\": \"Гибкая тарификация\",\n    \"input\": \"Вход\",\n    \"output\": \"Выход\",\n    \"learn-more\": \"Узнать больше\",\n    \"dialog-title\": \"Купить очки\",\n    \"dialog-desc\": \"Вы уверены, что хотите купить {{amount}} очков?\",\n    \"dialog-cancel\": \"Отмена\",\n    \"dialog-buy\": \"Купить\",\n    \"success\": \"Покупка прошла успешно\",\n    \"success-prompt\": \"Вы успешно приобрели {{amount}} очков.\",\n    \"failed\": \"Покупка не удалась\",\n    \"failed-prompt\": \"Не удалось приобрести очки. Пожалуйста, убедитесь, что у вас достаточно баланса.\",\n    \"gpt4-tip\": \"Совет: функция веб-поиска может потреблять больше входных очков\",\n    \"go\": \"Перейти к\",\n    \"redeem\": \"Обмен валюты\",\n    \"redeem-placeholder\": \"Введите код погашения\",\n    \"exchange-success\": \"Успешный обмен\",\n    \"exchange-success-prompt\": \"Вы успешно использовали {{amount}} кредита (-ов).\",\n    \"exchange-failed\": \"Сбой обмена\",\n    \"exchange-failed-prompt\": \"Не удалось погасить по {{reason}}\",\n    \"buy-link\": \"Перейти к покупке\",\n    \"deeptrain-tip\": \"Совет: как только Deeptrain перезагрузится на ваш кошелек, вернитесь сюда и нажмите, чтобы купить соответствующие кредиты\",\n    \"not-config-link\": \"Ссылка на покупку не настроена в фоновом режиме\",\n    \"title\": \"Мои баллы\",\n    \"quota-info\": \"Баллы могут использовать все модели этой станции, оплачивать по мере поступления, подходят для гибких вариантов выставления счетов\",\n    \"deeptrain-step-1\": \"Выберите кредиты и нажмите Купить.\",\n    \"deeptrain-step-2\": \"Перейти к пополнению кошелька Deeptrain\",\n    \"deeptrain-step-3\": \"После успешного пополнения вернитесь сюда, чтобы купить снова\",\n    \"deeptrain-step-4\": \"(Если на кошельке достаточно средств, он автоматически пополнится после покупки)\",\n    \"plan-info\": \"Подписки могут использовать модель in-subscription по фиксированной цене за цикл, что подходит для фиксированных вариантов долгосрочного использования.\",\n    \"buy-description\": \"Выберите кредиты, которые вы хотите купить\",\n    \"redeem-title\": \"Использовать код\",\n    \"redeem-description\": \"Введите код погашения, чтобы получить кредиты\"\n  },\n  \"pkg\": {\n    \"title\": \"Пакеты\",\n    \"go\": \"Перейти к проверке\",\n    \"cert\": \"Пакет сертификации\",\n    \"cert-desc\": \"После сертификации подлинности вы можете получить 100 очков (стоимостью 5 CNY)\",\n    \"teen\": \"Подростковый пакет\",\n    \"teen-desc\": \"После сертификации подлинности подростки (до 18 лет) могут получить дополнительно 150 очков (стоимостью 15 CNY)\",\n    \"close\": \"Закрыть\",\n    \"state\": {\n      \"true\": \"Получено\",\n      \"false\": \"Не получено\"\n    },\n    \"manage\": \"Мои наборы\"\n  },\n  \"sub\": {\n    \"title\": \"Подписка\",\n    \"quota-link\": \"Ищете гибкую тарификацию? Купить очки\",\n    \"subscription-link\": \"Ищете фиксированную тарификацию? Подписаться\",\n    \"dialog-title\": \"Подписка\",\n    \"free\": \"Бесплатно\",\n    \"free-price\": \"Бесплатно навсегда\",\n    \"basic\": \"Базовый\",\n    \"standard\": \"Стандартный\",\n    \"pro\": \"Профессиональный\",\n    \"plan-price\": \"{{money}} CNY/месяц\",\n    \"include-tax\": \"Включая налог\",\n    \"enterprise\": \"Корпоративный\",\n    \"enterprise-service\": \"Приоритетная служба поддержки\",\n    \"enterprise-sla\": \"Гарантия SLA\",\n    \"enterprise-speed\": \"Увеличение скорости TPM\",\n    \"enterprise-security\": \"Гарантия безопасности данных SOC-2\",\n    \"enterprise-data\": \"Резервное копирование данных в другом месте\",\n    \"enterprise-deploy\": \"Поддержка частной облачной инфраструктуры\",\n    \"contact-sale\": \"Связаться с отделом продаж\",\n    \"current\": \"Текущая подписка\",\n    \"subscribe\": \"Подписаться\",\n    \"upgrade\": \"Обновить\",\n    \"downgrade\": \"Понизить\",\n    \"renew\": \"Продлить\",\n    \"cannot-select\": \"Невозможно выбрать\",\n    \"select-time\": \"Выберите время подписки\",\n    \"migrate-plan\": \"Перенести подписку\",\n    \"migrate-plan-desc\": \"После изменения подписки ваше время подписки будет рассчитываться на основе цены оставшихся дней, и время подписки будет пересчитано. (Например, понижение удваивает время, а повышение компенсирует разницу)\",\n    \"price\": \"Цена {{price}} CNY\",\n    \"price-tax\": \"Включая налог {{price}} CNY\",\n    \"upgrade-price\": \"Плата за обновление {{price}} CNY (для справки)\",\n    \"expired\": \"Осталось дней подписки\",\n    \"time\": {\n      \"1\": \"1 месяц\",\n      \"3\": \"3 месяца\",\n      \"6\": \"6 месяцев\",\n      \"12\": \"1 год\",\n      \"36\": \"3 года\"\n    },\n    \"success\": \"Подписка успешна\",\n    \"success-prompt\": \"Вы успешно подписались на {{month}} месяцев.\",\n    \"migrate-success\": \"Перенос подписки успешен\",\n    \"migrate-success-prompt\": \"Вы успешно перенесли подписку.\",\n    \"failed\": \"Подписка не удалась\",\n    \"failed-prompt\": \"Не удалось подписаться, пожалуйста, убедитесь, что у вас достаточно баланса.\",\n    \"migrate-failed\": \"Перенос подписки не удался\",\n    \"migrate-failed-prompt\": \"Ваша подписка не удалась.\",\n    \"plan-usage\": \"{{name}} использует {{times}} раз в месяц\",\n    \"plan-tip\": \"Вызываемая модель\",\n    \"disable\": \"Функция подписки на этом сайте отключена\",\n    \"plan-unlimited-usage\": \"{{name}} имеет неограниченное количество пользователей\",\n    \"plan-not-support-relay\": \"Квота подписки на сайт не распространяется на промежуточный API, пожалуйста, используйте гибкие биллинговые кредиты для промежуточного API\",\n    \"failed-quota-prompt\": \"Не удалось оформить подписку, недостаточно средств ({{quota}} кредитов)\",\n    \"sub-migrate-failed-prompt\": \"Изменение подписки не выполнено по {{reason}}\",\n    \"month\": \"(мм)\",\n    \"year\": \"Год\",\n    \"best-choice\": \"Лучший выбор\",\n    \"including-model\": \"Покрываемые модели\",\n    \"including-model-tip\": \"Доступные кредиты на использование модели, включенные в эту подписку\",\n    \"none\": \"Подписка не оформлена\",\n    \"plan-item-usage\": \"{{times}} раз\",\n    \"plan-item-unlimited-usage\": \"бесконечно\",\n    \"year-earn-tip\": \"Годовая экономия по плану {{percent}}\",\n    \"quota-manage\": \"Квота подписки\",\n    \"expired-days\": \"Срок действия подписки истекает через {{days}} дн.\",\n    \"month-plan\": \"Месячный план\",\n    \"year-plan\": \"Годовой план\",\n    \"new\": \"Новый план\",\n    \"select-duration\": \"Выберите, на какой срок вы хотите оформить подписку\",\n    \"price-summary\": \"Сводка цен\",\n    \"total-price\": \"Общая цена\",\n    \"upgrade-price-label\": \"Плата за повышение класса обслуживания\",\n    \"upgrade-price-notice\": \"Только для информации\",\n    \"upgrade-price-notice-tip\": \"Плата за обновление указана только для справки, а фактическая цена основана на точном расчете сервера.\",\n    \"refresh-days\": \"Квота обновится через {{refresh_days}} дн.\",\n    \"get-refresh-days\": \"Начните с квот, чтобы получить даты обновления\"\n  },\n  \"cancel\": \"Отмена\",\n  \"confirm\": \"Подтвердить\",\n  \"percent\": \"{{cent}}0%\",\n  \"file\": {\n    \"upload\": \"Загрузить\",\n    \"type\": \"Поддержка pdf, docx, pptx, xlsx, изображений, текста и других форматов\",\n    \"drop\": \"Перетащите файлы сюда или нажмите, чтобы загрузить\",\n    \"parse-error\": \"Ошибка разбора\",\n    \"parse-error-prompt\": \"Ошибка разбора: {{reason}}\",\n    \"max-length\": \"Слишком длинный контент\",\n    \"max-length-prompt\": \"Содержимое было усечено из-за ограничения длины контекста\",\n    \"over-size\": \"Слишком большой файл\",\n    \"over-size-prompt\": \"Размер одного вложения не может превышать {{size}} MB\",\n    \"large-file\": \"Большой файл разбора\",\n    \"large-file-prompt\": \"Загрузка и разбор большого файла, пожалуйста, подождите\",\n    \"number\": \"{{number}} файлов\",\n    \"zipper\": \"{{filename}} и другие {{number}} файлов\",\n    \"empty-file\": \"Пустой файл\",\n    \"empty-file-prompt\": \"Содержимое файла пустое, автоматически проигнорировано\",\n    \"large-file-success\": \"Синтаксический анализ успешно завершен\",\n    \"large-file-success-prompt\": \"Большой файл успешно проанализирован за {{time}} секунды\",\n    \"file\": \"Документ\",\n    \"parse-success-prompt\": \"Файл успешно проанализирован: {{file}}\",\n    \"uploading\": \"Передать файлы\",\n    \"uploading-prompt\": \"Загрузка файла, пожалуйста, будьте терпеливы\"\n  },\n  \"generate\": {\n    \"title\": \"Генератор AI проектов\",\n    \"input-placeholder\": \"сгенерировать python игру\",\n    \"failed\": \"Генерация не удалась\",\n    \"reason\": \"Причина: \",\n    \"success\": \"Генерация успешна\",\n    \"success-prompt\": \"Проект успешно сгенерирован! Пожалуйста, выберите формат загрузки.\",\n    \"empty\": \"генерация...\",\n    \"download\": \"Загрузить {{name}} формат\"\n  },\n  \"api\": {\n    \"title\": \"Настройки API\",\n    \"copied\": \"Скопировано\",\n    \"copied-description\": \"Ключ API скопирован в буфер обмена\",\n    \"learn-more\": \"Узнать больше\",\n    \"reset\": \"Кнопка сброса\",\n    \"reset-description\": \"Вы уверены? Это действие нельзя отменить. Это приведет к окончательному сбросу ключа API, и срок действия существующего ключа API истечет.\"\n  },\n  \"service\": {\n    \"title\": \"Доступна новая версия\",\n    \"version\": \"Версия\",\n    \"description\": \"Доступна новая версия. Хотите обновить сейчас?\",\n    \"update\": \"Обновить\",\n    \"offline-title\": \"Режим оффлайн\",\n    \"offline\": \"Приложение в настоящее время находится в автономном режиме.\",\n    \"update-success\": \"Обновление успешно\",\n    \"update-success-prompt\": \"Вы обновились до последней версии.\"\n  },\n  \"share\": {\n    \"title\": \"Поделиться\",\n    \"share-conversation\": \"Поделиться разговором\",\n    \"description\": \"Поделитесь этим разговором с другими: \",\n    \"copy-link\": \"Скопировать ссылку\",\n    \"view\": \"Посмотреть\",\n    \"success\": \"Поделиться успешно\",\n    \"failed\": \"Поделиться не удалось\",\n    \"copied\": \"Скопировано\",\n    \"copied-description\": \"Ссылка скопирована в буфер обмена\",\n    \"not-found\": \"Разговор не найден\",\n    \"not-found-description\": \"Разговор не найден, пожалуйста, проверьте, правильная ли ссылка или разговор был удален\",\n    \"manage\": \"Управление обменом\",\n    \"sync-error\": \"Ошибка синхронизации\",\n    \"name\": \"Название разговора\",\n    \"time\": \"Время\",\n    \"action\": \"Действие\",\n    \"empty\": \"Вы еще не поделились записями. Поделитесь ими сейчас!\",\n    \"share-tip\": \"Перейдите в панель диалога и нажмите кнопку Поделиться, чтобы поделиться беседой.\"\n  },\n  \"docs\": {\n    \"title\": \"Открыть документы\"\n  },\n  \"invitation\": {\n    \"title\": \"Код приглашения\",\n    \"input-placeholder\": \"Введите код приглашения\",\n    \"cancel\": \"Отмена\",\n    \"check\": \"Проверить\",\n    \"check-success\": \"Успешно\",\n    \"check-success-description\": \"Успешно! Вы получили {{amount}} очков, начните свое путешествие в мир AI!\",\n    \"check-failed\": \"Не удалось\",\n    \"invitation\": \"Код приглашения\"\n  },\n  \"contact\": {\n    \"title\": \"Связаться с нами\",\n    \"community\": \"Присоединиться к сообществу\"\n  },\n  \"settings\": {\n    \"title\": \"Настройки\",\n    \"description\": \"Настройки\",\n    \"version\": \"Версия\",\n    \"language\": \"Язык\",\n    \"sender\": \"Отправить ключ\",\n    \"context\": \"Сохранить контекст\",\n    \"history\": \"Максимальное количество исторических разговоров\",\n    \"align\": \"Выравнивание чата по центру\",\n    \"memory\": \"Использование памяти\",\n    \"temperature\": \"Температура\",\n    \"temperature-tip\": \"Коэффициент случайной выборки, высокая температура создает больше случайности, низкая температура создает более концентрированный и детерминированный текст\",\n    \"max-tokens\": \"Максимальное количество маркеров ответа\",\n    \"max-tokens-tip\": \"Максимальное количество маркеров ответа, превышающее это значение, будет усечено (слишком высокое значение может привести к сбою запроса из-за превышения максимального маркера модели)\",\n    \"top-p\": \"Порог вероятности отбора проб ядра\",\n    \"top-p-tip\": \"(TopP) Чем выше значение вероятности, тем выше генерируемая случайность; чем ниже значение, тем выше генерируемая определенность\",\n    \"top-k\": \"Размер набора образцов-кандидатов\",\n    \"top-k-tip\": \"(TopK) Размер набора кандидатов, чем больше случайность генерации, чем меньше генерация, тем выше определенность\",\n    \"presence-penalty\": \"Наличие штрафных санкций\",\n    \"presence-penalty-tip\": \"(PresencePenalty) Существует штраф за контроль вероятности появления новых тем, генерируемых моделью, увеличение этого значения может увеличить вероятность разговора о новых темах\",\n    \"frequency-penalty\": \"Частотное наказание\",\n    \"frequency-penalty-tip\": \"(FrequencyPenalty) Штраф за частоту, контроль степени повторения слов, генерируемых моделью, увеличение этого значения может снизить возможность повторения слов\",\n    \"repetition-penalty\": \"Повторяющееся наказание\",\n    \"repetition-penalty-tip\": \"(RepetitionPenalty) Управляет степенью повторяемости, генерируемой моделью. Увеличение этого значения может уменьшить повторение, но может привести к тому, что модель будет генерировать некогерентный текст (аналогично FrequencyPenalty)\",\n    \"reset-settings\": \"Сбросить все настройки\",\n    \"reset-settings-description\": \"Вы уверены? Это действие нельзя отменить. Это приведет к окончательному сбросу всех настроек.\",\n    \"hide-model\": \"Скрыть выбор модели\",\n    \"hide-toolbar\": \"Скрыть панель инструментов по умолчанию\",\n    \"hide-toolbar-text\": \"Скрыть текст панели инструментов\",\n    \"theme\": \"предмет\",\n    \"light\": \"Светлый цвет:\",\n    \"dark\": \"Приглушить цвет\"\n  },\n  \"article\": {\n    \"title\": \"Пакет генерации статей\",\n    \"input-placeholder\": \"Введите заголовок статьи (одна строка)\",\n    \"prompt-placeholder\": \"Введите предустановку (помогите AI сгенерировать статью, например: формат научной статьи, 800 слов)\",\n    \"web-checkbox\": \"Включить веб-поиск\",\n    \"generate\": \"Генерировать\",\n    \"progress-title\": \"Генерация (всего {{total}} статей, {{current}} статей сгенерировано)\",\n    \"generate-success\": \"Успешно\",\n    \"generate-success-prompt\": \"Статья успешно сгенерирована! Пожалуйста, выберите формат загрузки.\",\n    \"generate-failed\": \"Не удалось\",\n    \"generate-failed-prompt\": \"Не удалось сгенерировать статью. Пожалуйста, проверьте свою сеть и попробуйте еще раз.\",\n    \"download-format\": \"Загрузить {{name}} формат\"\n  },\n  \"admin\": {\n    \"dashboard\": \"Анализ данных\",\n    \"users\": \"Управление пользователями\",\n    \"broadcast\": \"Управление объявлениями\",\n    \"channel\": \"Настройки канала\",\n    \"settings\": \"Настройки системы\",\n    \"prize\": \"Настройки цен\",\n    \"billing-today\": \"Сегодняшний доход\",\n    \"billing-month\": \"Доход за месяц\",\n    \"subscription-users\": \"Подписчики\",\n    \"seat\": \"место\",\n    \"model-chart\": \"Статистика использования моделей\",\n    \"request-chart\": \"Статистика запросов\",\n    \"billing-chart\": \"Статистика доходов\",\n    \"error-chart\": \"Статистика ошибок\",\n    \"requests\": \"Запросы\",\n    \"times\": \"Количество ошибок\",\n    \"empty\": \"Пусто\",\n    \"cancel\": \"Отмена\",\n    \"confirm\": \"Подтвердить\",\n    \"invitation\": \"Управление кодами приглашений\",\n    \"code\": \"Код\",\n    \"quota\": \"Квота\",\n    \"type\": \"Тип\",\n    \"used\": \"Статус\",\n    \"number\": \"Количество\",\n    \"username\": \"Имя пользователя\",\n    \"month\": \"Месяц\",\n    \"poster\": \"Автор\",\n    \"post-at\": \"Дата\",\n    \"broadcast-content\": \"Содержание\",\n    \"create-broadcast\": \"Создать объявление\",\n    \"broadcast-placeholder\": \"Введите содержание объявления\",\n    \"post\": \"Отправить\",\n    \"post-success\": \"Успешно\",\n    \"post-success-prompt\": \"Объявление успешно отправлено.\",\n    \"post-failed\": \"Не удалось\",\n    \"post-failed-prompt\": \"Не удалось отправить объявление, причина: {{reason}}\",\n    \"level\": \"Уровень\",\n    \"is-admin\": \"Админ\",\n    \"used-quota\": \"Использовано\",\n    \"is-subscribed\": \"Подписан\",\n    \"total-month\": \"Всего месяцев\",\n    \"enterprise\": \"Корпоративный\",\n    \"action\": \"Действие\",\n    \"search-username\": \"Поиск по имени пользователя\",\n    \"quota-action\": \"Изменение квоты\",\n    \"quota-action-desc\": \"Пожалуйста, введите значение изменения квоты (положительное для увеличения, отрицательное для уменьшения)\",\n    \"subscription-action\": \"Управление временем подписки\",\n    \"subscription-action-desc\": \"Установите срок действия подписки для пользователя {{username}}\",\n    \"operate-success\": \"Успешно\",\n    \"operate-success-prompt\": \"Ваша операция была успешно выполнена.\",\n    \"operate-failed\": \"Не удалось\",\n    \"operate-failed-prompt\": \"Не удалось выполнить операцию, причина: {{reason}}\",\n    \"updated-at\": \"Время обновления\",\n    \"used-true\": \"Использовано\",\n    \"used-false\": \"Не использовано\",\n    \"generate\": \"Генерировать\",\n    \"generate-result\": \"Результат\",\n    \"error\": \"Ошибка запроса\",\n    \"channels\": {\n      \"id\": \"ID канала\",\n      \"name\": \"Название\",\n      \"name-tip\": \"Название канала, используется для идентификации канала\",\n      \"name-placeholder\": \"Введите название канала\",\n      \"type\": \"Тип\",\n      \"priority\": \"Приоритет\",\n      \"priority-tip\": \"При наличии нескольких каналов запрос выполняется в порядке приоритета, чем выше приоритет, тем выше приоритет\",\n      \"weight\": \"Вес\",\n      \"weight-tip\": \"При равном приоритете вызов балансировки нагрузки выполняется в соответствии с весовым соотношением\",\n      \"retry\": \"Максимальное количество попыток\",\n      \"retry-tip\": \"При сбое запроса канала максимальное количество повторных попыток\",\n      \"model\": \"Модель\",\n      \"secret\": \"Секрет\",\n      \"secret-placeholder\": \"Введите секрет, формат: {{format}}\\nПри наличии нескольких секретов при запросе загрузки выбирается одна строка случайным образом\",\n      \"endpoint\": \"Конечная точка\",\n      \"endpoint-placeholder\": \"Введите конечную точку (т.е. прокси)\",\n      \"mapper\": \"Модельный маппер\",\n      \"mapper-tip\": \"Преобразование имени модели для достижения асимметричного запроса модели\",\n      \"mapper-placeholder\": \"Введите модельный маппер, по одной строке, формат: model>model\\nПервая модель - запрошенная модель, вторая модель - отображаемая модель (которая должна существовать в модели), разделенная > посередине\\nФормат предшествует! Означает, что исходная модель не включена в доступный диапазон этого канала, например: !gpt-4-slow>gpt-4, тогда gpt-4 не будет охвачен в доступных моделях, которые можно запросить в этом канале\",\n      \"group\": \"Группа пользователей\",\n      \"group-tip\": \"Группа пользователей, группа, которая не включена, не будет включена в доступный диапазон этого канала (когда группа пуста, все пользователи могут использовать этот канал)\",\n      \"state\": \"Статус\",\n      \"action\": \"Действие\",\n      \"edit\": \"Редактировать канал\",\n      \"enable\": \"Включить канал\",\n      \"disable\": \"Отключить канал\",\n      \"delete\": \"Удалить канал\",\n      \"create\": \"Создать канал\",\n      \"search-model\": \"Поиск по имени модели\",\n      \"fill-template-models\": \"Заполнить шаблонные модели ({{number}})\",\n      \"add-custom-model\": \"Добавить пользовательскую модель (несколько моделей разделяются пробелами)\",\n      \"add-model\": \"Добавить модель\",\n      \"clear-models\": \"Очистить все модели\",\n      \"advanced\": \"Дополнительные настройки\",\n      \"group-placeholder\": \"Выбрано групп: {{length}}\",\n      \"group-desc\": \"Группировка типов пользователей, не включенные группы не будут включены в доступную область этого канала (когда группировка пуста, все пользователи могут использовать этот канал), нет необходимости устанавливать группировку для неспециальных случаев\",\n      \"groups\": {\n        \"anonymous\": \"Анонимный пользователь\",\n        \"normal\": \"обычный пользователь\",\n        \"basic\": \"Базовые подписчики\",\n        \"standard\": \"Стандартные подписчики\",\n        \"pro\": \"Подписчики Pro\",\n        \"admin\": \"Пользователь-администратор\",\n        \"custom\": \"Пользовательская группировка\"\n      },\n      \"joint\": \"Док-станция выше по течению\",\n      \"joint-endpoint\": \"Адрес выше по потоку\",\n      \"joint-endpoint-placeholder\": \"Введите API-адрес вышестоящего CoAI, например: https://api.chatnio.net\",\n      \"joint-secret\": \"Ключ API\",\n      \"joint-secret-placeholder\": \"Пожалуйста, введите ключ API для восходящего чата Nio\",\n      \"sync-failed\": \"Не удалось синхронизировать\",\n      \"sync-failed-prompt\": \"Адрес не может быть запрошен или модель рынка пуста\\n(Конечная точка: {{endpoint}})\",\n      \"sync-success\": \"Успешная синхронизация\",\n      \"sync-success-prompt\": \"{{length}} модели были добавлены из синхронизации восходящего потока.\",\n      \"upstream-endpoint-placeholder\": \"Введите вышестоящий адрес OpenAI, например, https://api.openai.com\",\n      \"sync-secret-placeholder\": \"Пожалуйста, введите ключ API для восходящего канала\",\n      \"proxy-type\": \"Тип прокси- сервера\",\n      \"proxy-endpoint\": \"Адрес прокси-сервера\",\n      \"proxy-endpoint-placeholder\": \"Введите адрес прямого прокси-сервера, например: socks5://example.com: 1080\",\n      \"proxy-desc\": \"Прямой прокси-сервер, поддерживает HTTP/HTTPS/Socks5 прокси-сервер (обратный прокси-сервер, пожалуйста, заполните точку доступа, нет необходимости устанавливать прокси-сервер в неспециальных случаях)\",\n      \"proxy-username\": \"Имя пользователя прокси-сервера\",\n      \"proxy-username-placeholder\": \"Введите имя пользователя для аутентификации агента (необязательно)\",\n      \"proxy-password\": \"Пароль прокси-сервера\",\n      \"proxy-password-placeholder\": \"Введите пароль аутентификации агента (необязательно)\",\n      \"search-channel\": \"Поиск по названию канала, модели, ключу...\",\n      \"retry-name\": \"повторить\",\n      \"secret-number\": \"Количество клавиш\",\n      \"loading\": \"Загрузка...\",\n      \"new\": \"Создать новый канал\",\n      \"import\": \"Импортировать существующие каналы\",\n      \"first-message-as-user\": \"Преобразовать первое сообщение в сообщение пользователя по умолчанию\",\n      \"first-message-as-user-tip\": \"Если включено, будет преобразовано в роль пользователя, когда первое сообщение является ролью помощника\",\n      \"first-message-as-user-desc\": \"Некоторые модели (например, DeepSeek) не поддерживают первое сообщение в качестве роли помощника. Включите этот параметр, чтобы преобразовать первое сообщение роли помощника в роль пользователя.\",\n      \"merge-consecutive-user-messages\": \"Объединить непрерывные сообщения пользователя\",\n      \"merge-consecutive-user-messages-tip\": \"Если включено, будет объединено в одно сообщение, когда оба последовательных сообщения являются сообщениями пользователя\",\n      \"merge-consecutive-user-messages-desc\": \"Некоторые модели, такие как DeepSeek, не поддерживают два последовательных сообщения пользователя, включив эту опцию, можно объединить два последовательных сообщения пользователя в одно сообщение.\"\n    },\n    \"charge\": {\n      \"id\": \"ID\",\n      \"type\": \"Тип\",\n      \"model\": \"Модель\",\n      \"quota\": \"Квота\",\n      \"action\": \"Действие\",\n      \"input\": \"Вход\",\n      \"output\": \"Выход\",\n      \"support-anonymous\": \"Поддержка анонимных вызовов\",\n      \"non-billing\": \"Не тарифицируется\",\n      \"times-billing\": \"Тарификация по времени\",\n      \"token-billing\": \"Тарификация по токену\",\n      \"anonymous\": \"Поддержка анонимных вызовов\",\n      \"time-count\": \"Квота одного запроса\",\n      \"input-count\": \"Квота входа\",\n      \"output-count\": \"Квота выхода\",\n      \"add-rule\": \"Добавить правило\",\n      \"update-rule\": \"Обновить правило\",\n      \"unused-model\": \"Некоторые правила выставления счетов модели не установлены\",\n      \"unused-model-tip\": \"Модели не настроены по правилам выставления счетов Чтобы избежать потерь, обычные пользователи не смогут запрашивать\",\n      \"sync\": \"Синхронизация выше ПО потоку\",\n      \"sync-option\": \"Параметры синхронизации\",\n      \"sync-site\": \"Адрес выше по потоку\",\n      \"sync-tip\": \"Синхронизация правил выставления счетов выше по потоку\",\n      \"sync-placeholder\": \"Введите API-адрес вышестоящего CoAI, например: https://api.chatnio.net\",\n      \"sync-failed\": \"Не удалось синхронизировать\",\n      \"sync-failed-prompt\": \"Адрес не может быть запрошен или правило выставления счетов пусто\\n(Конечная точка: {{endpoint}})\",\n      \"sync-prompt\": \"Правила для {{length}} моделей были взяты из верхнего потока и повлияют на правила для текущих {{influence}} моделей. Продолжить?\",\n      \"sync-overwrite\": \"Перезаписать существующие правила\",\n      \"sync-confirm\": \"Подтвердить синхронизацию\",\n      \"sync-builtin\": \"Встроенная цена в приложении\",\n      \"usd-currency\": \"Обменный курс доллара США к юаню\",\n      \"group-pricing\": \"Коэффициент ценообразования группы пользователей\",\n      \"new-group\": \"Идентификатор группы пользователей\",\n      \"add-group\": \"Добавить группу пользователей\",\n      \"group-pricing-description\": \"Коэффициент ценообразования группы пользователей можно использовать для различения цены выставления счетов для разных групп пользователей с базовым коэффициентом 1, то есть соотношением цена пользователя = цена модели *\",\n      \"group-pricing-sample\": \"Пример: Если модель взимает 0,2 балла, а соотношение групп пользователей составляет 0,8, фактический заряд составляет 0,2 * 0,8 = 0,16 балла\",\n      \"default-price\": \"Цена по умолчанию\",\n      \"custom-price\": \"Специальные цены\",\n      \"group-pricing-tip\": \"Множитель может использоваться для различения цены выставления счета для разных групп пользователей, * * Базовый множитель равен 1 * *, то есть * * цена пользователя = цена модели * множитель * *\\n\\nПример: если модель взимает 0,2 балла, а соотношение групп пользователей составляет 0,8, фактический заряд составляет 0,2 x 0,8 = 0,16 балла\\n\\n- Множитель покупки: когда пользователь покупает кредиты, множитель цены вычета\\n- Мультипликатор потребления: мультипликатор цены вычета, когда пользователь тратит кредиты\",\n      \"new-group-price\": \"Цена\",\n      \"add-new-group\": \"Добавить новую группу пользователей\",\n      \"new-group-buy-price\": \"Курс покупки\",\n      \"new-group-consume-price\": \"Коэффициент потребления\",\n      \"new-group-description\": \"Описание\",\n      \"update-group\": \"Обновить группу пользователей\"\n    },\n    \"system\": {\n      \"general\": \"Общие настройки\",\n      \"search\": \"Веб-поиск\",\n      \"mail\": \"SMTP Настройки почты\",\n      \"save\": \"Сохранить\",\n      \"backend\": \"Домен бэкэнда\",\n      \"backendTip\": \"Имя домена бэкенда (путь установки докера по умолчанию -/api), используется для получения обратных вызовов и хранения и т. д., значение по умолчанию пусто\\nПример: {{backend}}\",\n      \"mailHost\": \"Почтовый хост\",\n      \"mailPort\": \"Порт SMTP\",\n      \"mailUser\": \"Имя пользователя\",\n      \"mailPass\": \"Пароль\",\n      \"searchEndpoint\": \"Конечная точка поиска\",\n      \"searchQuery\": \"Максимальное количество результатов поиска\",\n      \"searchTip\": \"[SearXNG](https://github.com/searxng/searxng) Поисковая система с открытым исходным кодом, предоставляющая возможности сетевого поиска. Пример развертывания SearXNG Docker Privatization: [SearXNG Docker](https://github.com/zmh-program/searxng)\",\n      \"mailFrom\": \"От\",\n      \"test\": \"Тест исходящий\",\n      \"updateRoot\": \"Изменить корневой пароль\",\n      \"updateRootTip\": \"Пожалуйста, соблюдайте осторожность, после смены пароля root вам нужно будет снова войти в систему.\",\n      \"updateRootPlaceholder\": \"Введите новый пароль root\",\n      \"updateRootRepeatPlaceholder\": \"Введите новый пароль root еще раз\",\n      \"title\": \"Название сайта\",\n      \"titleTip\": \"Название сайта для отображения в заголовке сайта, оставьте пустым по умолчанию\",\n      \"logo\": \"Логотип сайта\",\n      \"logoTip\": \"Ссылка на логотип сайта для отображения в заголовке сайта, оставьте поле пустым по умолчанию (например, {{logo}})\",\n      \"backendPlaceholder\": \"Имя домена обратного вызова Backend, пустое по умолчанию, требуется для приема обратных вызовов\",\n      \"docs\": \"Ссылка на документ\",\n      \"docsTip\": \"Ссылка на документ, оставьте пустым для по умолчанию https://coai.dev\",\n      \"file\": \"Служба разбора файлов\",\n      \"filePlaceholder\": \"Служба разбора файлов, оставьте пустым для по умолчанию https://blob.coai.dev (стабильность не гарантируется)\",\n      \"fileTip\": \"Для получения услуг по разбору файлов обратитесь к проекту [coai-blob-service] (https://github.com/zmh-program/blob-service) для сборки.\",\n      \"site\": \"Настройки сайта\",\n      \"quota\": \"Начальные точки пользователя\",\n      \"quotaTip\": \"Кредиты, предоставленные после регистрации пользователя\",\n      \"announcement\": \"Объявление о площадке\",\n      \"announcementPlaceholder\": \"Введите объявление сайта (поддерживается формат Markdown/HTML)\",\n      \"mailEnableWhitelist\": \"Включить белый список суффиксов домена\",\n      \"mailWhitelist\": \"Белый список суффиксов доменов\",\n      \"mailWhitelistSelected\": \"Выбрано адрес эл. почты домена {{length}}\",\n      \"mailWhitelistSearchPlaceholder\": \"Поиск суффиксов доменов\",\n      \"customWhitelistPlaceholder\": \"Введите список пользовательских суффиксов домена (которые появятся в списке опций на выбор), разделенных запятыми, например: example.com, example.net\",\n      \"buyLink\": \"Ссылка на покупку\",\n      \"buyLinkPlaceholder\": \"Введите ссылку на секретную покупку карты, оставьте поле пустым, чтобы не показывать кнопку покупки\",\n      \"mailConfNotValid\": \"Параметры отправки SMTP настроены неправильно, проверка почтового ящика отключена\",\n      \"contact\": \"shops|Контактные данные\",\n      \"contactPlaceholder\": \"Введите контактную информацию (поддерживается Markdown/HTML)\",\n      \"common\": \"Основные параметры\",\n      \"article\": \"Группировка функций генерации пакетной записи\",\n      \"articleTip\": \"Группировка функций пакетного пост-генерации, после проверки текущей группы пользователей можно использовать функцию пакетного пост-генерации\",\n      \"generate\": \"Группировка конструкторов ИИ-проектов\",\n      \"generateTip\": \"Группировка генераторов ИИ-проектов, после проверки текущей группы пользователей можно использовать генератор ИИ-проектов\",\n      \"groupPlaceholder\": \"Выбрано групп: {{length}}\",\n      \"cache\": \"Кэшируемая модель\",\n      \"cacheTip\": \"Кэшируемая модель, после проверки текущая модель может быть кэширована и попасть в кэш\",\n      \"cachePlaceholder\": \"Выбрано моделей: {{length}}\",\n      \"cacheAll\": \"Сделать все кэшируемыми\",\n      \"cacheFree\": \"Сделать бесплатную модель кэшируемой\",\n      \"cacheNone\": \"Сделать все некэшированными\",\n      \"cacheExpired\": \"Время истечения срока действия кэша\",\n      \"cacheExpiredTip\": \"Время истечения срока действия кэша (в секундах), по умолчанию 1 час\",\n      \"cacheSize\": \"Максимальный размер вероятности кэширования\",\n      \"cacheSizeTip\": \"Максимальная вероятность кэширования, то есть максимальная вероятность кэширования одного и того же типа входного параметра. Если параметр равен 1, максимальное содержимое кэша равно 1, и запрашиваемое содержимое будет напрямую затронуто. Если параметр равен 4, возвращается 4 содержимого, и запрашиваемое содержимое будет затронуто одним из них.\",\n      \"closeRegistration\": \"Регистрация приостановлена\",\n      \"closeRegistrationTip\": \"Регистрация приостановлена, новые пользователи не смогут зарегистрироваться после закрытия\",\n      \"footer\": \"Информация нижнего колонтитула\",\n      \"footerPlaceholder\": \"Пожалуйста, введите информацию нижнего колонтитула (поддерживается формат Markdown/HTML)\",\n      \"authFooter\": \"Скрыть нижний колонтитул после входа в систему\",\n      \"relayPlan\": \"API промежуточной поддержки квот подписки\",\n      \"relayPlanTip\": \"Квота подписки поддерживает транзитный API, после открытия транзитного API биллинг будет отдавать приоритет использованию пользовательской квоты подписки\\n(Совет: Подписка - это квота раз, модель биллинга для токенов может повлиять на стоимость)\",\n      \"searchQueryTip\": \"Максимальное количество результатов поиска, по умолчанию 5\",\n      \"searchPlaceholder\": \"Точка доступа к службе SearXNG (например, http://ip: 7980)\",\n      \"image_store\": \"Хранение изображений\",\n      \"image_storeTip\": \"Изображения, сгенерированные каналом OpenAI DALL-E, будут храниться на сервере, чтобы предотвратить недействительность изображений\",\n      \"image_storeNoBackend\": \"Нет настроенного внутреннего домена, невозможно включить хранение изображений\",\n      \"closeRelay\": \"Отключить Staging API\",\n      \"closeRelayTip\": \"Отключите промежуточный API, промежуточный API будет недоступен после отключения\",\n      \"debugMode\": \"Режим отладки\",\n      \"debugModeTip\": \"Режим отладки, после включения журнал выведет подробные параметры запроса и другие журналы для устранения неполадок\",\n      \"operation\": \"Эксплуатационные настройки\",\n      \"chat\": \"Настройки чата\",\n      \"payment\": \"Настройки оплаты\",\n      \"epayTitle\": \"Легко оплатить\",\n      \"epayEnabled\": \"Включить EasyPay\",\n      \"epayDomain\": \"Домен EasyPay\",\n      \"epayDomainPlaceholder\": \"Введите доменное имя EasyPay, например: https://pay.example.com\",\n      \"epayMethods\": \"Способ оплаты\",\n      \"epayMethodsPlaceholder\": \"Проверьте включенные способы оплаты (выбрано {{length}})\",\n      \"epayBusinessId\": \"Идентификатор продавца\",\n      \"epayBusinessIdPlaceholder\": \"Введите идентификатор продавца EasyPay\",\n      \"epayBusinessKey\": \"Ключ продавца\",\n      \"epayBusinessKeyPlaceholder\": \"Введите ключ продавца EasyPay\",\n      \"security\": \"Настройки безопасности\",\n      \"securityCheckType\": \"Режим просмотра\",\n      \"securityCheckTypePlaceholder\": \"Выберите тип отзыва\",\n      \"securityTextDatabase\": \"Тезаурус черного списка\",\n      \"securityTextDatabasePlaceholder\": \"Введите словарь черного списка, разделенный пробелами в середине слов, например: Чувствительное слово 1 Чувствительное слово 2\",\n      \"securityRegexDatabase\": \"Регулярное выражение черного списка\",\n      \"securityRegexDatabasePlaceholder\": \"Введите регулярное выражение черного списка, разделенное разрывами строк в середине выражения, например:\\n^ Чувствительные слова 1 $\\n^ Чувствительное слово 2 $\",\n      \"securityBaiduApiKey\": \"Ключ API аудита Baidu Cloud\",\n      \"securityBaiduApiKeyPlaceholder\": \"Введите ключ API Baidu Cloud Review\",\n      \"securityBaiduSecretKey\": \"Секретный ключ аудита Baidu Cloud\",\n      \"securityBaiduSecretKeyPlaceholder\": \"Введите секретный ключ Baidu Cloud Review\",\n      \"securityCheckModels\": \"Конкретная модель аудита\",\n      \"securityCheckModelsPlaceholder\": \"Выбрано {{length}} конкретных моделей аудита\",\n      \"securityCheckModelsTip\": \"Проверка конкретной модели. После проверки текущая модель может быть проверена конкретной моделью аудита. * * По умолчанию все модели будут проверены в соответствии с режимом аудита * *. Если выбрана конкретная модель аудита, * * будет проверена только в соответствии с конкретной моделью аудита * *, * * другие модели не будут проверены * *\",\n      \"securityBaiduTip\": \"Требуются Baidu Cloud Audit Mode, Baidu Cloud Audit * * API Key * * и * * Secret Key * * \\n Для получения дополнительной информации и настройки детализации стратегии аудита, пожалуйста, обратитесь к [Baidu Cloud Audit Quick Start] (https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy) \\n Запрещенная стратегия обзора лексики Пожалуйста, настройте политику в консоли Baidu Cloud в соответствии с документом Baidu Cloud выше\",\n      \"securityTypes\": {\n        \"none\": \"Без режима аудита\",\n        \"dict\": \"Режим обзора текстового тезауруса\",\n        \"regex\": \"Режим текстового регулярного отзыва\",\n        \"baidu\": \"Режим аудита Baidu Cloud\",\n        \"custom\": \"Пользовательский режим аудита бэкенда\"\n      },\n      \"epayTip\": \"EasyPay - это стороннее соглашение об агрегированном платеже на рынке * * Generic * *. * * Это не отдельный платеж или программное обеспечение * *. Вы можете выбрать платформу в зависимости от ситуации. * * Мы не даем никаких рекомендаций и гарантий ответственности * *.\\nЕсли вы имеете право на создание собственной платформы ePayment или напрямую подключаетесь к чужой платформе ePayment: платформы ePayment обычно включают две платформы: * * EasyPay * * (сборы для бизнеса/самозанятых - это относительно стабильный ежемесячный доход) и * * CodePayment * * (личные коды сбора имеют низкие показатели прибытия в режиме реального времени).\\nНастройки EasyPay, обратите внимание, что вы должны нажать на опцию * * Включить EasyPay * *, чтобы включить EasyPay\\nEasyPay необходимо настроить доменное имя обратного вызова, пожалуйста, настройте * * внутреннее доменное имя * * в * * общих настройках * * перед обычным асинхронным обратным вызовом\",\n      \"epayAggregation\": \"Модель агрегированного платежа\",\n      \"epayAggregationTip\": \"Агрегированный способ оплаты, нажав на него, не выберет способ оплаты * * непосредственно на странице агрегированного платежа * *. Убедитесь, что EasyPay поддерживает агрегированную модель оплаты.\",\n      \"prompt_store\": \"Оперативное хранение записей\",\n      \"prompt_storeTip\": \"Оперативное хранение записей, после открытия на сервере будет храниться оперативная запись пользователя\",\n      \"searchCrop\": \"Включить усечение результатов\",\n      \"searchCropTip\": \"Включите усечение результатов, если количество символов в содержимом результатов поиска превышает максимальное количество символов, содержимое будет усечено\",\n      \"searchCropLen\": \"Максимальное количество символов результата\",\n      \"searchEngines\": \"Настройки поисковой системы\",\n      \"searchEnginesPlaceholder\": \"Выбрано поисковых систем: {{length}}\",\n      \"searchEnginesSearchPlaceholder\": \"Введите название поисковой системы, например: Google\",\n      \"searchEnginesEmptyTip\": \"Когда поисковая система пуста, по умолчанию используется поисковая система по умолчанию, настроенная в SearXNG\",\n      \"searchSafeSearch\": \"Безопасный режим поиска\",\n      \"searchSafeSearchModes\": {\n        \"none\": \"закрыть\",\n        \"moderation\": \"Среднее\",\n        \"strict\": \"Строгие\"\n      },\n      \"searchImageProxy\": \"Включить прокси-сервер изображений\",\n      \"searchImageProxyTip\": \"Image proxy, изображение, возвращаемое поисковой системой после открытия, будет загружено через прокси сервисного узла SearXNG\",\n      \"searchTest\": \"Поиск викторины\",\n      \"searchTestTip\": \"Поиск теста, введите запрос для поиска теста\",\n      \"customTitle\": \"Настройка тем\",\n      \"customJS\": \"Пользовательский JS\",\n      \"customJSTip\": \"Пожалуйста, введите пользовательский JS\",\n      \"customCSS\": \"Пользовательский CSS\",\n      \"customCSSTip\": \"Введите пользовательский CSS\",\n      \"custom\": \"Настройки темы\",\n      \"customJs\": \"Пользовательский JS\",\n      \"customJsPlaceholder\": \"Пожалуйста, введите пользовательский JS\",\n      \"customCss\": \"Пользовательский CSS\",\n      \"customCssPlaceholder\": \"Введите пользовательский CSS\",\n      \"mailProtocol\": \"Соглашение О доставке\",\n      \"description\": \"Описание сайта\",\n      \"descriptionTip\": \"Описание сайта, используемое для описания в SEO, оставьте пустым по умолчанию\",\n      \"uploadFaviconSuccess\": \"Логотип успешно загружен! Не забудьте сохранить, чтобы применить новый логотип\",\n      \"customHtml\": \"Пользовательский HTML\",\n      \"customHtmlPlaceholder\": \"Пожалуйста, введите пользовательский HTML\",\n      \"customThemeAlert\": \"Примечание. Если вы используете службы защиты безопасности, такие как WAF (например, Cloudflare WAF, Long Pavilion, 1 Panel WAF, Pagoda WAF), ваша пользовательская тема может быть ошибочно принята за вредоносный код WAF и заблокирована с помощью кода ошибки, такого как 403 Forbidden. Проверьте конфигурацию WAF или отключите конфигурацию WAF.\",\n      \"gaTrackingId\": \"Службы Google Analytics\",\n      \"gaTrackingIdPlaceholder\": \"Введите идентификатор Google Analytics\",\n      \"displayCurrency\": \"Показать валюту\",\n      \"displayCurrencyTip\": \"Отображаемая валютная единица веб-сайта\",\n      \"update\": \"Обновить\",\n      \"group\": \"Группа\",\n      \"group-price\": \"Коэффициент группировки\",\n      \"token-group\": \"Группировка токенов\",\n      \"token-group-tip\": \"Группировка маркеров, если отмечено, текущая группировка будет поддерживать группировку маркеров, эта группировка будет отображаться во всех выбираемых пользователем группах маркеров (все встроенные группы не могут включить группировку маркеров)\",\n      \"edit\": \"Редактировать\",\n      \"delete\": \"удалять\",\n      \"type\": \"Тип\",\n      \"actions\": \"Операция\",\n      \"buy-price\": \"Курс покупки\",\n      \"consume-price\": \"Коэффициент потребления\",\n      \"gravatar\": \"Аватар Gravatar\",\n      \"gravatarPlaceholder\": \"Адрес прокси-сервера Gravatar, оставьте пустым, чтобы отключить аватар Gravatar по умолчанию\",\n      \"oauth\": {\n        \"title\": \"Настройки стороннего входа\",\n        \"wechat\": \"Вход в WeChat\",\n        \"google\": \"Логин Google\",\n        \"github\": \"Вход на Github\",\n        \"telegram\": \"Вход в Telegram\",\n        \"enable\": \"Задействовать\",\n        \"disabled\": \"Нельзя использовать\",\n        \"client_id\": \"Идентификатор клиента\",\n        \"client_id_placeholder\": \"Введите идентификатор клиента\",\n        \"client_secret\": \"Секрет клиента\",\n        \"client_secret_placeholder\": \"Введите секрет клиента\",\n        \"redirect_uri\": \"URI перенаправления\",\n        \"redirect_uri_placeholder\": \"Введите URI перенаправления\",\n        \"scope\": \"Область авторизации\",\n        \"scope_placeholder\": \"Укажите область авторизации\",\n        \"auth_url\": \"URL-адрес авторизации\",\n        \"auth_url_placeholder\": \"Введите URL-адрес авторизации\",\n        \"token_url\": \"URL токена\",\n        \"token_url_placeholder\": \"Введите URL токена\",\n        \"user_info_url\": \"URL-адрес информации о пользователе\",\n        \"user_info_url_placeholder\": \"Введите URL-адрес информации о пользователе\",\n        \"bot_token\": \"Жетон робота\",\n        \"bot_token_placeholder\": \"Введите маркер робота\",\n        \"bot_name\": \"Название бота\",\n        \"bot_name_placeholder\": \"Введите имя бота\",\n        \"rainbow\": \"Вход в Rainbow Aggregation\",\n        \"methods\": \"Метод входа\",\n        \"methods_placeholder\": \"Введите метод входа через запятую, например: qq, wx, baidu, douyin\",\n        \"base_url\": \"Домен входа Rainbow Aggregation\",\n        \"base_url_placeholder\": \"Пожалуйста, введите имя домена для входа в радужную агрегацию, оставьте пустым по умолчанию официальное доменное имя для радужной агрегации https://u.cccyun.cc\",\n        \"require_email\": \"Включить проверку электронной почты\"\n      },\n      \"stripeTitle\": \"Stripe\",\n      \"stripeTip\": \"Stripe - это широко используемая международная система онлайн-платежей, которая поддерживает несколько способов оплаты, включая кредитные и дебетовые карты, Apple Pay, Google Pay и многое другое. Он предоставляет пользователям безопасный и удобный опыт оплаты, что особенно подходит для предприятий, которым необходимо обрабатывать международные платежи.\",\n      \"stripeEnabled\": \"Включить Stripe\",\n      \"stripeSecretKey\": \"Секретный ключ Stripe\",\n      \"stripeSecretKeyPlaceholder\": \"Введите секретный ключ Stripe\",\n      \"stripeWebhookSecret\": \"Секретный веб-перехватчик Stripe\",\n      \"stripeWebhookSecretPlaceholder\": \"Введите Stripe Webhook Secret\",\n      \"securityCustomEndpoint\": \"Точка доступа пользовательского аудита\",\n      \"securityCustomEndpointPlaceholder\": \"Введите пользовательскую точку доступа к аудиту\",\n      \"securityCustomToken\": \"Пользовательский маркер аудита\",\n      \"securityCustomTokenPlaceholder\": \"Введите пользовательский маркер аудита\",\n      \"securityCustomTip\": \"Пользовательский режим аудита с * * токеном * * и * * точкой доступа * * \\n Формат запроса и возврата соответствует Baidu Cloud Review, вы можете перейти к [Baidu Cloud Review Text Review Request Instructions] (https://cloud.baidu.com/doc/ANTIPORN/s/Rk3h6xb3i) для справочной адаптации\",\n      \"wechatPayTitle\": \"Оплата WeChat\",\n      \"wechatPayTip\": \"WeChat Pay всегда стремился предоставлять пользователям и предприятиям безопасные, удобные и профессиональные услуги онлайн-платежей. С помощью основной концепции «WeChat Payment, More Than Payment» мы создали множество удобных сервисов и сценариев приложений для отдельных пользователей, предоставляя профессиональные возможности сбора, операционные возможности, решения для расчетов по фондам и безопасность для всех видов предприятий, а также малых и микро мерчантов. Предприятия, продукты, магазины и пользователи были связаны через WeChat, что делает умную жизнь реальностью.\",\n      \"wechatPayEnabled\": \"Включить WeChat Pay\",\n      \"wechatPayAppId\": \"Идентификатор приложения WeChat Pay\",\n      \"wechatPayAppIdPlaceholder\": \"Введите идентификатор приложения WeChat Pay\",\n      \"wechatPayMchId\": \"Номер продавца WeChat Pay\",\n      \"wechatPayMchIdPlaceholder\": \"Введите номер продавца WeChat Pay\",\n      \"wechatPayKey\": \"WeChat Pay API v3 Key\",\n      \"wechatPayKeyPlaceholder\": \"Введите ключ WeChat Pay API v3\",\n      \"wechatPaySerialNo\": \"Серийный номер сертификата платежной платформы WeChat\",\n      \"wechatPaySerialNoPlaceholder\": \"Введите серийный номер сертификата платежной платформы WeChat\",\n      \"wechatPayCertificate\": \"Сертификат платежной платформы WeChat\",\n      \"wechatPayCertificatePlaceholder\": \"Вставьте сертификат платформы WeChat Pay здесь\",\n      \"wechatPayCertificateTip\": \"Продавец получает возвращенный контент интерфейса API v3 и должен использовать открытый ключ сертификата для проверки. Кроме того, некоторые параметры конфиденциальной информации (например, имя, идентификационный номер) также необходимо зашифровать с помощью открытого ключа сертификата для передачи. Подробнее см. [Сертификат платежной платформы WeChat] (https://pay.weixin.qq.com/doc/v3/merchant/4012068814)\",\n      \"searchLLMExtract\": \"Включить извлечение ключевых слов LLM\",\n      \"searchLLMExtractTip\": \"Использование модели LLM для интеллектуального извлечения поисковых ключевых слов может повысить точность поиска\",\n      \"searchLLMModel\": \"Модель извлечения ключевых слов\",\n      \"searchLLMModelPlaceholder\": \"Выберите модель для извлечения ключевых слов\",\n      \"securityBlacklistIPs\": \"IP-адреса в черном списке\",\n      \"securityBlacklistIPsPlaceholder\": \"Введите IP-адрес черного списка\",\n      \"securityWhitelistIPs\": \"Белый список IP\",\n      \"securityWhitelistIPsPlaceholder\": \"Введите IP-адрес из белого списка\",\n      \"securityWhitelistIPsTip\": \"IP-адреса в черном списке * * действительны только для промежуточного программного обеспечения, ограничивающего скорость, для запросов API * *, если вы хотите ограничить другие запросы или доступ к внешнему интерфейсу, используйте службы безопасности, такие как WAF\",\n      \"securityAddIPAddress\": \"Добавить IP-адрес\",\n      \"securityRemoveIPAddress\": \"Удалить IP-адрес\",\n      \"preDeductQuota\": \"Включить удержание\",\n      \"preDeductQuotaTip\": \"При включении сборы удерживаются в начале запроса, а при выключении - в конце запроса.\",\n      \"affiliateTitle\": \"Настройки партнерского маркетинга\",\n      \"affiliateEnabled\": \"Включить партнерский маркетинг\",\n      \"affiliateCommissionRate\": \"Ставка комиссии\",\n      \"affiliateMinWithdraw\": \"Минимальная сумма вывода\",\n      \"affiliateAllowExistingBind\": \"Разрешить зарегистрированным пользователям привязывать марширующие коды\",\n      \"realtime\": {\n        \"title\": \"Конфигурация WebSocket Live Stream\",\n        \"wsBufferSize\": \"Размер буфера WS\",\n        \"wsBufferSizeTip\": \"Управляет длиной очереди нисходящего шардирования сервера во внешний интерфейс. Меньший (например, 1) уменьшает ожидание хвоста после окончания восходящего потока; больший (например, 24) совместим со старым поведением, но может иметь более длинный хвост.\",\n        \"wsAggregate\": \"Агрегация фрагментации РС\",\n        \"wsAggregateTip\": \"После включения агрегируйте несколько небольших шардов по временному окну, а затем выдавайте их, уменьшая частоту повторного рендеринга внешнего интерфейса и улучшая плавность. Если закрыто, каждый шард выдается немедленно (старое поведение).\",\n        \"wsAggregateWindow\": \"Временное окно агрегации АРМ (мс)\",\n        \"wsAggregateWindowTip\": \"Временное окно для агрегации фрагментов, рекомендуется 15-33 мс. Чем больше значение, тем больше вы объединяетесь и тем плавнее обновление, но первый абзац может быть немного позже.\"\n      },\n      \"hideKeyDocs\": \"Скрыть руководство по стыковке ключевой страницы\",\n      \"xunhupayTitle\": \"Оплата Tiger Pepper\",\n      \"xunhupayTip\": \"Tiger Pepper - это платформа агрегированных платежей, которая поддерживает различные способы оплаты, такие как WeChat и Alipay. После настройки пользователь может долить тигровый перец. WeChat и Alipay должны настроить разные идентификаторы приложений и секреты приложений соответственно.\",\n      \"xunhupayWechatEnabled\": \"Включить WeChat Tiger Pepper\",\n      \"xunhupayAlipayEnabled\": \"Включить Tiger Pepper Alipay\",\n      \"xunhupayWechatAppId\": \"Идентификатор приложения WeChat Tiger Pepper\",\n      \"xunhupayWechatAppIdPlaceholder\": \"Введите идентификатор приложения Tiger Pepper WeChat Payment\",\n      \"xunhupayWechatAppSecret\": \"Секрет приложения Tiger Pepper WeChat\",\n      \"xunhupayWechatAppSecretPlaceholder\": \"Введите в приложение Secret of Tiger Pepper WeChat Payment (Key)\",\n      \"xunhupayAlipayAppId\": \"Идентификатор приложения Tiger Pepper Alipay\",\n      \"xunhupayAlipayAppIdPlaceholder\": \"Введите идентификатор приложения Tiger Pepper Alipay\",\n      \"xunhupayAlipayAppSecret\": \"Секрет приложения Tiger Pepper Alipay\",\n      \"xunhupayAlipayAppSecretPlaceholder\": \"Войдите в приложение Secret of Tiger Pepper Alipay (ключ)\",\n      \"xunhupayEndpoint\": \"Адрес интерфейса Tiger Pepper\",\n      \"xunhupayEndpointPlaceholder\": \"https://api.xunhupay.com или https://api.dpweixin.com\",\n      \"autoTitle\": {\n        \"title\": \"Название автоматической сессии\",\n        \"enabled\": \"Включить автоматические заголовки\",\n        \"model\": \"Создать модель\",\n        \"modelPlaceholder\": \"Оставьте пустым, чтобы использовать текущую модель сеанса\",\n        \"maxLen\": \"Наименование Максимальная длина\",\n        \"minMsgs\": \"Вывести количество сообщений\",\n        \"overwrite\": \"Перезаписать существующий заголовок\",\n        \"prompt\": \"Пользовательская подсказка\",\n        \"promptPlaceholder\": \"{max_len} можно использовать в качестве заполнителя\",\n        \"tip\": \"Используйте LLM для автоматического подведения итогов и установки названия сессии после первого раунда разговоров. Когда пользовательское слово-подсказка установлено на пустое, используется слово-подсказка по умолчанию в конфигурации по умолчанию в CoAI.\"\n      }\n    },\n    \"user\": \"Управление пользователями\",\n    \"invitation-code\": \"Код приглашения\",\n    \"invitation-manage\": \"Управление кодом приглашения\",\n    \"invitation-tips\": \"Пригласительные коды используются для погашения баллов. Каждый тип пригласительного кода может использоваться только один раз одним пользователем (может использоваться для рекламы)\",\n    \"redeem-tips\": \"Коды погашения используются для погашения кредитов и могут быть использованы для оплаты выпуска карт и т. д.\",\n    \"redeem\": {\n      \"quota\": \"Вихри\",\n      \"used\": \"Количество использованных\",\n      \"total\": \"Итого\",\n      \"code\": \"Промокод\"\n    },\n    \"market\": {\n      \"title\": \"Модельный рынок\",\n      \"model-name\": \"Модели засоренности\",\n      \"model-name-placeholder\": \"Введите псевдоним модели (например, GPT-4)\",\n      \"model-id\": \"Идентификатор модели\",\n      \"model-id-placeholder\": \"Введите идентификатор модели (например, gpt-4-0613)\",\n      \"model-description\": \"Введение в модель\",\n      \"model-description-placeholder\": \"Пожалуйста, введите введение в модель\",\n      \"model-context\": \"Высокий контекст\",\n      \"model-context-tip\": \"Является ли модель высококонтекстной моделью (файлы высококонтекстной модели не усекаются длинным содержимым при синтаксическом анализе)\",\n      \"model-is-default\": \"Модель по умолчанию\",\n      \"model-is-default-tip\": \"Добавлена ли модель в список моделей по умолчанию (модели, не добавленные в список моделей по умолчанию, не будут отображаться в списке моделей дома по умолчанию)\",\n      \"model-tag\": \"Этикетка модели\",\n      \"update-success\": \"Успешно обновлено\",\n      \"update-success-prompt\": \"Модель Marketplace успешно обновлена (обновите браузер, чтобы подать заявку сейчас)\",\n      \"update-failed\": \"Ошибка обновления\",\n      \"update-failed-prompt\": \"Запрос на обновление не выполнен по {{reason}}\",\n      \"model-image\": \"Изображение модели\",\n      \"custom-image\": \"Пользовательское изображение\",\n      \"custom-image-placeholder\": \"Введите пользовательский URL-адрес изображения (например, https://example.com/image.jpg)\",\n      \"update\": \"Обновить\",\n      \"new-model\": \"Новая модель\",\n      \"migrate\": \"передавать\",\n      \"sync\": \"Синхронизация выше ПО потоку\",\n      \"sync-tip\": \"Синхронизация рынков восходящих моделей\",\n      \"sync-placeholder\": \"Введите API-адрес вышестоящего CoAI, например: https://api.chatnio.net\",\n      \"sync-all\": \"Синхронизировать все ({{length}})\",\n      \"sync-self\": \"Синхронизация поддерживаемых моделей ({{length}})\",\n      \"sync-site\": \"Адрес выше по потоку\",\n      \"sync-option\": \"Параметры синхронизации\",\n      \"sync-failed\": \"Не удалось синхронизировать\",\n      \"sync-failed-prompt\": \"Адрес не может быть запрошен или модель рынка пуста\\n(Конечная точка: {{endpoint}})\",\n      \"sync-items\": \"Всего найдено {{length}} моделей, {{exist}} моделей найдено (не будет перезаписано), {{new}} моделей добавлено (все синхронизировано), {{support}} моделей поддерживается этим каналом сайта (синхронизированные поддерживаемые модели)\",\n      \"sync-success\": \"Успешная синхронизация\",\n      \"sync-success-prompt\": \"Синхронизировано с вышестоящими, добавлено {{length}} моделей, проверьте и нажмите «Отправить», чтобы вступить в силу, иначе оно не будет сохранено\",\n      \"not-use\": \"Некоторые модели не используются\",\n      \"import-all\": \"Импортировать все...\",\n      \"function-calling\": \"Вызовы функций\",\n      \"function-calling-tip\": \"Поддерживает ли модель вызовы функций (некоторые модели и обратное проектирование не поддерживают вызовы функций)\",\n      \"vision-model\": \"Картографическая модель\",\n      \"vision-model-tip\": \"Является ли модель моделью карты (модель карты поддерживает ввод изображений, например GPT-4 Turbo)\",\n      \"ocr-model\": \"OCR Assist\",\n      \"ocr-model-tip\": \"Если сама модель не поддерживает ввод изображений, распознавание текста OCR может быть включено, чтобы в некоторой степени дополнить визуальные возможности модели (подсказка: службы анализа файлов должны поддерживать службы OCR)\",\n      \"reverse-model\": \"Обратная модель\",\n      \"reverse-model-tip\": \"Если модель реверс-инжиниринга поддерживает полный синтаксический анализ файлов по URL-адресу (например, PDF, Word), эта опция может быть включена, и все типы синтаксического анализа файлов будут обеспечены восходящим потоком, что уменьшит потребление маркеров. Если он не включен по умолчанию, он будет разрешен этим проектом, который применим к большинству моделей. Убедитесь, что служба разбора файлов настроила внешнюю схему хранения URL-адресов (например, S3/R2/MinIO и т. д.) и что внешние файлы разбора URL-адресов поддерживаются выше по потоку от модели.\",\n      \"thinking-model\": \"Модели мышления\",\n      \"thinking-model-tip\": \"Поддерживает ли модель глубокое мышление (модели глубокого мышления выводят цепочку мыслей при выводе контента, например, сонет Клода 3.7)\"\n    },\n    \"model-chart-tip\": \"Использование токенов\",\n    \"subscription\": \"Управление подписками\",\n    \"logger\": {\n      \"title\": \"Журнал обслуживания\",\n      \"console\": \"Консоль\",\n      \"consoleLength\": \"Количество записей в журнале\"\n    },\n    \"plan\": {\n      \"enable\": \"Включить подписку\",\n      \"price\": \"Цена\",\n      \"price-tip\": \"Цена подписки за январь (единица: юань)\",\n      \"item-id\": \"Айди\",\n      \"item-id-placeholder\": \"Введите идентификатор организации (идентификатор позиции не может использоваться более одного раза, например: gpt-4)\",\n      \"item-name\": \"\",\n      \"item-name-placeholder\": \"Пожалуйста, введите название объекта (название объекта используется для отображения названия объекта в списке подписки, например GPT-4)\",\n      \"item-value\": \"Всего\",\n      \"item-value-tip\": \"Ежемесячная квота (единица измерения: раз)\",\n      \"item-icon\": \"Иконка\",\n      \"item-icon-tip\": \"Значки сущностей (значки, используемые значками элементов для отображения в списке подписки)\",\n      \"item-models\": \"Модели\",\n      \"item-models-tip\": \"Модели, охватываемые сущностью (модели элементов используются для отображения моделей в списке подписки)\",\n      \"item-models-search-placeholder\": \"Поиск по идентификатору модели\",\n      \"item-models-placeholder\": \"Выбрано моделей: {{length}}\",\n      \"add-item\": \"Добавить\",\n      \"import-item\": \"Импорт\",\n      \"sync\": \"Синхронизация выше ПО потоку\",\n      \"sync-option\": \"Параметры синхронизации\",\n      \"sync-site\": \"Адрес выше по потоку\",\n      \"sync-placeholder\": \"Введите API-адрес вышестоящего CoAI, например: https://api.chatnio.net\",\n      \"sync-result\": \"Было обнаружено, что количество правил подписки выше по потоку составляет {{length}}, охватывая {{models}} моделей. Перезаписать правила подписки на этом сайте?\",\n      \"discounts\": \"Настройки скидок\",\n      \"discounts-tip\": \"Настройки скидок (по умолчанию, если они не включены, по умолчанию 90% для полугодовых подписок, 80% для годовых подписок)\",\n      \"discount-value\": \"Величина скидки\",\n      \"discount-off\": \"Скидка\"\n    },\n    \"model-usage-chart\": \"Доля используемых моделей\",\n    \"user-type-chart\": \"Доля типов пользователей\",\n    \"identity\": {\n      \"normal\": \"обычный пользователь\",\n      \"api_paid\": \"Другие платящие пользователи\",\n      \"basic_plan\": \"Базовые подписчики\",\n      \"standard_plan\": \"Стандартные подписчики\",\n      \"pro_plan\": \"Подписчики Pro\"\n    },\n    \"user-type-chart-info\": \"Всего пользователей: {{total}}\",\n    \"user-type-chart-tip\": \"Другие платящие пользователи: относится к пользователям, которые подписываются на пользователей с истекшим сроком действия или пользователей, чьи баллы превышают текущие начальные баллы (такие операции, как использование кодов приглашений, также будут учитываться при увеличении изменений в баллах)\",\n    \"is-banned\": \"Бан\",\n    \"email\": \"Почта\",\n    \"quota-set-action\": \"Настройки баллов\",\n    \"quota-set-action-desc\": \"Установить кредиты пользователя\",\n    \"release-subscription-action\": \"Освободить использование подписки\",\n    \"release-subscription-action-desc\": \"Освободить использование подписки для пользователей?\",\n    \"subscription-level\": \"Установите уровень платежей\",\n    \"subscription-level-desc\": \"Установка уровня подписки для пользователя\",\n    \"password-action\": \"изменить пароль\",\n    \"password-action-desc\": \"Введите новый пароль пользователя\",\n    \"email-action\": \"Изменение почтового ящика\",\n    \"email-action-desc\": \"Введите новый адрес электронной почты пользователя\",\n    \"default-password\": \"Запрос на изменение пароля\",\n    \"default-password-prompt\": \"Пароль администратора является паролем по умолчанию. В целях безопасности вашей учетной записи, пожалуйста, измените пароль как можно скорее. (Перейдите в Back Office - System Settings - Change Root Password)\",\n    \"set-admin-action\": \"Сделать администратором\",\n    \"set-admin-action-desc\": \"Вы уверены, что хотите сделать этого пользователя администратором?\",\n    \"cancel-admin-action\": \"Отменить администрирование\",\n    \"cancel-admin-action-desc\": \"Вы уверены, что хотите удалить доступ администратора для этого пользователя?\",\n    \"ban-action\": \"Пользователь-призрак\",\n    \"ban-action-desc\": \"Вы уверены, что хотите заблокировать этого пользователя?\",\n    \"unban-action\": \"Разблокировать пользователя\",\n    \"unban-action-desc\": \"Вы уверены, что хотите разблокировать этого пользователя?\",\n    \"billing\": \"Доходы\",\n    \"coai-format-only\": \"Этот формат уникален для CoAI\",\n    \"exit\": \"Выйти из фонового режима\",\n    \"view\": \"проверить\",\n    \"broadcast-tip\": \"Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.\",\n    \"created-at\": \"Время создания\",\n    \"used-at\": \"Время награждения\",\n    \"used-username\": \"Получить пользователя\",\n    \"payment\": \"Оплата заказа\",\n    \"pay\": {\n      \"epay\": \"Легко оплатить\",\n      \"afdian\": \"Производство энергии любви\",\n      \"order\": \"Номер заказа\",\n      \"amount\": \"Сумма\",\n      \"status\": \"Статус платежа\",\n      \"service\": \"Платежные каналы\",\n      \"type\": \"Дата оплаты\",\n      \"device\": \"Оборудование\",\n      \"username\": \"имя пользователя\",\n      \"status-true\": \"Оплачено\",\n      \"status-false\": \"Не оплачено\",\n      \"created-at\": \"Время создания\",\n      \"updated-at\": \"Время обновления\",\n      \"action\": \"Эксплуатация\",\n      \"copy-order\": \"Дубликат номера заказа\",\n      \"search\": \"Поиск по номеру заказа или имени пользователя\",\n      \"check-order\": \"Проверить статус заказа\",\n      \"check-result-same\": \"Статус согласованного заказа\",\n      \"check-result-diff\": \"Обновление статуса заказа\",\n      \"check-result-same-prompt\": \"Согласованный статус заказа, не нужно обновлять\",\n      \"check-result-diff-prompt\": \"Статус заказа обновлен, оплата завершена\",\n      \"wechatpay\": \"Оплата WeChat\",\n      \"stripe\": \"Stripe\"\n    },\n    \"delete-broadcast\": \"Удалить уведомление\",\n    \"delete-broadcast-desc\": \"Вы уверены? Это действие нельзя отменить. Уведомление будет удалено без возможности восстановления.\",\n    \"expired-at\": \"Срок действия подписки\",\n    \"is-subscribed-tips\": \"Логика суждения о подписке: существует уровень подписки, и период подписки не истек\",\n    \"online-chats\": \"Количество чатов\",\n    \"cdn\": {\n      \"warmup\": \"Прогрев ресурса\",\n      \"warm-tip\": \"> Если вы используете службу CDN, вы можете разогреть ресурсы с помощью этой функции.\\n* * После каждого обновления можно выполнить обновление ресурса для обеспечения стабильности ресурса и увеличения скорости загрузки. * *\\nРесурсы CDN (Content Delivery Network) предварительно нагреваются, и после предварительного нагрева ресурсы будут кэшироваться на узле CDN для ускорения доступа.\\nС помощью функции разогрева вы можете кэшировать популярные ресурсы на CDN-узел до пика бизнеса, снижая нагрузку на исходную станцию и улучшая пользовательский интерфейс.\\nСовет: * * Прогрев приведет к извлечению большого количества данных из CDN на исходную станцию, обратите внимание на широкополосную нагрузку исходной станции. * *\",\n      \"copy-data\": \"Копировать список ресурсов предварительно нагретых URL-адресов\"\n    },\n    \"license\": {\n      \"title\": \"Управление наделением полномочиями\",\n      \"domain\": \"Авторизованный домен\",\n      \"digest\": \"Краткое описание подписи\",\n      \"module\": \"Управление модулями\",\n      \"modules\": {\n        \"bought\": \"Куплено\",\n        \"not-bought\": \"Не куплено\",\n        \"multiKey\": {\n          \"title\": \"Управление несколькими токенами\",\n          \"description\": \"Управление ключами Multi-API, поддержка управления распределением нескольких токенов в одном устройстве, поддержка установки вызываемых моделей, ограничений баланса, журналов вызовов, управления состоянием, руководств по интеграции и других расширенных функций\"\n        },\n        \"stripe\": {\n          \"title\": \"Платежи Stripe\",\n          \"description\": \"Модуль предоплаты Stripe, поддерживает оплату картой Stripe и док-станцию AlipayHK, поддерживает мультивалютные платежи\"\n        },\n        \"paypal\": {\n          \"title\": \"Оплата через PayPal\",\n          \"description\": \"Модуль предоплаты PayPal, поддержка оплаты картой PayPal и другие функции\"\n        },\n        \"afdian\": {\n          \"title\": \"Производство энергии любви\",\n          \"description\": \"Модуль Love Power Generation Payment Webhook для поддержки покупки баланса Love Power Generation\"\n        },\n        \"bot\": {\n          \"title\": \"Робот\",\n          \"description\": \"Модуль SaaS робота WeChat/Flybook/Telegram/Discord\"\n        },\n        \"digital\": {\n          \"title\": \"Цифровой человек\",\n          \"description\": \"Настройка модуля генерации цифрового человеческого видео, передовая динамическая голосовая технология, поддержка клонирования голоса и лица, поддержка частного вывода о развертывании и нескольких движков, поддержка сцен в масштабах всей отрасли, поддержка многомерной настройки\"\n        },\n        \"buy-tip\": \"Обратитесь к своему торговому представителю, чтобы приобрести этот модуль\",\n        \"contact-for-price\": \"Получите доступ к документации, чтобы получить ценовое предложение\",\n        \"coai-pro\": {\n          \"title\": \"CoAI Pro\",\n          \"description\": \"Авторизация CoAI Pro Business Edition для разблокировки всех бизнес-функций, таких как стыковка нескольких каналов оплаты, управление увеличением пользователя (группы), просмотр контента, журналы сеансов и т. д.\"\n        }\n      },\n      \"info\": \"Информация об авторизации\",\n      \"description\": \"Управление лицензией версии CoAI Pro\",\n      \"purchase\": \"Разрешение на покупку\",\n      \"pro-required\": \"Эта функция является эксклюзивной для CoAI Pro, пожалуйста, приобретите лицензию CoAI Pro на странице управления лицензией, чтобы использовать эту функцию\"\n    },\n    \"group\": \"Группа\",\n    \"custom-group\": \"Пользовательская группировка\",\n    \"custom-group-action\": \"Установить пользовательскую группировку\",\n    \"custom-group-action-desc\": \"Введите имя пользовательской группы\",\n    \"group-setting\": \"Параметры группы\",\n    \"notifications\": \"Push-центр\",\n    \"notify-all\": \"Уведомить всех пользователей\"\n  },\n  \"mask\": {\n    \"title\": \"Пред\",\n    \"search\": \"Поиск по имени маски\",\n    \"context\": \"Содержит {{length}} контекст\",\n    \"system\": \"Системные предустановки\",\n    \"custom\": \"Мои предустановки\",\n    \"edit\": \"Изменить профили\",\n    \"create\": \"Новая предустановка\",\n    \"avatar\": \"Предустановленный аватар\",\n    \"conversation\": \"Запланированные беседы\",\n    \"name\": \"Заголовок предустановки\",\n    \"name-placeholder\": \"Введите предустановленный заголовок\",\n    \"description\": \"Введение в предустановки\",\n    \"description-placeholder\": \"Введите предустановленный профиль\",\n    \"search-emoji\": \"Поиск эмодзи\",\n    \"actions\": {\n      \"clone\": \"Клонировать предустановку\",\n      \"use\": \"Использовать предустановку\",\n      \"edit\": \"Изменить профили\",\n      \"delete\": \"Удалить предустановку\"\n    },\n    \"market\": \"Предварительно настроенные рынки\",\n    \"switch-preset\": \"Переключить предустановку\",\n    \"switch-preset-desc\": \"Начал (-а) новую беседу и переключился (-ась) на предустанов\"\n  },\n  \"register\": \"Постановка на учет\",\n  \"auth\": {\n    \"username\": \"имя пользователя\",\n    \"password\": \"пароль\",\n    \"username-or-email\": \"Имя пользователя или адрес электронной почты\",\n    \"username-or-email-placeholder\": \"Введите имя пользователя или адрес электронной почты\",\n    \"password-placeholder\": \"Пожалуйста, введите пароль\",\n    \"forgot-password\": \"Забыли пароль?\",\n    \"reset-password\": \"Восстановить пароль\",\n    \"no-account\": \"У вас нет аккаунта?\",\n    \"register\": \"Зарегистрируйтесь на\",\n    \"username-placeholder\": \"Введите здесь имя пользователя\",\n    \"check-password\": \"Подтверждение пароля\",\n    \"check-password-placeholder\": \"Введите, пожалуйста, пароль снова\",\n    \"email\": \"Эл. почта\",\n    \"email-placeholder\": \"Введите адрес электронной почты\",\n    \"have-account\": \"Уже есть аккаунт?\",\n    \"login\": \"Войти\",\n    \"next-step\": \"Cледующий шаг\",\n    \"verify\": \"Сертификация\",\n    \"code\": \"Код подтверждения\",\n    \"code-placeholder\": \"Введите проверочный код\",\n    \"send-code\": \"Посл\",\n    \"incorrect-info\": \"Неверная информация?\",\n    \"fall-back\": \"Вернитесь на шаг назад\",\n    \"length-range\": \"Ожидаемые цифры: {{min}} ~ {{max}}\",\n    \"same-rule\": \"Несогласованные входные данные\",\n    \"invalid-email\": \"Неверный формат электронной почты\",\n    \"reset-success\": \"Сброс выполнен успешно\",\n    \"reset-success-prompt\": \"Ваш пароль был сброшен, пожалуйста, войдите с новым паролем.\",\n    \"send-code-success\": \"Отправка прошла успешно\",\n    \"send-code-success-prompt\": \"Код подтверждения отправлен на ваш адрес электронной почты. Проверьте его.\",\n    \"send-code-failed\": \"Не удалось отправить\",\n    \"send-code-failed-prompt\": \"Не удалось отправить код подтверждения, причина: {{reason}}\",\n    \"register-success\": \"Регистрация прошла успешно\",\n    \"register-success-prompt\": \"Вы успешно зарегистрировались, добро пожаловать!\",\n    \"disabled-mail\": \"Почтовый ящик текущего сайта отключен. Чтобы включить функцию рассылки, обратитесь к администратору.\",\n    \"code-disabled-placeholder\": \"Подтверждение адреса электронной почты не требуется\",\n    \"wechat\": \"WeChat\",\n    \"connected\": \"Привязка прошла успешно\",\n    \"connected-prompt\": \"Вы успешно привязали свои аккаунты!\",\n    \"providers\": {\n      \"baidu\": \"Baidu\",\n      \"huawei\": \"Huawei\",\n      \"weibo\": \"Weibo\",\n      \"sina\": \"Weibo\",\n      \"wx\": \"WeChat\",\n      \"qq\": \"QQ\",\n      \"xiaomi\": \"Xiaomi\",\n      \"douyin\": \"Douyin\",\n      \"dingtalk\": \"Дюбель\",\n      \"alipay\": \"Alipay\",\n      \"microsoft\": \"Microsoft\"\n    }\n  },\n  \"reset\": \"сброс\",\n  \"request-error\": \"Запрос не выполнен по {{reason}}\",\n  \"update\": \"Обновить\",\n  \"delete\": \"удалять\",\n  \"remove\": \"Убрать\",\n  \"upward\": \"Выше\",\n  \"downward\": \"Ниже\",\n  \"save\": \"Сохранить\",\n  \"announcement\": \"Объявление о площадке\",\n  \"i-know\": \"Мне известно о\",\n  \"submit\": \"передавать\",\n  \"empty\": \"Пусто\",\n  \"exit\": \"Закрыть\",\n  \"model\": \"модель\",\n  \"min-quota\": \"Минимальный баланс\",\n  \"your-quota\": \"Ваш баланс\",\n  \"title\": \"заглавие\",\n  \"my-account\": \"Моя учетная запись\",\n  \"payment\": {\n    \"wechat\": \"WeChat Pay\",\n    \"wxpay\": \"WeChat Pay\",\n    \"alipay\": \"Alipay\",\n    \"paypal\": \"Пайпал\",\n    \"stripe\": \"Stripe\",\n    \"afdian\": \"Производство энергии любви\",\n    \"qqpay\": \"QQ Wallet\",\n    \"order\": {\n      \"quota\": \"{{quota}} з.е.\"\n    },\n    \"wechatpay\": \"Оплата WeChat\",\n    \"dialog-wechatpay\": {\n      \"title\": \"Оплата WeChat\",\n      \"description\": \"Для оплаты воспользуйтесь WeChat, чтобы отсканировать QR-код ниже\",\n      \"success\": \"Оплата прошла успешно\",\n      \"loading\": \"Загрузка...\",\n      \"remaining-time\": \"Оставшееся время оплаты\"\n    },\n    \"notify-stripe\": {\n      \"success\": \"Оплата прошла успешно\",\n      \"canceled\": \"Платеж отменен\",\n      \"processing\": \"Обработка платежа...\"\n    },\n    \"xunhupay-wechat\": \"WeChat Tiger Pepper\",\n    \"xunhupay-alipay\": \"Тигровый перец Alipay\",\n    \"dialog-xunhupay\": {\n      \"title\": \"Оплата Tiger Pepper\",\n      \"description\": \"Используйте WeChat или Alipay, чтобы отсканировать QR-код ниже и оплатить\",\n      \"success\": \"Оплата прошла успешно\",\n      \"remaining-time\": \"Оставшееся время оплаты\"\n    }\n  },\n  \"back-home\": \"Вернуться на главную страницу\",\n  \"copied\": {\n    \"prompt\": \"копировать\",\n    \"success\": \"Скопировано\",\n    \"success-description\": \"Содержимое скопировано в буфер обмена\",\n    \"failed\": \"Ошибка копирования\",\n    \"failed-description\": \"Ошибка копирования по {{reason}}\"\n  },\n  \"record\": {\n    \"user\": \"пользователи\",\n    \"title\": \"История использования\",\n    \"created-at\": \"Дата проведения инструктажа\",\n    \"type\": \"Тип\",\n    \"model\": \"модель\",\n    \"token\": \"Токен\",\n    \"input-tokens\": \"Ввод\",\n    \"output-tokens\": \"Экспорт\",\n    \"quota\": \"Вихри\",\n    \"duration\": \"Прошло времени\",\n    \"detail\": \"Примечания\",\n    \"types\": {\n      \"system\": \"Система\",\n      \"consume\": \"Потребление\",\n      \"topup\": \"Пополнение счета\",\n      \"all\": \"полностью\"\n    },\n    \"billing-today\": \"Потратьте сегодня\",\n    \"billing-month\": \"Потратьте в этом месяце\",\n    \"cond\": {\n      \"model\": \"модель\",\n      \"model-placeholder\": \"Введите название модели\",\n      \"token-name\": \"Название токена\",\n      \"token-name-placeholder\": \"Введите имя токена\",\n      \"start_time\": \"Время начала\",\n      \"end_time\": \"Время окончания\",\n      \"username\": \"Укажите имя пользователя\",\n      \"username-placeholder\": \"Введите здесь имя пользователя\",\n      \"type\": \"Укажите тип\"\n    },\n    \"request-today\": \"Запрос дня\",\n    \"request-month\": \"Запросы месяца\",\n    \"detail-info\": {\n      \"input\": \"Введите цену\",\n      \"output\": \"Выходная цена\",\n      \"times\": \"Per-Per-Price\",\n      \"no-cost\": \"Без выставления счетов\",\n      \"cached\": \"Hit Cache\",\n      \"plan\": \"Выставление счетов за подписку\",\n      \"empty\": \"Нет несущей\",\n      \"error\": \"Ошибка запроса\",\n      \"percent\": \"Коэффициент группировки\"\n    },\n    \"rpm-tips\": \"Текущие обороты (запросов в минуту)\",\n    \"tpm-tips\": \"Текущий TPM (токенов в минуту)\",\n    \"query\": \"Записи запросов\",\n    \"channel\": \"Каналы\"\n  },\n  \"date\": {\n    \"pick\": \"Выберите дату\",\n    \"today\": \"сегодня\",\n    \"clean\": \"Вернуться к нулю\",\n    \"add-day\": \"Добавить один день\",\n    \"sub-day\": \"Уменьшить на один день\",\n    \"add-month\": \"Добавить один месяц\",\n    \"sub-month\": \"Уменьшить на один месяц\",\n    \"add-year\": \"Добавить один год\",\n    \"sub-year\": \"Уменьшение на один год\"\n  },\n  \"renderer\": {\n    \"viewImage\": \"Показать изображение\",\n    \"imageLoadFailed\": \"Не удалось загрузить изображение {{src}}\",\n    \"base64Image\": \"Развернуть изображение Base64\",\n    \"base64ImageCollapse\": \"Свернуть базу изображений64\",\n    \"viewVideo\": \"Смотреть\",\n    \"videoLoadFailed\": \"Не удалось загрузить видео {{src}}\"\n  },\n  \"login-action\": \"Войдите, чтобы пользоваться другими функциями\",\n  \"anonymous\": \"Не вошёл в\",\n  \"bar\": {\n    \"chat\": \"Диалог\",\n    \"model\": \"модель\",\n    \"wallet\": \"кошелек\",\n    \"log\": \"Журнал\",\n    \"admin\": \"За кулисами\",\n    \"preset\": \"Пред\",\n    \"account\": \"Аккаунт\",\n    \"chat-full\": \"Начать беседу\",\n    \"model-full\": \"Ознакомьтесь с моделью\",\n    \"preset-full\": \"Предварительно настроенные рынки\",\n    \"wallet-full\": \"Мой бумажник\",\n    \"log-full\": \"История использования\",\n    \"account-full\": \"Управление учетными записями\",\n    \"admin-full\": \"управление задней частью\",\n    \"key\": \"ключ шифрования\",\n    \"key-full\": \"Управление токенами\"\n  },\n  \"notify\": \"Уведомления\",\n  \"new-notify\": \"Новое уведомление\",\n  \"view-all\": \"Посмотреть все\",\n  \"filter\": {\n    \"filter\": \"Фильтр\",\n    \"conds\": \"Отфильтровано критериев: {{count}}\",\n    \"plan\": \"Подписаться или нет\",\n    \"all\": \"полностью\",\n    \"subscribed\": \"Подписаны\",\n    \"unsubscribed\": \"Подписка не оформлена\",\n    \"admin\": \"администратор\",\n    \"not-admin\": \"Не администратор\",\n    \"ban\": \"Призрак или нет\",\n    \"banned\": \"Призрачный\",\n    \"not-banned\": \"Не привидение\",\n    \"sorts\": {\n      \"sort\": \"Сортировать по\",\n      \"id-desc\": \"Идентификатор по убыванию\",\n      \"id-asc\": \"ID по возрастанию\",\n      \"quota-desc\": \"Точки по убыванию\",\n      \"quota-asc\": \"Точки по возрастанию\",\n      \"used-quota-desc\": \"Используемые точки по убыванию\",\n      \"used-quota-asc\": \"Используемые очки по возрастанию\",\n      \"plan-desc\": \"Срок действия подписки по убыванию\",\n      \"plan-asc\": \"Срок действия подписки истекает в порядке возрастания\"\n    }\n  },\n  \"not-login\": \"Не вошёл в\",\n  \"account\": {\n    \"title\": \"Управление учетными записями\",\n    \"my-account\": \"Мой аккаунт\",\n    \"registerDays\": \"Зарегистрируйтесь на {{days}} дн.\",\n    \"current-quota\": \"Текущие очки:\",\n    \"used-quota\": \"Использованные кредиты\",\n    \"plan-total-month\": \"Всего месяцев подписки\",\n    \"plan-total-month-tips\": \"Изменения количества месяцев из-за повышения и понижения уровня подписки не учитываются в этой статистике\",\n    \"deeptrain\": \"Унифицированное управление учетными записями DeepTrain\",\n    \"deeptrain-description\": \"Чат Nio - это продукт DeepTrain. Единая система управления учетными записями DeepTrain предоставляет пользователям единые услуги по управлению учетными записями. На этой странице вы можете просмотреть информацию о своей учетной записи, привязку сторонней учетной записи, настройки 2FA, настройки кошелька, информацию об аутентификации и многое другое. Унифицированное управление учетными записями Deeptrain обычно используется в CoAI, Fystart, LightNotes и других продуктах.\",\n    \"my-account-description\": \"Информация о вашем аккаунте, информация о привязке стороннего аккаунта и т. д.\",\n    \"api-description\": \"Информация об API вашего глобального аккаунта\",\n    \"share-description\": \"Просмотр бесед в истории и управление ими\",\n    \"share-delete\": \"Удалить общий ресурс\",\n    \"share-delete-description\": \"Вы уверены, что хотите удалить этот общий ресурс?\",\n    \"notification\": {\n      \"title\": \"Центр сообщений\",\n      \"description\": \"Управление способами уведомлений\",\n      \"fetchError\": \"Не удалось получить конфигурацию уведомления\",\n      \"fetchErrorDesc\": \"Не удалось получить конфигурацию уведомления, проверьте сеть и повторите попытку.\",\n      \"updateSuccess\": \"Уведомление об обновлении успешно настроено\",\n      \"updateSuccessDesc\": \"Конфигурация уведомлений успешно обновлена (обновите браузер, чтобы применить сейчас)\",\n      \"save\": \"Сохранить\",\n      \"updateError\": \"Не удалось обновить конфигурацию уведомлений\",\n      \"updateErrorDesc\": \"Не удалось обновить конфигурацию уведомлений. Проверьте сеть и повторите попытку.\",\n      \"testSuccess\": \"Тестовое уведомление успешно настроено\",\n      \"testSuccessDesc\": \"Конфигурация уведомлений успешно протестирована\",\n      \"testError\": \"Не удалось настроить тестовое уведомление\",\n      \"testErrorDesc\": \"Не удалось выполнить настройку тестового уведомления. Проверьте сеть и повторите попытку.\",\n      \"enabled\": \"Задействовать\",\n      \"disabled\": \"Нельзя использовать\",\n      \"appToken\": \"Токен приложения\",\n      \"topicId\": \"Идентификатор субъекта\",\n      \"webhookUrl\": \"URL-адрес веб-перехватчика\",\n      \"botToken\": \"Токен бота\",\n      \"chatId\": \"Идентификатор чата\",\n      \"test\": \"Тест\",\n      \"testDesc\": \"Конфигурация тестового уведомления\",\n      \"alertTitle\": \"Push-центр\",\n      \"alertDescription\": \"Поддерживает WeChat (WxPusher), Discord, Telegram, Flybook push\",\n      \"type\": {\n        \"wxpusher\": \"WeChat (WxPusher)\",\n        \"discord\": \"Discord\",\n        \"telegram\": \"Telegram\",\n        \"feishu\": \"Летающая книга\",\n        \"email\": \"Почта\",\n        \"webhook\": \"Веб-перехватчики\"\n      },\n      \"tiplist\": {\n        \"wxpusher\": \"- WeChat Push с использованием сервиса [WxPusher] (https://wxpusher.zjiecode.com)\\n- Зарегистрируйтесь и создайте приложение на сайте WxPusher\\n- Получите AppToken для приложения и заполните конфигурацию\\n- TopicId необязателен для массовой рассылки\\n- Отсканируйте QR-код приложения, чтобы получить push-код.\",\n        \"discord\": \"- Discord push на базе механизма webhook\\n- Выберите целевой канал на сервере Discord\\n- Перейдите в «Настройки канала» > «Интеграции» > «Создать веб-перехватчик».\\n- Пользовательское имя веб-перехватчика и аватар (необязательно)\\n- Скопируйте сгенерированную конфигурацию популяции URL веб-перехватчика\",\n        \"telegram\": \"- Telegram push нужно создать собственного бота\\n- Найдите @ BotFather в Telegram и начните разговор\\n- Отправьте команду/newbot и следуйте подсказкам, чтобы установить имя бота и имя пользователя\\n- Получите токен бота и заполните конфигурацию\\n- Присоединиться или приватно пообщаться с ботом в целевой группе\\n- Используйте @ userinfobot, чтобы получить идентификатор чата и заполнить его\",\n        \"feishu\": \"- Flybook push использует групповых пользовательских ботов\\n- Добавление пользовательских ботов в целевую группу летающих книг\\n- Задайте имя бота, аватар и описание\\n- Выберите группу, которая получит сообщение\\n- Скопируйте сгенерированную конфигурацию популяции URL веб-перехватчика\\n- Для дополнительной безопасности можно задать ключевые слова (необязательно)\",\n        \"email\": \"- Push-электронная почта является опцией по умолчанию, и уведомления будут отправляться на ваш зарегистрированный адрес электронной почты.\",\n        \"webhook\": \"- Пользовательский URL-адрес веб-перехватчика\\n\\n`` `json\\nОпубликовать ${WEBHOOK_URL}\\n{\\n  \\\"type\\\": \\\"string\\\",\\n  \\\"content\\\": \\\"string\\\",\\n  \\\"time\\\": \\\"number\\\",\\n  \\\"utc_time\\\": \\\"string\\\",\\n  \\\"account_id\\\": \\\"номер\\\",\\n  \\\"additional_data\\\": {...}\\n}\\n```\"\n      },\n      \"method\": \"Способ уведомления\",\n      \"event\": \"События подписки\",\n      \"url\": \"URL\",\n      \"events\": {\n        \"broadcast_event\": \"Информация о push-уведомлениях\",\n        \"payment_event\": \"Платежи, уведомления о подписке\",\n        \"key_quota_not_enough_event\": \"Предупреждение о предельном значении ключа\",\n        \"account_quota_not_enough_event\": \"Предупреждение о лимите аккаунта\"\n      },\n      \"userId\": \"UID пользователя\"\n    },\n    \"oauth\": \"Вход третьих лиц\",\n    \"oauth-description\": \"Привяжите сторонние логины и управляйте ими\"\n  },\n  \"manage\": \"руководство\",\n  \"loading\": \"Загрузка...\",\n  \"send\": \"Отправить\",\n  \"stop\": \"Остановлено\",\n  \"new-chat\": \"Новая беседа\",\n  \"enter\": \"ПЕРЕВОД СТРОКИ\",\n  \"key\": {\n    \"title\": \"Мои токены\",\n    \"name\": \"Название\",\n    \"namePlaceholder\": \"Пожалуйста, введите имя\",\n    \"status\": \"Статус\",\n    \"quota\": \"Предел\",\n    \"quotaPlaceholder\": \"Введите доступную квоту\",\n    \"usedQuota\": \"Использованная квота\",\n    \"remainQuota\": \"Оставшиеся кредиты\",\n    \"infiniteQuota\": \"Неограниченное количество кредитов\",\n    \"createdAt\": \"Время создания\",\n    \"expiredAt\": \"Время истечения срока действия\",\n    \"key\": \"ключ шифрования\",\n    \"advanced\": \"Дополнительные настройки\",\n    \"ipWhiteList\": \"Белый список IP\",\n    \"enableIpWhiteList\": \"Включить белый список IP\",\n    \"enableIpWhiteListTip\": \"Включите белый список IP-адресов. Только IP-адреса в белом списке могут использовать этот ключ, если он не заполнен, разрешены все IP-адреса (не обязательно, не рекомендуется)\",\n    \"ipWhiteListPlaceholder\": \"Введите IP-адрес или сегмент сети в формате: 127.0.0.1,192.168.0.0/16\",\n    \"modelWhiteList\": \"Белый список моделей\",\n    \"enableModelWhiteList\": \"Включить белый список моделей\",\n    \"enableModelWhiteListTip\": \"Включить белый список моделей. Только модели в белом списке могут использовать этот ключ. Оставьте пустым, чтобы использовать все модели (необязательно, не рекомендуется)\",\n    \"modelWhiteListPlaceholder\": \"Проверено моделей: {{length}}\",\n    \"create\": \"Создать токен\",\n    \"update\": \"Обновить маркер\",\n    \"nameEmpty\": \"Имя не может быть пустым\",\n    \"searchPlaceholder\": \"Поиск имени ключа...\",\n    \"disabled\": \"Выкл.\",\n    \"active\": \"Активный отряд\",\n    \"delete\": \"Удалить токен\",\n    \"never\": \"Срок действия никогда не истекает\",\n    \"oneHour\": \"Один час\",\n    \"oneDay\": \"Один день\",\n    \"oneWeek\": \"Одна неделя\",\n    \"oneMonth\": \"1 месяц\",\n    \"oneYear\": \"На один год\",\n    \"disable\": \"Нельзя использовать\",\n    \"disableToken\": \"Отключить токен\",\n    \"docs\": \"Руководство по стыковке\",\n    \"slogan\": \"«Стыковка передовых продуктов ИИ в один клик!»\",\n    \"selectKey\": \"Выбрать ключи\",\n    \"bindLobeChat\": \"Привязать лепестковый чат\",\n    \"bindLobeChatTip\": \"После нажатия кнопки вы будете перенаправлены в Lobe Chat и автоматически привяжете ключ и другую информацию\",\n    \"bindNextChat\": \"Привязать следующий чат\",\n    \"bindNextChatTip\": \"После нажатия кнопки вы будете перенаправлены в следующий чат и автоматически введете предустановленную информацию, такую как ключ\",\n    \"bindOpenCat\": \"Привязать открытого кота\",\n    \"bindOpenCatTip\": \"После нажатия кнопки вы будете перенаправлены на Open Cat и привязаны к заданным параметрам (сначала установите Open Cat на своем устройстве)\",\n    \"bindOneAPIStep1\": \"Перейдите на страницу «Управление каналами» и нажмите «Добавить канал».\",\n    \"bindOneAPIStep2\": \"Выберите тип OpenAI и введите соответствующую информацию в соответствии с точкой доступа, ключ ниже\",\n    \"bindCoAIStep1\": \"Перейдите на страницу управления каналом в фоновом режиме и нажмите Connect upstream\",\n    \"bindCoAIStep2\": \"Заполните соответствующую информацию в соответствии с точкой доступа и ключевой информацией ниже\",\n    \"description\": \"Поддерживает вызов всех моделей ИИ этого сайта в стандартном формате OpenAI API, без учета проблем совместимости API, поддерживает бесшовную интеграцию разработчиков/сторонних инструментов, встроенное управление квотами/временем/областью/разрешениями\",\n    \"noKey\": \"Нет ключа\",\n    \"apiBase\": \"Точки доступа к API\",\n    \"apiBaseTip\": \"Напоминание: Пожалуйста, настройте эту базовую точку доступа API в клиенте. Общий доступ к инструментам может непосредственно просматривать метод доступа в руководстве по доступу ниже. Некоторые другие инструменты могут потребовать добавления суффикса (например,/v1). Пожалуйста, заполните в соответствии с требованиями клиента.\",\n    \"noKeyWarning\": \"нет\",\n    \"noKeyWarningTip\": \"Перед использованием руководства по стыковке создайте ключ\",\n    \"default\": \"Группировка по умолчанию\",\n    \"unknown\": \"Неизвестная группировка\",\n    \"createTip\": \"Не раскрывайте ключ другим лицам (например, не помещайте его в публичный репозиторий Github), иначе ваш ключевой баланс может быть украден, пожалуйста, храните ключ осторожно! Если произошла утечка ключа, своевременно сбросьте/удалите токен.\",\n    \"tokenGroup\": \"Группировка токенов\",\n    \"tokenGroupTip\": \"Группировка пользовательских каналов токенов\"\n  },\n  \"coming-soon\": \"Эта функция находится в разработке, следите за обновлениями!\",\n  \"starred\": \"Любимые модели\",\n  \"unstarred\": \"Общие модели\",\n  \"assistant-suggest\": \"Предустановленные рекомендации\",\n  \"change-suggest\": \"Изменить группу\",\n  \"new-announcement\": \"Уведомление об объявлении\",\n  \"no-announcement\": \"Объявлений пока нет\",\n  \"readed\": \"Прочитано\",\n  \"learn-more\": \"Узнать подробнее\",\n  \"none\": \"Нет\",\n  \"description\": \"Описание\",\n  \"tip\": \"Теплая подсказка\",\n  \"get\": \"Получить\",\n  \"authenticating\": \"Аутентификация\",\n  \"authenticating-prompt\": \"Подождите, пока мы подтвердим ваш аккаунт...\",\n  \"authentication-failed\": \"Сбой аутентификации\",\n  \"oops-quota-exceeded\": \"Упс, недостаточный баланс\",\n  \"oops-quota-exceeded-tip\": \"Ваш баланс недостаточен. Чтобы продолжить, перейдите в раздел «Приобрести кредиты» или «План подписки».\",\n  \"only-one-step\": \"Еще один шаг\",\n  \"verify-email-description\": \"Введите адрес электронной почты, чтобы завершить подтверждение\",\n  \"are-you-sure\": \"Вы уверены?\",\n  \"this-action-cannot-be-undone\": \"Это действие не может быть отменено\",\n  \"connect\": \"Привязать\",\n  \"disconnect\": \"Отменить привязку\",\n  \"plugin\": {\n    \"title\": \"Управление плагинами\",\n    \"add\": \"Добавить плагин\",\n    \"create\": \"Создать плагин\",\n    \"save\": \"Сохранить плагин\",\n    \"cancel\": \"Отмена\",\n    \"enable\": \"Задействовать\",\n    \"disable\": \"Нельзя использовать\",\n    \"enabled\": \"Включено {{name}}\",\n    \"disabled\": \"{{name}} отключен\",\n    \"enabled-badge\": \"Активный отряд\",\n    \"import\": \"Импорт профилей\",\n    \"quick-import\": \"Конфигурация быстрого импорта MCP\",\n    \"name\": \"Имя плагина\",\n    \"name-placeholder\": \"Введите имя плагина\",\n    \"avatar\": \"Аватар плагина\",\n    \"avatar-placeholder\": \"Введите ссылку на аватар\",\n    \"description\": \"Описание плагина\",\n    \"description-placeholder\": \"Введите описание плагина\",\n    \"server-url\": \"Адрес сервера:\",\n    \"server-url-placeholder\": \"Введите адрес сервера MCP\",\n    \"loading\": \"Загрузка...\",\n    \"no-plugins\": \"Нет плагинов\",\n    \"save-success\": \"Сохранение выполнено успешно\",\n    \"save-error\": \"Ошибка сохранения\",\n    \"delete-success\": \"Удаление выполнено успешно\",\n    \"delete-error\": \"Ошибка при удалении\",\n    \"test\": \"Тест класса\",\n    \"testing\": \"Соединение\",\n    \"test-success\": \"Успешное выполнение соединения\",\n    \"test-error\": \"Не удалось подключить\",\n    \"import-success\": \"«% 1 » успешно записан.\",\n    \"import-error\": {\n      \"empty\": \"Введите конфигурацию\",\n      \"invalid-json\": \"Неверный формат JSON\",\n      \"invalid-format\": \"Неправильный формат конфигурации, проверьте, включено ли поле mcpServers\",\n      \"no-servers\": \"Сервер MCP не найден в конфигурации\",\n      \"stdio-not-supported\": \"Текущая версия поддерживает только сервер MCP типа HTTP, тип STDIO не поддерживается\",\n      \"unknown\": \"Импорт не удался, проверьте формат конфигурации\"\n    },\n    \"mcp\": {\n      \"tool-call\": \"Вызов инструмента\",\n      \"tool-calling\": \"Инструмент вызова: {{name}}\",\n      \"tool-executing\": \"Инструмент выполнения: {{name}}\",\n      \"tool-success\": \"Инструмент успешно выполнен: {{name}}\",\n      \"tool-error\": \"Ошибка выполнения инструмента: {{name}}\",\n      \"arguments\": \"Параметры инструмента\",\n      \"result\": \"Результаты выполнения\",\n      \"error\": \"Ошибка выполнения\",\n      \"status\": \"Статус выполнения\",\n      \"status-start\": \"Подготовка инструментов выполнения...\",\n      \"status-executing\": \"Выполнение инструмента...\",\n      \"hide-details\": \"Скрыть сведения о вызове инструмента\",\n      \"show-details\": \"Показать сведения о вызове инструмента\",\n      \"plugin-name\": \"Плагин MCP\",\n      \"copy-param-value\": \"Копировать значение параметра\",\n      \"save\": \"Сохранить\",\n      \"edit\": \"Редактировать\",\n      \"raw-arguments\": \"Необработанные параметры (JSON)\",\n      \"no-arguments\": \"Нет параметров\",\n      \"parsed-result\": \"Результат после парсинга\",\n      \"error-info\": \"Сообщение об ошибке\",\n      \"status-prepare\": \"Подготовка к вызову инструмента...\",\n      \"status-success\": \"Инструмент успешно выполнен\",\n      \"status-error\": \"Сбой при выполнении инструмента\",\n      \"status-calling\": \"Инструмент вызова...\",\n      \"hide-debug\": \"Скрыть отладочную информацию\",\n      \"show-debug\": \"Показать отладочную информацию\",\n      \"tool-arguments\": \"Параметры инструмента\",\n      \"no-arguments-needed\": \"Инструмент не требует параметров\"\n    },\n    \"load-error\": \"Не удалось загрузить\",\n    \"refresh\": \"Обновить\",\n    \"refresh-success\": \"Обновить успешно\",\n    \"test-success-desc\": \"Доступно инструментов: {{count}}\",\n    \"import-json-config\": \"Импорт конфигурации JSON\",\n    \"import-http-only-tip\": \"Текущая версия поддерживает только серверы MCP типа HTTP\",\n    \"import-confirm\": \"Проверка импорта\",\n    \"server-url-required\": \"Введите адрес сервера\",\n    \"test-required\": \"Требует тестирования\",\n    \"test-description\": \"Нажмите кнопку тестирования, чтобы проверить подключение плагина\",\n    \"available-tools\": \"Доступные инструменты\",\n    \"connection-test\": \"Проверка соединения\",\n    \"test-required-error\": \"Вам необходимо проверить соединение перед созданием нового плагина\",\n    \"test-required-hint\": \"Новый плагин должен протестировать соединение, прежде чем его можно будет создать\",\n    \"form-error\": \"Пожалуйста, заполните обязательные поля\"\n  },\n  \"aff\": {\n    \"title\": \"Доля в промоакции\",\n    \"bind-desc\": \"Привяжите реферальный код и начните получать рибейты после покупок приглашенного пользователя.\",\n    \"placeholder\": {\n      \"code\": \"Введите промокод\"\n    },\n    \"generate-code-first\": \"Станьте промокодом\",\n    \"get\": \"Получить код\",\n    \"get-placeholder\": \"Нажмите, чтобы получить промокод\",\n    \"get-success\": \"Сгенерирован промокод\",\n    \"bind-existing\": \"Привязать промокод\",\n    \"bind-success\": \"Привязка прошла успешно\",\n    \"bind-failed\": \"Ошибка\",\n    \"bind-failed-prompt\": \"Ошибка привязки! Причина: {{reason}}\",\n    \"withdraw\": \"Изымать\",\n    \"withdraw-all\": \"Вывести все\",\n    \"withdraw-title\": \"Вывод средств\",\n    \"withdraw-desc\": \"Воспользуйтесь накопленным промодоходом за баллы. Оставьте пустым для полного погашения.\",\n    \"withdraw-placeholder\": \"Введите сумму вывода (оставьте пустым для всех)\",\n    \"withdraw-success\": \"Вывод средств успешно завершен\",\n    \"withdraw-success-prompt\": \"Вы успешно конвертировали {{amount}} в {{quota}} кредитов.\",\n    \"withdraw-failed\": \"Вывод средств не удался\",\n    \"withdraw-failed-prompt\": \"Вывод средств не удался! Причина: {{reason}}\",\n    \"invalid-amount\": \"Недопустимая сумма\",\n    \"cancel\": \"Отмена\",\n    \"confirm\": \"Подтвердить\",\n    \"stats\": {\n      \"referrals\": \"Приглашенные\",\n      \"earnings\": \"Совокупный доход\",\n      \"pending\": \"Подлежит урегулированию\",\n      \"rate\": \"Ставка возврата\"\n    }\n  }\n}"
  },
  {
    "path": "app/src/resources/i18n/tw.json",
    "content": "{\n  \"end\": \"。\",\n  \"add\": \"新增\",\n  \"not-found\": \"找不到頁面\",\n  \"home\": \"首頁\",\n  \"login\": \"登入\",\n  \"register\": \"註冊\",\n  \"reset\": \"重設\",\n  \"login-require\": \"您需要登入才能使用此功能\",\n  \"logout\": \"登出\",\n  \"quota\": \"點數\",\n  \"download\": \"下載\",\n  \"offline\": \"應用程式離線\",\n  \"try-again\": \"再試一次\",\n  \"invalid-token\": \"無效的權杖\",\n  \"invalid-token-prompt\": \"請再試一次。\",\n  \"login-failed\": \"登入失敗\",\n  \"login-failed-prompt\": \"登入失敗！原因：{{reason}}\",\n  \"login-success\": \"登入成功\",\n  \"login-success-prompt\": \"您已成功登入。\",\n  \"server-error\": \"伺服器錯誤\",\n  \"server-error-prompt\": \"登入時發生錯誤，請再試一次。\",\n  \"error\": \"請求失敗\",\n  \"request-failed\": \"請求失敗，請檢查您的網路並再試一次。\",\n  \"success\": \"請求成功\",\n  \"request-success\": \"您的操作已成功執行。\",\n  \"close\": \"關閉\",\n  \"edit\": \"編輯\",\n  \"editor\": \"文字編輯器\",\n  \"pricing\": \"更多計費詳情請參閱模型定價表\",\n  \"true\": \"是\",\n  \"false\": \"否\",\n  \"unknown\": \"未知\",\n  \"update\": \"更新\",\n  \"scroll-down\": \"捲動至最新\",\n  \"broadcast\": \"公告\",\n  \"fatal\": \"應用程式當機\",\n  \"download-fatal-log\": \"下載錯誤日誌\",\n  \"fatal-tips\": \"請先檢查您的網路、瀏覽器相容性，嘗試清除瀏覽器快取並重新整理頁面。若問題仍然存在，請下載日誌並提供完整的重現步驟給開發者，以便我們排查問題。\",\n  \"request-error\": \"請求失敗，原因：{{reason}}\",\n  \"delete\": \"刪除\",\n  \"remove\": \"移除\",\n  \"upward\": \"上移\",\n  \"downward\": \"下移\",\n  \"save\": \"儲存\",\n  \"submit\": \"送出\",\n  \"announcement\": \"網站公告\",\n  \"i-know\": \"我知道了\",\n  \"empty\": \"空空如也\",\n  \"exit\": \"離開\",\n  \"model\": \"模型\",\n  \"min-quota\": \"最低餘額\",\n  \"your-quota\": \"您的餘額\",\n  \"title\": \"標題\",\n  \"my-account\": \"我的帳戶\",\n  \"auth\": {\n    \"username\": \"使用者名稱\",\n    \"username-placeholder\": \"請輸入使用者名稱\",\n    \"password\": \"密碼\",\n    \"password-placeholder\": \"請輸入密碼\",\n    \"check-password\": \"確認密碼\",\n    \"check-password-placeholder\": \"請再次輸入密碼\",\n    \"email\": \"電子郵件\",\n    \"email-placeholder\": \"請輸入電子郵件\",\n    \"username-or-email\": \"使用者名稱或電子郵件\",\n    \"username-or-email-placeholder\": \"請輸入使用者名稱或電子郵件\",\n    \"code\": \"驗證碼\",\n    \"code-placeholder\": \"請輸入驗證碼\",\n    \"code-disabled-placeholder\": \"無需進行電子郵件驗證\",\n    \"send-code\": \"傳送\",\n    \"incorrect-info\": \"填錯資訊？\",\n    \"fall-back\": \"回上一步\",\n    \"forgot-password\": \"忘記密碼？\",\n    \"reset-password\": \"重設密碼\",\n    \"no-account\": \"還沒有帳號？\",\n    \"register\": \"註冊一個\",\n    \"have-account\": \"已有帳號？\",\n    \"login\": \"立即登入\",\n    \"next-step\": \"下一步\",\n    \"verify\": \"驗證\",\n    \"length-range\": \"應為 {{min}} ~ {{max}} 位\",\n    \"same-rule\": \"兩次輸入不一致\",\n    \"invalid-email\": \"電子郵件格式錯誤\",\n    \"reset-success\": \"重設成功\",\n    \"reset-success-prompt\": \"您的密碼已重設，請使用新密碼登入。\",\n    \"send-code-success\": \"傳送成功\",\n    \"send-code-success-prompt\": \"驗證碼已傳送至您的電子郵件，請注意查收。\",\n    \"send-code-failed\": \"傳送失敗\",\n    \"send-code-failed-prompt\": \"驗證碼傳送失敗，原因：{{reason}}\",\n    \"register-success\": \"註冊成功\",\n    \"register-success-prompt\": \"您已成功註冊，歡迎加入！\",\n    \"disabled-mail\": \"本站的電子郵件功能已被停用，請聯絡管理員開啟發信功能。\",\n    \"wechat\": \"同步微信圖片成功\",\n    \"connected\": \"綁定成功\",\n    \"connected-prompt\": \"您已成功綁定帳號！\",\n    \"providers\": {\n      \"baidu\": \"百度\",\n      \"huawei\": \"華為\",\n      \"weibo\": \"微博\",\n      \"sina\": \"微博\",\n      \"wx\": \"同步微信圖片成功\",\n      \"qq\": \"QQ\",\n      \"xiaomi\": \"小米\",\n      \"douyin\": \"抖音\",\n      \"dingtalk\": \"釘釘\",\n      \"alipay\": \"支付寶\",\n      \"microsoft\": \"微軟\"\n    }\n  },\n  \"tag\": {\n    \"free\": \"免費\",\n    \"official\": \"官方\",\n    \"unstable\": \"不穩定\",\n    \"web\": \"網路\",\n    \"high-quality\": \"高品質\",\n    \"high-context\": \"高上下文\",\n    \"high-price\": \"高定價\",\n    \"open-source\": \"開源\",\n    \"image-generation\": \"影像生成\",\n    \"multi-modal\": \"多模態\",\n    \"fast\": \"快速\",\n    \"english-model\": \"英文模型\",\n    \"badges\": {\n      \"non-billing\": \"免費\",\n      \"times-billing\": \"{{price}} / 次\",\n      \"token-billing\": \"輸入 {{input}} / 1k tokens 輸出 {{output}} / 1k tokens\",\n      \"add\": \"新增工作台\",\n      \"remove\": \"移除工作台\",\n      \"plan-included\": \"訂閱包含\",\n      \"plan-included-tip\": \"您的訂閱已包含此模型，將優先使用訂閱內額度\"\n    }\n  },\n  \"market\": {\n    \"title\": \"模型市集\",\n    \"list\": \"模型列表\",\n    \"model\": \"探索更多模型\",\n    \"go\": \"前往模型市集\",\n    \"explore\": \"探索\",\n    \"search\": \"搜尋模型名稱或簡介\",\n    \"model-api\": \"API 請求的模型 ID 名稱\",\n    \"show-pricing\": \"顯示價格\",\n    \"show-1m-pricing\": \"1M TOKENS\",\n    \"switch-model\": \"切換模型\",\n    \"switch-model-desc\": \"已切換至模型\",\n    \"switch-bookmark\": \"工作台\",\n    \"remove-bookmark\": \"已將模型從功能表列移除\",\n    \"add-bookmark\": \"已將模型新增至功能表欄\"\n  },\n  \"conversation\": {\n    \"title\": \"對話\",\n    \"empty\": \"空空如也\",\n    \"refresh-failed\": \"重新整理失敗\",\n    \"refresh-failed-prompt\": \"請求出錯，請再試一次。\",\n    \"remove-title\": \"確定要刪除嗎？\",\n    \"remove-description\": \"此操作無法復原。這將永久刪除對話 \",\n    \"remove-all-title\": \"清除歷史紀錄\",\n    \"remove-all-description\": \"此操作無法復原。這將永久刪除所有對話，是否繼續？\",\n    \"cancel\": \"取消\",\n    \"delete\": \"刪除\",\n    \"edit-title\": \"編輯標題\",\n    \"delete-conversation\": \"刪除對話\",\n    \"delete-success\": \"對話已刪除\",\n    \"delete-success-prompt\": \"對話已刪除。\",\n    \"delete-failed\": \"刪除失敗\",\n    \"delete-failed-prompt\": \"刪除對話失敗，請檢查您的網路並再試一次。\",\n    \"search\": \"搜尋對話...\",\n    \"empty-anonymous\": \"您目前處於匿名模式，對話將不會被儲存。\"\n  },\n  \"chat\": {\n    \"web\": \"網路搜尋\",\n    \"web-aria\": \"切換網路搜尋功能\",\n    \"placeholder\": \"輸入些什麼... (Ctrl+Enter 傳送)\",\n    \"placeholder-enter\": \"輸入些什麼... (Enter 傳送)\",\n    \"placeholder-raw\": \"輸入些什麼...\",\n    \"recall\": \"歷史訊息復原\",\n    \"recall-desc\": \"偵測到您上次有未傳送的訊息，已為您復原。\",\n    \"recall-cancel\": \"取消\",\n    \"send-message\": \"傳送訊息\",\n    \"send-message-desc\": \"您確定要傳送此訊息嗎？\",\n    \"actions\": {\n      \"upscale\": \"放大\",\n      \"variant\": \"變化\",\n      \"reroll\": \"重繪\",\n      \"subtle-upscale\": \"細微放大\",\n      \"creative-upscale\": \"創意放大\",\n      \"subtle-vary\": \"細微變化\",\n      \"strong-vary\": \"強烈變化\",\n      \"region-vary\": \"局部重繪\",\n      \"zoom\": \"縮放\",\n      \"zoom-1\": {},\n      \"zoom-2x\": \"縮放2 倍\",\n      \"zoom-custom\": \"自訂縮放\",\n      \"pan-left\": \"向左移動\",\n      \"pan-right\": \"向右移動\",\n      \"pan-up\": \"向上移動\",\n      \"pan-down\": \"向下移動\",\n      \"bookmark\": \"點讚\"\n    },\n    \"web-search\": \"網路搜尋\",\n    \"web-page-summary\": \"逐頁總結\",\n    \"web-depth\": \"搜尋深度\",\n    \"web-quick-search\": \"快速搜索\",\n    \"web-detailed-search\": \"詳細搜尋\",\n    \"web-enable-toast\": \"已開啟連網搜尋\",\n    \"web-enable-tip\": \"網路搜尋可能會消耗更多Token\",\n    \"web-disable-toast\": \"已關閉連網搜尋\",\n    \"web-enable-page-summary-toast\": \"已開啟逐頁總結\",\n    \"web-enable-page-summary-tip\": \"逐頁總結可能會消耗更多Token 並使輸出變慢\",\n    \"web-disable-page-summary-toast\": \"已關閉逐頁總結\",\n    \"web-search-quick-toast\": \"已將搜尋深度切換至快速搜尋\",\n    \"web-search-detailed-toast\": \"已將搜尋深度切換至詳細搜索\",\n    \"web-search-results\": \"已搜尋到 {{count}} 條結果\",\n    \"web-search-results-hide\": \"收起搜尋結果\",\n    \"web-search-results-query\": \"搜尋關鍵字\",\n    \"web-search-results-visit-source\": \"訪問來源網站\",\n    \"web-search-no-results\": \"暫無搜尋結果\",\n    \"deep-thinking\": \"深度思考\",\n    \"deep-thinking-enable-toast\": \"已開啟深度思考\",\n    \"deep-thinking-enable-tip\": \"深度思考可能會導致輸出較慢\",\n    \"deep-thinking-disable-toast\": \"已關閉深度思考\",\n    \"model-not-support-thinking-desc\": \"目前模型不支援開啟深度思考\",\n    \"plugin\": \"外掛程式\",\n    \"voice\": \"語音辨識\",\n    \"empty-preview\": \"輸入的內容將會被渲染在此(支援Markdown 語法) ~\"\n  },\n  \"message\": {\n    \"copy\": \"複製訊息\",\n    \"save\": \"儲存為檔案\",\n    \"save-image\": \"儲存圖片\",\n    \"use\": \"使用訊息\",\n    \"edit\": \"編輯訊息\",\n    \"stop\": \"暫停回覆\",\n    \"remove\": \"刪除訊息\",\n    \"restart\": \"重新回覆\",\n    \"copy-area\": \"複製選取區域\",\n    \"saving-image-prompt\": \"圖片產生中\",\n    \"saving-image-prompt-desc\": \"正在產生圖片，請稍候...\",\n    \"saving-image-failed\": \"圖片產生失敗\",\n    \"saving-image-failed-prompt\": \"圖片產生失敗，原因：{{reason}}\",\n    \"saving-image-success\": \"圖片產生成功\",\n    \"saving-image-success-prompt\": \"圖片已成功儲存。\",\n    \"sharing\": {\n      \"title\": \"標題\",\n      \"time\": \"時間\",\n      \"message\": \"訊息\"\n    },\n    \"thinking-process\": \"思考過程\"\n  },\n  \"quota-description\": \"訊息的點數支出\",\n  \"buy\": {\n    \"not-config-link\": \"後台未設定購買連結\",\n    \"choose\": \"選擇一個金額\",\n    \"other\": \"其他\",\n    \"other-desc\": \"需要多少點數？\",\n    \"buy\": \"購買 {{amount}} 點數\",\n    \"dalle\": \"DALL·E AI 繪圖\",\n    \"dalle-free\": \"DALL·E 2 繪圖永久免費\",\n    \"flex\": \"彈性計費\",\n    \"input\": \"輸入\",\n    \"output\": \"輸出\",\n    \"learn-more\": \"了解更多\",\n    \"buy-link\": \"前往購買\",\n    \"dialog-title\": \"購買點數\",\n    \"dialog-desc\": \"您確定要購買 {{amount}} 點數嗎？\",\n    \"dialog-cancel\": \"取消\",\n    \"dialog-buy\": \"購買\",\n    \"success\": \"購買成功\",\n    \"success-prompt\": \"您已成功購買 {{amount}} 點數。\",\n    \"redeem\": \"兌換\",\n    \"redeem-placeholder\": \"請輸入兌換碼\",\n    \"deeptrain-tip\": \"提示：在 Deeptrain 儲值至錢包後，請返回此處，點選購買相應點數\",\n    \"exchange-success\": \"兌換成功\",\n    \"exchange-success-prompt\": \"您已成功兌換 {{amount}} 點數。\",\n    \"failed\": \"購買失敗\",\n    \"failed-prompt\": \"購買點數失敗，請確認您有足夠的餘額。\",\n    \"exchange-failed\": \"兌換失敗\",\n    \"exchange-failed-prompt\": \"兌換失敗，原因：{{reason}}\",\n    \"gpt4-tip\": \"提示：網路搜尋功能可能會導致更多的輸入點數消耗\",\n    \"go\": \"前往\",\n    \"title\": \"我的點數\",\n    \"buy-description\": \"請選擇您要購買的點數\",\n    \"redeem-title\": \"領取兌換碼\",\n    \"redeem-description\": \"請輸入您的兌換碼領取點數\",\n    \"quota-info\": \"點數可以使用本站全部模型, 隨用隨付, 適合彈性計費選擇\",\n    \"plan-info\": \"訂閱可以按週期以固定價格使用常用模型, 適合固定長期使用選擇\",\n    \"deeptrain-step-1\": \"選擇點數並點擊購買\",\n    \"deeptrain-step-2\": \"跳Deeptrain 錢包充值\",\n    \"deeptrain-step-3\": \"儲值成功後返回此處再次購買\",\n    \"deeptrain-step-4\": \"(如果錢包已有足夠餘額購買後會自動儲值)\"\n  },\n  \"pkg\": {\n    \"title\": \"禮包\",\n    \"go\": \"前往實名認證\",\n    \"cert\": \"實名認證禮包\",\n    \"cert-desc\": \"完成實名認證後可獲得 50 點數（價值 5 元）\",\n    \"teen\": \"學生福利\",\n    \"teen-desc\": \"實名認證後未成年人（18 歲及以下）可額外獲得 150 點數（價值 15 元）\",\n    \"close\": \"關閉\",\n    \"state\": {\n      \"true\": \"已領取\",\n      \"false\": \"無法領取\"\n    },\n    \"manage\": \"我的禮包\"\n  },\n  \"sub\": {\n    \"title\": \"訂閱\",\n    \"disable\": \"本站訂閱功能已被關閉\",\n    \"quota-link\": \"想要彈性計費？購買點數\",\n    \"plan-not-support-relay\": \"站台訂閱額度不涵蓋中繼 API，中繼 API 請使用彈性計費點數\",\n    \"subscription-link\": \"想要固定計費？訂閱方案\",\n    \"dialog-title\": \"訂閱方案\",\n    \"free\": \"免費版\",\n    \"free-price\": \"永久免費\",\n    \"basic\": \"基本版\",\n    \"standard\": \"標準版\",\n    \"pro\": \"專業版\",\n    \"plan-price\": \"{{money}} 元/月\",\n    \"include-tax\": \"含稅\",\n    \"plan-usage\": \"{{name}} 每月使用 {{times}} 次\",\n    \"plan-unlimited-usage\": \"{{name}} 無限次使用\",\n    \"plan-tip\": \"可呼叫模型\",\n    \"enterprise\": \"企業版\",\n    \"enterprise-service\": \"優先技術支援\",\n    \"enterprise-sla\": \"SLA 保障\",\n    \"enterprise-speed\": \"TPM 速率提升\",\n    \"enterprise-security\": \"SOC-2 標準資料安全保障\",\n    \"enterprise-data\": \"異地資料災備\",\n    \"enterprise-deploy\": \"支援私有化部署\",\n    \"contact-sale\": \"聯絡業務\",\n    \"current\": \"目前方案\",\n    \"subscribe\": \"訂閱\",\n    \"upgrade\": \"升級\",\n    \"downgrade\": \"降級\",\n    \"renew\": \"續訂\",\n    \"cannot-select\": \"無法選擇\",\n    \"select-time\": \"選擇訂閱時間\",\n    \"migrate-plan\": \"變更訂閱方案\",\n    \"migrate-plan-desc\": \"變更訂閱後，您的訂閱時間將會根據剩餘天數價格重新計算。（如降級會延長時間，升級則會補足差價）\",\n    \"price\": \"價格 {{price}} 元\",\n    \"price-tax\": \"含稅 {{price}} 元\",\n    \"upgrade-price\": \"升級費用 {{price}} 元 (僅供參考)\",\n    \"expired\": \"訂閱剩餘天數\",\n    \"time\": {\n      \"1\": \"1 個月\",\n      \"3\": \"3 個月\",\n      \"6\": \"半年\",\n      \"12\": \"1 年\",\n      \"36\": \"3 年\"\n    },\n    \"success\": \"訂閱成功\",\n    \"success-prompt\": \"您已成功訂閱 {{month}} 個月。\",\n    \"migrate-success\": \"變更成功\",\n    \"migrate-success-prompt\": \"您已成功變更訂閱方案。\",\n    \"failed\": \"訂閱失敗\",\n    \"failed-prompt\": \"訂閱失敗，請確認您有足夠的餘額。\",\n    \"failed-quota-prompt\": \"訂閱失敗，您的餘額不足 ({{quota}} 點數)\",\n    \"migrate-failed\": \"變更失敗\",\n    \"sub-migrate-failed-prompt\": \"您的訂閱變更失敗，原因：{{reason}}\",\n    \"month\": \"月\",\n    \"year\": \"年\",\n    \"new\": \"新計劃\",\n    \"month-plan\": \"每月計劃\",\n    \"year-plan\": \"年度計劃\",\n    \"best-choice\": \"最佳選擇\",\n    \"including-model\": \"涵蓋模型\",\n    \"including-model-tip\": \"包含在本訂閱中的可用模型使用額度\",\n    \"select-duration\": \"選擇訂閱時長\",\n    \"price-summary\": \"價格匯總\",\n    \"total-price\": \"總價\",\n    \"none\": \"未訂閱\",\n    \"plan-item-usage\": \"{{times}} 次\",\n    \"plan-item-unlimited-usage\": \"無限\",\n    \"year-earn-tip\": \"年度計劃省 {{percent}}\",\n    \"upgrade-price-label\": \"升級費用\",\n    \"upgrade-price-notice\": \"僅供參考\",\n    \"upgrade-price-notice-tip\": \"升級費用僅供參考，實際價格以伺服器精準計算為準\",\n    \"quota-manage\": \"訂閱配額\",\n    \"expired-days\": \"您的訂閱將於 {{days}} 天後到期\",\n    \"refresh-days\": \"您的配額將於{{refresh_days}} 天後刷新\",\n    \"get-refresh-days\": \"開始使用配額以取得刷新日期\"\n  },\n  \"cancel\": \"取消\",\n  \"confirm\": \"確認\",\n  \"percent\": \"{{cent}} 折\",\n  \"file\": {\n    \"file\": \"檔案\",\n    \"upload\": \"上傳檔案\",\n    \"type\": \"支援 PDF、DOCX、PPTX、XLSX、圖片、文字等格式\",\n    \"drop\": \"將檔案拖曳至此處或點選上傳\",\n    \"parse-error\": \"解析失敗\",\n    \"parse-error-prompt\": \"解析失敗：{{reason}}\",\n    \"max-length\": \"內容過長\",\n    \"max-length-prompt\": \"由於上下文長度限制，內容已被截斷\",\n    \"over-size\": \"檔案過大\",\n    \"over-size-prompt\": \"單一附件大小不能超過 {{size}} MB\",\n    \"large-file\": \"大型檔案解析\",\n    \"large-file-prompt\": \"正在上傳並解析大型檔案中，請耐心等候\",\n    \"large-file-success\": \"解析成功\",\n    \"large-file-success-prompt\": \"大型檔案解析成功，共耗時 {{time}} 秒\",\n    \"number\": \"{{number}} 個檔案\",\n    \"zipper\": \"{{filename}} 和其他 {{number}} 個檔案\",\n    \"empty-file\": \"空白檔案\",\n    \"empty-file-prompt\": \"檔案內容為空，已自動忽略\",\n    \"parse-success-prompt\": \"檔案解析成功: {{file}}\",\n    \"uploading\": \"文件上傳中...\",\n    \"uploading-prompt\": \"正在上傳文件中，請耐心等待\"\n  },\n  \"generate\": {\n    \"title\": \"AI 專案產生器\",\n    \"input-placeholder\": \"產生一個 Python 小遊戲\",\n    \"failed\": \"產生失敗\",\n    \"reason\": \"原因：\",\n    \"success\": \"產生成功\",\n    \"success-prompt\": \"成功產生專案！請選擇下載格式。\",\n    \"empty\": \"產生中...\",\n    \"download\": \"下載 {{name}} 格式\"\n  },\n  \"api\": {\n    \"title\": \"API 設定\",\n    \"copied\": \"複製成功\",\n    \"copied-description\": \"API 金鑰已複製到剪貼簿\",\n    \"learn-more\": \"了解更多\",\n    \"reset\": \"重設金鑰\",\n    \"reset-description\": \"是否確定？此操作無法復原。這將永久重設 API 金鑰，已有的 API 金鑰將會失效。\"\n  },\n  \"service\": {\n    \"title\": \"發現新版本\",\n    \"version\": \"版本\",\n    \"description\": \"發現新版本，是否立即更新？\",\n    \"update\": \"更新\",\n    \"offline-title\": \"離線模式\",\n    \"offline\": \"應用程式目前處於離線狀態。\",\n    \"update-success\": \"更新成功\",\n    \"update-success-prompt\": \"您已更新至最新版本。\"\n  },\n  \"share\": {\n    \"title\": \"分享\",\n    \"share-conversation\": \"分享對話\",\n    \"description\": \"將此對話與他人分享：\",\n    \"copy-link\": \"複製連結\",\n    \"view\": \"檢視\",\n    \"success\": \"分享成功\",\n    \"failed\": \"分享失敗\",\n    \"copied\": \"複製成功\",\n    \"copied-description\": \"連結已複製到剪貼簿\",\n    \"not-found\": \"找不到對話\",\n    \"not-found-description\": \"找不到對話，請檢查連結是否正確或對話是否已被刪除\",\n    \"manage\": \"分享管理\",\n    \"sync-error\": \"同步失敗\",\n    \"name\": \"對話標題\",\n    \"time\": \"時間\",\n    \"action\": \"操作\",\n    \"empty\": \"還沒有分享紀錄，快來分享吧！\",\n    \"share-tip\": \"前往對話區，點選分享按鈕分享對話\"\n  },\n  \"docs\": {\n    \"title\": \"開放文件\"\n  },\n  \"invitation\": {\n    \"title\": \"兌換碼\",\n    \"invitation\": \"禮品碼\",\n    \"input-placeholder\": \"請輸入禮品碼\",\n    \"cancel\": \"取消\",\n    \"check\": \"驗證\",\n    \"check-success\": \"兌換成功\",\n    \"check-success-description\": \"兌換成功！您已獲得 {{amount}} 點數，開始您的 AI 之旅吧！\",\n    \"check-failed\": \"兌換失敗\"\n  },\n  \"contact\": {\n    \"title\": \"聯絡我們\",\n    \"community\": \"加入社區\"\n  },\n  \"settings\": {\n    \"title\": \"設定\",\n    \"description\": \"偏好設定\",\n    \"version\": \"目前版本\",\n    \"language\": \"顯示語言\",\n    \"sender\": \"傳送鍵\",\n    \"context\": \"保留上下文\",\n    \"history\": \"最大歷史對話數\",\n    \"align\": \"對話方塊置中\",\n    \"memory\": \"記憶體用量\",\n    \"max-tokens\": \"最大回覆 Token 數\",\n    \"max-tokens-tip\": \"最大回覆 Token 數，超過此數值將會被截斷（過高的數值可能會導致超過模型的最大 Token 而請求失敗）\",\n    \"temperature\": \"溫度\",\n    \"temperature-tip\": \"隨機取樣的比例，高溫度會產生更多的隨機性，低溫度會產生較集中和確定性的文字\",\n    \"top-p\": \"核心取樣機率閾值\",\n    \"top-p-tip\": \"(TopP) 機率取值越大，產生的隨機性越高；取值越低，產生的確定性越高\",\n    \"top-k\": \"取樣候選集大小\",\n    \"top-k-tip\": \"(TopK) 候選集大小，越大產生的隨機性越高，越小產生的確定性越高\",\n    \"presence-penalty\": \"出現懲罰\",\n    \"presence-penalty-tip\": \"(PresencePenalty) 存在懲罰，控制模型產生新話題的可能性，提高此值可以增加談論新話題的可能性\",\n    \"frequency-penalty\": \"頻率懲罰\",\n    \"frequency-penalty-tip\": \"(FrequencyPenalty) 頻率懲罰，控制模型產生字詞的重複程度，提高此值可以降低重複字詞出現頻率的可能性\",\n    \"repetition-penalty\": \"重複懲罰\",\n    \"repetition-penalty-tip\": \"(RepetitionPenalty) 控制模型產生的重複程度，提高此值可以減少重複，但可能會導致模型產生不連貫的文字（與 FrequencyPenalty 相似）\",\n    \"reset-settings\": \"重設所有設定\",\n    \"reset-settings-description\": \"是否確定？此操作無法復原。這將永久重設所有設定。\",\n    \"theme\": \"主題\",\n    \"light\": \"亮色\",\n    \"dark\": \"暗色\",\n    \"hide-model\": \"隱藏模型選擇區\",\n    \"hide-toolbar\": \"預設隱藏工具列\",\n    \"hide-toolbar-text\": \"隱藏工具列文字\"\n  },\n  \"article\": {\n    \"title\": \"批次產生文章\",\n    \"input-placeholder\": \"請輸入文章標題（一行一個）\",\n    \"prompt-placeholder\": \"請輸入預設（幫助 AI 產生文章，如：學術論文格式，800 字）\",\n    \"web-checkbox\": \"是否開啟網路搜尋功能\",\n    \"generate\": \"產生\",\n    \"progress-title\": \"產生中（總共 {{total}} 篇，{{current}} 篇已產生）\",\n    \"generate-success\": \"產生成功\",\n    \"generate-success-prompt\": \"文章產生成功！請選擇下載格式。\",\n    \"generate-failed\": \"產生失敗\",\n    \"generate-failed-prompt\": \"文章產生失敗，請檢查您的網路並再試一次。\",\n    \"download-format\": \"下載 {{name}} 格式\"\n  },\n  \"admin\": {\n    \"dashboard\": \"儀錶板\",\n    \"users\": \"後台管理\",\n    \"user\": \"使用者管理\",\n    \"broadcast\": \"通知管理\",\n    \"channel\": \"管道設定\",\n    \"settings\": \"系統設定\",\n    \"prize\": \"價格設定\",\n    \"subscription\": \"訂閱管理\",\n    \"exit\": \"退出後台\",\n    \"billing\": \"收入\",\n    \"billing-today\": \"今日入帳\",\n    \"billing-month\": \"本月入帳\",\n    \"subscription-users\": \"訂閱使用者\",\n    \"seat\": \"席\",\n    \"view\": \"檢視\",\n    \"model-chart\": \"模型使用統計\",\n    \"model-chart-tip\": \"Token 用量\",\n    \"model-usage-chart\": \"模型使用佔比\",\n    \"user-type-chart\": \"使用者類型佔比\",\n    \"user-type-chart-tip\": \"其他付費使用者：指訂閱過期使用者或點數超過當前初始點數的使用者（使用禮品碼等操作也會被算作點數增加的變更）\",\n    \"user-type-chart-info\": \"總共 {{total}} 位使用者\",\n    \"request-chart\": \"請求量統計\",\n    \"billing-chart\": \"收入統計\",\n    \"error-chart\": \"錯誤統計\",\n    \"requests\": \"請求量\",\n    \"times\": \"異常次數\",\n    \"empty\": \"無資料\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"確認\",\n    \"invitation\": \"兌換碼管理\",\n    \"code\": \"兌換碼\",\n    \"invitation-code\": \"禮品碼\",\n    \"invitation-manage\": \"禮品碼管理\",\n    \"invitation-tips\": \"禮品碼用於兌換點數，每一類禮品碼一位使用者只能使用一次（可作宣傳使用）\",\n    \"redeem-tips\": \"兌換碼用於兌換點數，可用於支付發卡等\",\n    \"quota\": \"點數\",\n    \"type\": \"類型\",\n    \"used\": \"狀態\",\n    \"number\": \"數量\",\n    \"username\": \"使用者名稱\",\n    \"email\": \"電子郵件\",\n    \"month\": \"月數\",\n    \"poster\": \"發布者\",\n    \"post-at\": \"發布時間\",\n    \"broadcast-content\": \"公告內容\",\n    \"create-broadcast\": \"發布公告\",\n    \"broadcast-placeholder\": \"請輸入通知內容 (支援 Markdown / HTML)\",\n    \"broadcast-tip\": \"通知僅會顯示最新一條，並且只會通知一次。系統設定中可設定網站公告，首次接收將會彈窗顯示於首頁並支援後續檢視。\",\n    \"post\": \"發布\",\n    \"post-success\": \"發布成功\",\n    \"post-success-prompt\": \"公告發布成功。\",\n    \"post-failed\": \"發布失敗\",\n    \"post-failed-prompt\": \"發布失敗，原因：{{reason}}\",\n    \"level\": \"等級\",\n    \"is-admin\": \"管理員\",\n    \"is-banned\": \"封鎖\",\n    \"used-quota\": \"已用點數\",\n    \"is-subscribed\": \"是否訂閱\",\n    \"is-subscribed-tips\": \"是否訂閱評判邏輯：有訂閱等級且訂閱時間未過期\",\n    \"total-month\": \"總計訂閱月數\",\n    \"expired-at\": \"訂閱到期時間\",\n    \"enterprise\": \"企業版\",\n    \"action\": \"操作\",\n    \"search-username\": \"搜尋使用者名稱\",\n    \"password-action\": \"變更密碼\",\n    \"password-action-desc\": \"請輸入使用者的新密碼\",\n    \"set-admin-action\": \"設為管理員\",\n    \"set-admin-action-desc\": \"確定將該使用者設為管理員？\",\n    \"cancel-admin-action\": \"取消管理員\",\n    \"cancel-admin-action-desc\": \"確定取消該使用者的管理員權限？\",\n    \"ban-action\": \"封鎖使用者\",\n    \"ban-action-desc\": \"確定要封鎖該使用者？\",\n    \"unban-action\": \"解除封鎖使用者\",\n    \"unban-action-desc\": \"確定要解除封鎖該使用者？\",\n    \"email-action\": \"變更電子郵件\",\n    \"email-action-desc\": \"請輸入使用者的新電子郵件\",\n    \"quota-action\": \"點數變更\",\n    \"quota-action-desc\": \"請輸入點數變更值（正數為增加，負數為減少）\",\n    \"quota-set-action\": \"點數設定\",\n    \"quota-set-action-desc\": \"設定使用者的點數\",\n    \"subscription-action\": \"訂閱時間管理\",\n    \"subscription-action-desc\": \"請設定使用者 {{username}} 的訂閱到期時間\",\n    \"release-subscription-action\": \"釋放訂閱用量\",\n    \"release-subscription-action-desc\": \"是否釋放使用者的訂閱用量？\",\n    \"subscription-level\": \"設定訂閱等級\",\n    \"subscription-level-desc\": \"設定使用者的訂閱等級\",\n    \"operate-success\": \"操作成功\",\n    \"operate-success-prompt\": \"您的操作已成功執行。\",\n    \"operate-failed\": \"操作失敗\",\n    \"operate-failed-prompt\": \"操作失敗，原因：{{reason}}\",\n    \"created-at\": \"建立時間\",\n    \"updated-at\": \"更新時間\",\n    \"used-at\": \"領取時間\",\n    \"used-username\": \"領取使用者\",\n    \"used-true\": \"已使用\",\n    \"used-false\": \"未使用\",\n    \"generate\": \"批次產生\",\n    \"generate-result\": \"產生結果\",\n    \"error\": \"請求失敗\",\n    \"default-password\": \"密碼變更提示\",\n    \"default-password-prompt\": \"您的管理員密碼為預設密碼，為了您的帳戶安全，請盡快變更密碼。（前往後台管理 - 系統設定 - 變更 Root 密碼）\",\n    \"chatnio-format-only\": \"此格式為 CoAI.Dev 獨有格式\",\n    \"identity\": {\n      \"normal\": \"一般使用者\",\n      \"api_paid\": \"其他付費使用者\",\n      \"basic_plan\": \"基本版訂閱使用者\",\n      \"standard_plan\": \"標準版訂閱使用者\",\n      \"pro_plan\": \"專業版訂閱使用者\"\n    },\n    \"market\": {\n      \"title\": \"模型市場\",\n      \"model-name\": \"模型名稱\",\n      \"not-use\": \"部分模型未使用\",\n      \"import-all\": \"匯入全部\",\n      \"new-model\": \"新增模型\",\n      \"model-name-placeholder\": \"請輸入模型暱稱（如：GPT-4）\",\n      \"model-id\": \"模型 ID\",\n      \"model-id-placeholder\": \"請輸入模型 ID（如：gpt-4-0613）\",\n      \"model-description\": \"模型簡介\",\n      \"model-description-placeholder\": \"請輸入模型簡介\",\n      \"model-context\": \"高上下文\",\n      \"model-context-tip\": \"模型是否為高上下文模型（高上下文模型檔案解析時不會被長內容截斷）\",\n      \"model-is-default\": \"預設模型\",\n      \"model-is-default-tip\": \"模型是否新增至預設模型列表（未新增至預設模型列表的模型預設不會出現在首頁模型列表中）\",\n      \"model-tag\": \"模型標籤\",\n      \"model-image\": \"模型圖片\",\n      \"custom-image\": \"自訂圖片\",\n      \"custom-image-placeholder\": \"請輸入圖片連結\",\n      \"update\": \"更新\",\n      \"migrate\": \"提交\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步選項\",\n      \"sync-site\": \"上游地址\",\n      \"sync-tip\": \"同步上游模型市場\",\n      \"sync-placeholder\": \"請輸入上游 CoAI.Dev 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-failed\": \"同步失敗\",\n      \"sync-failed-prompt\": \"地址無法請求或者模型市場模型為空\\n（端點：{{endpoint}}）\",\n      \"sync-success\": \"同步成功\",\n      \"sync-success-prompt\": \"已從上游同步，新增 {{length}} 個模型，請檢查後點選提交方可生效，否則將不會儲存\",\n      \"sync-items\": \"共發現 {{length}} 個模型，已有模型 {{exist}} 個（不會覆蓋），新增模型 {{new}} 個（同步全部），本站台管道已支援模型 {{support}} 個（同步已支援模型）\",\n      \"sync-all\": \"同步全部 ({{length}} 個)\",\n      \"sync-self\": \"同步已支援模型 ({{length}} 個)\",\n      \"update-success\": \"更新成功\",\n      \"update-success-prompt\": \"模型市場已成功更新（重新整理瀏覽器即可立即套用）\",\n      \"update-failed\": \"更新失敗\",\n      \"update-failed-prompt\": \"更新請求失敗，原因：{{reason}}\",\n      \"function-calling\": \"函數呼叫\",\n      \"function-calling-tip\": \"模型是否支援Function Calling 函數呼叫(一些模型和逆向工程不支援函數呼叫)\",\n      \"vision-model\": \"識圖模型\",\n      \"vision-model-tip\": \"模型是否為識圖模型（識圖模型支援圖片輸入，如GPT-4 Turbo）\",\n      \"thinking-model\": \"思考模型\",\n      \"thinking-model-tip\": \"模型是否支援深度思考（深度思考模型會在輸出內容時一併輸出思考鏈，如Claude 3.7 Sonnet）\",\n      \"ocr-model\": \"OCR 輔助\",\n      \"ocr-model-tip\": \"如果模型本身不支援圖片輸入，可以開啟OCR 文字辨識以在一定程度上使模型具有視覺能力作為補充(提示：文件解析服務必須支援OCR 服務)\",\n      \"reverse-model\": \"逆向模型\",\n      \"reverse-model-tip\": \"如果逆向工程的模型透過URL 支援全檔案(如PDF, WORD) 解析，可以開啟此選項，所有類型的檔案解析將由上游提供，減少​​Token 消耗。預設不開啟則由本項目解析，適用於大部分模型。請保證文件解析服務已配置外部URL 儲存方案(如S3 / R2 / MinIO 等), 且模型上游支援外部URL 解析檔。\"\n    },\n    \"redeem\": {\n      \"quota\": \"點數\",\n      \"used\": \"已用個數\",\n      \"total\": \"總個數\",\n      \"code\": \"兌換碼\"\n    },\n    \"plan\": {\n      \"enable\": \"啟用訂閱\",\n      \"price\": \"價格\",\n      \"price-tip\": \"一月訂閱價格（單位：元）\",\n      \"item-id\": \"ID\",\n      \"item-id-placeholder\": \"請輸入實體 ID（Item ID 不能重複使用，如：gpt-4）\",\n      \"item-name\": \"名稱\",\n      \"item-name-placeholder\": \"請輸入實體名稱（Item Name 用於顯示在訂閱列表中的實體名稱，如：GPT-4）\",\n      \"item-value\": \"額度\",\n      \"item-value-tip\": \"每月額度（單位：次）\",\n      \"item-icon\": \"圖示\",\n      \"item-icon-tip\": \"實體圖示（Item Icon 用於顯示在訂閱列表中的圖示）\",\n      \"item-models\": \"模型\",\n      \"item-models-tip\": \"實體涵蓋的模型（Item Models 用於顯示在訂閱列表中的模型）\",\n      \"item-models-search-placeholder\": \"搜尋模型 ID\",\n      \"item-models-placeholder\": \"已選擇 {{length}} 個模型\",\n      \"add-item\": \"新增\",\n      \"import-item\": \"匯入\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步選項\",\n      \"sync-site\": \"上游地址\",\n      \"sync-placeholder\": \"請輸入上游 CoAI.Dev 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-result\": \"發現上游訂閱規則數 {{length}} 個，涵蓋模型 {{models}} 個，是否覆蓋本站台訂閱規則？\",\n      \"discounts\": \"折扣設定\",\n      \"discounts-tip\": \"折扣設定(不啟用即為預設設置，半年期訂閱預設90% 一年期訂閱80%)\",\n      \"discount-value\": \"折扣值\",\n      \"discount-off\": \"折扣\"\n    },\n    \"channels\": {\n      \"id\": \"管道 ID\",\n      \"name\": \"名稱\",\n      \"name-tip\": \"管道名稱，用於識別管道\",\n      \"name-placeholder\": \"請輸入管道名稱\",\n      \"type\": \"類型\",\n      \"priority\": \"優先順序\",\n      \"priority-tip\": \"多管道時，根據優先順序請求，數值越大優先順序越高\",\n      \"weight\": \"權重\",\n      \"weight-tip\": \"同優先順序時，根據權重比例進行均衡負載呼叫\",\n      \"retry\": \"最大重試次數\",\n      \"retry-tip\": \"當管道請求失敗時，最多重試的次數\",\n      \"model\": \"模型\",\n      \"secret\": \"金鑰\",\n      \"secret-placeholder\": \"請輸入金鑰，格式：{{format}}（<> 不用填）\\n多個金鑰時，一行一個，請求時隨機選取負載\",\n      \"endpoint\": \"接入點\",\n      \"endpoint-placeholder\": \"請輸入接入點（即代理）\",\n      \"mapper\": \"模型對應\",\n      \"mapper-tip\": \"模型名稱轉換，實現非對稱的模型請求\",\n      \"mapper-placeholder\": \"請輸入模型對應，一行一個，格式：model>model\\n前者為請求的模型，後者為對應的模型（需要在模型中存在），中間用 > 分隔\\n格式前加 ! 表示原模型不包含在此管道的可用範圍內，如：!gpt-4-slow>gpt-4，那麼 gpt-4 將不會被涵蓋在此管道的可請求模型中\",\n      \"group\": \"使用者群組\",\n      \"advanced\": \"進階設定\",\n      \"group-tip\": \"使用者群組，未包含的群組將不包含在此管道的可用範圍內（群組為空時，所有使用者都可以使用此管道）\",\n      \"state\": \"狀態\",\n      \"action\": \"操作\",\n      \"edit\": \"編輯管道\",\n      \"enable\": \"啟用管道\",\n      \"disable\": \"停用管道\",\n      \"delete\": \"刪除管道\",\n      \"create\": \"建立管道\",\n      \"joint\": \"對接上游\",\n      \"joint-endpoint\": \"上游地址\",\n      \"joint-endpoint-placeholder\": \"請輸入上游 CoAI.Dev 的 API 地址，如：https://api.chatnio.net\",\n      \"upstream-endpoint-placeholder\": \"請輸入上游 OpenAI 地址，如：https://api.openai.com\",\n      \"sync-secret-placeholder\": \"請輸入上游管道的 API 金鑰\",\n      \"joint-secret\": \"API 金鑰\",\n      \"joint-secret-placeholder\": \"請輸入上游 CoAI.Dev 的 API 金鑰\",\n      \"sync-failed\": \"同步失敗\",\n      \"sync-failed-prompt\": \"地址無法請求或者模型市場模型為空\\n（端點：{{endpoint}}）\",\n      \"sync-success\": \"同步成功\",\n      \"sync-success-prompt\": \"已從上游同步新增 {{length}} 個模型。\",\n      \"search-model\": \"搜尋模型\",\n      \"fill-template-models\": \"填入範本模型（{{number}} 個）\",\n      \"add-custom-model\": \"新增自訂模型（多個模型用空格分隔）\",\n      \"add-model\": \"新增模型\",\n      \"clear-models\": \"清空所有模型\",\n      \"group-placeholder\": \"已選擇 {{length}} 個群組\",\n      \"group-desc\": \"使用者類型群組，未包含的群組將不包含在此管道的可用範圍內（群組為空時，所有使用者都可以使用此管道），非特殊情況無需設定群組\",\n      \"groups\": {\n        \"anonymous\": \"匿名使用者\",\n        \"normal\": \"一般使用者\",\n        \"basic\": \"基本版訂閱使用者\",\n        \"standard\": \"標準版訂閱使用者\",\n        \"pro\": \"專業版訂閱使用者\",\n        \"admin\": \"管理員使用者\",\n        \"custom\": \"自訂分組\"\n      },\n      \"proxy-type\": \"代理類型\",\n      \"proxy-endpoint\": \"代理地址\",\n      \"proxy-endpoint-placeholder\": \"請輸入正向代理地址，如：socks5://example.com:1080\",\n      \"proxy-username\": \"代理使用者名稱\",\n      \"proxy-username-placeholder\": \"請輸入代理的驗證使用者名稱（可選）\",\n      \"proxy-password\": \"代理密碼\",\n      \"proxy-password-placeholder\": \"請輸入代理的驗證密碼（可選）\",\n      \"proxy-desc\": \"正向代理，支援 HTTP/HTTPS/SOCKS5 代理（反向代理請填寫接入點，非特殊情況無需設定正向代理）\",\n      \"loading\": \"正在加載...\",\n      \"search-channel\": \"搜尋頻道名稱, 模型, 密鑰...\",\n      \"retry-name\": \"重試\",\n      \"secret-number\": \"密鑰數\",\n      \"new\": \"新通路\",\n      \"import\": \"導入已有管道\",\n      \"first-message-as-user\": \"將第一條訊息預設轉換為使用者訊息\",\n      \"first-message-as-user-tip\": \"如果開啟，當第一則訊息為assistant 角色時，將會轉換為user 角色\",\n      \"first-message-as-user-desc\": \"某些模型（如DeepSeek）不支援第一則訊息為assistant 角色，開啟此選項可將第一個assistant 角色的訊息轉換為user 角色。\",\n      \"merge-consecutive-user-messages\": \"合併連續用戶訊息\",\n      \"merge-consecutive-user-messages-tip\": \"若開啟，連續兩則訊息均為使用者訊息時，將會合併為一則訊息\",\n      \"merge-consecutive-user-messages-desc\": \"某些模型（如DeepSeek）不支援連續兩個用戶訊息，開啟此選項可以將連續兩個用戶訊息合併為一則訊息。\"\n    },\n    \"charge\": {\n      \"id\": \"ID\",\n      \"type\": \"類型\",\n      \"model\": \"模型\",\n      \"quota\": \"點數\",\n      \"action\": \"操作\",\n      \"input\": \"輸入\",\n      \"output\": \"輸出\",\n      \"support-anonymous\": \"支援匿名\",\n      \"non-billing\": \"不計費\",\n      \"times-billing\": \"按次計費\",\n      \"token-billing\": \"按 Token 計費\",\n      \"anonymous\": \"支援匿名呼叫\",\n      \"time-count\": \"單次請求點數\",\n      \"input-count\": \"輸入點數\",\n      \"output-count\": \"輸出點數\",\n      \"add-rule\": \"新增規則\",\n      \"update-rule\": \"更新規則\",\n      \"unused-model\": \"部分模型計費規則未設定\",\n      \"unused-model-tip\": \"計費規則未設定的模型為避免損失，一般使用者將無法請求\",\n      \"sync\": \"同步上游\",\n      \"sync-option\": \"同步選項\",\n      \"sync-site\": \"上游地址\",\n      \"sync-tip\": \"同步上游計費規則\",\n      \"sync-placeholder\": \"請輸入上游 CoAI.Dev 的 API 地址，如：https://api.chatnio.net\",\n      \"sync-failed\": \"同步失敗\",\n      \"sync-failed-prompt\": \"地址無法請求或者計費規則為空\\n（端點：{{endpoint}}）\",\n      \"sync-prompt\": \"已從上游取得 {{length}} 個模型的規則，將影響目前 {{influence}} 個模型的規則，是否繼續？\",\n      \"sync-overwrite\": \"覆蓋現有規則\",\n      \"sync-confirm\": \"確認同步\",\n      \"sync-builtin\": \"套用內建價格\",\n      \"usd-currency\": \"美元兌人民幣匯率\",\n      \"group-pricing\": \"用戶群定價比例\",\n      \"new-group\": \"使用者群組ID\",\n      \"new-group-price\": \"價格\",\n      \"add-group\": \"新增使用者群組\",\n      \"update-group\": \"更新使用者群組\",\n      \"group-pricing-description\": \"使用者群組定價比例可用於區分不同使用者群組的計費價格，基準比例為1，即使用者價格= 模型價格* 比例\",\n      \"group-pricing-sample\": \"例：模型扣費0.2 點數，使用者組比例為0.8，則實際扣費0.2 * 0.8 = 0.16 點數\",\n      \"group-pricing-tip\": \"倍率可用於區分不同使用者群組的計費價格，**基準倍率為1**，即**使用者價格= 模型價格* 倍率**&#10;&#10;例：模型扣費0.2 點數，使用者組比例為0.8，則實際扣費0.2 x 0.8 = 0.16 點數&#10;&#10;- 購買倍率：用戶購買點數時，扣費價格倍率&#10;- 消費倍率：用戶消費點數時，扣費價格倍率\",\n      \"default-price\": \"預設價格\",\n      \"custom-price\": \"自訂價格\",\n      \"add-new-group\": \"新增用戶群組\",\n      \"new-group-buy-price\": \"購買倍率\",\n      \"new-group-consume-price\": \"消費倍率\",\n      \"new-group-description\": \"描述\"\n    },\n    \"system\": {\n      \"general\": \"一般設定\",\n      \"search\": \"網路搜尋\",\n      \"site\": \"網站設定\",\n      \"mail\": \"SMTP 發信設定\",\n      \"common\": \"通用設定\",\n      \"save\": \"儲存\",\n      \"updateRoot\": \"變更 Root 密碼\",\n      \"updateRootTip\": \"請謹慎操作，變更 Root 密碼後，您需要重新登入。\",\n      \"updateRootPlaceholder\": \"請輸入新的 Root 密碼\",\n      \"updateRootRepeatPlaceholder\": \"請再次輸入新的 Root 密碼\",\n      \"test\": \"測試發信\",\n      \"title\": \"網站名稱\",\n      \"titleTip\": \"網站名稱，用於顯示在網站標題，留空為預設值\",\n      \"logo\": \"網站 Logo\",\n      \"docs\": \"文件連結\",\n      \"docsTip\": \"文件連結，留空為預設值 https://coai.dev\",\n      \"logoTip\": \"網站 Logo 的連結，用於顯示在網站標題，留空為預設值（如 {{logo}}）\",\n      \"file\": \"檔案解析服務\",\n      \"filePlaceholder\": \"檔案解析服務，留空為預設值 https://blob.chatnio.net（不保證穩定性）\",\n      \"fileTip\": \"檔案解析服務，請參考 [chatnio-blob-service](https://github.com/coaidev/blob-service) 專案進行建置\",\n      \"backend\": \"後端網域\",\n      \"backendTip\": \"後端網域（Docker 安裝預設路徑為 /api），用於接收回呼和儲存等，預設為空\\n範例：{{backend}}\",\n      \"backendPlaceholder\": \"後端回呼網域，預設為空，接收回呼必填\",\n      \"debugMode\": \"除錯模式\",\n      \"debugModeTip\": \"除錯模式，開啟後日誌將輸出詳細的請求參數等日誌，用於排查問題\",\n      \"mailHost\": \"發信網域\",\n      \"mailProtocol\": \"發信協定\",\n      \"mailPort\": \"SMTP 埠\",\n      \"mailUser\": \"使用者名稱\",\n      \"mailPass\": \"密碼\",\n      \"mailFrom\": \"寄件人\",\n      \"mailEnableWhitelist\": \"啟用網域名稱後綴白名單\",\n      \"mailConfNotValid\": \"SMTP 發信參數未正確設定，已停用電子郵件驗證\",\n      \"mailWhitelist\": \"網域名稱後綴白名單\",\n      \"mailWhitelistSelected\": \"已選擇 {{length}} 個網域電子郵件\",\n      \"mailWhitelistSearchPlaceholder\": \"搜尋網域名稱後綴\",\n      \"customWhitelistPlaceholder\": \"請輸入自訂網域名稱後綴列表（輸入後將出現在選項列表中可供選擇），使用英文逗號分隔，如：example.com,example.net\",\n      \"searchEndpoint\": \"搜尋接入點\",\n      \"searchQuery\": \"最大搜尋結果數\",\n      \"searchQueryTip\": \"最大搜尋結果數，預設為 5\",\n      \"searchCrop\": \"開啟結果截斷\",\n      \"searchCropTip\": \"開啟結果截斷，開啟後搜尋結果內容的字元數如果超過最大結果字元數，則內容後面會被截斷\",\n      \"searchCropLen\": \"最大結果字元數\",\n      \"searchEngines\": \"搜尋引擎設定\",\n      \"searchEnginesPlaceholder\": \"已選擇 {{length}} 個搜尋引擎\",\n      \"searchEnginesSearchPlaceholder\": \"請輸入搜尋引擎名稱，如：Google\",\n      \"searchEnginesEmptyTip\": \"設定搜尋引擎為空時，預設使用 SearXNG 內預設設定的搜尋引擎\",\n      \"searchTest\": \"搜尋測試\",\n      \"searchTestTip\": \"搜尋測試，輸入查詢內容進行搜尋測試\",\n      \"searchSafeSearch\": \"安全搜尋模式\",\n      \"searchSafeSearchModes\": {\n        \"none\": \"關閉\",\n        \"moderation\": \"中等\",\n        \"strict\": \"嚴格\"\n      },\n      \"searchImageProxy\": \"開啟圖片代理\",\n      \"searchImageProxyTip\": \"圖片代理，開啟後搜尋引擎回傳的圖片將會透過 SearXNG 服務節點代理載入\",\n      \"searchTip\": \"[SearXNG](https://github.com/searxng/searxng) 開源搜尋引擎提供網路搜尋能力。SearXNG Docker 私有化部署範例：[SearXNG Docker](https://github.com/zmh-program/searxng)\",\n      \"searchPlaceholder\": \"SearXNG 服務接入點（例如 http://ip:7980）\",\n      \"closeRegistration\": \"暫停註冊\",\n      \"closeRegistrationTip\": \"暫停註冊，關閉後新使用者將無法註冊\",\n      \"closeRelay\": \"關閉中繼 API\",\n      \"closeRelayTip\": \"關閉中繼 API，關閉後中繼 API 將無法使用\",\n      \"relayPlan\": \"訂閱額度支援中繼 API\",\n      \"relayPlanTip\": \"訂閱額度支援中繼 API，開啟後中繼 API 計費會優先考慮使用使用者訂閱額度\\n（提示：訂閱為次數額度，對 Token 計費的模型可能會影響成本）\",\n      \"quota\": \"使用者初始點數\",\n      \"quotaTip\": \"使用者註冊後贈送的點數\",\n      \"buyLink\": \"購買連結\",\n      \"buyLinkPlaceholder\": \"請輸入卡密的購買連結，留空不顯示購買按鈕\",\n      \"announcement\": \"網站公告\",\n      \"announcementPlaceholder\": \"請輸入網站公告（支援 Markdown / HTML 格式）\",\n      \"contact\": \"聯絡資訊\",\n      \"contactPlaceholder\": \"請輸入聯絡資訊（支援 Markdown / HTML 格式）\",\n      \"footer\": \"頁尾資訊\",\n      \"footerPlaceholder\": \"請輸入頁尾資訊（支援 Markdown / HTML 格式）\",\n      \"authFooter\": \"登入後隱藏頁尾\",\n      \"article\": \"批次文章生成功能群組\",\n      \"articleTip\": \"批次文章生成功能群組，勾選後目前使用者組可使用批次文章生成功能\",\n      \"generate\": \"AI 專案產生器群組\",\n      \"generateTip\": \"AI 專案產生器群組，勾選後目前使用者組可使用 AI 專案產生器\",\n      \"groupPlaceholder\": \"已選擇 {{length}} 個群組\",\n      \"cache\": \"可快取的模型\",\n      \"cacheTip\": \"可快取的模型，勾選後目前模型可被快取並命中快取\",\n      \"cachePlaceholder\": \"已選擇 {{length}} 個模型\",\n      \"image_store\": \"圖片儲存\",\n      \"image_storeTip\": \"OpenAI 管道 DALL-E 產生的圖片將儲存於伺服器端以防止圖片失效\",\n      \"image_storeNoBackend\": \"未設定後端網域，無法啟用圖片儲存\",\n      \"cacheAll\": \"設定為全部可快取\",\n      \"cacheFree\": \"設定為免費模型可快取\",\n      \"cacheNone\": \"設定為全部不可快取\",\n      \"cacheExpired\": \"快取過期時間\",\n      \"cacheExpiredTip\": \"快取過期時間（單位：秒），預設 1 小時\",\n      \"cacheSize\": \"最大快取可能性區塊大小\",\n      \"cacheSizeTip\": \"最大快取可能性大小，即同一類型輸入參數的最大快取可能性大小，若參數為 1，則最大快取的內容為 1 個，後請求的內容會被直接命中，若參數為 4，則有 4 種回傳的內容，後請求的內容會被命中其中一個\",\n      \"operation\": \"營運設定\",\n      \"chat\": \"聊天設定\",\n      \"security\": \"安全設定\",\n      \"payment\": \"支付設定\",\n      \"update\": \"更新\",\n      \"group\": \"分組\",\n      \"group-price\": \"分組倍率\",\n      \"buy-price\": \"購買倍率\",\n      \"consume-price\": \"消費倍率\",\n      \"token-group\": \"令牌分組\",\n      \"token-group-tip\": \"令牌分組，勾選後目前分組將支援令牌分組，此分組將顯示在所有使用者可選的令牌分組中（所有內建分組無法開啟令牌分組）\",\n      \"edit\": \"編輯\",\n      \"delete\": \"刪除\",\n      \"type\": \"類型\",\n      \"actions\": \"操作\",\n      \"description\": \"網站描述\",\n      \"descriptionTip\": \"網站描述，用於SEO 搜尋引擎優化中的描述，留空默認\",\n      \"realtime\": {\n        \"title\": \"WebSocket 即時流配置\",\n        \"wsBufferSize\": \"WS 緩衝大小\",\n        \"wsBufferSizeTip\": \"控制服務端向前端下行分片的佇列長度。較小(如1)可降低上游結束後的尾部等待；較大(如24)相容於舊行為但可能出現更長尾拖。\",\n        \"wsAggregate\": \"WS 分片聚合\",\n        \"wsAggregateTip\": \"啟用後按時間窗聚合多個小分片再下發，減少前端重渲染頻次、提升流暢度。關閉則每個分片立即下發（舊行為）。\",\n        \"wsAggregateWindow\": \"WS 聚合時間窗（毫秒）\",\n        \"wsAggregateWindowTip\": \"分片聚合的時間窗口，建議15–33ms。數值越大，合併越多、刷新更平滑，但首段可能稍晚。\"\n      },\n      \"gravatar\": \"Gravatar 頭像\",\n      \"gravatarPlaceholder\": \"Gravatar 代理地址，留空預設不開啟Gravatar 頭像\",\n      \"searchLLMExtract\": \"啟用LLM 關鍵字擷取\",\n      \"searchLLMExtractTip\": \"使用LLM 模型智慧擷取搜尋關鍵字，可以提高搜尋準確度\",\n      \"searchLLMModel\": \"關鍵字擷取模型\",\n      \"searchLLMModelPlaceholder\": \"選擇用於擷取關鍵字的模型\",\n      \"displayCurrency\": \"顯示貨幣\",\n      \"displayCurrencyTip\": \"網站顯示貨幣單位\",\n      \"preDeductQuota\": \"啟用預扣費\",\n      \"preDeductQuotaTip\": \"開啟後將在請求開始時預扣費用，關閉後將在請求結束時扣費\",\n      \"hideKeyDocs\": \"隱藏金鑰頁面對接指南\",\n      \"prompt_store\": \"Prompt 記錄存儲\",\n      \"prompt_storeTip\": \"Prompt 記錄存儲，開啟後用戶的Prompt 記錄將會儲存在服務端\",\n      \"epayTitle\": \"易支付\",\n      \"epayEnabled\": \"啟用易支付\",\n      \"epayDomain\": \"易支付域名\",\n      \"epayDomainPlaceholder\": \"請輸入易支付域名，如：https://pay.example.com\",\n      \"epayMethods\": \"支付方式\",\n      \"epayMethodsPlaceholder\": \"勾選啟用的付款方式(已選 {{length}} 種)\",\n      \"epayTip\": \"易支付是市面上一種**通用**的第三方聚合支付協議，**並非單獨某家支付或軟體**，您可以根據情況自行選擇平台，**我們不做任何推薦和責任擔保**。&#10;若您有資質可自建易付平台，或直接連接別人的易支付平台：易支付平台一般包含**易支付**(企業/自營收款相對穩定收入月結)類型及**碼支付**(個人收款碼即時至帳費率較低)兩種平台。&#10;易支付設置，請注意一定要點擊開啟**啟用易支付** 選項後，才會啟用易支付功能易支付需要配置回調域名, 請在**常規設置** 中配置**後端域名** 後才可正常異步回調\",\n      \"epayBusinessId\": \"商家ID\",\n      \"epayBusinessIdPlaceholder\": \"請輸入易支付商家ID\",\n      \"epayBusinessKey\": \"商家密鑰\",\n      \"epayBusinessKeyPlaceholder\": \"請輸入易支付商家密鑰\",\n      \"epayAggregation\": \"聚合支付模式\",\n      \"epayAggregationTip\": \"聚合支付模式，開啟後點選將不會選擇付款方式**直接跳轉至聚合支付頁面**&#10;請確保您的易支付支援聚合支付模式\",\n      \"stripeTitle\": \"Stripe\",\n      \"stripeTip\": \"Stripe 是一種廣泛使用的國際線上支付系統，支援多種支付方式，包括信用卡、金融卡、Apple Pay、Google Pay等。它為用戶提供了安全、便捷的支付體驗，特別適合需要處理國際支付的業務。\",\n      \"stripeEnabled\": \"啟用Stripe\",\n      \"stripeSecretKey\": \"Stripe Secret Key\",\n      \"stripeSecretKeyPlaceholder\": \"請輸入Stripe Secret Key\",\n      \"stripeWebhookSecret\": \"Stripe Webhook Secret\",\n      \"stripeWebhookSecretPlaceholder\": \"請輸入Stripe Webhook Secret\",\n      \"wechatPayTitle\": \"微信支付\",\n      \"wechatPayTip\": \"微信支付一直致力於為用戶和企業提供安全、便利、專業的線上支付服務。以「微信支付，不只支付」為核心理念，為個人用戶創造了多種便民服務和應用場景，為各類企業以及小微商家提供專​​業的收款能力，營運能力，資金結算解決方案，以及安全保障。企業、商品、門市、使用者已經透過微信連在了一起，讓智慧生活，變成了現實。\",\n      \"wechatPayEnabled\": \"啟用微信支付\",\n      \"wechatPayAppId\": \"微信支付App ID\",\n      \"wechatPayAppIdPlaceholder\": \"請輸入微信支付App ID\",\n      \"wechatPayMchId\": \"微信支付商戶號\",\n      \"wechatPayMchIdPlaceholder\": \"請輸入微信支付商戶號\",\n      \"wechatPayKey\": \"微信支付API v3 金鑰\",\n      \"wechatPayKeyPlaceholder\": \"請輸入微信支付API v3 金鑰\",\n      \"wechatPaySerialNo\": \"微信支付平台證書序號\",\n      \"wechatPaySerialNoPlaceholder\": \"請輸入微信支付平台證書序號\",\n      \"wechatPayCertificate\": \"微信支付平台證書\",\n      \"wechatPayCertificatePlaceholder\": \"請在此貼上您的微信支付平台證書\",\n      \"wechatPayCertificateTip\": \"商家接收到API v3 介面的回傳內容，需要使用此憑證公鑰進行驗簽，另外某些敏感資訊參數(如姓名、身分證號碼)也需要使用此憑證公鑰加密後傳輸，詳見[微信支付平台憑證](https://pay.weixin.qq.com/doc/v3/merchant/401206814)\",\n      \"xunhupayTitle\": \"虎皮椒支付\",\n      \"xunhupayTip\": \"虎皮椒是一個聚合支付平台，支援微信、支付寶等多種支付方式。配置後用戶可透過虎皮椒進行儲值。微信和支付寶需要分別配置不同的APP ID 和APP Secret。\",\n      \"xunhupayWechatEnabled\": \"啟用虎皮椒微信\",\n      \"xunhupayAlipayEnabled\": \"啟用虎皮椒支付寶\",\n      \"xunhupayWechatAppId\": \"虎皮椒微信APP ID\",\n      \"xunhupayWechatAppIdPlaceholder\": \"請輸入虎皮椒微信支付的APP ID\",\n      \"xunhupayWechatAppSecret\": \"虎皮椒微信APP Secret\",\n      \"xunhupayWechatAppSecretPlaceholder\": \"請輸入虎皮椒微信支付的APP Secret (密鑰)\",\n      \"xunhupayAlipayAppId\": \"虎皮椒支付寶APP ID\",\n      \"xunhupayAlipayAppIdPlaceholder\": \"請輸入虎皮椒支付寶的APP ID\",\n      \"xunhupayAlipayAppSecret\": \"虎皮椒支付寶APP Secret\",\n      \"xunhupayAlipayAppSecretPlaceholder\": \"請輸入虎皮椒支付寶的APP Secret (金鑰)\",\n      \"xunhupayEndpoint\": \"虎皮椒介面位址\",\n      \"xunhupayEndpointPlaceholder\": \"https://api.xunhupay.com 或https://api.dpweixin.com\",\n      \"securityCheckType\": \"審核模式\",\n      \"securityCheckTypePlaceholder\": \"請選擇審核類型\",\n      \"securityTextDatabase\": \"黑名單字庫\",\n      \"securityTextDatabasePlaceholder\": \"請輸入黑名字庫，詞中間使用空格分隔, 如：敏感詞1 敏感詞2\",\n      \"securityRegexDatabase\": \"正規黑名單表達式\",\n      \"securityRegexDatabasePlaceholder\": \"請輸入正規黑名單表達式，表達式中間使用換行分隔, 例如：&#10; ^敏感詞1$&#10; ^敏感詞2$\",\n      \"securityBaiduApiKey\": \"百度雲審核API Key\",\n      \"securityBaiduApiKeyPlaceholder\": \"請輸入百度雲審核API Key\",\n      \"securityBaiduSecretKey\": \"百度雲審核Secret Key\",\n      \"securityBaiduSecretKeyPlaceholder\": \"請輸入百度雲審核Secret Key\",\n      \"securityCheckModels\": \"特定審核模型\",\n      \"securityCheckModelsPlaceholder\": \"已選 {{length}} 個特定審核模型\",\n      \"securityCheckModelsTip\": \"特定模型審核，勾選後當前模型可被特定審核模型審核，**預設所有模型都會根據審核模式進行審核**，如果特定審核模型，則**只會根據特定審核模型進行審核**，**其他模型不會進行審核**\",\n      \"securityBaiduTip\": \"百度雲審核模式，需填寫百度雲審核**API Key** 和**Secret Key**&#10;詳情資訊及配置審核策略粒度請參考[百度雲審核快速入門](https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy)&#10;違禁詞彙審核策略請根據上方百度雲文件在百度雲控制台配置策略\",\n      \"securityCustomEndpoint\": \"自訂審核存取點\",\n      \"securityCustomEndpointPlaceholder\": \"請輸入自訂審核存取點\",\n      \"securityCustomToken\": \"自訂審核Token\",\n      \"securityCustomTokenPlaceholder\": \"請輸入自訂審核Token\",\n      \"securityCustomTip\": \"自訂審核模式，需填寫自訂審核**Token** 和**存取點**&#10;請求與返回格式與百度雲審核一致，可前往[百度雲審核文本審核請求說明](https://cloud.baidu.com/doc/ANTIPORN/s/Rk3h6xb3i) 進行參考適配\",\n      \"securityTypes\": {\n        \"none\": \"無審核模式\",\n        \"dict\": \"文字詞庫審核模式\",\n        \"regex\": \"文字正規審核模式\",\n        \"baidu\": \"百度雲端審核模式\",\n        \"custom\": \"自訂後端審核模式\"\n      },\n      \"securityBlacklistIPs\": \"黑名單IP\",\n      \"securityBlacklistIPsPlaceholder\": \"請輸入黑名單IP\",\n      \"securityWhitelistIPs\": \"白名單IP\",\n      \"securityWhitelistIPsPlaceholder\": \"請輸入白名單IP\",\n      \"securityWhitelistIPsTip\": \"黑白名單IP **僅對API 請求的速率限制中間件生效** ，如需限制其他請求或前端訪問請使用WAF 等安全防護服務\",\n      \"securityAddIPAddress\": \"新增IP 位址\",\n      \"securityRemoveIPAddress\": \"刪除IP 位址\",\n      \"oauth\": {\n        \"title\": \"第三方登入設定\",\n        \"wechat\": \"微信登入\",\n        \"google\": \"Google 登入\",\n        \"github\": \"Github 登入\",\n        \"telegram\": \"Telegram 登入\",\n        \"rainbow\": \"彩虹聚合登入\",\n        \"enable\": \"啟用\",\n        \"disabled\": \"禁用\",\n        \"methods\": \"登入方式\",\n        \"methods_placeholder\": \"請輸入登入方式，使用英文逗號分隔，如：qq,wx,baidu,douyin\",\n        \"client_id\": \"客戶端ID\",\n        \"client_id_placeholder\": \"請輸入客戶端ID\",\n        \"client_secret\": \"客戶端密鑰\",\n        \"client_secret_placeholder\": \"請輸入客戶端密鑰\",\n        \"redirect_uri\": \"重定向URI\",\n        \"redirect_uri_placeholder\": \"請輸入重定向URI\",\n        \"base_url\": \"彩虹聚合登入域名\",\n        \"base_url_placeholder\": \"請輸入彩虹聚合登入域名，留空則預設為彩虹聚合登入官方域名https://u.cccyun.cc\",\n        \"require_email\": \"啟用郵箱驗證\",\n        \"scope\": \"授權範圍\",\n        \"scope_placeholder\": \"請輸入授權範圍\",\n        \"auth_url\": \"授權URL\",\n        \"auth_url_placeholder\": \"請輸入授權URL\",\n        \"token_url\": \"令牌URL\",\n        \"token_url_placeholder\": \"請輸入令牌URL\",\n        \"user_info_url\": \"用戶資訊URL\",\n        \"user_info_url_placeholder\": \"請輸入使用者資訊URL\",\n        \"bot_token\": \"機器人Token\",\n        \"bot_token_placeholder\": \"請輸入機器人Token\",\n        \"bot_name\": \"機器人名稱\",\n        \"bot_name_placeholder\": \"請輸入機器人名稱\"\n      },\n      \"custom\": \"主題設定\",\n      \"customJs\": \"自訂JS\",\n      \"customJsPlaceholder\": \"請輸入自訂JS\",\n      \"customCss\": \"自訂CSS\",\n      \"customCssPlaceholder\": \"請輸入自訂CSS\",\n      \"customHtml\": \"自訂HTML\",\n      \"customHtmlPlaceholder\": \"請輸入自訂HTML\",\n      \"gaTrackingId\": \"Google Analytics 服務\",\n      \"gaTrackingIdPlaceholder\": \"請輸入Google Analytics ID\",\n      \"customThemeAlert\": \"注意：如果您使用了WAF (如: Cloudflare WAF、長亭、1 Panel WAF、 寶塔WAF) 等安全防護服務，您的自訂主題可能會被WAF 誤認為是惡意程式碼而被攔截顯示錯誤碼如403 Forbidden，請注意檢查您的WAF 配置或關閉WAF 配置。\",\n      \"uploadFaviconSuccess\": \"Logo 上傳成功! 請記得儲存以套用新的Logo\",\n      \"affiliateTitle\": \"聯盟式行銷設置\",\n      \"affiliateEnabled\": \"啟用聯盟式行銷\",\n      \"affiliateCommissionRate\": \"佣金率\",\n      \"affiliateMinWithdraw\": \"最低提現金額\",\n      \"affiliateAllowExistingBind\": \"允許已註冊用戶綁定行銷碼\",\n      \"autoTitle\": {\n        \"title\": \"自動會話標題\",\n        \"enabled\": \"啟用自動標題\",\n        \"model\": \"生成模型\",\n        \"modelPlaceholder\": \"留空表示使用目前會話模型\",\n        \"maxLen\": \"標題最大長度\",\n        \"minMsgs\": \"抽取訊息條數\",\n        \"overwrite\": \"覆蓋已有標題\",\n        \"prompt\": \"自訂提示詞\",\n        \"promptPlaceholder\": \"可使用{max_len} 作為佔位符\",\n        \"tip\": \"使用LLM 在首輪對話後自動總結並設定會話標題。設定自訂提示詞為空時，預設使用CoAI 內預設配置的提示詞\"\n      }\n    },\n    \"logger\": {\n      \"title\": \"服務日誌\",\n      \"console\": \"控制台\",\n      \"consoleLength\": \"日誌數量\"\n    },\n    \"join-community\": \"🎉 加入社區\",\n    \"community-description\": \"🥳 我們在Discord 創建了一個新的用戶交流社區，您現在可以加入我們的Discord 社群與我們進行交流與回饋，或是獲取專案的最新動態！ 🥳\",\n    \"community-join-button\": \"加入我們\",\n    \"community-confirm-close\": \"您確定要關閉此公告嗎？\",\n    \"community-banner-confirm\": \"您真的不考慮加入社區與我們一起共建項目或是獲取項目的更新與動態嗎？該橫幅在不清除瀏覽器快取的情況下僅會顯示一次\",\n    \"community-stay\": \"加入我們\",\n    \"community-close\": \"確認關閉\",\n    \"notifications\": \"推送中心\",\n    \"payment\": \"支付訂單\",\n    \"online-chats\": \"線上對話數\",\n    \"notify-all\": \"通知全體用戶\",\n    \"group\": \"分組\",\n    \"group-setting\": \"分組設定\",\n    \"custom-group\": \"自訂分組\",\n    \"custom-group-action\": \"設定自訂分組\",\n    \"custom-group-action-desc\": \"請輸入自訂分組名稱\",\n    \"coai-format-only\": \"此格式為CoAI 獨有格式\",\n    \"delete-broadcast\": \"刪除通知\",\n    \"delete-broadcast-desc\": \"是否確定？此操作無法撤銷。這將永久刪除通知。\",\n    \"pay\": {\n      \"epay\": \"易支付\",\n      \"wechatpay\": \"微信支付\",\n      \"stripe\": \"Stripe\",\n      \"afdian\": \"愛發電\",\n      \"order\": \"訂單號：\",\n      \"amount\": \"金額\",\n      \"status\": \"支付狀態\",\n      \"service\": \"支付管道\",\n      \"type\": \"支付類型\",\n      \"device\": \"設備\",\n      \"username\": \"帳號\",\n      \"status-true\": \"已支付\",\n      \"status-false\": \"未支付\",\n      \"created-at\": \"創建時間\",\n      \"updated-at\": \"更新時間\",\n      \"action\": \"操作\",\n      \"copy-order\": \"複製訂單編號\",\n      \"check-order\": \"檢查訂單狀態\",\n      \"check-result-same\": \"訂單狀態一致\",\n      \"check-result-diff\": \"訂單狀態更新\",\n      \"check-result-same-prompt\": \"訂單狀態一致，無需更新\",\n      \"check-result-diff-prompt\": \"訂單狀態已更新，已完成支付\",\n      \"search\": \"搜尋訂單號碼或用戶名\"\n    },\n    \"cdn\": {\n      \"warmup\": \"資源預熱\",\n      \"copy-data\": \"複製預熱URL 資源列表\",\n      \"warm-tip\": \"&gt; 如果您正在使用CDN 服務，可以透過此功能預熱CSS / JS 等資源。&#10; **每次更新後，您可以進行一次資源刷新以確保資源穩定性並提升載入速度。 **&#10; CDN (內容傳遞網路) 資源預熱，預熱後資源將會被快取到CDN 節點，加速存取。&#10;透過預熱功能，您可以在業務高峰前預先將熱門資源快取到CDN節點，並降低源站壓力提升使用者體驗。&#10;提示：**預熱執行會從CDN到源站拉取大量的數據，請注意源站寬頻負載情況。 **\"\n    },\n    \"license\": {\n      \"title\": \"授權管理\",\n      \"description\": \"CoAI Pro 版本授權管理\",\n      \"domain\": \"授權域名：\",\n      \"digest\": \"簽名摘要\",\n      \"module\": \"模組管理\",\n      \"info\": \"授權資訊\",\n      \"modules\": {\n        \"bought\": \"已購買\",\n        \"not-bought\": \"購買\",\n        \"buy-tip\": \"請聯絡您的銷售代表以購買此模組\",\n        \"multiKey\": {\n          \"title\": \"多代幣管理\",\n          \"description\": \"多API KEY 管理, 支援一單元內多令牌分發管理, 支援設定可呼叫模型，餘額限制，呼叫日誌，狀態管理，對接指南等高階功能\"\n        },\n        \"stripe\": {\n          \"title\": \"Stripe 支付\",\n          \"description\": \"Stripe Hosted Checkout 高級支付模組，支援Stripe 銀行卡/Bank/Link/微信/Alipay+ 等數十餘種支付對接，支援多種貨幣支付\"\n        },\n        \"paypal\": {\n          \"title\": \"PayPal 支付\",\n          \"description\": \"PayPal 高級支付模組，支援PayPal 銀行卡支付等多貨幣支付功能\"\n        },\n        \"afdian\": {\n          \"title\": \"愛發電\",\n          \"description\": \"愛發電支付Webhook 模組，支援愛發電餘額購買\"\n        },\n        \"bot\": {\n          \"title\": \"機器人\",\n          \"description\": \"微信/飛書/Telegram/Discord 機器人SaaS 模組\"\n        },\n        \"digital\": {\n          \"title\": \"數位人\",\n          \"description\": \"數位人視訊生成模組定制，採用高級動態語音技術，支援語音和臉部克隆，支援私有化部署推理與多種引擎，全行業場景支持，支持高維度定制\"\n        },\n        \"contact-for-price\": \"訪問文件以取得報價\",\n        \"coai-pro\": {\n          \"title\": \"CoAI Pro\",\n          \"description\": \"CoAI Pro 商業版授權，解鎖對接多種支付管道、使用者（群組）倍率控制、內容審核、會話日誌等全部商業功能\"\n        }\n      },\n      \"purchase\": \"購買授權\",\n      \"pro-required\": \"此功能為CoAI Pro 專屬，請在授權管理頁面購買CoAI Pro 授權以使用該功能\"\n    }\n  },\n  \"mask\": {\n    \"title\": \"預設設定\",\n    \"search\": \"搜尋預設名稱\",\n    \"system\": \"系統預設\",\n    \"custom\": \"我的預設\",\n    \"edit\": \"編輯預設\",\n    \"create\": \"新增預設\",\n    \"context\": \"包含 {{length}} 條上下文\",\n    \"avatar\": \"預設頭像\",\n    \"conversation\": \"預設對話\",\n    \"name\": \"預設標題\",\n    \"name-placeholder\": \"請輸入預設標題\",\n    \"description\": \"預設簡介\",\n    \"description-placeholder\": \"請輸入預設簡介\",\n    \"search-emoji\": \"搜尋 Emoji\",\n    \"actions\": {\n      \"clone\": \"複製預設\",\n      \"use\": \"使用預設\",\n      \"edit\": \"編輯預設\",\n      \"delete\": \"刪除預設\"\n    },\n    \"market\": \"預設市場\",\n    \"switch-preset\": \"切換預設\",\n    \"switch-preset-desc\": \"已開始新對話並切換至預設\"\n  },\n  \"date\": {\n    \"pick\": \"選擇日期\",\n    \"today\": \"今天\",\n    \"clean\": \"歸零\",\n    \"add-day\": \"增加一天\",\n    \"sub-day\": \"減少一天\",\n    \"add-month\": \"增加一個月\",\n    \"sub-month\": \"減少一個月\",\n    \"add-year\": \"增加一年\",\n    \"sub-year\": \"減少一年\"\n  },\n  \"send\": \"發送\",\n  \"stop\": \"停止\",\n  \"new-chat\": \"新對話\",\n  \"not-login\": \"未登入\",\n  \"login-action\": \"登入以享受更多功能\",\n  \"anonymous\": \"未登入\",\n  \"coming-soon\": \"此功能開發中, 敬請期待!\",\n  \"starred\": \"收藏模型\",\n  \"unstarred\": \"常用模型\",\n  \"loading\": \"載入中...\",\n  \"manage\": \"管理\",\n  \"enter\": \"換行\",\n  \"assistant-suggest\": \"預設推薦\",\n  \"change-suggest\": \"換一組\",\n  \"new-announcement\": \"公告通知\",\n  \"no-announcement\": \"暫無公告\",\n  \"none\": \"無\",\n  \"readed\": \"已讀\",\n  \"description\": \"描述\",\n  \"only-one-step\": \"還差一步\",\n  \"are-you-sure\": \"是否確認？\",\n  \"this-action-cannot-be-undone\": \"此操作無法撤銷。\",\n  \"verify-email-description\": \"請輸入您的信箱以完成驗證\",\n  \"filter\": {\n    \"filter\": \"篩選\",\n    \"conds\": \"已篩選 {{count}} 項條件\",\n    \"plan\": \"是否訂閱\",\n    \"all\": \"全部\",\n    \"subscribed\": \"已訂閱\",\n    \"unsubscribed\": \"未訂閱\",\n    \"admin\": \"管理員\",\n    \"not-admin\": \"非管理員\",\n    \"ban\": \"是否封鎖\",\n    \"banned\": \"已封鎖\",\n    \"not-banned\": \"未封鎖\",\n    \"sorts\": {\n      \"sort\": \"排序方式\",\n      \"id-desc\": \"ID 降序\",\n      \"id-asc\": \"ID 升序\",\n      \"quota-desc\": \"點數降序\",\n      \"quota-asc\": \"點數升序\",\n      \"used-quota-desc\": \"已用點數降序\",\n      \"used-quota-asc\": \"已用點數升序\",\n      \"plan-desc\": \"訂閱到期時間降序\",\n      \"plan-asc\": \"訂閱到期時間升序\"\n    }\n  },\n  \"learn-more\": \"瞭解更多\",\n  \"connect\": \"綁定\",\n  \"disconnect\": \"解綁\",\n  \"notify\": \"通知\",\n  \"new-notify\": \"新通知\",\n  \"view-all\": \"看全部\",\n  \"back-home\": \"返回首頁\",\n  \"tip\": \"溫馨提示\",\n  \"get\": \"獲取\",\n  \"authenticating\": \"認證中\",\n  \"authentication-failed\": \"認證失敗\",\n  \"authenticating-prompt\": \"稍等片刻，我們正在認證您的帳戶...\",\n  \"oops-quota-exceeded\": \"糟糕，餘額不足\",\n  \"oops-quota-exceeded-tip\": \"您的餘額不足，請前往購買點數或訂閱方案以繼續\",\n  \"plugin\": {\n    \"title\": \"插件管理\",\n    \"add\": \"新增插件\",\n    \"create\": \"創建插件\",\n    \"save\": \"儲存插件\",\n    \"cancel\": \"取消\",\n    \"enable\": \"啟用\",\n    \"disable\": \"禁用\",\n    \"enabled\": \"已啟用 {{name}}\",\n    \"disabled\": \"已停用 {{name}}\",\n    \"enabled-badge\": \"已啟用\",\n    \"import\": \"導入配置\",\n    \"quick-import\": \"快速導入MCP 配置\",\n    \"import-json-config\": \"導入JSON 配置\",\n    \"import-http-only-tip\": \"目前版本僅支援HTTP 類型的MCP 伺服器\",\n    \"import-confirm\": \"確認導入\",\n    \"name\": \"插件名稱\",\n    \"name-placeholder\": \"請輸入插件名稱\",\n    \"avatar\": \"插件頭像\",\n    \"avatar-placeholder\": \"請輸入頭像鏈接\",\n    \"description\": \"插件描述\",\n    \"description-placeholder\": \"請輸入插件描述\",\n    \"server-url\": \"伺服器位址\",\n    \"server-url-placeholder\": \"請輸入MCP 伺服器位址\",\n    \"server-url-required\": \"請輸入伺服器位址\",\n    \"loading\": \"載入中...\",\n    \"no-plugins\": \"暫無插件\",\n    \"save-success\": \"保存成功!\",\n    \"save-error\": \"保存失敗!\",\n    \"delete-success\": \"刪除成功！\",\n    \"delete-error\": \"刪除失敗\",\n    \"load-error\": \"載入失敗\",\n    \"refresh\": \"刷新\",\n    \"refresh-success\": \"刷新成功\",\n    \"test\": \"測試連接\",\n    \"testing\": \"連接中...\",\n    \"test-success\": \"連線成功\",\n    \"test-success-desc\": \"發現 {{count}} 個可用工具\",\n    \"test-error\": \"連線失敗\",\n    \"test-required\": \"需要測試\",\n    \"test-description\": \"點擊測試按鈕驗證插件連接\",\n    \"available-tools\": \"可用工具\",\n    \"connection-test\": \"連接測試\",\n    \"test-required-error\": \"建立新插件前需要先測試連接\",\n    \"test-required-hint\": \"新插件需要先測試連接才能創建\",\n    \"form-error\": \"請填寫必填字段\",\n    \"import-success\": \"導入成功!\",\n    \"import-error\": {\n      \"empty\": \"請輸入配置內容\",\n      \"invalid-json\": \"無效的JSON格式\",\n      \"invalid-format\": \"配置格式不正確，請檢查是否包含mcpServers字段\",\n      \"no-servers\": \"配置中未找到MCP伺服器\",\n      \"stdio-not-supported\": \"目前版本僅支援HTTP類型的MCP伺服器，不支援STDIO類型\",\n      \"unknown\": \"匯入失敗，請檢查配置格式\"\n    },\n    \"mcp\": {\n      \"tool-call\": \"工具調用\",\n      \"tool-calling\": \"正在呼叫工具: {{name}}\",\n      \"tool-executing\": \"正在執行工具: {{name}}\",\n      \"tool-success\": \"工具執行成功: {{name}}\",\n      \"tool-error\": \"工具執行失敗: {{name}}\",\n      \"arguments\": \"工具參數\",\n      \"result\": \"執行結果\",\n      \"error\": \"執行錯誤\",\n      \"status\": \"執行狀態\",\n      \"status-start\": \"準備執行工具...\",\n      \"status-executing\": \"正在執行工具...\",\n      \"hide-details\": \"隱藏工具呼叫詳情\",\n      \"show-details\": \"顯示工具呼叫詳情\",\n      \"plugin-name\": \"MCP插件\",\n      \"copy-param-value\": \"複製參數值\",\n      \"save\": \"儲存\",\n      \"edit\": \"編輯\",\n      \"raw-arguments\": \"原始參數(JSON)\",\n      \"no-arguments\": \"無參數\",\n      \"parsed-result\": \"解析後的結果\",\n      \"error-info\": \"，錯誤信息：\",\n      \"status-prepare\": \"準備調用工具...\",\n      \"status-success\": \"工具執行成功\",\n      \"status-error\": \"工具執行失敗\",\n      \"status-calling\": \"工具調用中...\",\n      \"hide-debug\": \"隱藏偵錯訊息\",\n      \"show-debug\": \"顯示偵錯資訊\",\n      \"tool-arguments\": \"工具參數\",\n      \"no-arguments-needed\": \"該工具無需參數\"\n    }\n  },\n  \"record\": {\n    \"user\": \"用戶\",\n    \"channel\": \"渠道\",\n    \"query\": \"查詢記錄\",\n    \"title\": \"使用記錄\",\n    \"created-at\": \"時間\",\n    \"type\": \"類型\",\n    \"model\": \"模型\",\n    \"token\": \"令牌\",\n    \"input-tokens\": \"輸入\",\n    \"output-tokens\": \"輸出\",\n    \"quota\": \"點數\",\n    \"duration\": \"用時\",\n    \"detail\": \"備註：\",\n    \"rpm-tips\": \"當前RPM (每分鐘請求數)\",\n    \"tpm-tips\": \"目前TPM (每分鐘Token 數)\",\n    \"types\": {\n      \"system\": \"系統\",\n      \"consume\": \"消費\",\n      \"topup\": \"儲值\",\n      \"all\": \"全部\"\n    },\n    \"detail-info\": {\n      \"input\": \"輸入價格\",\n      \"output\": \"輸出價格\",\n      \"percent\": \"分組倍率\",\n      \"times\": \"按次價格\",\n      \"no-cost\": \"不計費\",\n      \"cached\": \"擊中緩存\",\n      \"plan\": \"訂閱計費\",\n      \"empty\": \"無回應\",\n      \"error\": \"請求錯誤\"\n    },\n    \"billing-today\": \"今日消費\",\n    \"billing-month\": \"本月消費\",\n    \"request-today\": \"今日請求\",\n    \"request-month\": \"本月請求\",\n    \"cond\": {\n      \"model\": \"指定模型\",\n      \"type\": \"指定類型\",\n      \"model-placeholder\": \"請輸入模型名\",\n      \"token-name\": \"指定令牌\",\n      \"token-name-placeholder\": \"請輸入令牌名\",\n      \"start_time\": \"開始時間\",\n      \"end_time\": \"結束時間:\",\n      \"username\": \"指定使用者名稱\",\n      \"username-placeholder\": \"請輸入使用者名\"\n    }\n  },\n  \"payment\": {\n    \"wechat\": \"微信支付\",\n    \"wxpay\": \"微信支付\",\n    \"wechatpay\": \"微信支付\",\n    \"alipay\": \"支付寶\",\n    \"paypal\": \"PayPal\",\n    \"stripe\": \"Stripe\",\n    \"afdian\": \"愛發電\",\n    \"qqpay\": \"QQ 錢包\",\n    \"xunhupay-wechat\": \"微信支付\",\n    \"xunhupay-alipay\": \"支付寶\",\n    \"order\": {\n      \"quota\": \"{{quota}} 點數\"\n    },\n    \"dialog-wechatpay\": {\n      \"title\": \"微信支付\",\n      \"description\": \"請使用微信掃描下方二維碼進行支付\",\n      \"success\": \"支付成功\",\n      \"loading\": \"載入中...\",\n      \"remaining-time\": \"剩餘支付時間\"\n    },\n    \"dialog-xunhupay\": {\n      \"title\": \"支付\",\n      \"description\": \"請使用微信或支付寶掃描下方二維碼進行支付\",\n      \"success\": \"支付成功\",\n      \"remaining-time\": \"剩餘支付時間\"\n    },\n    \"notify-stripe\": {\n      \"success\": \"支付成功\",\n      \"canceled\": \"付款取消\",\n      \"processing\": \"支付處理中...\"\n    }\n  },\n  \"copied\": {\n    \"prompt\": \"複製\",\n    \"success\": \"複製成功\",\n    \"success-description\": \"內容已複製到剪貼簿\",\n    \"failed\": \"複製失敗\",\n    \"failed-description\": \"複製失敗，原因：{{reason}}\"\n  },\n  \"renderer\": {\n    \"viewImage\": \"看圖片\",\n    \"imageLoadFailed\": \"圖片{{src}} 載入失敗\",\n    \"base64Image\": \"展開圖片Base64\",\n    \"base64ImageCollapse\": \"收起圖片Base64\",\n    \"viewVideo\": \"查看影片\",\n    \"videoLoadFailed\": \"影片{{src}} 載入失敗\"\n  },\n  \"bar\": {\n    \"chat\": \"對話\",\n    \"chat-full\": \"開始對話\",\n    \"model\": \"模型\",\n    \"model-full\": \"探索模型\",\n    \"preset\": \"預設\",\n    \"preset-full\": \"預設市場\",\n    \"wallet\": \"錢包\",\n    \"key\": \"金鑰\",\n    \"key-full\": \"令牌管理\",\n    \"wallet-full\": \"我的錢包\",\n    \"log\": \"紀錄\",\n    \"log-full\": \"使用記錄\",\n    \"account\": \"帳戶\",\n    \"account-full\": \"帳戶管理\",\n    \"admin\": \"後台\",\n    \"admin-full\": \"後台管理\"\n  },\n  \"account\": {\n    \"title\": \"帳號管理\",\n    \"deeptrain\": \"DeepTrain 統一帳號管理\",\n    \"my-account\": \"我的帳號\",\n    \"my-account-description\": \"您的帳號資訊、第三方帳號綁定資訊等\",\n    \"api-description\": \"您的全域帳號API 訊息\",\n    \"deeptrain-description\": \"您的DeepTrain 統一帳號綁定訊息\",\n    \"registerDays\": \"註冊 {{days}} 天\",\n    \"current-quota\": \"目前點數\",\n    \"used-quota\": \"已用點數\",\n    \"plan-total-month\": \"總計訂閱月數\",\n    \"plan-total-month-tips\": \"訂閱等級升級、降級導致的月數變動不計入此統計\",\n    \"share-description\": \"查看、管理您的歷史對話分享記錄\",\n    \"share-delete\": \"刪除分享\",\n    \"share-delete-description\": \"確定要刪除此分享嗎？\",\n    \"oauth\": \"第三方登入\",\n    \"oauth-description\": \"綁定、管理您的第三方登入訊息\",\n    \"notification\": {\n      \"title\": \"通知中心\",\n      \"method\": \"通知方式\",\n      \"event\": \"訂閱事件\",\n      \"description\": \"管理您的通知方式\",\n      \"fetchError\": \"取得通知配置失敗\",\n      \"fetchErrorDesc\": \"取得通知配置失敗，請檢查網路並重試。\",\n      \"updateSuccess\": \"更新通知配置成功\",\n      \"updateSuccessDesc\": \"通知配置已成功更新（刷新瀏覽器即可立即套用）\",\n      \"save\": \"儲存\",\n      \"updateError\": \"更新通知配置失敗\",\n      \"updateErrorDesc\": \"更新通知配置失敗，請檢查網路並重試。\",\n      \"testSuccess\": \"測試通知配置成功\",\n      \"testSuccessDesc\": \"通知配置已成功測試\",\n      \"testError\": \"測試通知配置失敗\",\n      \"testErrorDesc\": \"測試通知配置失敗，請檢查網路並重試。\",\n      \"enabled\": \"啟用\",\n      \"disabled\": \"禁用\",\n      \"appToken\": \"應用程式Token\",\n      \"topicId\": \"主題ID\",\n      \"userId\": \"用戶UID\",\n      \"webhookUrl\": \"Webhook URL\",\n      \"botToken\": \"Bot Token\",\n      \"chatId\": \"聊天ID\",\n      \"url\": \"URL：\",\n      \"test\": \"測試\",\n      \"testDesc\": \"測試通知配置\",\n      \"alertTitle\": \"推送中心\",\n      \"alertDescription\": \"支援微信(WxPusher)、Discord、Telegram、飛書推播\",\n      \"type\": {\n        \"email\": \"郵件\",\n        \"wxpusher\": \"微信(WxPusher)\",\n        \"discord\": \"Discord\",\n        \"telegram\": \"Telegram\",\n        \"feishu\": \"飛書\",\n        \"webhook\": \"Webhook\"\n      },\n      \"events\": {\n        \"broadcast_event\": \"推播通知訊息\",\n        \"payment_event\": \"付款、訂閱通知\",\n        \"key_quota_not_enough_event\": \"密鑰額度預警\",\n        \"account_quota_not_enough_event\": \"帳戶額度預警\"\n      },\n      \"tiplist\": {\n        \"email\": \"- 郵件推送為預設選項，通知將推送至您的註冊郵箱\",\n        \"wxpusher\": \"- 微信推送使用[WxPusher](https://wxpusher.zjiecode.com) 服務&#10;- 在WxPusher 官網註冊並創建應用&#10;- 取得應用程式的AppToken 並填入配置&#10;- 訂閱後點選「我的UID」取得使用者UID 填入設定(多個UID 請以逗號分隔)&#10; - 掃描應用二維碼關注以接收推播\",\n        \"discord\": \"- Discord 推送基於Webhook 機制&#10;- 在Discord 伺服器中選擇目標頻道&#10;- 進入頻道設定&gt; 整合&gt; 建立Webhook&#10; - 自訂Webhook 名稱和頭像（可選）&#10; - 複製產生的Webhook URL 填入配置\",\n        \"telegram\": \"- Telegram 推送需要建立自己的Bot&#10; - 在Telegram 中搜尋@BotFather 並開始對話&#10;- 發送/newbot 指令，按提示設定Bot 名稱和使用者名&#10;- 取得Bot Token 並填入配置&#10;- 將Bot 加入目標群組或與其私聊&#10;- 使用@userinfobot 取得聊天的Chat ID 並填入\",\n        \"feishu\": \"- 飛書推送使用群組自訂機器人&#10;- 在目標飛書群組中新增自訂機器人&#10;- 設定機器人名稱、頭像和描述&#10;- 選擇接收訊息的群組&#10;- 複製產生的Webhook URL 填入配置&#10;- 可設定關鍵字以增強安全性（可選）\",\n        \"webhook\": \"- 自訂Webhook URL 推送&#10;&#10;```json&#10; POST ${WEBHOOK_URL}&#10; {&#10; &quot;type&quot;: &quot;string&quot;,&#10; &quot;content&quot;: &quot;string&quot;,&#10; &quot;time&quot;: &quot;number&quot;,&#10; &quot;utc_time&quot;: &quot;string&quot;,&#10; &quot;account_id&quot;: &quot;number&quot;,&#10; &quot;additional_data&quot;: {...}&#10; }&#10; ```\"\n      }\n    }\n  },\n  \"key\": {\n    \"title\": \"我的令牌\",\n    \"description\": \"支援以OpenAI API 標準格式呼叫本站全部AI 大模型, 無需考慮API 相容性問題, 支援開發者/第三方工具無縫對接, 內建額度/時間/作用域/權限管理\",\n    \"name\": \"名稱\",\n    \"noKey\": \"無密鑰\",\n    \"apiBase\": \"API 存取點\",\n    \"apiBaseTip\": \"小提醒：請在客戶端設定此API Base 存取點，常見工具存取可直接查看下方存取指南中的存取方法，其他部分工具可能需要新增後綴(例如/v1), 請依照客戶端要求填寫\",\n    \"noKeyWarning\": \"無可用密鑰\",\n    \"noKeyWarningTip\": \"請您先建立一個金鑰後即可使用對接指南\",\n    \"namePlaceholder\": \"請輸入名稱\",\n    \"status\": \"狀態\",\n    \"quota\": \"額度\",\n    \"quotaPlaceholder\": \"請輸入可用額度\",\n    \"usedQuota\": \"已用額度\",\n    \"remainQuota\": \"剩餘額度\",\n    \"infiniteQuota\": \"無限額度\",\n    \"createdAt\": \"創建時間\",\n    \"expiredAt\": \"過期時間\",\n    \"key\": \"金鑰\",\n    \"default\": \"預設分組\",\n    \"unknown\": \"未知分組\",\n    \"createTip\": \"請勿將密鑰洩漏給他人（如推送至Github 公共倉庫），否則可能導致您的密鑰餘額被盜用，請妥善保管密鑰！如果出現密鑰洩露，請及時重設/刪除令牌。\",\n    \"advanced\": \"進階設定\",\n    \"ipWhiteList\": \"IP 白名單\",\n    \"enableIpWhiteList\": \"啟用IP 白名單\",\n    \"enableIpWhiteListTip\": \"啟用IP 白名單，開啟後只有白名單中的IP 位址才能使用此金鑰, 不填則允許全部IP 使用(非必要, 不建議啟用)\",\n    \"ipWhiteListPlaceholder\": \"請輸入IP 位址或網段，格式： 127.0.0.1,192.168.0.0/16\",\n    \"modelWhiteList\": \"模型白名單\",\n    \"tokenGroup\": \"令牌分組\",\n    \"tokenGroupTip\": \"令牌自訂頻道分組\",\n    \"enableModelWhiteList\": \"啟用模型白名單\",\n    \"enableModelWhiteListTip\": \"啟用模型白名單，開啟後只有白名單中的模型才能使用此密鑰, 不填可使用全部模型(非必要, 不建議啟用)\",\n    \"modelWhiteListPlaceholder\": \"已勾選 {{length}} 個模型\",\n    \"create\": \"創建令牌\",\n    \"update\": \"更新令牌\",\n    \"nameEmpty\": \"名稱不能為空\",\n    \"searchPlaceholder\": \"搜尋密鑰名稱...\",\n    \"disabled\": \"已停用\",\n    \"disable\": \"禁用\",\n    \"disableToken\": \"禁用令牌\",\n    \"active\": \"已啟用\",\n    \"delete\": \"刪除令牌\",\n    \"never\": \"永不過期\",\n    \"oneHour\": \"一小時\",\n    \"oneDay\": \"一天\",\n    \"oneWeek\": \"一週\",\n    \"oneMonth\": \"一個月\",\n    \"oneYear\": \"一年\",\n    \"docs\": \"對接指南\",\n    \"slogan\": \"“一鍵對接最前沿的AI 產品！”\",\n    \"selectKey\": \"選擇密鑰\",\n    \"bindLobeChat\": \"綁定 Lobe Chat\",\n    \"bindLobeChatTip\": \"點擊按鈕後，將會跳轉至 Lobe Chat 並自動綁定金鑰等訊息\",\n    \"bindNextChat\": \"綁定 Next Chat\",\n    \"bindNextChatTip\": \"點選按鈕後，將會跳轉至  Next Chat 並自動輸入金鑰等預設設定訊息\",\n    \"bindOpenCat\": \"綁定 Open Cat\",\n    \"bindOpenCatTip\": \"點擊按鈕後，將會跳轉至 Open Cat 並綁定預置參數(請先在您的裝置中安裝 Open Cat)\",\n    \"bindOneAPIStep1\": \"進入頻道管理頁，點選新增頻道\",\n    \"bindOneAPIStep2\": \"選擇OpenAI 類型並根據下方存取點、金鑰填寫對應訊息\",\n    \"bindCoAIStep1\": \"進入後台的頻道管理頁，點選對接上游\",\n    \"bindCoAIStep2\": \"根據下方存取點、密鑰資訊填寫對應信息\"\n  },\n  \"aff\": {\n    \"title\": \"推廣分成\",\n    \"bind-desc\": \"綁定推廣碼，開始獲得被邀請用戶購買後的返傭收益。\",\n    \"placeholder\": {\n      \"code\": \"請輸入推廣碼\"\n    },\n    \"generate-code-first\": \"請先生成推廣碼\",\n    \"get\": \"取得推廣碼\",\n    \"get-placeholder\": \"點擊取得推廣碼\",\n    \"get-success\": \"推廣碼已生成\",\n    \"bind-existing\": \"綁定推廣碼\",\n    \"bind-success\": \"綁定成功\",\n    \"bind-failed\": \"綁定失敗\",\n    \"bind-failed-prompt\": \"綁定失敗！原因: {{reason}}\",\n    \"withdraw\": \"提現\",\n    \"withdraw-all\": \"全部提現\",\n    \"withdraw-title\": \"收益提現\",\n    \"withdraw-desc\": \"將推廣累計收益兌換為點數。輸入金額留空為全部兌換。\",\n    \"withdraw-placeholder\": \"請輸入提現金額（留空為全部）\",\n    \"withdraw-success\": \"提現成功\",\n    \"withdraw-success-prompt\": \"您已成功將 {{amount}} 金額兌換為 {{quota}} 點數。\",\n    \"withdraw-failed\": \"提現失敗\",\n    \"withdraw-failed-prompt\": \"提現失敗！原因: {{reason}}\",\n    \"invalid-amount\": \"無效金額\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"確定\",\n    \"stats\": {\n      \"referrals\": \"邀請人數\",\n      \"earnings\": \"累計收益\",\n      \"pending\": \"待結算\",\n      \"rate\": \"返傭率\"\n    }\n  }\n}"
  },
  {
    "path": "app/src/router.tsx",
    "content": "import {\n  createBrowserRouter,\n  RouterProvider,\n  useLocation,\n  useNavigate,\n} from \"react-router-dom\";\nimport Home from \"./routes/Home.tsx\";\nimport NotFound from \"./routes/NotFound.tsx\";\nimport Auth from \"./routes/Auth.tsx\";\nimport React, { Suspense, useEffect } from \"react\";\nimport { useDeeptrain } from \"@/conf/env.ts\";\nimport Register from \"@/routes/Register.tsx\";\nimport Forgot from \"@/routes/Forgot.tsx\";\nimport { lazyFactor } from \"@/utils/loader.tsx\";\nimport { useSelector } from \"react-redux\";\nimport { selectAdmin, selectAuthenticated, selectInit } from \"@/store/auth.ts\";\nimport Index from \"@/routes/Index.tsx\";\nimport License from \"@/routes/admin/License.tsx\";\n\nconst Model = lazyFactor(() => import(\"@/routes/Model.tsx\"));\nconst Wallet = lazyFactor(() => import(\"@/routes/Wallet.tsx\"));\nconst Account = lazyFactor(() => import(\"@/routes/Account.tsx\"));\n\nconst Generation = lazyFactor(() => import(\"@/routes/Generation.tsx\"));\nconst Sharing = lazyFactor(() => import(\"@/routes/Sharing.tsx\"));\nconst Article = lazyFactor(() => import(\"@/routes/Article.tsx\"));\n\nconst AdminPage = lazyFactor(() => import(\"@/routes/Admin.tsx\"));\nconst AdminDashboard = lazyFactor(() => import(\"@/routes/admin/DashBoard.tsx\"));\nconst AdminMarket = lazyFactor(() => import(\"@/routes/admin/Market.tsx\"));\nconst AdminChannel = lazyFactor(() => import(\"@/routes/admin/Channel.tsx\"));\nconst AdminSystem = lazyFactor(() => import(\"@/routes/admin/System.tsx\"));\nconst AdminLicense = lazyFactor(() => import(\"@/routes/admin/License.tsx\"));\nconst AdminCharge = lazyFactor(() => import(\"@/routes/admin/Charge.tsx\"));\nconst AdminUsers = lazyFactor(() => import(\"@/routes/admin/Users.tsx\"));\nconst AdminBroadcast = lazyFactor(() => import(\"@/routes/admin/Broadcast.tsx\"));\nconst AdminSubscription = lazyFactor(\n  () => import(\"@/routes/admin/Subscription.tsx\"),\n);\nconst AdminLogger = lazyFactor(() => import(\"@/routes/admin/Logger.tsx\"));\n\nconst router = createBrowserRouter([\n  {\n    id: \"index\",\n    path: \"/\",\n    Component: Index,\n    ErrorBoundary: NotFound,\n    children: [\n      {\n        id: \"not-found\",\n        path: \"*\",\n        element: <NotFound />,\n      },\n      {\n        id: \"home\",\n        path: \"\",\n        element: <Home />,\n      },\n      {\n        id: \"model\",\n        path: \"model\",\n        element: (\n          <Suspense>\n            <Model />\n          </Suspense>\n        ),\n      },\n      {\n        id: \"wallet\",\n        path: \"wallet\",\n        element: (\n          <Suspense>\n            <Wallet />\n          </Suspense>\n        ),\n      },\n      // {\n      //   id: \"log\",\n      //   path: \"log\",\n      //   element: (\n      //     <Suspense>\n      //       <License />\n      //     </Suspense>\n      //   ),\n      // },\n      // {\n      //   id: \"preset\",\n      //   path: \"preset\",\n      //   element: (\n      //     <Suspense>\n      //       <Preset />\n      //     </Suspense>\n      //   ),\n      // },\n      // {\n      //   id: \"key\",\n      //   path: \"key\",\n      //   element: (\n      //     <Suspense>\n      //       <License />\n      //     </Suspense>\n      //   ),\n      // },\n      {\n        id: \"account\",\n        path: \"account\",\n        element: (\n          <Suspense>\n            <Account />\n          </Suspense>\n        ),\n      },\n      {\n        id: \"login\",\n        path: \"/login\",\n        element: (\n          <AuthForbidden>\n            <Auth />\n          </AuthForbidden>\n        ),\n        ErrorBoundary: NotFound,\n      },\n      {\n        id: \"admin\",\n        path: \"/admin\",\n        element: (\n          <AdminRequired>\n            <Suspense>\n              <AdminPage />\n            </Suspense>\n          </AdminRequired>\n        ),\n        children: [\n          {\n            id: \"admin-dashboard\",\n            path: \"\",\n            element: (\n              <Suspense>\n                <AdminDashboard />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-users\",\n            path: \"users\",\n            element: (\n              <Suspense>\n                <AdminUsers />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-market\",\n            path: \"market\",\n            element: (\n              <Suspense>\n                <AdminMarket />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-channel\",\n            path: \"channel\",\n            element: (\n              <Suspense>\n                <AdminChannel />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-system\",\n            path: \"system\",\n            element: (\n              <Suspense>\n                <AdminSystem />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-warm-up\",\n            path: \"warmup\",\n            element: (\n              <Suspense>\n                <License />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-license\",\n            path: \"license\",\n            element: (\n              <Suspense>\n                <AdminLicense />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-charge\",\n            path: \"charge\",\n            element: (\n              <Suspense>\n                <AdminCharge />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-broadcast\",\n            path: \"broadcast\",\n            element: (\n              <Suspense>\n                <AdminBroadcast />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-subscription\",\n            path: \"subscription\",\n            element: (\n              <Suspense>\n                <AdminSubscription />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-record\",\n            path: \"record\",\n            element: (\n              <Suspense>\n                <License />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-payment\",\n            path: \"pay\",\n            element: (\n              <Suspense>\n                <License />\n              </Suspense>\n            ),\n          },\n          {\n            id: \"admin-logger\",\n            path: \"logger\",\n            element: (\n              <Suspense>\n                <AdminLogger />\n              </Suspense>\n            ),\n          },\n        ],\n        ErrorBoundary: NotFound,\n      },\n      {\n        id: \"generation\",\n        path: \"/generate\",\n        element: (\n          <AuthRequired>\n            <Suspense>\n              <Generation />\n            </Suspense>\n          </AuthRequired>\n        ),\n        ErrorBoundary: NotFound,\n      },\n      {\n        id: \"article\",\n        path: \"/article\",\n        element: (\n          <AuthRequired>\n            <Suspense>\n              <Article />\n            </Suspense>\n          </AuthRequired>\n        ),\n        ErrorBoundary: NotFound,\n      },\n\n      ...(useDeeptrain\n        ? []\n        : [\n            {\n              id: \"register\",\n              path: \"/register\",\n              element: (\n                <AuthForbidden>\n                  <Register />\n                </AuthForbidden>\n              ),\n              ErrorBoundary: NotFound,\n            },\n            {\n              id: \"forgot\",\n              path: \"/forgot\",\n              element: (\n                <AuthForbidden>\n                  <Forgot />\n                </AuthForbidden>\n              ),\n              ErrorBoundary: NotFound,\n            },\n          ]),\n    ],\n  },\n  {\n    id: \"share\",\n    path: \"/share/:hash\",\n    element: (\n      <Suspense>\n        <Sharing />\n      </Suspense>\n    ),\n    ErrorBoundary: NotFound,\n  },\n]);\n\nexport function AuthRequired({ children }: { children: React.ReactNode }) {\n  const init = useSelector(selectInit);\n  const authenticated = useSelector(selectAuthenticated);\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  useEffect(() => {\n    if (init && !authenticated) {\n      navigate(\"/login\", { state: { from: location.pathname } });\n    }\n  }, [init, authenticated]);\n\n  return <>{children}</>;\n}\n\nexport function AuthForbidden({ children }: { children: React.ReactNode }) {\n  const init = useSelector(selectInit);\n  const authenticated = useSelector(selectAuthenticated);\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  useEffect(() => {\n    if (init && authenticated) {\n      navigate(\"/\", { state: { from: location.pathname } });\n    }\n  }, [init, authenticated]);\n\n  return <>{children}</>;\n}\n\nexport function AdminRequired({ children }: { children: React.ReactNode }) {\n  const init = useSelector(selectInit);\n  const admin = useSelector(selectAdmin);\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  useEffect(() => {\n    if (init && !admin) {\n      navigate(\"/\", { state: { from: location.pathname } });\n    }\n  }, [init, admin]);\n\n  return <>{children}</>;\n}\n\nexport function AppRouter() {\n  return <RouterProvider router={router} />;\n}\n\nexport default router;\n"
  },
  {
    "path": "app/src/routes/Account.tsx",
    "content": "import \"@/assets/pages/package.less\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport React, { useState } from \"react\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport Avatar from \"@/components/Avatar.tsx\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  logout,\n  selectAuthenticated,\n  selectInit,\n  selectUsername,\n} from \"@/store/auth.ts\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport { copyClipboard, useClipboard } from \"@/utils/dom.ts\";\nimport { useGroup } from \"@/utils/groups.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport {\n  CalendarClock,\n  Clock,\n  Cloud,\n  CloudRain,\n  Copy,\n  ExternalLink,\n  HandIcon,\n  HelpCircle,\n  Plug,\n  Power,\n  RotateCw,\n  Share2,\n  Trash2,\n  Undo2,\n  UserRoundCog,\n  UserRoundIcon,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  getUserInfo,\n  initialUserInfo,\n  UserInfo,\n} from \"@/api/auth.ts\";\nimport { CommonResponse, withNotify } from \"@/api/common.ts\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { quotaSelector } from \"@/store/quota.ts\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { getSharedLink, SharingPreviewForm } from \"@/api/sharing.ts\";\nimport { openWindow } from \"@/utils/device.ts\";\nimport { dataSelector, deleteData, syncData } from \"@/store/sharing.ts\";\nimport { DeeptrainOnly } from \"@/conf/deeptrain.tsx\";\nimport { deeptrainEndpoint, docsEndpoint } from \"@/conf/env.ts\";\nimport { getApiKey, keySelector, regenerateApiKey } from \"@/store/api.ts\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog.tsx\";\nimport { toast } from \"sonner\";\nimport Emoji from \"@/components/Emoji\";\n\ntype AccountCardProps = {\n  title: string;\n  description: string;\n  icon?: React.ReactElement;\n  children: React.ReactNode;\n  footer?: React.ReactNode;\n  className?: string;\n  classNameWrapper?: string;\n};\n\nfunction AccountCard({\n  title,\n  description,\n  icon,\n  children,\n  footer,\n  className,\n  classNameWrapper,\n}: AccountCardProps) {\n  const { t } = useTranslation();\n\n  return (\n    <div\n      className={cn(\n        `flex flex-col bg-background rounded-lg shadow border overflow-hidden`,\n        classNameWrapper,\n      )}\n    >\n      <div\n        className={`select-none inline-flex flex-row items-center h-fit w-full border-b px-4 py-2.5 bg-muted/20`}\n      >\n        <div className=\"flex items-center mr-2.5\">\n          {icon && (\n            <Icon\n              icon={icon}\n              className=\"w-8 h-8 p-2 rounded-lg bg-muted text-secondary\"\n            />\n          )}\n        </div>\n        <div className=\"flex flex-col\">\n          <p className=\"text-sm font-medium\">{t(title)}</p>\n          {description && (\n            <p className=\"text-xs text-secondary\">{t(description)}</p>\n          )}\n        </div>\n      </div>\n      <div className={cn(\"p-4\", className)}>{children}</div>\n      {footer && (\n        <div className={`flex flex-row items-center px-4 pb-4 pt-2`}>\n          {footer}\n        </div>\n      )}\n    </div>\n  );\n}\n\ntype ShareContentProps = {\n  data: SharingPreviewForm[];\n};\n\nfunction ShareContent({ data }: ShareContentProps) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n\n  const formatTime = (timestamp: string) => {\n    const date = new Date(timestamp);\n    return `${date.getMonth() + 1}-${date.getDate()} ${date\n      .getHours()\n      .toString()\n      .padStart(2, \"0\")}:${date.getMinutes().toString().padStart(2, \"0\")}`;\n  };\n\n  return (\n    <div className=\"space-y-3 pt-2 pb-6\">\n      {data.map((row) => (\n        <div\n          key={row.conversation_id}\n          onClick={() => openWindow(getSharedLink(row.hash), \"_blank\")}\n          className=\"flex items-center justify-between w-full border border-input p-4 rounded-lg hover:bg-muted/20 duration-200 cursor-pointer transition-colors\"\n        >\n          <div className=\"flex-grow mr-4\">\n            <div className=\"flex items-center mb-1\">\n              <h3 className=\"text-sm font-medium line-clamp-1\">{row.name}</h3>\n            </div>\n            <div className=\"flex items-center text-xs text-muted-foreground\">\n              <Clock className=\"h-3 w-3 mr-1\" />\n              {formatTime(row.time)}\n            </div>\n          </div>\n          <AlertDialog>\n            <AlertDialogTrigger asChild>\n              <Button\n                variant=\"light-destructive\"\n                size=\"icon\"\n                onClick={(e) => e.stopPropagation()}\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            </AlertDialogTrigger>\n            <AlertDialogContent>\n              <AlertDialogHeader>\n                <AlertDialogTitle>{t(\"account.share-delete\")}</AlertDialogTitle>\n                <AlertDialogDescription>\n                  {t(\"account.share-delete-description\")}\n                </AlertDialogDescription>\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogCancel>{t(\"cancel\")}</AlertDialogCancel>\n                <AlertDialogAction\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    deleteData(dispatch, row.hash);\n                  }}\n                >\n                  {t(\"confirm\")}\n                </AlertDialogAction>\n              </AlertDialogFooter>\n            </AlertDialogContent>\n          </AlertDialog>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction Account() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const init = useSelector(selectInit);\n  const username = useSelector(selectUsername);\n  const auth = useSelector(selectAuthenticated);\n  const quota = useSelector(quotaSelector);\n  const copy = useClipboard();\n  const group = useGroup(true);\n\n  const apiKey = useSelector(keySelector);\n  const [loadingApiKey, setLoadingApiKey] = useState(false);\n  const [openResetApiKey, setOpenResetApiKey] = useState(false);\n\n  const getSystemKey = async () => {\n    if (!init) return;\n\n    setLoadingApiKey(true);\n    await getApiKey(dispatch);\n    setLoadingApiKey(false);\n  };\n\n  useEffectAsync(getSystemKey, [init]);\n\n  async function copySystemKey() {\n    await copyClipboard(apiKey);\n    toast.success(t(\"api.copied\"), {\n      description: t(\"api.copied-description\"),\n    });\n  }\n\n  async function resetSystemKey() {\n    const resp = await regenerateApiKey(dispatch);\n    withNotify(t, resp as CommonResponse, true);\n\n    if (resp.status) {\n      setOpenResetApiKey(false);\n    }\n  }\n\n  const [info, setInfo] = React.useState<UserInfo>({\n    ...initialUserInfo,\n  });\n\n  const sharingData = useSelector(dataSelector);\n\n  useEffectAsync(async () => {\n    if (auth) {\n      if (sharingData.length > 0) return;\n      const resp = await syncData(dispatch);\n      if (resp) {\n        toast.error(t(\"share.sync-error\"), {\n          description: resp,\n        });\n      }\n    }\n  }, [auth]);\n\n  const updateUserInfo = async () => {\n    if (!auth) {\n      return;\n    }\n\n    const resp = await getUserInfo();\n    console.log(`[account api] get user info:`, resp);\n    withNotify(t, resp);\n\n    if (resp.status) {\n      setInfo(resp.data);\n    }\n  };\n  useEffectAsync(updateUserInfo, [auth]);\n\n  return (\n    <ScrollArea\n      className={`relative w-full h-full flex flex-col bg-background`}\n    >\n      <div\n        className={`px-4 py-6 md:py-12 lg:py-16 h-full flex flex-col w-full max-w-3xl mx-auto space-y-4`}\n      >\n        <AccountCard\n          icon={<UserRoundIcon />}\n          title={\"account.my-account\"}\n          description={t(\"account.my-account-description\")}\n          footer={\n            !auth ? (\n              <Button\n                classNameWrapper={`ml-auto`}\n                className={`flex flex-row items-center`}\n                onClick={goAuth}\n              >\n                <HandIcon className={`h-4 w-4 mr-1.5`} />\n                {t(\"login\")}\n              </Button>\n            ) : (\n              <Button\n                classNameWrapper={`ml-auto`}\n                className={`flex flex-row items-center`}\n                onClick={() => dispatch(logout())}\n              >\n                <Undo2 className={`h-4 w-4 mr-1.5`} />\n                {t(\"logout\")}\n              </Button>\n            )\n          }\n        >\n          <div className=\"flex flex-col space-y-4\">\n            <div className=\"flex items-center space-x-4\">\n              <Avatar\n                username={username}\n                className=\"w-16 h-16 shrink-0 shadow text-lg rounded-full\"\n              />\n              <div className=\"flex flex-row w-full\">\n                <div className=\"flex flex-col w-fit\">\n                  <p\n                    className=\"text-xl font-semibold cursor-pointer select-none\"\n                    onClick={() => copy(username)}\n                  >\n                    {auth ? username : t(\"anonymous\")}\n                  </p>\n                  <p className=\"text-sm text-muted-foreground\">#{info.id}</p>\n                </div>\n              </div>\n            </div>\n\n            <div className=\"flex flex-wrap gap-2\">\n              <Badge className=\"px-3 py-1 text-sm font-medium\">\n                {t(`admin.channels.groups.${group}`)}\n              </Badge>\n              <Badge\n                variant=\"outline\"\n                className=\"px-3 py-1 text-sm font-medium\"\n              >\n                {t(`account.registerDays`, {\n                  days: Math.ceil(info.register_days),\n                })}\n              </Badge>\n            </div>\n          </div>\n          <div className=\"mt-6 grid grid-cols-1 md:grid-cols-3 gap-4\">\n            <div className=\"bg-card shadow-sm rounded-lg p-4 transition-all border\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"text-sm font-medium text-muted-foreground\">\n                  {t(\"account.current-quota\")}\n                </span>\n                <Cloud className=\"w-10 h-10 p-2 rounded-lg bg-muted/40 text-secondary stroke-[1]\" />\n              </div>\n              <p className=\"text-md\">{quota.toFixed(2)}</p>\n            </div>\n            <div className=\"bg-card shadow-sm rounded-lg p-4 transition-all border\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"text-sm font-medium text-muted-foreground\">\n                  {t(\"account.used-quota\")}\n                </span>\n                <CloudRain className=\"w-10 h-10 p-2 rounded-lg bg-muted/40 text-secondary stroke-[1]\" />\n              </div>\n              <p className=\"text-md\">{info.used_quota.toFixed(2)}</p>\n            </div>\n            <div className=\"bg-card shadow-sm rounded-lg p-4 transition-all border\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <span className=\"text-sm font-medium text-muted-foreground\">\n                  {t(\"account.plan-total-month\")}\n                </span>\n                <CalendarClock className=\"w-10 h-10 p-2 rounded-lg bg-muted/40 text-secondary stroke-[1]\" />\n              </div>\n              <div className=\"flex items-center\">\n                <p className=\"text-md mr-2\">{info.plan_total_month}</p>\n                <Tips\n                  className=\"text-muted-foreground hover:text-foreground transition-colors\"\n                  content={t(\"account.plan-total-month-tips\")}\n                />\n              </div>\n            </div>\n          </div>\n        </AccountCard>\n        <DeeptrainOnly>\n          <AccountCard\n            title={\"account.deeptrain\"}\n            description={t(\"account.deeptrain-description\")}\n            icon={<UserRoundCog />}\n            footer={\n              auth ? (\n                <Button\n                  className={`flex flex-row items-center`}\n                  classNameWrapper={`ml-auto`}\n                  onClick={() => openWindow(`${deeptrainEndpoint}/home`)}\n                >\n                  <ExternalLink className={`h-4 w-4 mr-1.5`} />\n                  {t(\"manage\")}\n                </Button>\n              ) : (\n                <Button classNameWrapper={`ml-auto`} onClick={goAuth}>\n                  <HandIcon className={`h-4 w-4 mr-1.5`} />\n                  {t(\"login\")}\n                </Button>\n              )\n            }\n          >\n            <div className={`flex flex-row items-center space-x-2`}>\n              <img\n                src={`${deeptrainEndpoint}/favicon.ico`}\n                alt={``}\n                className={`w-12 h-12 select-none cursor-pointer`}\n                onClick={() => openWindow(`${deeptrainEndpoint}/home`)}\n              />\n              <div className={`inline-flex flex-col`}>\n                <p className={`text-common text-sm font-bold`}>DeepTrain SSO</p>\n                <p className={`text-secondary text-xs`}>\n                  {t(\"account.deeptrain-description\")}\n                </p>\n              </div>\n            </div>\n          </AccountCard>\n        </DeeptrainOnly>\n        <AccountCard\n          title={\"api.title\"}\n          description={t(\"account.api-description\")}\n          icon={<Plug />}\n        >\n          <div className={`api-dialog`}>\n            <div className={`api-wrapper flex flex-row space-x-1`}>\n              <Button\n                variant={`outline`}\n                size={`icon-sm`}\n                className={`shrink-0`}\n                onClick={getSystemKey}\n              >\n                <RotateCw\n                  className={cn(\"h-3.5 w-3.5\", loadingApiKey && \"animate-spin\")}\n                />\n              </Button>\n              <Input\n                type={`password`}\n                value={apiKey}\n                readOnly={true}\n                classNameWrapper={`grow`}\n                className={`text-xs h-8`}\n              />\n              <Button\n                variant={`default`}\n                className={`shrink-0`}\n                size={`icon-sm`}\n                onClick={copySystemKey}\n              >\n                <Copy className={`h-3.5 w-3.5`} />\n              </Button>\n            </div>\n            <div className={`flex flex-row mt-2 items-center justify-center`}>\n              <AlertDialog\n                open={openResetApiKey}\n                onOpenChange={setOpenResetApiKey}\n              >\n                <AlertDialogTrigger asChild>\n                  <Button\n                    variant={`destructive`}\n                    size={`default-sm`}\n                    className={`text-xs mr-2`}\n                  >\n                    <Power className={`h-3.5 w-3.5 mr-2`} />\n                    {t(\"api.reset\")}\n                  </Button>\n                </AlertDialogTrigger>\n                <AlertDialogContent>\n                  <AlertDialogHeader>\n                    <AlertDialogTitle>{t(\"api.reset\")}</AlertDialogTitle>\n                    <AlertDialogDescription>\n                      {t(\"api.reset-description\")}\n                    </AlertDialogDescription>\n                  </AlertDialogHeader>\n                  <AlertDialogFooter>\n                    <Button\n                      variant={`destructive`}\n                      loading={true}\n                      onClick={resetSystemKey}\n                      unClickable\n                    >\n                      {t(\"confirm\")}\n                    </Button>\n                    <AlertDialogCancel>{t(\"cancel\")}</AlertDialogCancel>\n                  </AlertDialogFooter>\n                </AlertDialogContent>\n              </AlertDialog>\n\n              <Button\n                variant={`outline`}\n                size={`default-sm`}\n                className={`text-xs`}\n                asChild\n              >\n                <a href={docsEndpoint} target={`_blank`}>\n                  <ExternalLink className={`h-3.5 w-3.5 mr-2`} />\n                  {t(\"api.learn-more\")}\n                </a>\n              </Button>\n            </div>\n          </div>\n        </AccountCard>\n        <AccountCard\n          icon={<Share2 />}\n          title={\"share.manage\"}\n          description={t(\"account.share-description\")}\n          className={`bg-background px-1`}\n        >\n          {sharingData.length > 0 ? (\n            <ScrollArea className={`h-48 md:h-64 px-4`}>\n              <div className={`w-full`}>\n                <ShareContent data={sharingData} />\n              </div>\n            </ScrollArea>\n          ) : (\n            <div\n              className={`flex flex-col items-center text-sm select-none py-8`}\n            >\n              <Emoji\n                emoji={`1f4c2`}\n                className=\"w-12 h-12 p-2 rounded-md bg-muted/80 mb-4\"\n              />\n              <p>{t(\"share.empty\")}</p>\n\n              <p\n                className={`flex flex-row items-center text-xs text-secondary mt-1.5`}\n              >\n                <HelpCircle className={`h-3 w-3 mr-1`} />\n                {t(\"share.share-tip\")}\n              </p>\n            </div>\n          )}\n        </AccountCard>\n      </div>\n    </ScrollArea>\n  );\n}\n\nexport default Account;\n"
  },
  {
    "path": "app/src/routes/Admin.tsx",
    "content": "import \"@/assets/admin/all.less\";\nimport MenuBar from \"@/components/admin/MenuBar.tsx\";\nimport { Outlet } from \"react-router-dom\";\nimport { useSelector } from \"react-redux\";\nimport { selectAdmin, selectInit } from \"@/store/auth.ts\";\nimport { useEffect } from \"react\";\nimport router from \"@/router.tsx\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\n\nfunction Admin() {\n  const init = useSelector(selectInit);\n  const admin = useSelector(selectAdmin);\n\n  useEffect(() => {\n    if (init && !admin) router.navigate(\"/\");\n  }, [init]);\n\n  return (\n    <div className={`home-page flex flex-row flex-1`}>\n      <div className={`admin-page`}>\n        <MenuBar />\n        <ScrollArea className={`admin-content`}>\n          <Outlet />\n        </ScrollArea>\n      </div>\n    </div>\n  );\n}\n\nexport default Admin;\n"
  },
  {
    "path": "app/src/routes/Article.tsx",
    "content": "import \"@/assets/pages/article.less\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport router from \"@/router.tsx\";\nimport { Check, ChevronLeft, Cloud, Files, Globe, Loader2 } from \"lucide-react\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useState } from \"react\";\nimport ModelArea from \"@/components/home/ModelArea.tsx\";\nimport { Toggle } from \"@/components/ui/toggle.tsx\";\nimport { selectModel, selectWeb, setWeb } from \"@/store/chat.ts\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport {\n  apiEndpoint,\n  tokenField,\n  websocketEndpoint,\n} from \"@/conf/bootstrap.ts\";\nimport { getMemory } from \"@/utils/memory.ts\";\nimport { Progress } from \"@/components/ui/progress.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { toast } from \"sonner\";\n\ntype ProgressProps = {\n  current: number;\n  total: number;\n};\n\nfunction GenerateProgress({\n  current,\n  total,\n  quota,\n}: ProgressProps & { quota: number }) {\n  const { t } = useTranslation();\n\n  return (\n    <div className={`article-progress w-full mb-4`}>\n      <p\n        className={`select-none mt-4 mb-2.5 flex flex-row items-center content-center w-full justify-center text-center`}\n      >\n        {total !== 0 && current === total ? (\n          <>\n            <Check\n              className={`h-5 w-5 mr-2 inline-block animate-out shrink-0`}\n            />\n            {t(\"article.generate-success\")}\n          </>\n        ) : (\n          <>\n            <Loader2\n              className={`h-5 w-5 mr-2 inline-block animate-spin shrink-0`}\n            />\n            {t(\"article.progress-title\", { current, total })}\n          </>\n        )}\n      </p>\n      <Progress value={(100 * current) / total} />\n      <div\n        className={`article-quota flex flex-row mt-4 border border-input rounded-md py-1 px-3 select-none w-max items-center mx-auto`}\n      >\n        <Cloud className={`h-4 w-4 mr-2`} />\n        <p>{quota.toFixed(2)}</p>\n      </div>\n    </div>\n  );\n}\n\nfunction ArticleContent() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const web = useSelector(selectWeb);\n  const model = useSelector(selectModel);\n\n  const [prompt, setPrompt] = useState(\"\");\n  const [title, setTitle] = useState(\"\");\n  const [progress, setProgress] = useState(false);\n\n  const [state, setState] = useState<ProgressProps>({ current: 0, total: 0 });\n  const [quota, setQuota] = useState<number>(0);\n  const [hash, setHash] = useState(\"\");\n\n  function clear() {\n    setPrompt(\"\");\n    setTitle(\"\");\n    setHash(\"\");\n    setProgress(false);\n    setQuota(0);\n    setState({ current: 0, total: 0 });\n  }\n\n  function generate() {\n    setProgress(true);\n    const connection = new WebSocket(`${websocketEndpoint}/article/create`);\n\n    connection.onopen = () => {\n      connection.send(\n        JSON.stringify({\n          token: getMemory(tokenField),\n          web,\n          title,\n          prompt,\n          model,\n        }),\n      );\n    };\n\n    connection.onmessage = (e) => {\n      const data = JSON.parse(e.data);\n\n      data.data && data.data.quota && setQuota(quota + data.data.quota);\n      if (!data.hash) setState(data.data as ProgressProps);\n      else {\n        toast.success(t(\"article.generate-success\"), {\n          description: t(\"article.generate-success-prompt\"),\n        });\n        setHash(data.hash);\n      }\n    };\n\n    connection.onerror = (e: Event) => {\n      console.debug(`[article] error during generation: ${e}`);\n      toast.error(t(\"article.generate-failed\"), {\n        description: `${t(\"article.generate-failed-prompt\")} (${e.toString()})`,\n      });\n      setProgress(false);\n      connection.close();\n    };\n  }\n\n  return progress ? (\n    <>\n      <GenerateProgress {...state} quota={quota} />\n      {hash && (\n        <div className={`article-action flex flex-row items-center my-4 gap-4`}>\n          <Button\n            variant={`outline`}\n            className={`w-full whitespace-nowrap`}\n            onClick={() => {\n              location.href = `${apiEndpoint}/article/download/zip?hash=${hash}`;\n            }}\n          >\n            {\" \"}\n            {t(\"article.download-format\", { name: \"zip\" })}{\" \"}\n          </Button>\n\n          <Button\n            variant={`outline`}\n            className={`w-full whitespace-nowrap`}\n            onClick={() => {\n              location.href = `${apiEndpoint}/article/download/tar?hash=${hash}`;\n            }}\n          >\n            {\" \"}\n            {t(\"article.download-format\", { name: \"tar\" })}{\" \"}\n          </Button>\n        </div>\n      )}\n      <Button\n        variant={`default`}\n        className={`mt-5 w-full mx-auto`}\n        onClick={clear}\n      >\n        {t(\"close\")}\n      </Button>\n    </>\n  ) : (\n    <>\n      <div className={`flex flex-row items-center mx-auto`}>\n        <Toggle\n          aria-label={t(\"chat.web-aria\")}\n          defaultPressed={false}\n          onPressedChange={(state: boolean) => dispatch(setWeb(state))}\n          variant={`outline`}\n        >\n          <Globe className={cn(\"h-4 w-4 web\", web && \"enable\")} />\n        </Toggle>\n        <Label className={`ml-2.5 whitespace-nowrap`}>\n          {t(\"article.web-checkbox\")}\n        </Label>\n      </div>\n      <Textarea\n        placeholder={t(\"article.prompt-placeholder\")}\n        rows={3}\n        value={prompt}\n        onChange={(e) => setPrompt(e.target.value)}\n      />\n      <Textarea\n        placeholder={t(\"article.input-placeholder\")}\n        rows={8}\n        value={title}\n        onChange={(e) => setTitle(e.target.value)}\n      />\n      <ModelArea side={`bottom`} />\n      <Button\n        variant={`default`}\n        className={`mt-5 w-full mx-auto`}\n        onClick={generate}\n        disabled={progress || !title}\n      >\n        {t(\"article.generate\")}\n      </Button>\n    </>\n  );\n}\n\nfunction Wrapper() {\n  const { t } = useTranslation();\n\n  return (\n    <Card className={`article-wrapper`}>\n      <CardHeader className={`py-4`}>\n        <CardTitle className={`article-title`}>\n          <Files className={`h-5 w-5 mr-2`} />\n          {t(\"article.title\")}\n        </CardTitle>\n      </CardHeader>\n      <CardContent className={`article-content`}>\n        <ArticleContent />\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction Article() {\n  return (\n    <div className={`article-page`}>\n      <div className={`article-container`}>\n        <Button\n          className={`action`}\n          variant={`ghost`}\n          size={`icon`}\n          onClick={() => router.navigate(\"/\")}\n        >\n          <ChevronLeft className={`h-5 w-5 back`} />\n        </Button>\n        <Wrapper />\n      </div>\n    </div>\n  );\n}\n\nexport default Article;\n"
  },
  {
    "path": "app/src/routes/Auth.tsx",
    "content": "import { tokenField } from \"@/conf/bootstrap.ts\";\nimport { useEffect, useReducer } from \"react\";\nimport Loader from \"@/components/Loader.tsx\";\nimport \"@/assets/pages/auth.less\";\nimport { validateToken } from \"@/store/auth.ts\";\nimport { useDispatch } from \"react-redux\";\nimport router from \"@/router.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { getQueryParam } from \"@/utils/path.ts\";\nimport { setMemory } from \"@/utils/memory.ts\";\nimport { appLogo, appName, useDeeptrain } from \"@/conf/env.ts\";\nimport { Card, CardContent } from \"@/components/ui/card.tsx\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport Require, { LengthRangeRequired } from \"@/components/Require.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { formReducer, isTextInRange } from \"@/utils/form.ts\";\nimport { doLogin, LoginForm } from \"@/api/auth.ts\";\nimport { getErrorMessage, isEnter } from \"@/utils/base.ts\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { toast } from \"sonner\";\n\nfunction DeepAuth() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const token = getQueryParam(\"token\").trim();\n\n  useEffect(() => {\n    if (!token.length) {\n      toast.warning(t(\"invalid-token\"), {\n        description: t(\"invalid-token-prompt\"),\n        action: {\n          label: t(\"try-again\"),\n          onClick: goAuth,\n        },\n      });\n\n      setTimeout(goAuth, 2500);\n      return;\n    }\n\n    setMemory(tokenField, token);\n\n    doLogin({ token })\n      .then((data) => {\n        if (!data.status) {\n          toast.error(t(\"login-failed\"), {\n            description: t(\"login-failed-prompt\", { reason: data.error }),\n            action: {\n              label: t(\"try-again\"),\n              onClick: goAuth,\n            },\n          });\n        } else\n          validateToken(dispatch, data.token, async () => {\n            toast.success(t(\"login-success\"), {\n              description: t(\"login-success-prompt\"),\n            });\n\n            await router.navigate(\"/\");\n          });\n      })\n      .catch((err) => {\n        console.debug(err);\n\n        toast.error(t(\"server-error\"), {\n          description: `${t(\"server-error-prompt\")}\\n${err.message}`,\n          action: {\n            label: t(\"try-again\"),\n            onClick: goAuth,\n          },\n        });\n      });\n  }, []);\n\n  return (\n    <div className={`auth`}>\n      <Loader prompt={t(\"login\")} />\n    </div>\n  );\n}\n\nfunction Login() {\n  const { t } = useTranslation();\n  const globalDispatch = useDispatch();\n  const [form, dispatch] = useReducer(formReducer<LoginForm>(), {\n    username: sessionStorage.getItem(\"username\") || \"\",\n    password: sessionStorage.getItem(\"password\") || \"\",\n  });\n\n  const onSubmit = async () => {\n    if (\n      !isTextInRange(form.username, 1, 255) ||\n      !isTextInRange(form.password, 6, 36)\n    )\n      return;\n\n    try {\n      const resp = await doLogin(form);\n      if (!resp.status) {\n        toast.warning(t(\"login-failed\"), {\n          description: t(\"login-failed-prompt\", { reason: resp.error }),\n        });\n        return;\n      }\n\n      toast.success(t(\"login-success\"), {\n        description: t(\"login-success-prompt\"),\n      });\n\n      if (\n        form.username.trim() === \"root\" &&\n        form.password.trim() === \"coai123456\"\n      ) {\n        toast.warning(t(\"admin.default-password\"), {\n          description: t(\"admin.default-password-prompt\"),\n          duration: 15000,\n        });\n      }\n\n      validateToken(globalDispatch, resp.token);\n      await router.navigate(\"/\");\n    } catch (err) {\n      console.debug(err);\n      toast.error(t(\"server-error\"), {\n        description: `${t(\"server-error-prompt\")}\\n${getErrorMessage(err)}`,\n      });\n    }\n  };\n\n  useEffect(() => {\n    // listen to enter key and auto submit\n    const listener = async (e: KeyboardEvent) => {\n      if (isEnter(e)) await onSubmit();\n    };\n\n    document.addEventListener(\"keydown\", listener);\n    return () => document.removeEventListener(\"keydown\", listener);\n  }, []);\n\n  return (\n    <ScrollArea className={`w-full h-full grid place-items-center`}>\n      <div className={`auth-container`}>\n        <img className={`logo`} src={appLogo} alt=\"\" />\n        <div className={`title`}>\n          {t(\"login\")} {appName}\n        </div>\n        <Card className={`auth-card`}>\n          <CardContent className={`pb-0`}>\n            <div className={`auth-wrapper`}>\n              <Label>\n                <Require />\n                {t(\"auth.username-or-email\")}\n                <LengthRangeRequired\n                  content={form.username}\n                  min={1}\n                  max={255}\n                  hideOnEmpty={true}\n                />\n              </Label>\n              <Input\n                placeholder={t(\"auth.username-or-email-placeholder\")}\n                value={form.username}\n                onChange={(e) =>\n                  dispatch({ type: \"update:username\", payload: e.target.value })\n                }\n              />\n\n              <Label>\n                <Require />\n                {t(\"auth.password\")}\n                <LengthRangeRequired\n                  content={form.password}\n                  min={6}\n                  max={36}\n                  hideOnEmpty={true}\n                />\n              </Label>\n              <Input\n                placeholder={t(\"auth.password-placeholder\")}\n                value={form.password}\n                type={\"password\"}\n                onChange={(e) =>\n                  dispatch({ type: \"update:password\", payload: e.target.value })\n                }\n              />\n\n              <Button\n                tapScale={0.975}\n                classNameWrapper={`mt-2`}\n                onClick={onSubmit}\n                className={`w-full`}\n                loading={true}\n              >\n                {t(\"login\")}\n              </Button>\n            </div>\n          </CardContent>\n        </Card>\n        <div className={`auth-card addition-wrapper`}>\n          <div className={`row`}>\n            {t(\"auth.no-account\")}\n            <a className={`link`} onClick={() => router.navigate(\"/register\")}>\n              {t(\"auth.register\")}\n            </a>\n          </div>\n          <div className={`row`}>\n            {t(\"auth.forgot-password\")}\n            <a className={`link`} onClick={() => router.navigate(\"/forgot\")}>\n              {t(\"auth.reset-password\")}\n            </a>\n          </div>\n        </div>\n      </div>\n    </ScrollArea>\n  );\n}\n\nfunction Auth() {\n  return useDeeptrain ? <DeepAuth /> : <Login />;\n}\n\nexport default Auth;\n"
  },
  {
    "path": "app/src/routes/Forgot.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useReducer } from \"react\";\nimport { formReducer, isEmailValid, isTextInRange } from \"@/utils/form.ts\";\nimport { doReset, ResetForm, sendCode } from \"@/api/auth.ts\";\nimport router from \"@/router.tsx\";\nimport { Card, CardContent } from \"@/components/ui/card.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport Require, {\n  EmailRequire,\n  LengthRangeRequired,\n  SameRequired,\n} from \"@/components/Require.tsx\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport TickButton from \"@/components/TickButton.tsx\";\nimport { appLogo } from \"@/conf/env.ts\";\nimport { useSelector } from \"react-redux\";\nimport { infoMailSelector } from \"@/store/info.ts\";\nimport { AlertCircle } from \"lucide-react\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { toast } from \"sonner\";\n\nfunction Forgot() {\n  const { t } = useTranslation();\n  const enabled = useSelector(infoMailSelector);\n\n  const [form, dispatch] = useReducer(formReducer<ResetForm>(), {\n    email: \"\",\n    code: \"\",\n    password: \"\",\n    repassword: \"\",\n  });\n\n  const onVerify = async () => await sendCode(t, form.email);\n\n  const onSubmit = async () => {\n    if (\n      !isEmailValid(form.email) ||\n      !form.code.length ||\n      !isTextInRange(form.password, 6, 36) ||\n      form.password.trim() !== form.repassword.trim()\n    )\n      return;\n\n    const res = await doReset(form);\n    if (!res.status) {\n      toast.error(t(\"error\"), {\n        description: res.error,\n      });\n      return;\n    }\n\n    toast.info(t(\"auth.reset-success\"), {\n      description: t(\"auth.reset-success-prompt\"),\n    });\n\n    sessionStorage.setItem(\"username\", form.email);\n    sessionStorage.setItem(\"password\", form.password);\n    await router.navigate(\"/login\");\n  };\n\n  return (\n    <ScrollArea className={`w-full h-full grid place-items-center`}>\n      <div className={`auth-container`}>\n        <img className={`logo`} src={appLogo} alt=\"\" />\n        <div className={`title`}>{t(\"auth.reset-password\")}</div>\n        <Card className={`auth-card`}>\n          <CardContent className={`pb-0`}>\n            <div className={`auth-wrapper`}>\n              {!enabled && (\n                <Alert className={`p-4`}>\n                  <AlertCircle className={`h-4 w-4`} />\n                  <AlertDescription>{t(\"auth.disabled-mail\")}</AlertDescription>\n                </Alert>\n              )}\n              <Label>\n                <Require />\n                {t(\"auth.email\")}\n                <EmailRequire content={form.email} hideOnEmpty={true} />\n              </Label>\n              <Input\n                placeholder={t(\"auth.email-placeholder\")}\n                value={form.email}\n                onChange={(e) =>\n                  dispatch({\n                    type: \"update:email\",\n                    payload: e.target.value,\n                  })\n                }\n              />\n\n              <Label>\n                <Require /> {t(\"auth.code\")}\n              </Label>\n\n              <div className={`flex flex-row`}>\n                <Input\n                  placeholder={t(\"auth.code-placeholder\")}\n                  value={form.code}\n                  onChange={(e) =>\n                    dispatch({\n                      type: \"update:code\",\n                      payload: e.target.value,\n                    })\n                  }\n                />\n                <TickButton\n                  className={`ml-2 whitespace-nowrap`}\n                  loading={true}\n                  onClick={onVerify}\n                  tick={60}\n                  disabled={!enabled}\n                >\n                  {t(\"auth.send-code\")}\n                </TickButton>\n              </div>\n\n              <Label>\n                <Require />\n                {t(\"auth.password\")}\n                <LengthRangeRequired\n                  content={form.password}\n                  min={6}\n                  max={36}\n                  hideOnEmpty={true}\n                />\n              </Label>\n              <Input\n                placeholder={t(\"auth.password-placeholder\")}\n                value={form.password}\n                type={\"password\"}\n                onChange={(e) =>\n                  dispatch({ type: \"update:password\", payload: e.target.value })\n                }\n              />\n\n              <Label>\n                <Require />\n                {t(\"auth.check-password\")}\n                <SameRequired\n                  content={form.password}\n                  compare={form.repassword}\n                  hideOnEmpty={true}\n                />\n              </Label>\n              <Input\n                placeholder={t(\"auth.check-password-placeholder\")}\n                value={form.repassword}\n                type={\"password\"}\n                onChange={(e) =>\n                  dispatch({\n                    type: \"update:repassword\",\n                    payload: e.target.value,\n                  })\n                }\n              />\n\n              <Button\n                disabled={!enabled}\n                onClick={onSubmit}\n                tapScale={0.975}\n                classNameWrapper={`mt-2`}\n                className={`w-full`}\n                loading={true}\n              >\n                {t(\"reset\")}\n              </Button>\n            </div>\n          </CardContent>\n        </Card>\n        <div className={`auth-card addition-wrapper`}>\n          <div className={`row`}>\n            {t(\"auth.no-account\")}\n            <a className={`link`} onClick={() => router.navigate(\"/register\")}>\n              {t(\"auth.register\")}\n            </a>\n          </div>\n          <div className={`row`}>\n            {t(\"auth.have-account\")}\n            <a className={`link`} onClick={() => router.navigate(\"/login\")}>\n              {t(\"auth.login\")}\n            </a>\n          </div>\n        </div>\n      </div>\n    </ScrollArea>\n  );\n}\n\nexport default Forgot;\n"
  },
  {
    "path": "app/src/routes/Generation.tsx",
    "content": "import \"@/assets/pages/generation.less\";\nimport { useSelector } from \"react-redux\";\nimport { useTranslation } from \"react-i18next\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { ChevronLeft, Cloud, FileDown, Send } from \"lucide-react\";\nimport { apiEndpoint } from \"@/conf/bootstrap.ts\";\nimport router from \"@/router.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { manager } from \"@/api/generation.ts\";\nimport { handleGenerationData } from \"@/utils/processor.ts\";\nimport { selectModel } from \"@/store/chat.ts\";\nimport ModelArea from \"@/components/home/ModelArea.tsx\";\nimport { appLogo } from \"@/conf/env.ts\";\nimport { isEnter } from \"@/utils/base.ts\";\nimport { toast } from \"sonner\";\n\ntype WrapperProps = {\n  onSend?: (value: string, model: string) => boolean;\n};\n\nfunction Wrapper({ onSend }: WrapperProps) {\n  const { t } = useTranslation();\n  const ref = useRef(null);\n  const [stayed, setStayed] = useState<boolean>(false);\n  const [hash, setHash] = useState<string>(\"\");\n  const [data, setData] = useState<string>(\"\");\n  const [quota, setQuota] = useState<number>(0);\n  const model = useSelector(selectModel);\n  const modelRef = useRef(model);\n\n  function clear() {\n    setData(\"\");\n    setQuota(0);\n    setHash(\"\");\n  }\n\n  manager.setMessageHandler(({ message, quota }) => {\n    setData(message);\n    setQuota(quota);\n  });\n\n  manager.setErrorHandler((err: string) => {\n    toast.error(t(\"generate.failed\"), {\n      description: `${t(\"generate.reason\")} ${err}`,\n    });\n  });\n  manager.setFinishedHandler((hash: string) => {\n    toast.success(t(\"generate.success\"), {\n      description: t(\"generate.success-prompt\"),\n    });\n    setHash(hash);\n  });\n\n  function handleSend(model: string = \"gpt-3.5-16k\") {\n    const target = ref.current as HTMLInputElement | null;\n    if (!target) return;\n\n    const value = target.value.trim();\n    if (!value.length) return;\n\n    if (onSend?.(value, model)) {\n      setStayed(true);\n      clear();\n      target.value = \"\";\n    }\n  }\n\n  useEffect(() => {\n    const target = ref.current as HTMLInputElement | null;\n    if (!target) return;\n    target.focus();\n    target.removeEventListener(\"keydown\", () => {});\n    target.addEventListener(\"keydown\", (e) => {\n      if (isEnter(e)) {\n        // cannot use model here, because model is not updated\n        handleSend(modelRef.current);\n      }\n    });\n\n    return () => {\n      ref.current &&\n        (ref.current as HTMLInputElement).removeEventListener(\n          \"keydown\",\n          () => {},\n        );\n    };\n  }, [ref]);\n\n  useEffect(() => {\n    modelRef.current = model;\n  }, [model]);\n\n  return (\n    <div className={`generation-wrapper`}>\n      {stayed ? (\n        <div className={`box`}>\n          {quota > 0 && (\n            <div className={`quota-box`}>\n              <Cloud className={`h-4 w-4 mr-2`} />\n              {quota.toFixed(2)}\n            </div>\n          )}\n          <pre className={`message-box`}>\n            {handleGenerationData(data) || t(\"generate.empty\")}\n          </pre>\n          {hash.length > 0 && (\n            <div className={`hash-box`}>\n              <a\n                className={`download-box`}\n                href={`${apiEndpoint}/generation/download/tar?hash=${hash}`}\n              >\n                <FileDown className={`h-6 w-6`} />\n                <p>{t(\"generate.download\", { name: \"tar.gz\" })}</p>\n              </a>\n              <a\n                className={`download-box`}\n                href={`${apiEndpoint}/generation/download/zip?hash=${hash}`}\n              >\n                <FileDown className={`h-6 w-6`} />\n                <p>{t(\"generate.download\", { name: \"zip\" })}</p>\n              </a>\n            </div>\n          )}\n        </div>\n      ) : (\n        <div className={`product`}>\n          <img src={appLogo} alt={\"\"} />\n          AI Code Generator\n        </div>\n      )}\n      <div className={`generate-box`}>\n        <Input\n          className={`input`}\n          ref={ref}\n          placeholder={t(\"generate.input-placeholder\")}\n        />\n        <Button\n          size={`icon`}\n          className={`action`}\n          variant={`default`}\n          onClick={() => handleSend(model)}\n        >\n          <Send className={`h-5 w-5`} />\n        </Button>\n      </div>\n      <div className={`model-box`}>\n        <ModelArea side={`bottom`} />\n      </div>\n    </div>\n  );\n}\nfunction Generation() {\n  const [state, setState] = useState(false);\n  manager.setProcessingChangeHandler(setState);\n\n  return (\n    <div className={`generation-page`}>\n      <div className={`generation-container`}>\n        <Button\n          className={`action`}\n          variant={`ghost`}\n          size={`icon`}\n          onClick={() => router.navigate(\"/\")}\n          disabled={state}\n        >\n          <ChevronLeft className={`h-5 w-5 back`} />\n        </Button>\n        <Wrapper\n          onSend={(prompt: string, model: string) => {\n            console.debug(\n              `[generation] create generation request (prompt: ${prompt}, model: ${model})`,\n            );\n            return manager.generateWithBlock(prompt, model);\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default Generation;\n"
  },
  {
    "path": "app/src/routes/Home.tsx",
    "content": "import \"@/assets/pages/chat.less\";\nimport ChatWrapper from \"@/components/home/ChatWrapper.tsx\";\nimport SideBar from \"@/components/home/SideBar.tsx\";\nfunction Home() {\n  return (\n    <div className={`home-page flex flex-row flex-1`}>\n      <SideBar />\n      <ChatWrapper />\n    </div>\n  );\n}\n\nexport default Home;\n"
  },
  {
    "path": "app/src/routes/Index.tsx",
    "content": "import { Outlet, useLocation } from \"react-router-dom\";\nimport ErrorBoundary from \"@/components/ErrorBoundary.tsx\";\nimport \"@/assets/pages/home.less\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  ChevronDown,\n  MessageCircle,\n  Shield,\n  Wallet,\n  LibraryBig,\n  User,\n} from \"lucide-react\";\nimport React from \"react\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport router from \"@/router.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { useSelector } from \"react-redux\";\nimport { selectAdmin } from \"@/store/auth.ts\";\nimport {\n  hideToolbarSelector,\n  hideToolbarTextSelector,\n} from \"@/store/settings.ts\";\nimport { isMobile, useMobile } from \"@/utils/device.ts\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip.tsx\";\nimport NavBar from \"@/components/app/NavBar.tsx\";\n\ntype BarItemProps = {\n  icon: React.ReactElement;\n  path: string;\n  name: string;\n};\n\nfunction isPrefix(current: string, path: string): boolean {\n  if (location.pathname === path) return true;\n  if (location.pathname + \"/\" === path) return true;\n\n  return path.length > 1 && current.startsWith(path + \"/\");\n}\n\nfunction BarItem({ icon, path, name }: BarItemProps) {\n  const { t } = useTranslation();\n  const location = useLocation();\n  const active = isPrefix(location.pathname, path);\n\n  const hidden = useSelector(hideToolbarTextSelector);\n  const mobile = useMobile();\n\n  const [open, setOpen] = React.useState(false);\n\n  const onClick = async () => {\n    await router.navigate(path);\n  };\n\n  return (\n    <div className={`inline-flex flex-col`}>\n      <TooltipProvider delayDuration={100}>\n        <Tooltip open={open} onOpenChange={setOpen}>\n          <TooltipTrigger asChild>\n            <Button\n              size=\"icon\"\n              variant={active ? \"default\" : \"outline\"}\n              onClick={onClick}\n            >\n              <Icon icon={icon} className=\"h-4 w-4 stroke-[1.75]\" />\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent\n            side={mobile ? \"top\" : \"right\"}\n            align=\"center\"\n            className={`z-[100]`}\n          >\n            {t(`bar.${name}`)}\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n      <div\n        className={cn(\n          `toolbar-text text-secondary text-center text-xs mt-1.5 cursor-pointer select-none`,\n          active && `text-common`,\n          hidden && `hidden`,\n        )}\n        onClick={onClick}\n      >\n        {t(`bar.${name}`)}\n      </div>\n    </div>\n  );\n}\n\nfunction ToolBar() {\n  const admin = useSelector(selectAdmin);\n  const hideToolbar = useSelector(hideToolbarSelector);\n  const [stacked, setStacked] = React.useState(hideToolbar || isMobile());\n\n  return (\n    <div className={cn(\"toolbar\", stacked && \"stacked\")}>\n      <div\n        className={cn(\"bar-kit\", stacked && \"stacked\")}\n        onClick={(e) => {\n          e.stopPropagation();\n          e.preventDefault();\n          setStacked(!stacked);\n        }}\n      >\n        <ChevronDown className={`h-3.5 w-3.5`} />\n      </div>\n      <BarItem icon={<MessageCircle />} path={`/`} name={\"chat\"} />\n      <BarItem icon={<LibraryBig />} path={`/model`} name={\"model\"} />\n      {/* <BarItem icon={<Compass />} path={`/preset`} name={\"preset\"} /> */}\n      <BarItem icon={<Wallet />} path={`/wallet`} name={\"wallet\"} />\n      {/* <BarItem icon={<DraftingCompass />} path={`/key`} name={\"key\"} /> */}\n      {/* <BarItem icon={<PieChart />} path={`/log`} name={\"log\"} /> */}\n      <BarItem icon={<User />} path={`/account`} name={\"account\"} />\n      {admin && <BarItem icon={<Shield />} path={`/admin`} name={\"admin\"} />}\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <ErrorBoundary>\n      <NavBar />\n      <div className={`main relative`}>\n        <ToolBar />\n        <Outlet />\n      </div>\n    </ErrorBoundary>\n  );\n}\n\nexport default Home;\n"
  },
  {
    "path": "app/src/routes/Model.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport {\n  ArrowDownToDot,\n  ArrowRightLeft,\n  ArrowUpFromDot,\n  Award,\n  Bolt,\n  Cloud,\n  Cpu,\n  DollarSign,\n  EyeIcon,\n  Gem,\n  Github,\n  Globe,\n  Image,\n  Link,\n  Search,\n  Snail,\n  Sparkles,\n  Star,\n  Tag,\n  X,\n  Zap,\n} from \"lucide-react\";\nimport React, { useMemo, useState } from \"react\";\nimport { splitList } from \"@/utils/base.ts\";\nimport type { Model } from \"@/api/types.tsx\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport {\n  addModelList,\n  removeModelList,\n  selectModel,\n  selectModelList,\n  selectSupportModels,\n  setModel,\n} from \"@/store/chat.ts\";\nimport { levelSelector } from \"@/store/subscription.ts\";\nimport { teenagerSelector } from \"@/store/package.ts\";\nimport { selectAuthenticated } from \"@/store/auth.ts\";\nimport { docsEndpoint } from \"@/conf/env.ts\";\nimport { goAuth } from \"@/utils/app.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { includingModelFromPlan } from \"@/conf/subscription.tsx\";\nimport { subscriptionDataSelector } from \"@/store/globals.ts\";\nimport {\n  ChargeBaseProps,\n  nonBilling,\n  timesBilling,\n  tokenBilling,\n} from \"@/admin/charge.ts\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport router from \"@/router.tsx\";\nimport ModelAvatar from \"@/components/ModelAvatar.tsx\";\nimport { ToggleGroup } from \"@radix-ui/react-toggle-group\";\nimport { marketTags } from \"@/admin/market.ts\";\nimport { ToggleGroupItem } from \"@/components/ui/toggle-group.tsx\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { toast } from \"sonner\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport Tips from \"@/components/Tips\";\nimport Icon from \"@/components/utils/Icon\";\n\nconst tagIcons: { [key: string]: React.ReactNode } = {\n  official: <Award />,\n  \"multi-modal\": <EyeIcon />,\n  web: <Globe />,\n  \"high-quality\": <Sparkles />,\n  \"high-price\": <DollarSign />,\n  \"open-source\": <Github />,\n  \"image-generation\": <Image />,\n  fast: <Bolt />,\n  unstable: <Snail />,\n  \"high-context\": <Cpu />,\n  free: <Zap />,\n};\n\nconst notDisplayTags = [\"official\", \"fast\", \"unstable\", \"free\"];\n\ntype SearchBarProps = {\n  text: string;\n  onTextChange: (value: string) => void;\n  tags: string[];\n  onTagsChange: (value: string[]) => void;\n  displayPricing: boolean;\n  onDisplayPricingChange: (value: boolean) => void;\n  show1mPricing: boolean;\n  onShow1mPricingChange: (value: boolean) => void;\n};\n\nfunction getTags(model: Model): string[] {\n  let raw = model.tag || [];\n\n  if (model.free && !raw.includes(\"free\")) raw = [\"free\", ...raw];\n  if (!model.free && raw.includes(\"free\"))\n    raw = raw.filter((tag) => tag !== \"free\");\n  if (model.high_context && !raw.includes(\"high-context\"))\n    raw = [\"high-context\", ...raw];\n\n  return raw;\n}\n\nfunction SearchBar({\n  text,\n  onTextChange,\n  tags,\n  onTagsChange,\n  displayPricing,\n  onDisplayPricingChange,\n  show1mPricing,\n  onShow1mPricingChange,\n}: SearchBarProps) {\n  const { t } = useTranslation();\n\n  const supportModels = useSelector(selectSupportModels);\n  const availableTags = useMemo(\n    () =>\n      marketTags.filter((tag) =>\n        supportModels.some((model) => getTags(model).includes(tag)),\n      ),\n    [],\n  );\n\n  return (\n    <motion.div\n      className={`flex flex-col search-bar-wrapper`}\n      initial={{ opacity: 0, y: -20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.3 }}\n    >\n      <div className={`option-bar flex flex-row mb-2 items-center`}>\n        <div className={`grow`} />\n        <Label>{t(\"market.show-pricing\")}</Label>\n        <Switch\n          checked={displayPricing}\n          onCheckedChange={onDisplayPricingChange}\n          className={`ml-1.5 scale-90`}\n        />\n\n        {displayPricing && (\n          <>\n            <Label className={`ml-2`}>K/M</Label>\n            <Switch\n              checked={show1mPricing}\n              onCheckedChange={onShow1mPricingChange}\n              className={`ml-1.5 scale-90`}\n            />\n          </>\n        )}\n      </div>\n      <div className={`search-bar`}>\n        <Search size={16} className={`search-icon`} />\n        <Input\n          placeholder={t(\"market.search\")}\n          className={`input-box`}\n          value={text}\n          onChange={(e) => onTextChange(e.target.value)}\n        />\n        <X\n          size={16}\n          className={cn(\"clear-icon\", text.length > 0 && \"active\")}\n          onClick={() => onTextChange(\"\")}\n        />\n      </div>\n      <motion.div\n        className={`tags-search-area`}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ delay: 0.2, duration: 0.3 }}\n      >\n        <ToggleGroup\n          type={`multiple`}\n          value={tags}\n          onValueChange={onTagsChange}\n          className={`flex flex-row flex-wrap justify-center`}\n        >\n          {availableTags.map((tag, index) => (\n            <motion.div\n              key={index}\n              whileHover={{ scale: 1.05 }}\n              whileTap={{ scale: 0.95 }}\n            >\n              <ToggleGroupItem value={tag} variant={`outline`} size={`col`}>\n                {tagIcons[tag] && (\n                  <Icon icon={tagIcons[tag]} className={`w-3.5 h-3.5 mr-1`} />\n                )}\n                {t(`tag.${tag}`)}\n              </ToggleGroupItem>\n            </motion.div>\n          ))}\n        </ToggleGroup>\n      </motion.div>\n    </motion.div>\n  );\n}\n\ntype ModelProps = React.DetailedHTMLProps<\n  React.HTMLAttributes<HTMLDivElement>,\n  HTMLDivElement\n> & {\n  model: Model;\n  className?: string;\n  style?: React.CSSProperties;\n  forwardRef?: React.Ref<HTMLDivElement>;\n  showPricing?: boolean;\n  show1mPricing?: boolean;\n  index: number;\n};\n\ntype PriceColumnProps = ChargeBaseProps & {\n  pro: boolean;\n  anonymous?: boolean;\n  show1mPricing?: boolean;\n};\n\nfunction PriceColumn({\n  type,\n  input,\n  output,\n  pro,\n  show1mPricing,\n}: PriceColumnProps) {\n  const { t } = useTranslation();\n\n  const unitName = !show1mPricing ? \"1K TOKENS\" : \"1M TOKENS\";\n  const unitValue = !show1mPricing ? 1 : 1000;\n\n  const className = cn(\n    \"flex flex-row text-sm items-center px-2 pr-1 py-1 w-full rounded-md border transition-all\",\n    pro && \"pro\",\n  );\n\n  const iconClassName =\n    \"h-4 w-4 scale-110 mr-2 p-0.5 rounded-full bg-primary/5\";\n\n  switch (type) {\n    case nonBilling:\n      return (\n        <motion.div\n          className={cn(className, \"bg-secondary/5 hover:bg-secondary/10\")}\n          whileHover={{ scale: 1.02 }}\n          transition={{ type: \"spring\", stiffness: 300 }}\n        >\n          <Cloud className={iconClassName} />\n          <span className=\"flex-grow\">{t(\"tag.badges.non-billing\")}</span>\n          <span className=\"text-2xs ml-1 px-1.5 bg-input/40 select-none rounded-sm\">\n            FREE\n          </span>\n        </motion.div>\n      );\n    case timesBilling:\n      return (\n        <motion.div\n          className={cn(className, \"bg-secondary/5 hover:bg-secondary/10\")}\n          whileHover={{ scale: 1.02 }}\n          transition={{ type: \"spring\", stiffness: 300 }}\n        >\n          <Cloud className={iconClassName} />\n          <span className=\"flex-grow\">\n            {t(\"tag.badges.times-billing\", { price: output })}\n          </span>\n          <span className=\"text-2xs ml-1 px-1.5 bg-input/40 select-none rounded-sm\">\n            TIME\n          </span>\n        </motion.div>\n      );\n    case tokenBilling:\n      const inputValue = input * unitValue;\n      const outputValue = output * unitValue;\n\n      return (\n        <div className=\"grid grid-cols-2 gap-1\">\n          <motion.div\n            className={cn(className, \"bg-secondary/5 hover:bg-secondary/10\")}\n            whileHover={{ scale: 1.02 }}\n            transition={{ type: \"spring\", stiffness: 300 }}\n          >\n            <ArrowUpFromDot className={iconClassName} />\n            <span className=\"flex-grow\">{inputValue}</span>\n            <span className=\"text-2xs ml-1 px-1.5 bg-input/40 select-none rounded-sm\">\n              {unitName}\n            </span>\n          </motion.div>\n          <motion.div\n            className={cn(className, \"bg-secondary/5 hover:bg-secondary/10\")}\n            whileHover={{ scale: 1.02 }}\n            transition={{ type: \"spring\", stiffness: 300 }}\n          >\n            <ArrowDownToDot className={iconClassName} />\n            <span className=\"flex-grow\">{outputValue}</span>\n            <span className=\"text-2xs ml-1 px-1.5 bg-input/40 select-none rounded-sm\">\n              {unitName}\n            </span>\n          </motion.div>\n        </div>\n      );\n  }\n}\n\nfunction ModelItem({\n  model,\n  className,\n  style,\n  forwardRef,\n  showPricing,\n  show1mPricing,\n  index,\n  ...props\n}: ModelProps) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const list = useSelector(selectModelList);\n  const current = useSelector(selectModel);\n\n  const level = useSelector(levelSelector);\n  const student = useSelector(teenagerSelector);\n  const auth = useSelector(selectAuthenticated);\n\n  const subscriptionData = useSelector(subscriptionDataSelector);\n\n  const state = useMemo(() => list.includes(model.id), [model, current, list]);\n\n  const pro = useMemo(() => {\n    return includingModelFromPlan(subscriptionData, level, model.id);\n  }, [subscriptionData, model, level, student]);\n\n  const tags = useMemo(\n    (): string[] => getTags(model).filter((tag) => tag !== \"free\"),\n    [model],\n  );\n\n  return (\n    <motion.div\n      className={cn(\"model-item rounded-md\", className)}\n      style={style} //@ts-ignore\n      ref={forwardRef}\n      {...props}\n      onClick={() => {\n        if (!auth && model.auth) {\n          toast(t(\"login-require\"), {\n            action: {\n              label: t(\"login\"),\n              onClick: goAuth,\n            },\n          });\n          return;\n        }\n\n        dispatch(setModel(model.id));\n        router.navigate(\"/\");\n\n        toast.info(t(\"market.switch-model\"), {\n          description: (\n            <div\n              className={`inline-flex flex-row items-center flex-wrap space-x-1`}\n            >\n              <ArrowRightLeft className={`w-3 h-3`} />\n              <p>{t(\"market.switch-model-desc\")}</p>\n              <ModelAvatar size={20} model={model} />\n              <p>{model.name}</p>\n            </div>\n          ),\n        });\n      }}\n      initial={{ opacity: 0, x: -50 }}\n      animate={{ opacity: 1, x: 0 }}\n      transition={{ duration: 0.5, delay: index * 0.1 }}\n      whileHover={{ scale: 1.05 }}\n    >\n      <motion.div\n        className={`model-info-wrapper w-full h-max flex flex-row`}\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.3, delay: index * 0.1 + 0.2 }}\n      >\n        <div\n          className={`model-info flex flex-row items-center flex-wrap w-full mt-1 ml-1`}\n        >\n          <motion.div\n            className={`model-avatar-wrapper mr-1.5 -translate-x-2 -translate-y-2 flex w-max h-max border rounded-full`}\n            whileHover={{ scale: 1.1, rotate: 360 }}\n            whileTap={{ scale: 0.9 }}\n            initial={{ opacity: 0, rotate: -180 }}\n            animate={{ opacity: 1, rotate: 0 }}\n            transition={{ duration: 0.5, delay: index * 0.1 + 0.4 }}\n          >\n            <ModelAvatar className={`model-avatar`} model={model} size={24} />\n          </motion.div>\n          <div className={\"flex flex-row items-center model-name mr-2\"}>\n            {model.name}\n          </div>\n          {/* <Tips\n            content={model.id}\n            trigger={<Tag className={`w-5 h-5 p-1 bg-primary/5 rounded-sm`} />}\n          /> */}\n          {pro && (\n            <Tips\n              content={t(\"tag.badges.plan-included-tip\")}\n              trigger={\n                <Gem\n                  className={`w-5 h-5 p-1 rounded-sm ml-1 text-amber-600 bg-amber-500/20`}\n                />\n              }\n            />\n          )}\n          {tags\n            .filter((tag) => !notDisplayTags.includes(tag))\n            .map((tag, index) => (\n              <Tips\n                key={index}\n                content={t(`tag.${tag}`)}\n                trigger={\n                  tagIcons[tag] ? (\n                    <Icon\n                      icon={tagIcons[tag]}\n                      className={cn(\n                        `w-5 h-5 p-1 rounded-sm ml-1 bg-primary/5`,\n                        {\n                          \"text-amber-600 bg-amber-500/20\": tag === \"official\",\n                          \"text-blue-600 bg-blue-500/20\": tag === \"multi-modal\",\n                          \"text-green-600 bg-green-500/20\": tag === \"web\",\n                          \"text-purple-600 bg-purple-500/20\":\n                            tag === \"high-quality\",\n                          \"text-red-600 bg-red-500/20\": tag === \"high-price\",\n                          \"text-gray-600 bg-gray-500/20\": tag === \"open-source\",\n                          \"text-indigo-600 bg-indigo-500/20\":\n                            tag === \"image-generation\",\n                          \"text-yellow-600 bg-yellow-500/20\": tag === \"fast\",\n                          \"text-orange-600 bg-orange-500/20\":\n                            tag === \"unstable\",\n                          \"text-teal-600 bg-teal-500/20\":\n                            tag === \"high-context\",\n                          \"text-emerald-600 bg-emerald-500/20\": tag === \"free\",\n                        },\n                      )}\n                    />\n                  ) : undefined\n                }\n              />\n            ))}\n        </div>\n      </motion.div>\n      <motion.p\n        className={`model-description text-sm my-1.5 ml-1`}\n        initial={{ opacity: 0, y: 20 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.3, delay: index * 0.1 + 0.5 }}\n      >\n        <div className=\"px-1.5 py-0.5 bg-primary/5 border rounded-md inline-block mr-1 text-xs text-muted-foreground\">\n          <Tag className={`w-3 h-3 scale-90 mr-1 inline`} />\n          {model.id}\n        </div>\n        {model.description}\n      </motion.p>\n\n      <div className={`flex-grow`} />\n      {showPricing && model.price && (\n        <motion.div\n          className={`mt-2.5`}\n          initial={{ opacity: 0, y: 0 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.3, delay: index * 0.1 + 0.6 }}\n        >\n          <PriceColumn\n            type={model.price.type}\n            input={model.price.input}\n            output={model.price.output}\n            pro={pro}\n            show1mPricing={show1mPricing}\n            anonymous={true}\n          />\n        </motion.div>\n      )}\n\n      <div className=\"flex flex-row mt-1.5\">\n        <div className=\"flex-grow\" />\n        <motion.span\n          className={`clickable w-fit h-fit p-1 border hover:border-hover transition-all rounded-md`}\n          whileHover={{ scale: 1.05 }}\n          whileTap={{ scale: 0.95 }}\n          onClick={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            dispatch(\n              state ? removeModelList(model.id) : addModelList(model.id),\n            );\n\n            toast.info(t(\"market.switch-bookmark\"), {\n              description: (\n                <div\n                  className={`inline-flex flex-row items-center flex-wrap space-x-1 space-y-1`}\n                >\n                  <p className={`translate-y-[1px]`}>\n                    {state\n                      ? t(\"market.remove-bookmark\")\n                      : t(\"market.add-bookmark\")}\n                  </p>\n                  <ModelAvatar size={20} model={model} />\n                  <p>{model.name}</p>\n                </div>\n              ),\n            });\n          }}\n        >\n          {state ? (\n            <Star className={`w-4 h-4 shrink-0 fill-current text-amber-500`} />\n          ) : (\n            <Star className={`w-4 h-4 shrink-0 text-muted-foreground`} />\n          )}\n        </motion.span>\n      </div>\n    </motion.div>\n  );\n}\n\ntype MarketPlaceProps = {\n  search: string;\n  showPricing: boolean;\n  show1mPricing: boolean;\n};\n\nfunction MarketPlace({ search, showPricing, show1mPricing }: MarketPlaceProps) {\n  const { t } = useTranslation();\n  const select = useSelector(selectModel);\n  const supportModels = useSelector(selectSupportModels);\n\n  const models = useMemo(() => {\n    if (search.length === 0) return supportModels;\n    // fuzzy search\n    const raw = splitList(search.toLowerCase(), [\" \", \",\", \";\", \"-\"]);\n    return supportModels.filter((model) => {\n      const name = model.name.toLowerCase();\n\n      const tag = getTags(model);\n\n      const tag_translated_name = tag\n        .map((item) => t(`tag.${item}`))\n        .join(\" \")\n        .toLowerCase();\n      const id = model.id.toLowerCase();\n\n      return raw.every(\n        (item) =>\n          name.includes(item) ||\n          tag_translated_name.includes(item) ||\n          id.includes(item),\n      );\n    });\n  }, [supportModels, search]);\n\n  return (\n    <motion.div\n      className={`model-list`}\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={{ duration: 0.5 }}\n    >\n      <AnimatePresence>\n        {models.map((model, index) => (\n          <ModelItem\n            key={index}\n            model={model}\n            className={cn(select === model.id && \"active\")}\n            showPricing={showPricing}\n            show1mPricing={show1mPricing}\n            index={index}\n          />\n        ))}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\nfunction MarketHeader() {\n  const { t } = useTranslation();\n\n  return (\n    <motion.div\n      className={`market-header`}\n      initial={{ opacity: 0, y: -20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5 }}\n    >\n      <div\n        className={`title select-none text-center text-primary font-bold flex flex-row items-center justify-center`}\n      >\n        <motion.div\n          className={`header-bar`}\n          initial={{ width: 0 }}\n          animate={{ width: \"0.75rem\" }}\n          transition={{ duration: 0.5, delay: 0.2 }}\n        />\n        {t(\"market.explore\")}\n        <motion.div\n          className={`header-bar reverse`}\n          initial={{ width: 0 }}\n          animate={{ width: \"0.75rem\" }}\n          transition={{ duration: 0.5, delay: 0.2 }}\n        />\n      </div>\n    </motion.div>\n  );\n}\n\nfunction MarketFooter() {\n  const { t } = useTranslation();\n\n  return (\n    <motion.div\n      className={`market-footer`}\n      initial={{ opacity: 0, y: 20 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5 }}\n    >\n      <motion.a\n        href={docsEndpoint}\n        target={`_blank`}\n        whileHover={{ scale: 1.05 }}\n        whileTap={{ scale: 0.95 }}\n      >\n        <Link size={14} className={`mr-1`} />\n        {t(\"pricing\")}\n      </motion.a>\n    </motion.div>\n  );\n}\n\nfunction Model() {\n  const { t } = useTranslation();\n  const [displayPricing, setDisplayPricing] = useState<boolean>(true);\n  const [show1mPricing, setShow1mPricing] = useState<boolean>(false);\n  const [searchText, setSearchText] = useState<string>(\"\");\n  const [searchTags, setSearchTags] = useState<string[]>([]);\n\n  const search = useMemo(() => {\n    return [\n      searchText,\n      ...searchTags.filter((tag) => tag !== \"\").map((v) => t(`tag.${v}`)),\n    ].join(\" \");\n  }, [searchText, searchTags]);\n\n  return (\n    <ScrollArea className={`model-market`}>\n      <motion.div\n        className={`market-wrapper`}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ duration: 0.5 }}\n      >\n        <motion.div\n          className=\"absolute inset-0 overflow-hidden pointer-events-none\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={{ duration: 1 }}\n        >\n          {[...Array(50)].map((_, i) => (\n            <motion.div\n              key={i}\n              className=\"absolute bg-primary/10 rounded-full\"\n              style={{\n                width: Math.random() * 4 + 1 + \"px\",\n                height: Math.random() * 4 + 1 + \"px\",\n                top: Math.random() * 100 + \"%\",\n                left: Math.random() * 100 + \"%\",\n              }}\n              animate={{\n                y: [0, Math.random() * 100 - 50],\n                x: [0, Math.random() * 100 - 50],\n                opacity: [0.7, 0],\n              }}\n              transition={{\n                duration: Math.random() * 10 + 10,\n                repeat: Infinity,\n                ease: \"linear\",\n              }}\n            />\n          ))}\n        </motion.div>\n        <MarketHeader />\n        <SearchBar\n          text={searchText}\n          onTextChange={setSearchText}\n          tags={searchTags}\n          onTagsChange={setSearchTags}\n          displayPricing={displayPricing}\n          onDisplayPricingChange={setDisplayPricing}\n          show1mPricing={show1mPricing}\n          onShow1mPricingChange={setShow1mPricing}\n        />\n        <MarketPlace\n          search={search}\n          showPricing={displayPricing}\n          show1mPricing={show1mPricing}\n        />\n        <MarketFooter />\n      </motion.div>\n    </ScrollArea>\n  );\n}\n\nexport default Model;\n"
  },
  {
    "path": "app/src/routes/NotFound.tsx",
    "content": "import \"@/assets/common/404.less\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { HelpCircle } from \"lucide-react\";\nimport router from \"@/router.tsx\";\nimport { useTranslation } from \"react-i18next\";\n\nfunction NotFound() {\n  const { t } = useTranslation();\n\n  return (\n    <div className={`error-page`}>\n      <HelpCircle className={`icon`} />\n      <h1>404</h1>\n      <p>{t(\"not-found\")}</p>\n      <Button onClick={() => router.navigate(\"/\")}>{t(\"home\")}</Button>\n    </div>\n  );\n}\n\nexport default NotFound;\n"
  },
  {
    "path": "app/src/routes/Register.tsx",
    "content": "import { Card, CardContent } from \"@/components/ui/card.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport Require, {\n  EmailRequire,\n  LengthRangeRequired,\n  SameRequired,\n} from \"@/components/Require.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport router from \"@/router.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { formReducer, isEmailValid, isTextInRange } from \"@/utils/form.ts\";\nimport React, { useReducer, useState } from \"react\";\nimport { doRegister, RegisterForm, sendCode } from \"@/api/auth.ts\";\nimport TickButton from \"@/components/TickButton.tsx\";\nimport { validateToken } from \"@/store/auth.ts\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { appLogo, appName } from \"@/conf/env.ts\";\nimport { infoMailSelector } from \"@/store/info.ts\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { toast } from \"sonner\";\n\ntype CompProps = {\n  form: RegisterForm;\n  dispatch: React.Dispatch<any>;\n  next: boolean;\n  setNext: (next: boolean) => void;\n};\n\nfunction Preflight({ form, dispatch, setNext }: CompProps) {\n  const { t } = useTranslation();\n\n  const onSubmit = () => {\n    if (\n      !isTextInRange(form.username, 2, 24) ||\n      !isTextInRange(form.password, 6, 36) ||\n      form.password.trim() !== form.repassword.trim()\n    )\n      return;\n\n    setNext(true);\n  };\n\n  return (\n    <div className={`auth-wrapper`}>\n      <Label>\n        <Require />\n        {t(\"auth.username\")}\n        <LengthRangeRequired\n          content={form.username}\n          min={2}\n          max={24}\n          hideOnEmpty={true}\n        />\n      </Label>\n      <Input\n        placeholder={t(\"auth.username-placeholder\")}\n        value={form.username}\n        onChange={(e) =>\n          dispatch({\n            type: \"update:username\",\n            payload: e.target.value,\n          })\n        }\n      />\n\n      <Label>\n        <Require />\n        {t(\"auth.password\")}\n        <LengthRangeRequired\n          content={form.password}\n          min={6}\n          max={36}\n          hideOnEmpty={true}\n        />\n      </Label>\n      <Input\n        placeholder={t(\"auth.password-placeholder\")}\n        value={form.password}\n        type={\"password\"}\n        onChange={(e) =>\n          dispatch({\n            type: \"update:password\",\n            payload: e.target.value,\n          })\n        }\n      />\n\n      <Label>\n        <Require />\n        {t(\"auth.check-password\")}\n        <SameRequired\n          content={form.password}\n          compare={form.repassword}\n          hideOnEmpty={true}\n        />\n      </Label>\n      <Input\n        placeholder={t(\"auth.check-password-placeholder\")}\n        value={form.repassword}\n        type={\"password\"}\n        onChange={(e) =>\n          dispatch({\n            type: \"update:repassword\",\n            payload: e.target.value,\n          })\n        }\n      />\n\n      <Button\n        tapScale={0.975}\n        classNameWrapper={`mt-2`}\n        className={`w-full`}\n        onClick={onSubmit}\n      >\n        {t(\"auth.next-step\")}\n      </Button>\n    </div>\n  );\n}\n\nfunction doFormat(form: RegisterForm): RegisterForm {\n  return {\n    ...form,\n    username: form.username.trim(),\n    password: form.password.trim(),\n    repassword: form.repassword.trim(),\n    email: form.email.trim(),\n    code: form.code.trim(),\n  };\n}\n\nfunction Verify({ form, dispatch, setNext }: CompProps) {\n  const { t } = useTranslation();\n  const globalDispatch = useDispatch();\n\n  const mail = useSelector(infoMailSelector);\n\n  const onSubmit = async () => {\n    const data = doFormat(form);\n\n    if (!isEmailValid(data.email)) return;\n    if (mail && data.code.trim().length === 0) return;\n\n    const resp = await doRegister(data);\n    if (!resp.status) {\n      toast.error(t(\"error\"), {\n        description: resp.error,\n      });\n      return;\n    }\n\n    toast.success(t(\"auth.register-success\"), {\n      description: t(\"auth.register-success-prompt\"),\n    });\n\n    validateToken(globalDispatch, resp.token);\n    await router.navigate(\"/\");\n  };\n\n  const onVerify = async () => await sendCode(t, form.email, true);\n\n  return (\n    <div className={`auth-wrapper`}>\n      <Label>\n        <Require />\n        {t(\"auth.email\")}\n        <EmailRequire content={form.email} hideOnEmpty={true} />\n      </Label>\n      <Input\n        placeholder={t(\"auth.email-placeholder\")}\n        value={form.email}\n        onChange={(e) =>\n          dispatch({\n            type: \"update:email\",\n            payload: e.target.value,\n          })\n        }\n      />\n\n      <Label>\n        <Require /> {t(\"auth.code\")}\n      </Label>\n\n      <div className={`flex flex-row`}>\n        <Input\n          disabled={!mail}\n          placeholder={\n            mail\n              ? t(\"auth.code-placeholder\")\n              : t(\"auth.code-disabled-placeholder\")\n          }\n          value={form.code}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:code\",\n              payload: e.target.value,\n            })\n          }\n        />\n        <TickButton\n          className={`ml-2 whitespace-nowrap`}\n          loading={true}\n          onClick={onVerify}\n          tick={60}\n          disabled={!mail}\n        >\n          {t(\"auth.send-code\")}\n        </TickButton>\n      </div>\n\n      <Button\n        tapScale={0.975}\n        classNameWrapper={`mt-2`}\n        className={`w-full`}\n        loading={true}\n        onClick={onSubmit}\n      >\n        {t(\"register\")}\n      </Button>\n\n      <div className={`mt-1 translate-y-1 text-center text-sm`}>\n        {t(\"auth.incorrect-info\")}\n        <a\n          className={`underline underline-offset-4 cursor-pointer`}\n          onClick={() => setNext(false)}\n        >\n          {t(\"auth.fall-back\")}\n        </a>\n      </div>\n    </div>\n  );\n}\n\nfunction Register() {\n  const { t } = useTranslation();\n  const [next, setNext] = useState(false);\n  const [form, dispatch] = useReducer(formReducer<RegisterForm>(), {\n    username: \"\",\n    password: \"\",\n    repassword: \"\",\n    email: \"\",\n    code: \"\",\n  });\n\n  return (\n    <ScrollArea className={`w-full h-full grid place-items-center`}>\n      <div className={`auth-container`}>\n        <img className={`logo`} src={appLogo} alt=\"\" />\n        <div className={`title`}>\n          {t(\"register\")} {appName}\n        </div>\n        <Card className={`auth-card`}>\n          <CardContent className={`pb-0`}>\n            {!next ? (\n              <Preflight\n                form={form}\n                dispatch={dispatch}\n                next={next}\n                setNext={setNext}\n              />\n            ) : (\n              <Verify\n                form={form}\n                dispatch={dispatch}\n                next={next}\n                setNext={setNext}\n              />\n            )}\n          </CardContent>\n        </Card>\n        <div className={`auth-card addition-wrapper`}>\n          <div className={`row`}>\n            {t(\"auth.have-account\")}\n            <a className={`link`} onClick={() => router.navigate(\"/login\")}>\n              {t(\"auth.login\")}\n            </a>\n          </div>\n          <div className={`row`}>\n            {t(\"auth.forgot-password\")}\n            <a className={`link`} onClick={() => router.navigate(\"/forgot\")}>\n              {t(\"auth.reset-password\")}\n            </a>\n          </div>\n        </div>\n      </div>\n    </ScrollArea>\n  );\n}\n\nexport default Register;\n"
  },
  {
    "path": "app/src/routes/Sharing.tsx",
    "content": "import \"@/assets/pages/sharing.less\";\nimport { useParams } from \"react-router-dom\";\nimport { viewConversation, ViewData, ViewForm } from \"@/api/sharing.ts\";\nimport { saveImageAsFile } from \"@/utils/dom.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { useRef, useState } from \"react\";\nimport {\n  ArrowUp,\n  Clock,\n  Image,\n  Loader2,\n  Maximize,\n  MessagesSquare,\n  Minimize,\n  Newspaper,\n  RssIcon,\n  Undo2,\n} from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nimport MessageSegment from \"@/components/Message.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport router from \"@/router.tsx\";\nimport { Message } from \"@/api/types.tsx\";\nimport Avatar from \"@/components/Avatar.tsx\";\nimport { toJpeg } from \"html-to-image\";\nimport { appLogo, appName } from \"@/conf/env.ts\";\nimport { extractMessage } from \"@/utils/processor.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { isMobile, useMobile } from \"@/utils/device.ts\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { useConversationActions } from \"@/store/chat.ts\";\nimport { toast } from \"sonner\";\nimport Emoji from \"@/components/Emoji\";\n\ntype SharingFormProps = {\n  refer?: string;\n  data: ViewData | null;\n};\n\nfunction SharingForm({ data }: SharingFormProps) {\n  if (data === null) return null;\n\n  const { t } = useTranslation();\n  const mobile = useMobile();\n  const { mask: setMask, selected: setModel } = useConversationActions();\n  const [maximized, setMaximized] = useState(isMobile());\n  const container = useRef<HTMLDivElement>(null);\n  const date = new Date(data.time);\n  const time = `${\n    date.getMonth() + 1\n  }-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;\n\n  const saveImage = async () => {\n    toast.info(t(\"message.saving-image-prompt\"), {\n      description: t(\"message.saving-image-prompt-desc\"),\n    });\n\n    setTimeout(() => {\n      if (!container.current) return;\n      toJpeg(container.current)\n        .then((blob) => {\n          saveImageAsFile(`${extractMessage(data.name, 12)}.png`, blob);\n          toast.success(t(\"message.saving-image-success\"), {\n            description: t(\"message.saving-image-success-prompt\"),\n          });\n        })\n        .catch((reason) => {\n          toast.error(t(\"message.saving-image-failed\"), {\n            description: t(\"message.saving-image-failed-prompt\", { reason }),\n          });\n        });\n    }, 10);\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative flex flex-col w-full h-full overflow-hidden transition-all duration-300 sm:p-4 md:p-6 mx-auto ease-out\",\n        maximized ? \"max-w-full\" : \"max-w-4xl\",\n      )}\n    >\n      <div className=\"absolute opacity-0 pointer-events-none z-[-999] h-max w-full\">\n        <div className=\"bg-background p-6\" ref={container}>\n          <div className=\"border border-border rounded-lg overflow-hidden\">\n            <div className=\"flex items-center justify-between p-4 border-b border-border\">\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center\">\n                  <Newspaper className=\"w-4 h-4 mr-2 text-muted-foreground\" />\n                  <span className=\"text-sm font-medium text-muted-foreground mr-2\">\n                    {t(\"message.sharing.title\")}:\n                  </span>\n                  <span className=\"text-sm font-semibold\">\n                    {mobile ? extractMessage(data.name, 10) : data.name}\n                  </span>\n                </div>\n                <div className=\"flex items-center\">\n                  <Clock className=\"w-4 h-4 mr-2 text-muted-foreground\" />\n                  <span className=\"text-sm font-medium text-muted-foreground mr-2\">\n                    {t(\"message.sharing.time\")}:\n                  </span>\n                  <span className=\"text-sm\">{time}</span>\n                </div>\n                <div className=\"flex items-center\">\n                  <MessagesSquare className=\"w-4 h-4 mr-2 text-muted-foreground\" />\n                  <span className=\"text-sm font-medium text-muted-foreground mr-2\">\n                    {t(\"message.sharing.message\")}:\n                  </span>\n                  <span className=\"text-sm\">{data.messages.length}</span>\n                </div>\n              </div>\n              <div className=\"flex flex-col items-end\">\n                <img className=\"w-12 h-12 mb-2\" src={appLogo} alt=\"\" />\n                <div className=\"flex items-center\">\n                  <Avatar username={data.username} className=\"w-8 h-8 mr-2\" />\n                  <span className=\"text-sm font-semibold\">{data.username}</span>\n                </div>\n              </div>\n            </div>\n            <div className=\"p-4 space-y-4\">\n              {data.messages.map((message, i) => (\n                <MessageSegment\n                  message={message}\n                  key={i}\n                  index={i}\n                  username={data.username}\n                />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"relative z-5 flex flex-col w-full h-full bg-background border sm:rounded-md md:rounded-lg\">\n        <header className=\"flex items-center flex-col p-4 pb-2 border-b bg-muted/25\">\n          <div className=\"flex items-center flex-row w-full h-fit\">\n            <img src={appLogo} alt=\"\" className=\"w-8 h-8 shrink-0 mr-2\" />\n            <span className=\"font-semibold text-lg\">{appName}</span>\n            <div className=\"flex-grow\" />\n            <Button\n              className=\"flex flex-row items-center\"\n              variant=\"outline\"\n              onClick={() => router.navigate(\"/\")}\n            >\n              <ArrowUp className=\"w-4 h-4 mr-1.5\" />\n              {t(\"home\")}\n            </Button>\n          </div>\n\n          <div className=\"flex flex-row items-center w-full mt-3 px-1\">\n            <span className=\"text-sm font-semibold select-none mr-1.5 shrink-0\">\n              @{data.username}\n            </span>\n            <span className=\"text-sm text-secondary truncate flex-grow items-center\">\n              {data.name}\n            </span>\n            <span className=\"text-sm text-muted-foreground flex flex-row items-center shrink-0\">\n              <RssIcon className=\"w-4 h-4 mr-0.5\" /> {time}\n            </span>\n          </div>\n        </header>\n        <ScrollArea className=\"flex-grow\">\n          <div className=\"p-4 md:p-6 space-y-4\">\n            {data.messages.map((message, i) => (\n              <MessageSegment\n                message={message}\n                key={i}\n                index={i}\n                username={data.username}\n              />\n            ))}\n          </div>\n        </ScrollArea>\n        <footer className=\"flex items-center justify-between p-4 border-t border-border bg-muted/25\">\n          <div className=\"flex space-x-2 ml-auto md:ml-0\">\n            <Button variant=\"outline\" onClick={saveImage}>\n              <Image className=\"h-4 w-4 mr-2\" />\n              {t(\"message.save-image\")}\n            </Button>\n            <Button\n              variant=\"outline\"\n              onClick={async () => {\n                const message: Message[] = data?.messages || [];\n                await Promise.all([\n                  new Promise<void>((resolve) => {\n                    setMask({\n                      avatar: \"\",\n                      name: data.name,\n                      context: message,\n                    });\n                    resolve();\n                  }),\n                  new Promise<void>((resolve) => {\n                    setModel(data?.model);\n                    resolve();\n                  })\n                ]);\n                console.debug(\n                  `[sharing] switch to conversation (name: ${data.name}, model: ${data.model})`,\n                );\n                router.navigate(\"/\", { replace: true });\n              }}\n            >\n              <MessagesSquare className=\"h-4 w-4 mr-2\" />\n              {t(\"message.use\")}\n            </Button>\n          </div>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            onClick={() => setMaximized(!maximized)}\n            className=\"ml-2 hidden md:flex\"\n          >\n            {maximized ? (\n              <Minimize className=\"h-4 w-4\" />\n            ) : (\n              <Maximize className=\"h-4 w-4\" />\n            )}\n          </Button>\n        </footer>\n      </div>\n    </div>\n  );\n}\n\nfunction Sharing() {\n  const { t } = useTranslation();\n  const { hash } = useParams();\n  const [setup, setSetup] = useState(false);\n  const [data, setData] = useState<ViewForm | null>(null);\n\n  const loading = data === null;\n\n  useEffectAsync(async () => {\n    if (!hash || setup) return;\n\n    setSetup(true);\n\n    const resp = await viewConversation(hash as string);\n    setData(resp);\n    if (!resp.status) console.debug(`[sharing] error: ${resp.message}`);\n  }, []);\n\n  return (\n    <div\n      className={cn(\n        \"w-full h-full bg-gradient-to-br from-background to-muted/50 overflow-hidden\",\n        loading && \"flex flex-row items-center justify-center\",\n      )}\n    >\n      {loading ? (\n        <div className={`animate-spin select-none`}>\n          <Loader2 className={`loader w-12 h-12`} />\n        </div>\n      ) : data.status ? (\n        <SharingForm refer={hash} data={data.data} />\n      ) : (\n        <div className=\"w-full h-full flex flex-col items-center justify-center text-center select-none\">\n          <div className=\"flex flex-col items-center px-2\">\n            <Emoji\n              emoji=\"1f47b\"\n              className=\"w-16 h-16 md:w-20 md:h-20 mb-2.5 md:mb-4 p-2 shadow bg-muted/10 rounded-md\"\n            />\n            <p className=\"text-2xl font-bold mb-3 text-foreground\">\n              {t(\"share.not-found\")}\n            </p>\n            <p className=\"text-base text-muted-foreground\">\n              {t(\"share.not-found-description\")}\n            </p>\n            <Button\n              className=\"mt-4 flex flex-row items-center\"\n              onClick={() => router.navigate(\"/\")}\n            >\n              <Undo2 className=\"w-4 h-4 mr-2\" />\n              {t(\"home\")}\n            </Button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default Sharing;\n"
  },
  {
    "path": "app/src/routes/Wallet.tsx",
    "content": "import \"@/assets/pages/quota.less\";\nimport \"@/assets/pages/subscription.less\";\nimport { ScrollArea } from \"@/components/ui/scroll-area.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  BadgeCheck,\n  BadgeMinus,\n  CalendarClock,\n  Crown,\n  ExternalLink,\n  InfoIcon,\n  Star,\n  Rocket,\n  Zap,\n} from \"lucide-react\";\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { docsEndpoint } from \"@/conf/env.ts\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { useMemo, useState } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { infoRelayPlanSelector, useCurrency } from \"@/store/info.ts\";\nimport {\n  expiredSelector,\n  isSubscribedSelector,\n  levelSelector,\n  usageSelector,\n  refreshSelector,\n} from \"@/store/subscription.ts\";\nimport { subscriptionDataSelector } from \"@/store/globals.ts\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useInView } from \"react-intersection-observer\";\nimport { getPlan, getPlanName } from \"@/conf/subscription.tsx\";\nimport { Upgrade } from \"@/components/home/subscription/UpgradePlan.tsx\";\nimport SubscriptionUsage from \"@/components/home/subscription/SubscriptionUsage.tsx\";\nimport WalletQuotaBox from \"@/routes/wallet/WalletQuotaBox.tsx\";\nimport ModelAvatar from \"@/components/ModelAvatar\";\nimport Icon from \"@/components/utils/Icon\";\nimport Tips from \"@/components/Tips\";\n\ntype PlanItemProps = {\n  level: number;\n  isYearly: boolean;\n};\n\nfunction PlanItem({ level, isYearly }: PlanItemProps) {\n  const { t } = useTranslation();\n  const current = useSelector(levelSelector);\n  const subscriptionData = useSelector(subscriptionDataSelector);\n  const { symbol } = useCurrency();\n  const [ref, inView] = useInView({\n    triggerOnce: true,\n    threshold: 0.1,\n  });\n\n  const plan = useMemo(\n    () => getPlan(subscriptionData, level),\n    [subscriptionData, level],\n  );\n  const name = useMemo(() => getPlanName(level), [level]);\n\n  const containerVariants = {\n    hidden: { opacity: 0, y: 50 },\n    visible: {\n      opacity: 1,\n      y: 0,\n      transition: {\n        duration: 0.5,\n        ease: \"easeOut\",\n        staggerChildren: 0.1,\n      },\n    },\n  };\n\n  const itemVariants = {\n    hidden: { opacity: 0, x: -20 },\n    visible: {\n      opacity: 1,\n      x: 0,\n      transition: { duration: 0.3, ease: \"easeOut\" },\n    },\n  };\n\n  const pricing = useMemo(() => {\n    let discount = 1.0;\n    if (isYearly) {\n      if (plan.discounts && plan.discounts[\"12\"] !== undefined) {\n        discount = plan.discounts[\"12\"];\n      } else {\n        discount = 0.8;\n      }\n    }\n    \n    const result = plan.price * discount;\n    if (result % 1 !== 0) {\n      return result.toFixed(1);\n    }\n    return result;\n  }, [plan, isYearly]);\n\n  return (\n    <motion.div\n      ref={ref}\n      className={cn(\"plan relative shadow border rounded-lg mb-4\", name)}\n      variants={containerVariants}\n      initial=\"hidden\"\n      animate={inView ? \"visible\" : \"hidden\"}\n    >\n      <AnimatePresence>\n        {level === 2 && (\n          <motion.div\n            className=\"absolute top-2 right-2 bg-primary text-primary-foreground text-xs font-bold py-1 px-2 rounded-full\"\n            initial={{ scale: 0, opacity: 0 }}\n            animate={{ scale: 1, opacity: 1 }}\n            exit={{ scale: 0, opacity: 0 }}\n            transition={{\n              duration: 0.3,\n              type: \"spring\",\n              stiffness: 500,\n              damping: 25,\n            }}\n          >\n            {t(\"sub.best-choice\")}\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <motion.div\n        whileHover={{ scale: 1.05 }}\n        whileTap={{ scale: 0.95 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 17 }}\n        className=\"w-fit mb-1 cursor-pointer\"\n      >\n        <Icon\n          icon={\n            level === 1 ? (\n              <Zap />\n            ) : level === 2 ? (\n              <Rocket />\n            ) : level === 3 ? (\n              <Crown />\n            ) : (\n              <Star />\n            )\n          }\n          className={cn(\"w-10 h-10 p-2 border-2 rounded-lg\", {\n            \"border-gold text-gold fill-gold/20 bg-gold/5\": level === 3,\n            \"border-primary text-primary fill-primary/20 bg-primary/5\":\n              level === 2,\n            \"border-amber-600 text-amber-600 fill-amber-600/20 bg-amber-600/5\":\n              level === 1,\n            \"border-secondary text-secondary fill-secondary/20 bg-secondary/5\":\n              level === 0,\n          })}\n        />\n      </motion.div>\n\n      <motion.div className={`font-bold text-md`} variants={itemVariants}>\n        {t(`sub.${name}`)}\n      </motion.div>\n\n      <motion.div\n        className={`price mb-2 w-full flex items-end`}\n        variants={itemVariants}\n      >\n        <span className=\"text-xl md:text-2xl\">{symbol}</span>\n        <motion.span\n          className=\"text-2xl md:text-3xl font-bold mr-0.5\"\n          initial={{ opacity: 0, y: 20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.5, delay: 0.2 }}\n        >\n          {pricing}\n        </motion.span>\n        <span className=\"text-sm font-medium\">/{t(\"sub.month\")}</span>\n\n        {(() => {\n          const plan = subscriptionData.find(p => p.level === level);\n          let discountPercent = 0;\n          \n          if (plan && plan.discounts && plan.discounts[\"12\"] !== undefined) {\n            discountPercent = Math.round((1 - plan.discounts[\"12\"]) * 100);\n          } else if (isYearly) {\n            discountPercent = 20;\n          }\n          \n          return discountPercent > 0 ? (\n            <motion.span\n              className=\"text-xs text-secondary ml-auto !text-[#55b467] bg-[#f4fdeb] border border-[#55b467]/20 cursor-pointer rounded-sm px-1 py-0.5\"\n              initial={{ opacity: 0, y: 20 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.5, delay: 0.2 }}\n              whileHover={{ scale: 1.05 }}\n              whileTap={{ scale: 0.95 }}\n            >\n              {t(\"sub.year-earn-tip\", { percent: `${discountPercent}%` })}\n            </motion.span>\n          ) : null;\n        })()}\n      </motion.div>\n\n      <Upgrade level={level} current={current} isYearly={isYearly} />\n\n      <motion.div\n        className={`flex flex-col mt-4 space-y-1.5`}\n        variants={containerVariants}\n      >\n        <motion.p\n          className=\"text-sm text-secondary flex items-center mb-1\"\n          variants={itemVariants}\n        >\n          {t(\"sub.including-model\")}\n          <Tips content={t(\"sub.including-model-tip\")} />\n        </motion.p>\n        {plan.items.map((item, index) => (\n          <motion.div\n            key={index}\n            className=\"flex items-center\"\n            variants={itemVariants}\n          >\n            <div className={`mr-1.5`}>\n              <ModelAvatar\n                model={{\n                  id: item.id,\n                  name: item.name,\n                  avatar: item.icon,\n                }}\n                size={24}\n              />\n            </div>\n            <p className=\"text-sm mr-auto\">{item.name}</p>\n            <p className=\"text-md font-medium\">\n              {item.value !== -1\n                ? t(\"sub.plan-item-usage\", { times: item.value })\n                : t(\"sub.plan-item-unlimited-usage\")}\n\n              {item.value !== -1 && (\n                <span className=\"text-xs text-secondary\">\n                  /{t(\"sub.month\")}\n                </span>\n              )}\n            </p>\n          </motion.div>\n        ))}\n      </motion.div>\n    </motion.div>\n  );\n}\n\nfunction WalletPlanBox() {\n  const { t } = useTranslation();\n  const subscription = useSelector(isSubscribedSelector);\n  const level = useSelector(levelSelector);\n  const expired = useSelector(expiredSelector);\n  const refresh = useSelector(refreshSelector);\n  const usage = useSelector(usageSelector);\n  const [isYearly, setIsYearly] = useState(true);\n  const subscriptionData = useSelector(subscriptionDataSelector);\n  const relayPlan = useSelector(infoRelayPlanSelector);\n\n  const plan = useMemo(\n    () => getPlan(subscriptionData, level),\n    [subscriptionData, level],\n  );\n\n  const planName = useMemo(() => getPlanName(level), [level]);\n  const isSubscribed = useMemo(\n    () => subscriptionData.length > 0 && level > 0,\n    [subscriptionData, level],\n  );\n\n  const enablePlanFlag = subscriptionData.length > 0;\n\n  if (!enablePlanFlag) {\n    return null;\n  }\n\n  const containerVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        duration: 0.1,\n        delay: 0.25,\n        when: \"beforeChildren\",\n        staggerChildren: 0.1,\n      },\n    },\n  };\n\n  const itemVariants = {\n    hidden: { opacity: 0, y: 20 },\n    visible: {\n      opacity: 1,\n      y: 0,\n      transition: { duration: 0.5 },\n    },\n  };\n\n  return (\n    <motion.div\n      className={`w-full h-max mt-0 border rounded-lg p-2.5 bg-background`}\n      id={`plan`}\n      variants={containerVariants}\n      initial=\"hidden\"\n      animate=\"visible\"\n    >\n      <motion.div variants={itemVariants}>\n        <motion.div\n          className=\"flex flex-col w-full p-2.5\"\n          variants={itemVariants}\n        >\n          <motion.div\n            className=\"text-xs text-secondary mb-1\"\n            variants={itemVariants}\n          >\n            {t(\"sub.dialog-title\")}\n          </motion.div>\n          <motion.div\n            className=\"text-2xl font-medium mb-1 flex items-center\"\n            variants={itemVariants}\n          >\n            <Icon\n              icon={isSubscribed ? <BadgeCheck /> : <BadgeMinus />}\n              className={cn(\n                \"h-6 w-6 mr-1.5\",\n                isSubscribed\n                  ? \"text-green-500 fill-green-500/20\"\n                  : \"text-muted-foreground fill-muted-foreground/20\",\n              )}\n            />\n            <p>{t(`sub.${planName}`)}</p>\n          </motion.div>\n\n          {!relayPlan && (\n            <motion.div\n              className={`text-xs text-secondary mt-auto break-all whitespace-pre-wrap`}\n              variants={itemVariants}\n            >\n              <InfoIcon className=\"h-3 w-3 inline-block mr-1\" />\n              {t(\"sub.plan-not-support-relay\")}\n            </motion.div>\n          )}\n          <motion.div\n            className={`text-xs text-secondary mt-auto break-all whitespace-pre-wrap`}\n            variants={itemVariants}\n          >\n            {t(\"buy.plan-info\")}\n            <a\n              href={docsEndpoint}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-block text-sky-500 hover:text-sky-600\"\n            >\n              <ExternalLink className=\"h-3.5 w-3.5 mr-0.5 ml-1 inline-block\" />\n              {t(\"buy.learn-more\")}\n            </a>\n          </motion.div>\n        </motion.div>\n\n        <motion.div className={`sub-wrapper px-2`} variants={itemVariants}>\n          {subscription && (\n            <Accordion\n              type=\"single\"\n              collapsible\n              defaultValue=\"sub-items\"\n              className={`w-full sub-row border rounded-lg mt-2 !px-0 !pt-0`}\n            >\n              <AccordionItem value=\"sub-items\" className=\"border-none w-full\">\n                <AccordionTrigger\n                  className={`w-full text-left justify-start pl-4 pr-6 bg-muted/25 border-b flex items-center`}\n                >\n                  <CalendarClock className=\"h-8 w-8 mr-2 stroke-[1.5] !rotate-0\" />\n                  <div className=\"ml-2 mr-auto\">\n                    <h3 className=\"text-sm mb-0.5\">{t(\"sub.quota-manage\")}</h3>\n                    <p className=\"text-xs text-secondary\">\n                      {t(\"sub.expired-days\", { days: expired })}\n                    </p>\n                    <p className=\"text-xs text-secondary\">\n                      {refresh > 0\n                        ? t(\"sub.refresh-days\", { refresh_days: refresh })\n                        : t(\"sub.get-refresh-days\")}\n                    </p>\n                  </div>\n                </AccordionTrigger>\n                <AccordionContent className=\"p-0 h-fit\">\n                  <div\n                    className={`sub-items-wrapper p-2 px-4 pt-4 w-full h-fit grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2.5 md:gap-x-4`}\n                  >\n                    {plan.items.map(\n                      (item, index) =>\n                        usage?.[item.id] && (\n                          <SubscriptionUsage\n                            name={item.name}\n                            usage={usage?.[item.id]}\n                            key={index}\n                          />\n                        ),\n                    )}\n                  </div>\n                </AccordionContent>\n              </AccordionItem>\n            </Accordion>\n          )}\n\n          <motion.div\n            className=\"w-fit mx-auto mt-4 mb-2.5\"\n            variants={itemVariants}\n          >\n            <Tabs\n              value={isYearly ? \"yearly\" : \"monthly\"}\n              onValueChange={(value) => setIsYearly(value === \"yearly\")}\n              className=\"w-full\"\n            >\n              <TabsList className=\"grid w-full grid-cols-2\">\n                <TabsTrigger value=\"yearly\">\n                  {t(\"sub.year-plan\")}\n                  {(() => {\n                    const firstPlan = subscriptionData.find(p => p.level > 0);\n                    let discountPercent = 20;\n                    \n                    if (firstPlan && firstPlan.discounts && firstPlan.discounts[\"12\"] !== undefined) {\n                      discountPercent = Math.round((1 - firstPlan.discounts[\"12\"]) * 100);\n                    }\n                    \n                    return discountPercent > 0 ? (\n                      <p className=\"text-xs text-secondary !text-[#55b467] !bg-[#f4fdeb] !border !border-[#55b467]/20 px-1 py-0.5 rounded-sm ml-2\">\n                        -{discountPercent}%\n                      </p>\n                    ) : null;\n                  })()}\n                </TabsTrigger>\n                <TabsTrigger value=\"monthly\">{t(\"sub.month-plan\")}</TabsTrigger>\n              </TabsList>\n            </Tabs>\n          </motion.div>\n          <motion.div className={`plan-wrapper`} variants={itemVariants}>\n            {subscriptionData.map((item, index) => (\n              <PlanItem key={index} level={item.level} isYearly={isYearly} />\n            ))}\n          </motion.div>\n        </motion.div>\n      </motion.div>\n    </motion.div>\n  );\n}\n\nfunction Wallet() {\n  return (\n    <ScrollArea className={`w-full h-full flex flex-col p-2 pr-4 bg-muted/25`}>\n      <div className={`w-full h-fit max-w-5xl mx-auto py-2 md:py-6`}>\n        <WalletQuotaBox />\n        <WalletPlanBox />\n      </div>\n    </ScrollArea>\n  );\n}\n\nexport default Wallet;\n"
  },
  {
    "path": "app/src/routes/admin/Broadcast.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport BroadcastTable from \"@/components/admin/assemblies/BroadcastTable.tsx\";\n\nfunction Broadcast() {\n  const { t } = useTranslation();\n  return (\n    <div className={`broadcast`}>\n      <Card className={`admin-card broadcast-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.broadcast\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <BroadcastTable />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Broadcast;\n"
  },
  {
    "path": "app/src/routes/admin/Channel.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport ChannelSettings from \"@/components/admin/ChannelSettings.tsx\";\n\nfunction Channel() {\n  const { t } = useTranslation();\n\n  return (\n    <div className={`channel`}>\n      <Card className={`admin-card channel-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.channel\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <ChannelSettings />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Channel;\n"
  },
  {
    "path": "app/src/routes/admin/Charge.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport ChargeWidget from \"@/components/admin/ChargeWidget.tsx\";\n\nfunction Charge() {\n  const { t } = useTranslation();\n\n  return (\n    <div className={`charge`}>\n      <Card className={`admin-card charge-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.prize\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <ChargeWidget />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Charge;\n"
  },
  {
    "path": "app/src/routes/admin/DashBoard.tsx",
    "content": "import InfoBox from \"@/components/admin/InfoBox.tsx\";\nimport ChartBox from \"@/components/admin/ChartBox.tsx\";\n\nfunction DashBoard() {\n  return (\n    <div className=\"bg-background w-full h-full\">\n      <div className={`dashboard bg-muted/10`}>\n        <InfoBox />\n        <ChartBox />\n      </div>\n    </div>\n  );\n}\n\nexport default DashBoard;\n"
  },
  {
    "path": "app/src/routes/admin/License.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { CreditCard, Flame, InfoIcon, Puzzle, QrCode, Sparkles } from \"lucide-react\";\nimport { useEffect } from \"react\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { toast } from \"sonner\";\nimport { useCurrency } from \"@/store/info\";\n\ntype ModuleItemProps = {\n  id: string;\n  price: number;\n  bought: boolean;\n};\nfunction ModuleItem({ id, price, bought }: ModuleItemProps) {\n  const { t } = useTranslation();\n  const { symbol } = useCurrency();\n\n  return (\n    <div\n      className={`rounded-md p-4 border flex flex-col bg-muted/20 transition hover:border-primary/20 cursor-pointer hover:-translate-y-1 duration-300`}\n    >\n      <p\n        className={`flex flex-row items-center text-md font-bold text-primary mb-2.5`}\n      >\n        <Flame className={`w-4 h-4 mr-1`} />\n        {t(`admin.license.modules.${id}.title`)}\n\n        <Badge className={`ml-auto`} variant={`outline`}>\n          {price === -1 ? (\n            <p className={`text-2xs font-normal`}>\n              {t(\"admin.license.modules.contact-for-price\")}\n            </p>\n          ) : (\n            <>\n              <p className={`text-2xs font-normal`}>{symbol}</p>\n              {price}\n            </>\n          )}\n        </Badge>\n      </p>\n      <p className={`text-sm text-secondary`}>\n        {t(`admin.license.modules.${id}.description`)}\n      </p>\n      <div className={`grow`} />\n      <div className={`inline-flex flex-row mt-4`}>\n        <div className={`grow`} />\n        <Button\n          variant={bought ? `outline` : `default`}\n          onClick={() => {\n            if (!bought) {\n              if (price === -1) {\n                window.open(\"https://www.coai.dev\", \"_blank\");\n              } else {\n                toast.info(t(\"admin.license.modules.buy-tip\"));\n              }\n            }\n          }}\n        >\n          {t(\n            bought\n              ? \"admin.license.modules.bought\"\n              : \"admin.license.modules.not-bought\",\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction License() {\n  const { t } = useTranslation();\n  const data = { domain: \"\", digest: \"\" };\n\n  useEffect(() => {\n    toast.info(t(\"admin.license.pro-required\"));\n  }, [t]);\n\n  return (\n    <div className={`system`}>\n      <Card className={`admin-card system-card relative`}>\n        <Sparkles\n          className={`absolute bottom-4 right-4 select-none text-muted w-12 h-12 hover:text-gold/40 duration-500 transition cursor-pointer`}\n        />\n        <CardHeader>\n          <CardTitle className={`flex w-full flex-row flex-wrap items-center`}>\n            {t(\"admin.license.title\")}\n\n            <p\n              className={`inline-flex flex-row items-center py-1 px-2 ml-auto text-xs border select-none cursor-pointer rounded-lg text-unread font-bold hover:border-muted-foreground transition duration-300`}\n            >\n              <QrCode className={`w-3.5 h-3.5 mr-1`} />\n              0x{(data.digest || \"unauthorized\").toUpperCase()}\n            </p>\n          </CardTitle>\n          <CardDescription>{t(\"admin.license.description\")}</CardDescription>\n        </CardHeader>\n        <CardContent>\n          <h2\n            className={`mb-2 select-none font-bold text-lg inline-flex flex-row items-center`}\n          >\n            <InfoIcon className={`w-4 h-4 mr-1.5`} />\n            {t(\"admin.license.info\")}\n          </h2>\n\n          <div className={`mb-8`}>\n            <table className={`w-fit h-fit`}>\n              <tbody>\n                <tr>\n                  <td className={`font-bold`}>{t(\"admin.license.domain\")}</td>\n                  <td>\n                    <Badge variant={`outline`} className={`m-1 ml-4`}>\n                      {data.domain || \"unknown\"}\n                    </Badge>\n                  </td>\n                </tr>\n                <tr>\n                  <td className={`font-bold`}>{t(\"admin.license.digest\")}</td>\n                  <td>\n                    <Badge variant={`outline`} className={`m-1 ml-4`}>\n                      0x{(data.digest || \"unauthorized\").toUpperCase()}\n                    </Badge>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n          </div>\n\n          {(!data.digest || data.digest === \"unauthorized\") && (\n            <>\n              <h2\n                className={`mb-4 select-none font-bold text-lg inline-flex flex-row items-center`}\n              >\n                <CreditCard className={`w-4 h-4 mr-1.5`} />\n                {t(\"admin.license.purchase\")}\n              </h2>\n              <div\n                className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8`}\n              >\n                <ModuleItem id={`coai-pro`} price={-1} bought={false} />\n              </div>\n            </>\n          )}\n\n          <h2\n            className={`mb-4 select-none font-bold text-lg inline-flex flex-row items-center`}\n          >\n            <Puzzle className={`w-4 h-4 mr-1.5`} />\n            {t(\"admin.license.module\")}\n          </h2>\n          <div\n            className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-16`}\n          >\n            {/* afdian */}\n            <ModuleItem id={`afdian`} price={1000} bought={false} />\n            {/*paypal */}\n            <ModuleItem id={`paypal`} price={2000} bought={false} />\n            {/* stripe */}\n            <ModuleItem id={`stripe`} price={2000} bought={false} />\n            {/* digital */}\n            <ModuleItem id={`digital`} price={50000} bought={false} />\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default License;\n"
  },
  {
    "path": "app/src/routes/admin/Logger.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMemo, useState } from \"react\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport {\n  Logger,\n  listLoggers,\n  downloadLogger,\n  deleteLogger,\n  getLoggerConsole,\n} from \"@/admin/api/logger.ts\";\nimport { getSizeUnit } from \"@/utils/base.ts\";\nimport { Download, RotateCcw, Terminal, Trash } from \"lucide-react\";\nimport { withNotify } from \"@/api/common.ts\";\nimport Paragraph from \"@/components/Paragraph.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\ntype LoggerItemProps = Logger & {\n  onUpdate: () => void;\n};\nfunction LoggerItem({ path, size, onUpdate }: LoggerItemProps) {\n  const { t } = useTranslation();\n  const loggerSize = useMemo(() => getSizeUnit(size), [size]);\n\n  return (\n    <div className=\"flex items-center justify-between p-3 w-full max-w-full bg-background rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 mb-2\">\n      <div className=\"mr-4\">\n        <div className=\"text-sm font-medium text-foreground break-all whitespace-pre-wrap\">\n          {path}\n        </div>\n        <div className=\"text-xs text-muted-foreground\">{loggerSize}</div>\n      </div>\n      <div className=\"grow\" />\n      <div className=\"flex space-x-2\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={async () => downloadLogger(path)}\n          title={t(\"admin.logger.download\")}\n        >\n          <Download className=\"w-4 h-4\" />\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={async () => {\n            const resp = await deleteLogger(path);\n            if (resp) onUpdate();\n            withNotify(t, resp, true);\n          }}\n          title={t(\"admin.logger.delete\")}\n        >\n          <Trash className=\"w-4 h-4 text-destructive\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction LoggerList() {\n  const [data, setData] = useState<Logger[]>([]);\n\n  const sync = async () => setData(await listLoggers());\n\n  useEffectAsync(async () => {\n    await sync();\n  }, []);\n\n  return (\n    <div className={`logger-list`}>\n      {data.map((logger, i) => (\n        <LoggerItem {...logger} key={i} onUpdate={sync} />\n      ))}\n    </div>\n  );\n}\n\nfunction LoggerConsole() {\n  const { t } = useTranslation();\n  const [data, setData] = useState<string>(\"\");\n  const [loading, setLoading] = useState<boolean>(false);\n  const [length, setLength] = useState<number>(100);\n\n  const sync = async () => {\n    if (loading) return;\n    setLoading(true);\n    setData(await getLoggerConsole(length));\n    setLoading(false);\n  };\n  useEffectAsync(sync, []);\n\n  return (\n    <Paragraph\n      title={t(\"admin.logger.console\")}\n      className={`logger-container mb-2`}\n      isCollapsed={true}\n    >\n      <div className={`logger-toolbar`}>\n        <Label>{t(\"admin.logger.consoleLength\")}</Label>\n        <NumberInput\n          value={length}\n          onValueChange={setLength}\n          min={1}\n          max={1000}\n        />\n        <div className={`grow`} />\n        <Button onClick={sync} variant={`outline`} size={`icon`}>\n          <RotateCcw className={cn(\"w-4 h-4\", loading && \"animate-spin\")} />\n        </Button>\n      </div>\n      <div className={`logger-console bg-muted/20`}>\n        <Terminal\n          className={`w-6 h-6 p-1 bg-primary/80 hover:bg-primary/100 transition duration-300 backdrop-blur-sm text-primary-foreground rounded-sm absolute top-4 right-4`}\n        />\n        <pre\n          className={`no-scrollbar`}\n          style={{\n            fontFamily: \"var(--font-family-code) !important\",\n          }}\n        >\n          {data.split(\"\\n\").map((line, index) => {\n            const logLevelMatch = line.match(\n              /^\\[(DEBUG|INFO|WARN|ERROR|CRITICAL)\\]/,\n            );\n            const dateMatch = line.match(/\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/);\n            const numberMatch = line.match(/\\b\\d+(\\.\\d+)?\\b/g);\n\n            let processedLine = line;\n\n            if (logLevelMatch) {\n              const logLevel = logLevelMatch[1];\n              const colorClass =\n                {\n                  DEBUG: \"text-gray-500\",\n                  INFO: \"text-blue-500\",\n                  WARN: \"text-yellow-500\",\n                  ERROR: \"text-red-500\",\n                  CRITICAL: \"text-purple-700 font-bold\",\n                }[logLevel] || \"\";\n\n              processedLine = processedLine.replace(logLevelMatch[0], \"\");\n\n              return (\n                <span key={index}>\n                  <span className={colorClass}>[{logLevel}]</span>\n                  {dateMatch && (\n                    <span className=\"font-thin\"> {dateMatch[0]}</span>\n                  )}\n                  {processedLine\n                    .split(/((?:\\b\\d+(?:\\.\\d+)?\\b))/)\n                    .map((part, i) =>\n                      numberMatch && numberMatch.includes(part) ? (\n                        <span key={i} className=\"font-semibold\">\n                          {part}\n                        </span>\n                      ) : (\n                        <span key={i}>{part}</span>\n                      ),\n                    )}\n                  {\"\\n\"}\n                </span>\n              );\n            }\n\n            return (\n              <span key={index}>\n                {processedLine\n                  .split(/((?:\\b\\d+(?:\\.\\d+)?\\b))/)\n                  .map((part, i) =>\n                    numberMatch && numberMatch.includes(part) ? (\n                      <span key={i} className=\"font-semibold\">\n                        {part}\n                      </span>\n                    ) : (\n                      <span key={i}>{part}</span>\n                    ),\n                  )}\n                {\"\\n\"}\n              </span>\n            );\n          })}\n        </pre>\n      </div>\n    </Paragraph>\n  );\n}\n\nfunction Logger() {\n  const { t } = useTranslation();\n  return (\n    <div className={`logger`}>\n      <Card className={`admin-card logger-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.logger.title\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <LoggerConsole />\n          <LoggerList />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Logger;\n"
  },
  {
    "path": "app/src/routes/admin/Market.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport React, {\n  Dispatch,\n  useEffect,\n  useMemo,\n  useReducer,\n  useState,\n} from \"react\";\nimport { Model as RawModel } from \"@/api/types.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport {\n  Activity,\n  AlertCircle,\n  ChevronDown,\n  ChevronUp,\n  GripVertical,\n  Import,\n  Maximize,\n  Minimize,\n  Plus,\n  RotateCw,\n  Save,\n  Trash2,\n  UploadIcon,\n} from \"lucide-react\";\nimport { generateRandomChar } from \"@/utils/base.ts\";\nimport { Textarea } from \"@/components/ui/textarea.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { Toggle } from \"@/components/ui/toggle.tsx\";\nimport { deprecatedModelImages, marketEditableTags } from \"@/admin/market.ts\";\nimport { Button, UploadFileButton } from \"@/components/ui/button.tsx\";\nimport { Combobox } from \"@/components/ui/combo-box.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport PopupDialog, { popupTypes } from \"@/components/PopupDialog.tsx\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog.tsx\";\nimport { getApiMarket, getModelName, getV1Path } from \"@/api/v1.ts\";\nimport { updateMarket } from \"@/admin/api/market.ts\";\nimport { toast } from \"sonner\";\nimport { useChannelModels, useSupportModels } from \"@/admin/hook.tsx\";\nimport Icon from \"@/components/utils/Icon.tsx\";\nimport {\n  DragDropContext,\n  Draggable,\n  Droppable,\n  DropResult,\n} from \"react-beautiful-dnd\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { uploadResource } from \"@/admin/api/system.ts\";\nimport { withNotify } from \"@/api/common.ts\";\n\ntype Model = RawModel & {\n  seed?: string;\n};\n\ntype MarketForm = Model[];\n\nconst generateSeed = () => generateRandomChar(8);\n\nfunction reducer(state: MarketForm, action: any): MarketForm {\n  switch (action.type) {\n    case \"set\":\n      return [\n        ...action.payload.map((model: RawModel) => ({\n          ...model,\n          seed: generateSeed(),\n        })),\n      ];\n    case \"add\":\n      return [\n        ...state,\n        {\n          ...action.payload,\n          seed: generateSeed(),\n        },\n      ];\n    case \"add-multiple\":\n      return [\n        ...state,\n        ...action.payload.map((model: RawModel) => ({\n          id: model.id || \"\",\n          name: model.name || \"\",\n          free: false,\n          auth: false,\n          description: model.description || \"\",\n          high_context: model.high_context || false,\n          default: model.default || false,\n          tag: model.tag || [],\n          avatar: model.avatar || \"\",\n          seed: generateSeed(),\n        })),\n      ];\n    case \"new\":\n      return [\n        ...state,\n        {\n          id: \"\",\n          name: \"\",\n          free: false,\n          auth: false,\n          description: \"\",\n          high_context: false,\n          default: false,\n          tag: [],\n          avatar: \"\",\n          seed: generateSeed(),\n        },\n      ];\n    case \"new-template\":\n      return [\n        {\n          id: action.payload.id,\n          name: action.payload.name,\n          free: false,\n          auth: false,\n          description: \"\",\n          high_context: false,\n          default: true,\n          tag: [],\n          avatar: \"\",\n          seed: generateSeed(),\n        },\n        ...state,\n      ];\n    case \"batch-new-template\":\n      return [\n        ...action.payload.map((model: { id: string; name: string }) => ({\n          id: model.id,\n          name: model.name,\n          free: false,\n          auth: false,\n          description: \"\",\n          high_context: false,\n          default: true,\n          tag: [],\n          avatar: \"\",\n          seed: generateSeed(),\n        })),\n        ...state,\n      ];\n    case \"remove\":\n      let { idx } = action.payload;\n      return [...state.slice(0, idx), ...state.slice(idx + 1)];\n    case \"update\":\n      let { index, data } = action.payload;\n      return [...state.slice(0, index), data, ...state.slice(index + 1)];\n    case \"update-id\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, id: action.payload.id };\n          }\n          return model;\n        }),\n      ];\n    case \"update-name\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, name: action.payload.name };\n          }\n          return model;\n        }),\n      ];\n    case \"update-description\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, description: action.payload.description };\n          }\n          return model;\n        }),\n      ];\n    case \"update-context\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, high_context: action.payload.context };\n          }\n          return model;\n        }),\n      ];\n    case \"update-default\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, default: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-function-calling\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, function_calling: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-vision-model\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, vision_model: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-thinking-model\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, thinking_model: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-ocr-model\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, ocr_model: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-reverse-model\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, reverse_model: action.payload.default };\n          }\n          return model;\n        }),\n      ];\n    case \"update-tags\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, tag: action.payload.tags };\n          }\n          return model;\n        }),\n      ];\n    case \"add-tag\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            const tag = model.tag || [];\n            tag.push(action.payload.tag);\n            return {\n              ...model,\n              tag: [...tag],\n            };\n          }\n          return model;\n        }),\n      ];\n    case \"remove-tag\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            const tag = model.tag || [];\n            return {\n              ...model,\n              tag: tag.filter((t) => t !== action.payload.tag),\n            };\n          }\n          return model;\n        }),\n      ];\n    case \"set-avatar\":\n      return [\n        ...state.map((model, idx) => {\n          if (idx === action.payload.idx) {\n            return { ...model, avatar: action.payload.avatar };\n          }\n          return model;\n        }),\n      ];\n    case \"replace\":\n      const { from, to } = action.payload;\n      const [removed] = state.splice(from, 1);\n      state.splice(to, 0, removed);\n      return [...state];\n    case \"add-below\":\n      return [\n        ...state.slice(0, action.payload.idx + 1),\n        {\n          id: \"\",\n          name: \"\",\n          free: false,\n          auth: false,\n          description: \"\",\n          high_context: false,\n          default: false,\n          tag: [],\n          avatar: \"\",\n          seed: generateSeed(),\n        },\n        ...state.slice(action.payload.idx + 1),\n      ];\n    case \"upward\":\n      if (action.payload.idx === 0) return state;\n      const upward = state[action.payload.idx];\n      state[action.payload.idx] = state[action.payload.idx - 1];\n      state[action.payload.idx - 1] = upward;\n      return [...state];\n    case \"downward\":\n      if (action.payload.idx === state.length - 1) return state;\n      const downward = state[action.payload.idx];\n      state[action.payload.idx] = state[action.payload.idx + 1];\n      state[action.payload.idx + 1] = downward;\n      return [...state];\n    case \"move\":\n      const { fromIndex, toIndex } = action.payload;\n      const moved = state[fromIndex];\n      state.splice(fromIndex, 1);\n      state.splice(toIndex, 0, moved);\n      return [...state];\n    default:\n      throw new Error();\n  }\n}\n\ntype MarketTagsProps = {\n  tag: string[] | undefined;\n  idx: number;\n  dispatch: Dispatch<any>;\n};\n\nfunction MarketTags({ tag, idx, dispatch }: MarketTagsProps) {\n  const { t } = useTranslation();\n  const tags = useMemo((): Record<string, boolean> => {\n    const selected = tag || [];\n\n    return marketEditableTags.reduce(\n      (acc, name) => {\n        acc[name] = selected.includes(name);\n        return acc;\n      },\n      {} as Record<string, boolean>,\n    );\n  }, [tag]);\n\n  return (\n    <div className={`market-tags`}>\n      {tags &&\n        Object.keys(tags).map((name) => (\n          <Toggle\n            key={name}\n            variant={`outline`}\n            size={`sm`}\n            pressed={tags[name]}\n            className={`market-tag`}\n            onPressedChange={(state) => {\n              dispatch({\n                type: state ? \"add-tag\" : \"remove-tag\",\n                payload: {\n                  idx,\n                  tag: name,\n                },\n              });\n            }}\n          >\n            {t(`tag.${name}`)}\n          </Toggle>\n        ))}\n    </div>\n  );\n}\n\ntype MarketImageProps = {\n  image: string;\n  idx: number;\n  dispatch: Dispatch<any>;\n};\n\nfunction CustomMarketImage({ image, idx, dispatch }: MarketImageProps) {\n  const { t } = useTranslation();\n  const [customizedImage, setCustomizedImage] = useState<boolean>(\n    image.trim().length > 0 && !deprecatedModelImages.includes(image),\n  );\n\n  const setAvatar = (source: string) =>\n    dispatch({\n      type: \"set-avatar\",\n      payload: {\n        idx,\n        avatar: source,\n      },\n    });\n\n  useEffect(() => {\n    if (!customizedImage) {\n      setAvatar(\"\");\n    }\n  }, [customizedImage]);\n\n  return (\n    <>\n      <div className={`market-row flex flex-row items-center`}>\n        <Label>{t(\"admin.market.custom-image\")}</Label>\n        <div className={`grow`} />\n        <Switch\n          checked={customizedImage}\n          onCheckedChange={setCustomizedImage}\n        />\n      </div>\n      {customizedImage && (\n        <div\n          className={`market-row flex flex-row !flex-nowrap items-center market-custom-image !gap-0`}\n        >\n          <UploadFileButton\n            onFileChange={async (file) => {\n              const resp = await uploadResource(file);\n              withNotify(t, resp, true);\n              if (resp.status) {\n                resp.url && setAvatar(resp.url);\n              }\n            }}\n            variant={`outline`}\n            size={`icon`}\n            className={`!mr-1 shrink-0`}\n          >\n            <UploadIcon className={`h-4 w-4`} />\n          </UploadFileButton>\n          <Input\n            value={image}\n            placeholder={t(\"admin.market.custom-image-placeholder\")}\n            onChange={(e) => {\n              setAvatar(e.target.value);\n            }}\n          />\n        </div>\n      )}\n    </>\n  );\n}\n\ntype MarketItemProps = React.DetailedHTMLProps<\n  React.HTMLAttributes<HTMLDivElement>,\n  HTMLDivElement\n> & {\n  model: Model;\n  form: MarketForm;\n  dispatch: Dispatch<any>;\n  index: number;\n  stacked: boolean;\n  channelModels: string[];\n  forwardRef?: React.Ref<HTMLDivElement>;\n};\n\nfunction MarketItem({\n  model,\n  form,\n  stacked,\n  dispatch,\n  index,\n  channelModels,\n  forwardRef,\n  ...props\n}: MarketItemProps) {\n  const { t } = useTranslation();\n\n  const [stackedFilled, setStackedFilled] = useState<boolean>(false);\n\n  const checked = useMemo(\n    (): boolean => model.id.trim().length > 0 && model.name.trim().length > 0,\n    [model],\n  );\n\n  useEffect(() => {\n    setStackedFilled(stacked);\n  }, [stacked]);\n\n  const Actions = ({ stacked }: { stacked?: boolean }) => (\n    <div className={`market-row`}>\n      {!stacked && <div className={`grow`} />}\n      <Button\n        size={`icon`}\n        variant={`outline`}\n        onClick={() =>\n          dispatch({\n            type: \"add-below\",\n            payload: { idx: index },\n          })\n        }\n      >\n        <Plus className={`h-4 w-4`} />\n      </Button>\n\n      {!stacked && (\n        <Button\n          size={`icon`}\n          variant={`outline`}\n          onClick={() =>\n            dispatch({\n              type: \"upward\",\n              payload: { idx: index },\n            })\n          }\n          disabled={index === 0}\n        >\n          <ChevronUp className={`h-4 w-4`} />\n        </Button>\n      )}\n\n      {!stacked && (\n        <Button\n          size={`icon`}\n          variant={`outline`}\n          onClick={() =>\n            dispatch({\n              type: \"downward\",\n              payload: { idx: index },\n            })\n          }\n          disabled={index === form.length - 1}\n        >\n          <ChevronDown className={`h-4 w-4`} />\n        </Button>\n      )}\n\n      <Button\n        size={`icon`}\n        variant={`outline`}\n        onClick={() => setStackedFilled(!stackedFilled)}\n      >\n        {!stackedFilled ? (\n          <Minimize className={`h-4 w-4`} />\n        ) : (\n          <Maximize className={`h-4 w-4`} />\n        )}\n      </Button>\n\n      <Button\n        size={`icon`}\n        onClick={() =>\n          dispatch({\n            type: \"remove\",\n            payload: { idx: index },\n          })\n        }\n      >\n        <Trash2 className={`h-4 w-4`} />\n      </Button>\n    </div>\n  );\n\n  return stackedFilled ? (\n    <div\n      className={cn(\"market-item\", !checked && \"error\")}\n      {...props}\n      ref={forwardRef}\n    >\n      <div className={`model-wrapper`}>\n        <div\n          className={`flex flex-col md:grid md:grid-cols-2 gap-2 md:gap-x-4`}\n        >\n          <div className={`market-row md:!flex-nowrap`}>\n            <span>{t(\"admin.market.model-name\")}</span>\n            <Input\n              value={model.name}\n              className={`max-w-[320px] ml-auto`}\n              placeholder={t(\"admin.market.model-name-placeholder\")}\n              onChange={(e) => {\n                dispatch({\n                  type: \"update-name\",\n                  payload: {\n                    idx: index,\n                    name: e.target.value,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row md:!flex-nowrap`}>\n            <span>{t(\"admin.market.model-id\")}</span>\n            <Combobox\n              value={model.id}\n              onChange={(id: string) => {\n                dispatch({\n                  type: \"update-id\",\n                  payload: { idx: index, id },\n                });\n              }}\n              className={`model-combobox`}\n              list={channelModels}\n              placeholder={t(\"admin.market.model-id-placeholder\")}\n            />\n          </div>\n          <div className={`market-row col-span-2`}>\n            <span>{t(\"admin.market.model-description\")}</span>\n            <Textarea\n              value={model.description || \"\"}\n              placeholder={t(\"admin.market.model-description-placeholder\")}\n              onChange={(e) => {\n                dispatch({\n                  type: \"update-description\",\n                  payload: {\n                    idx: index,\n                    description: e.target.value,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.model-context\")}\n              <Tips content={t(\"admin.market.model-context-tip\")} />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.high_context}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-context\",\n                  payload: {\n                    idx: index,\n                    context: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          {/*function-calling*/}\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.function-calling\")}\n              <Tips content={t(\"admin.market.function-calling-tip\")} />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.function_calling || false}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-function-calling\",\n                  payload: {\n                    idx: index,\n                    default: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.vision-model\")}\n              <Tips content={t(\"admin.market.vision-model-tip\")} />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.vision_model || false}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-vision-model\",\n                  payload: {\n                    idx: index,\n                    default: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.thinking-model\")}\n              <Tips content={t(\"admin.market.thinking-model-tip\")} />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.thinking_model || false}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-thinking-model\",\n                  payload: {\n                    idx: index,\n                    default: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.reverse-model\")}\n              <Tips\n                hideTimeout={20000}\n                content={t(\"admin.market.reverse-model-tip\")}\n              />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.reverse_model || false}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-reverse-model\",\n                  payload: {\n                    idx: index,\n                    default: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          <div className={`market-row`}>\n            <span>\n              {t(\"admin.market.ocr-model\")}\n              <Tips content={t(\"admin.market.ocr-model-tip\")} />\n            </span>\n            <Switch\n              className={`ml-auto`}\n              checked={model.ocr_model || false}\n              onCheckedChange={(state) => {\n                dispatch({\n                  type: \"update-ocr-model\",\n                  payload: {\n                    idx: index,\n                    default: state,\n                  },\n                });\n              }}\n            />\n          </div>\n          <CustomMarketImage\n            image={model.avatar}\n            idx={index}\n            dispatch={dispatch}\n          />\n        </div>\n        <div className={`market-row`}>\n          <span>{t(\"admin.market.model-tag\")}</span>\n          <MarketTags tag={model.tag} idx={index} dispatch={dispatch} />\n        </div>\n        <Actions />\n      </div>\n    </div>\n  ) : (\n    <div\n      className={cn(\"market-item stacked\", !checked && \"error\")}\n      {...props}\n      ref={forwardRef}\n    >\n      <GripVertical className={`h-4 w-4 mr-2 cursor-pointer`} />\n      <Input\n        value={model.name}\n        placeholder={t(\"admin.market.model-name-placeholder\")}\n        className={`grow mr-2`}\n        onChange={(e) => {\n          dispatch({\n            type: \"update-name\",\n            payload: {\n              idx: index,\n              name: e.target.value,\n            },\n          });\n        }}\n      />\n      <Actions stacked={true} />\n    </div>\n  );\n}\n\ntype MarketGroupProps = {\n  form: MarketForm;\n  dispatch: Dispatch<any>;\n  stacked: boolean;\n  channelModels: string[];\n};\n\nfunction MarketGroup({\n  form,\n  dispatch,\n  stacked,\n  channelModels,\n}: MarketGroupProps) {\n  return form.map((model, index) => (\n    <Draggable\n      key={model.seed as string}\n      draggableId={model.seed as string}\n      index={index}\n    >\n      {(provided) => (\n        <MarketItem\n          key={index}\n          model={model}\n          form={form}\n          stacked={stacked}\n          dispatch={dispatch}\n          index={index}\n          channelModels={channelModels}\n          forwardRef={provided.innerRef}\n          {...provided.draggableProps}\n          {...provided.dragHandleProps}\n        />\n      )}\n    </Draggable>\n  ));\n}\n\ntype SyncDialogProps = {\n  open: boolean;\n  setOpen: (state: boolean) => void;\n  onConfirm: (form: MarketForm) => Promise<boolean>;\n  allModels: string[];\n  supportModels: Model[];\n};\n\nfunction SyncDialog({\n  open,\n  setOpen,\n  allModels,\n  supportModels,\n  onConfirm,\n}: SyncDialogProps) {\n  const { t } = useTranslation();\n  const [form, setForm] = useState<MarketForm>([]);\n\n  const siteModels = useMemo(\n    (): string[] => form.map((model) => model.id),\n    [form],\n  );\n  const existModels = useMemo(\n    (): string[] =>\n      supportModels\n        .filter((model) => siteModels.includes(model.id))\n        .map((model) => model.id),\n    [siteModels, supportModels],\n  );\n  const newModels = useMemo(\n    (): string[] => siteModels.filter((model) => !existModels.includes(model)),\n    [siteModels, existModels],\n  );\n  const newSupportedModels = useMemo(\n    (): string[] => newModels.filter((model) => allModels.includes(model)),\n    [newModels, allModels],\n  );\n\n  return (\n    <>\n      <Dialog\n        open={form.length > 0}\n        onOpenChange={(open: boolean) => {\n          if (open) return;\n          setOpen(false);\n          setForm([]);\n        }}\n      >\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"admin.market.sync-option\")}</DialogTitle>\n            <DialogDescription>\n              {t(\"admin.market.sync-items\", {\n                length: siteModels.length,\n                exist: existModels.length,\n                new: newModels.length,\n                support: newSupportedModels.length,\n              })}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              unClickable\n              variant={`outline`}\n              loading={true}\n              onClick={async () => {\n                const target = form.filter((model) =>\n                  newModels.includes(model.id),\n                );\n                if (await onConfirm(target)) {\n                  setForm([]);\n                  setOpen(false);\n                }\n              }}\n              disabled={newModels.length === 0}\n            >\n              {t(\"admin.market.sync-all\", { length: newModels.length })}\n            </Button>\n            <Button\n              unClickable\n              loading={true}\n              onClick={async () => {\n                const target = form.filter((model) =>\n                  newSupportedModels.includes(model.id),\n                );\n                if (await onConfirm(target)) {\n                  setForm([]);\n                  setOpen(false);\n                }\n              }}\n              disabled={newSupportedModels.length === 0}\n            >\n              {t(\"admin.market.sync-self\", {\n                length: newSupportedModels.length,\n              })}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n      <PopupDialog\n        title={t(\"admin.market.sync\")}\n        name={t(\"admin.market.sync-site\")}\n        placeholder={t(\"admin.market.sync-placeholder\")}\n        open={open}\n        setOpen={setOpen}\n        type={popupTypes.Text}\n        defaultValue={\"https://api.chatnio.net\"}\n        onSubmit={async (endpoint: string) => {\n          const raw = getV1Path(\"/v1/market\", { endpoint });\n          const resp = await getApiMarket({ endpoint });\n          if (resp.length === 0) {\n            toast.info(t(\"admin.market.sync-failed\"), {\n              description: t(\"admin.market.sync-failed-prompt\", {\n                endpoint: raw,\n              }),\n            });\n            return false;\n          }\n\n          setForm(resp);\n          return false;\n        }}\n      />\n    </>\n  );\n}\n\ntype MarketAlertProps = {\n  open: boolean;\n  models: string[];\n  onImport: (model: string) => void;\n  onImportAll: () => void;\n};\n\nfunction MarketAlert({\n  open,\n  models,\n  onImport,\n  onImportAll,\n}: MarketAlertProps) {\n  const { t } = useTranslation();\n\n  return (\n    open &&\n    models.length > 0 && (\n      <div className={`market-alert`}>\n        <div\n          className={`flex flex-row items-center mb-2 whitespace-nowrap select-none`}\n        >\n          <AlertCircle className={`h-4 w-4 mr-2 translate-y-[1px]`} />\n          <span>{t(\"admin.market.not-use\")}</span>\n          <Button\n            variant={`outline`}\n            size={`sm`}\n            className={`ml-auto`}\n            onClick={onImportAll}\n          >\n            <Import className={`h-4 w-4 mr-2`} />\n            {t(\"admin.market.import-all\")}\n          </Button>\n        </div>\n        <div className={`market-alert-wrapper`}>\n          {models.map((model, index) => (\n            <Button\n              key={index}\n              variant={`outline`}\n              size={`sm`}\n              className={`text-sm`}\n              onClick={() => onImport(model)}\n            >\n              {model}\n            </Button>\n          ))}\n        </div>\n      </div>\n    )\n  );\n}\n\nfunction Market() {\n  const { t } = useTranslation();\n\n  const [stepSupport, setStepSupport] = useState<boolean>(false);\n  const [stepAll, setStepAll] = useState<boolean>(false);\n\n  const [stacked, setStacked] = useState<boolean>(true);\n\n  const [form, dispatch] = useReducer(reducer, []);\n  const [open, setOpen] = useState<boolean>(false);\n\n  const { supportModels, update: updateSuppportModels } = useSupportModels(\n    (state, data) => {\n      setStepSupport(!state);\n      state && dispatch({ type: \"set\", payload: data });\n    },\n  );\n\n  const {\n    allModels,\n    channelModels,\n    update: updateAllModels,\n  } = useChannelModels((state) => setStepAll(!state));\n\n  const unusedModels = useMemo(\n    (): string[] =>\n      allModels.filter((model) => !form.map((m) => m.id).includes(model)),\n    [form, allModels],\n  );\n\n  const loading = stepSupport || stepAll;\n  const update = async () => {\n    await updateSuppportModels();\n    await updateAllModels();\n  };\n\n  const sync = async (): Promise<void> => {};\n\n  const onDragEnd = (result: DropResult) => {\n    const { destination, source } = result;\n    if (\n      !destination ||\n      destination.index === source.index ||\n      destination.index === -1\n    )\n      return;\n\n    const from = source.index;\n    const to = destination.index;\n\n    dispatch({\n      type: \"move\",\n      payload: {\n        fromIndex: from,\n        toIndex: to,\n      },\n    });\n  };\n\n  const submit = async (): Promise<void> => {\n    const preflight = form.filter(\n      (model) => model.id.trim().length > 0 && model.name.trim().length > 0,\n    );\n    const resp = await updateMarket(preflight);\n\n    if (!resp.status) {\n      toast(t(\"admin.market.update-failed\"), {\n        description: t(\"admin.market.update-failed-prompt\", {\n          reason: resp.error,\n        }),\n      });\n      return;\n    }\n\n    toast(t(\"admin.market.update-success\"), {\n      description: t(\"admin.market.update-success-prompt\"),\n    });\n    await sync();\n  };\n\n  const migrate = async (data: RawModel[]): Promise<void> => {\n    if (data.length === 0) return;\n    dispatch({ type: \"add-multiple\", payload: [...data] });\n  };\n\n  return (\n    <div className={`market`}>\n      <SyncDialog\n        open={open}\n        setOpen={setOpen}\n        allModels={allModels}\n        supportModels={supportModels}\n        onConfirm={async (data: MarketForm) => {\n          await migrate(data);\n          toast(t(\"admin.market.sync-success\"), {\n            description: t(\"admin.market.sync-success-prompt\", {\n              length: data.length,\n            }),\n          });\n\n          return true;\n        }}\n      />\n      <Card className={`admin-card market-card`}>\n        <CardHeader>\n          <CardTitle>{t(\"admin.market.title\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <div className={`market-actions flex flex-row items-center mb-4`}>\n            <Button\n              variant={`outline`}\n              className={`whitespace-nowrap`}\n              onClick={() => setOpen(true)}\n            >\n              <Activity className={`h-4 w-4 mr-2`} />\n              {t(\"admin.market.sync\")}\n            </Button>\n            <div className={`grow`} />\n            <Button\n              variant={`outline`}\n              size={`icon`}\n              className={`mr-2`}\n              onClick={() => setStacked(!stacked)}\n            >\n              <Icon\n                icon={!stacked ? <Minimize /> : <Maximize />}\n                className={`h-4 w-4`}\n              />\n            </Button>\n            <Button\n              size={`icon`}\n              variant={`outline`}\n              className={`mr-2`}\n              onClick={update}\n            >\n              <RotateCw className={cn(\"h-4 w-4\", loading && \"animate-spin\")} />\n            </Button>\n            <Button\n              size={`icon`}\n              className={`mr-2`}\n              loading={true}\n              onClick={submit}\n            >\n              <Save className={`h-4 w-4`} />\n            </Button>\n          </div>\n          <MarketAlert\n            open={!loading}\n            models={unusedModels}\n            onImport={(model: string) => {\n              dispatch({\n                type: \"new-template\",\n                payload: {\n                  id: model,\n                  name: getModelName(model),\n                },\n              });\n            }}\n            onImportAll={() => {\n              dispatch({\n                type: \"batch-new-template\",\n                payload: unusedModels.map((model) => ({\n                  id: model,\n                  name: getModelName(model),\n                })),\n              });\n            }}\n          />\n          <DragDropContext onDragEnd={onDragEnd}>\n            <Droppable droppableId={`market-list`}>\n              {(provided) => (\n                <div\n                  className={`market-list cursor-default`}\n                  {...provided.droppableProps}\n                  ref={provided.innerRef}\n                >\n                  {form.length > 0 ? (\n                    <MarketGroup\n                      form={form}\n                      dispatch={dispatch}\n                      stacked={stacked}\n                      channelModels={channelModels}\n                    />\n                  ) : (\n                    <p className={`align-center text-sm empty`}>\n                      {t(\"admin.empty\")}\n                    </p>\n                  )}\n                </div>\n              )}\n            </Droppable>\n          </DragDropContext>\n          <div className={`market-footer flex flex-row items-center mt-4`}>\n            <div className={`grow`} />\n            <Button\n              size={`sm`}\n              variant={`outline`}\n              className={`mr-2`}\n              onClick={() => dispatch({ type: \"new\" })}\n            >\n              <Plus className={`h-4 w-4 mr-2`} />\n              {t(\"admin.market.new-model\")}\n            </Button>\n            <Button size={`sm`} onClick={submit} loading={true}>\n              {t(\"admin.market.migrate\")}\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Market;\n"
  },
  {
    "path": "app/src/routes/admin/Notification.tsx",
    "content": "// Deprecated\nfunction Notification() {\n  return <></>;\n}\n\nexport default Notification;\n"
  },
  {
    "path": "app/src/routes/admin/Subscription.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { useMemo, useReducer, useState } from \"react\";\nimport {\n  getExternalPlanConfig,\n  getPlanConfig,\n  type PlanConfig,\n  setPlanConfig,\n} from \"@/admin/api/plan.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport {\n  Activity,\n  BookDashed,\n  ChevronDown,\n  ChevronUp,\n  Maximize,\n  Minimize,\n  Plus,\n  RotateCw,\n  Save,\n  Trash,\n} from \"lucide-react\";\nimport { getPlanName } from \"@/conf/subscription.tsx\";\nimport { Plan, PlanItem } from \"@/api/types.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { MultiCombobox } from \"@/components/ui/multi-combobox.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu.tsx\";\nimport { withNotify } from \"@/api/common.ts\";\nimport { dispatchSubscriptionData } from \"@/store/globals.ts\";\nimport { useDispatch } from \"react-redux\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { useChannelModels } from \"@/admin/hook.tsx\";\nimport PopupDialog, {\n  PopupAlertDialog,\n  popupTypes,\n} from \"@/components/PopupDialog.tsx\";\nimport { getUniqueList } from \"@/utils/base.ts\";\nimport Icon from \"@/components/utils/Icon.tsx\";\n\nconst planInitialConfig: PlanConfig = {\n  enabled: false,\n  plans: [],\n};\n\nfunction reducer(state: PlanConfig, action: Record<string, any>): PlanConfig {\n  switch (action.type) {\n    case \"set\":\n      return action.payload;\n    case \"set-enabled\":\n      return {\n        ...state,\n        enabled: action.payload,\n      };\n    case \"set-price\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              price: action.payload.price,\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"set-item-id\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.map((item: PlanItem, index: number) => {\n                if (index === action.payload.index) {\n                  return {\n                    ...item,\n                    id: action.payload.id,\n                  };\n                }\n                return item;\n              }),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"set-item-name\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.map((item: PlanItem, index: number) => {\n                if (index === action.payload.index) {\n                  return {\n                    ...item,\n                    name: action.payload.name,\n                  };\n                }\n                return item;\n              }),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"set-item-value\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.map((item: PlanItem, index: number) => {\n                if (index === action.payload.index) {\n                  return {\n                    ...item,\n                    value: action.payload.value,\n                  };\n                }\n                return item;\n              }),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"set-item-icon\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.map((item: PlanItem, index: number) => {\n                if (index === action.payload.index) {\n                  return {\n                    ...item,\n                    icon: action.payload.icon,\n                  };\n                }\n                return item;\n              }),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"add-item\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: [\n                ...plan.items,\n                {\n                  id: \"\",\n                  name: \"\",\n                  value: 0,\n                  icon: \"\",\n                  models: [],\n                },\n              ],\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"set-item-models\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.map((item: PlanItem, index: number) => {\n                if (index === action.payload.index) {\n                  return {\n                    ...item,\n                    models: action.payload.models,\n                  };\n                }\n                return item;\n              }),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"remove-item\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            return {\n              ...plan,\n              items: plan.items.filter(\n                (_: PlanItem, index: number) => index !== action.payload.index,\n              ),\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"upward-item\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            const items = plan.items;\n            const index = action.payload.index;\n            if (index > 0) {\n              const tmp = items[index];\n              items[index] = items[index - 1];\n              items[index - 1] = tmp;\n            }\n            return {\n              ...plan,\n              items,\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"downward-item\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            const items = plan.items;\n            const index = action.payload.index;\n            if (index < items.length - 1) {\n              const tmp = items[index];\n              items[index] = items[index + 1];\n              items[index + 1] = tmp;\n            }\n            return {\n              ...plan,\n              items,\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"import-item\":\n      const { level, id, target } = action.payload;\n      const plan = state.plans.find((p: Plan) => p.level === level);\n      const item = plan?.items.find((i: PlanItem) => i.id === id);\n      if (!plan || !item) return state;\n\n      return {\n        ...state,\n        plans: state.plans.map((p: Plan) => {\n          if (p.level === target) {\n            const items = p.items;\n            items.push(item);\n            return {\n              ...p,\n              items,\n            };\n          }\n          return p;\n        }),\n      };\n    case \"set-discount\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level) {\n            const discounts = plan.discounts || {};\n            discounts[action.payload.month] = action.payload.value;\n            return {\n              ...plan,\n              discounts,\n            };\n          }\n          return plan;\n        }),\n      };\n    case \"remove-discount\":\n      return {\n        ...state,\n        plans: state.plans.map((plan: Plan) => {\n          if (plan.level === action.payload.level && plan.discounts) {\n            const discounts = { ...plan.discounts };\n            delete discounts[action.payload.month];\n            return {\n              ...plan,\n              discounts,\n            };\n          }\n          return plan;\n        }),\n      };\n    default:\n      throw new Error();\n  }\n}\n\ntype ImportActionProps = {\n  plans: Plan[];\n  level: number;\n  dispatch: (action: Record<string, any>) => void;\n};\n\ntype ImportActionItem = {\n  item: PlanItem;\n  level: number;\n};\n\nfunction ImportAction({ plans, level, dispatch }: ImportActionProps) {\n  const { t } = useTranslation();\n  const usableItems = useMemo((): ImportActionItem[] => {\n    const raw = plans.filter((p: Plan) => p.level !== level);\n    return raw\n      .map((p: Plan) =>\n        p.items.map((item: PlanItem) => ({ level: p.level, item })),\n      )\n      .flat();\n  }, [plans, level]);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant={`outline`}>\n          <BookDashed className={`h-4 w-4 mr-1`} />\n          {t(\"admin.plan.import-item\")}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        {usableItems.map(\n          ({ level: from, item }: ImportActionItem, index: number) => (\n            <DropdownMenuItem\n              key={index}\n              onClick={() => {\n                dispatch({\n                  type: \"import-item\",\n                  payload: { level: from, id: item.id, target: level },\n                });\n              }}\n            >\n              {t(`sub.${getPlanName(from)}`)} - {item.name} ({item.id})\n            </DropdownMenuItem>\n          ),\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction PlanConfig() {\n  const { t } = useTranslation();\n  const [form, formDispatch] = useReducer(reducer, planInitialConfig);\n  const [loading, setLoading] = useState<boolean>(false);\n  const dispatch = useDispatch();\n\n  const { channelModels, update } = useChannelModels();\n\n  const [stacked, setStacked] = useState<boolean>(false);\n\n  const [open, setOpen] = useState<boolean>(false);\n  const [syncOpen, setSyncOpen] = useState<boolean>(false);\n  const [conf, setConf] = useState<PlanConfig | null>(null);\n\n  const confRules = useMemo(\n    () => (conf ? conf.plans.flatMap((p: Plan) => p.items) : []),\n    [conf],\n  );\n  const confIncluding = useMemo(\n    () => getUniqueList(confRules.flatMap((i: PlanItem) => i.models)),\n    [confRules],\n  );\n\n  const refresh = async (ignoreUpdate?: boolean) => {\n    setLoading(true);\n    const res = await getPlanConfig();\n    if (!ignoreUpdate) await update();\n    formDispatch({ type: \"set\", payload: res });\n    setLoading(false);\n  };\n\n  const save = async (data?: PlanConfig) => {\n    const res = await setPlanConfig(data ?? form);\n    withNotify(t, res, true);\n    if (res.status)\n      dispatchSubscriptionData(dispatch, form.enabled ? form.plans : []);\n  };\n\n  useEffectAsync(async () => await refresh(true), []);\n\n  return (\n    <div className={`plan-config`}>\n      <PopupDialog\n        type={popupTypes.Text}\n        title={t(\"admin.plan.sync\")}\n        name={t(\"admin.plan.sync-site\")}\n        placeholder={t(\"admin.plan.sync-placeholder\")}\n        open={open}\n        setOpen={setOpen}\n        defaultValue={\"https://api.chatnio.net\"}\n        alert={t(\"admin.coai-format-only\")}\n        onSubmit={async (endpoint): Promise<boolean> => {\n          const conf = await getExternalPlanConfig(endpoint);\n          setConf(conf);\n          setSyncOpen(true);\n\n          return true;\n        }}\n      />\n      <PopupAlertDialog\n        title={t(\"admin.plan.sync\")}\n        description={t(\"admin.plan.sync-result\", {\n          length: confRules.length,\n          models: confIncluding.length,\n        })}\n        open={syncOpen}\n        setOpen={setSyncOpen}\n        destructive={true}\n        onSubmit={async () => {\n          formDispatch({ type: \"set\", payload: conf });\n          conf && (await save(conf));\n\n          return true;\n        }}\n      />\n      <div className={`plan-config-row pb-2`}>\n        <Button variant={`outline`} onClick={() => setOpen(true)}>\n          <Activity className={`h-4 w-4 mr-2`} />\n          {t(\"admin.plan.sync\")}\n        </Button>\n        <div className={`grow`} />\n        <Button\n          variant={`outline`}\n          size={`icon`}\n          className={`mr-2`}\n          onClick={() => setStacked(!stacked)}\n        >\n          <Icon\n            icon={stacked ? <Minimize /> : <Maximize />}\n            className={`h-4 w-4`}\n          />\n        </Button>\n        <Button\n          variant={`outline`}\n          className={`mr-2`}\n          size={`icon`}\n          onClick={async () => await refresh()}\n        >\n          <RotateCw className={cn(`h-4 w-4`, loading && `animate-spin`)} />\n        </Button>\n        <Button\n          variant={`default`}\n          size={`icon`}\n          onClick={async () => await save()}\n          loading={true}\n        >\n          <Save className={`h-4 w-4`} />\n        </Button>\n      </div>\n\n      <div className={`plan-config-row`}>\n        <p>{t(\"admin.plan.enable\")}</p>\n        <div className={`grow`} />\n        <Switch\n          checked={form.enabled}\n          onCheckedChange={(checked: boolean) =>\n            formDispatch({ type: \"set-enabled\", payload: checked })\n          }\n        />\n      </div>\n\n      {form.enabled &&\n        form.plans.map((plan: Plan, index: number) => (\n          <div className={`plan-config-card`} key={index}>\n            <p className={`plan-config-title`}>\n              {t(`sub.${getPlanName(plan.level)}`)}\n            </p>\n            <div className={`plan-editor-row`}>\n              <p className={`select-none flex flex-row items-center mr-2`}>\n                {t(\"admin.plan.price\")}\n                <Tips\n                  className={`inline-block`}\n                  content={t(\"admin.plan.price-tip\")}\n                />\n              </p>\n              <NumberInput\n                value={plan.price}\n                onValueChange={(value: number) => {\n                  formDispatch({\n                    type: \"set-price\",\n                    payload: { level: plan.level, price: value },\n                  });\n                }}\n              />\n            </div>\n            <div className={`plan-items-wrapper`}>\n              {plan.items.map((item: PlanItem, index: number) => (\n                <div\n                  className={cn(\n                    \"plan-item grid grid-cols-1 md:grid-cols-2 gap-4\",\n                    stacked && \"stacked\",\n                  )}\n                  key={index}\n                >\n                  <div className={`plan-editor-row`}>\n                    <p className={`plan-editor-label mr-2`}>\n                      {t(`admin.plan.item-id`)}\n                      <Tips content={t(\"admin.plan.item-id-placeholder\")} />\n                    </p>\n                    <Input\n                      value={item.id}\n                      onChange={(e) => {\n                        formDispatch({\n                          type: \"set-item-id\",\n                          payload: {\n                            level: plan.level,\n                            id: e.target.value,\n                            index,\n                          },\n                        });\n                      }}\n                      placeholder={t(`admin.plan.item-id-placeholder`)}\n                    />\n                  </div>\n                  {!stacked && (\n                    <div className={`plan-editor-row`}>\n                      <p className={`plan-editor-label mr-2`}>\n                        {t(`admin.plan.item-name`)}\n                        <Tips content={t(\"admin.plan.item-name-placeholder\")} />\n                      </p>\n                      <Input\n                        value={item.name}\n                        onChange={(e) => {\n                          formDispatch({\n                            type: \"set-item-name\",\n                            payload: {\n                              level: plan.level,\n                              name: e.target.value,\n                              index,\n                            },\n                          });\n                        }}\n                        placeholder={t(`admin.plan.item-name-placeholder`)}\n                      />\n                    </div>\n                  )}\n\n                  <div className={`plan-editor-row`}>\n                    <p className={`plan-editor-label mr-2`}>\n                      {t(`admin.plan.item-value`)}\n                      <Tips content={t(\"admin.plan.item-value-tip\")} />\n                    </p>\n                    <NumberInput\n                      value={item.value}\n                      min={-1}\n                      acceptNegative={true}\n                      onValueChange={(value: number) => {\n                        formDispatch({\n                          type: \"set-item-value\",\n                          payload: { level: plan.level, value, index },\n                        });\n                      }}\n                    />\n                  </div>\n\n                  {!stacked && (\n                    <>\n                      <div className={`plan-editor-row`}>\n                        <p className={`plan-editor-label mr-2`}>\n                          {t(`admin.plan.item-models`)}\n                          <Tips content={t(\"admin.plan.item-models-tip\")} />\n                        </p>\n                        <MultiCombobox\n                          align={`start`}\n                          value={item.models}\n                          onChange={(value: string[]) => {\n                            formDispatch({\n                              type: \"set-item-models\",\n                              payload: {\n                                level: plan.level,\n                                models: value,\n                                index,\n                              },\n                            });\n                          }}\n                          placeholder={t(`admin.plan.item-models-placeholder`, {\n                            length: item.models.length,\n                          })}\n                          searchPlaceholder={t(\n                            `admin.plan.item-models-search-placeholder`,\n                          )}\n                          list={channelModels}\n                          className={`w-full max-w-full`}\n                        />\n                      </div>\n                    </>\n                  )}\n                  <div\n                    className={cn(\n                      `flex flex-row gap-1`,\n                      !stacked && \"flex-wrap\",\n                    )}\n                  >\n                    <Button\n                      variant={`outline`}\n                      size={stacked ? \"icon\" : \"default\"}\n                      onClick={() => {\n                        formDispatch({\n                          type: \"upward-item\",\n                          payload: { level: plan.level, index },\n                        });\n                      }}\n                      disabled={index === 0}\n                    >\n                      <ChevronUp\n                        className={cn(\"h-4 w-4\", !stacked && \"mr-1\")}\n                      />\n                      {!stacked && t(\"upward\")}\n                    </Button>\n                    <Button\n                      variant={`outline`}\n                      size={stacked ? \"icon\" : \"default\"}\n                      onClick={() => {\n                        formDispatch({\n                          type: \"downward-item\",\n                          payload: { level: plan.level, index },\n                        });\n                      }}\n                      disabled={index === plan.items.length - 1}\n                    >\n                      <ChevronDown\n                        className={cn(\"h-4 w-4\", !stacked && \"mr-1\")}\n                      />\n                      {!stacked && t(\"downward\")}\n                    </Button>\n                    <Button\n                      variant={`default`}\n                      size={stacked ? \"icon\" : \"default\"}\n                      onClick={() => {\n                        formDispatch({\n                          type: \"remove-item\",\n                          payload: { level: plan.level, index },\n                        });\n                      }}\n                    >\n                      <Trash className={cn(\"h-4 w-4\", !stacked && \"mr-1\")} />\n                      {!stacked && t(\"remove\")}\n                    </Button>\n                  </div>\n                </div>\n              ))}\n            </div>\n            <div className={`plan-items-action`}>\n              <ImportAction\n                plans={form.plans}\n                level={plan.level}\n                dispatch={formDispatch}\n              />\n              <Button\n                variant={`default`}\n                onClick={() => {\n                  formDispatch({\n                    type: \"add-item\",\n                    payload: { level: plan.level },\n                  });\n                }}\n              >\n                <Plus className={`h-4 w-4 mr-1`} />\n                {t(\"admin.plan.add-item\")}\n              </Button>\n            </div>\n            <div className=\"mt-6 border-t pt-4\">\n              <p className={`plan-config-title flex items-center`}>\n                {t(\"admin.plan.discounts\")}\n                <Tips content={t(\"admin.plan.discounts-tip\")} className=\"ml-1\" />\n              </p>\n              \n              <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-3\">\n                {[1, 3, 6, 12, 36].map((month) => {\n                  const hasDiscount = plan.discounts && plan.discounts[month.toString()] !== undefined;\n                  const discountValue = hasDiscount ? plan.discounts?.[month.toString()] : null;\n                  \n                  return (\n                    <div key={month} className=\"flex flex-col space-y-2 p-3 border rounded-md\">\n                      <div className=\"flex justify-between items-center\">\n                        <span className=\"font-medium\">{t(`sub.time.${month}`)}</span>\n                        <Switch\n                          checked={hasDiscount}\n                          onCheckedChange={(checked) => {\n                            if (checked) {\n                              let discountPercent = 0;\n                              if (month >= 36) {\n                                discountPercent = 30;\n                              } else if (month >= 12) {\n                                discountPercent = 20;\n                              } else if (month >= 6) {\n                                discountPercent = 10;\n                              }\n                              \n                              const discountFactor = 1 - (discountPercent / 100);\n                              \n                              formDispatch({\n                                type: \"set-discount\",\n                                payload: { \n                                  level: plan.level, \n                                  month: month.toString(),\n                                  value: discountFactor\n                                },\n                              });\n                            } else {\n                              formDispatch({\n                                type: \"remove-discount\",\n                                payload: { \n                                  level: plan.level, \n                                  month: month.toString() \n                                },\n                              });\n                            }\n                          }}\n                        />\n                      </div>\n                      \n                      {hasDiscount && (\n                        <div className=\"mt-2\">\n                          <div className=\"flex items-center justify-between\">\n                            <span className=\"text-sm text-muted-foreground\">\n                              {t(\"admin.plan.discount-value\")}\n                            </span>\n                            <span className=\"text-sm font-medium\">\n                              {Math.round((1 - (discountValue || 1)) * 100)}% {t(\"admin.plan.discount-off\")}\n                            </span>\n                          </div>\n                          <div className=\"mt-2\">\n                            <NumberInput\n                              value={Math.round((1 - (discountValue || 1)) * 100)}\n                              min={0}\n                              max={90}\n                              step={5}\n                              onValueChange={(value) => {\n                                const discountFactor = 1 - (value / 100);\n                                formDispatch({\n                                  type: \"set-discount\",\n                                  payload: { \n                                    level: plan.level, \n                                    month: month.toString(),\n                                    value: discountFactor\n                                  },\n                                });\n                              }}\n                            />\n                          </div>\n                        </div>\n                      )}\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        ))}\n      <div className={`flex flex-row flex-wrap gap-1`}>\n        <div className={`grow`} />\n        <Button loading={true} onClick={async () => await save()}>\n          {t(\"save\")}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction Subscription() {\n  const { t } = useTranslation();\n  return (\n    <div className={`admin-subscription`}>\n      <Card className={`admin-card sub-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.subscription\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <PlanConfig />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Subscription;"
  },
  {
    "path": "app/src/routes/admin/System.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport Paragraph, {\n  ParagraphDescription,\n  ParagraphFooter,\n  ParagraphItem,\n  ParagraphSpace,\n} from \"@/components/Paragraph.tsx\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select.tsx\";\nimport { Label } from \"@/components/ui/label.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useMemo, useReducer, useState } from \"react\";\nimport { formReducer } from \"@/utils/form.ts\";\nimport { NumberInput } from \"@/components/ui/number-input.tsx\";\nimport {\n  CommonState,\n  commonWhiteList,\n  GeneralState,\n  getConfig,\n  initialSystemState,\n  MailState,\n  SearchState,\n  setConfig,\n  SiteState,\n  SystemProps,\n  testWebSearching,\n  updateRootPassword,\n} from \"@/admin/api/system.ts\";\nimport { useEffectAsync } from \"@/utils/hook.ts\";\nimport { withNotify } from \"@/api/common.ts\";\nimport { doVerify } from \"@/api/auth.ts\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTrigger,\n} from \"@/components/ui/dialog.tsx\";\nimport { DialogTitle } from \"@radix-ui/react-dialog\";\nimport Require from \"@/components/Require.tsx\";\nimport { Loader2, PencilLine, RotateCw, Save, Settings2 } from \"lucide-react\";\nimport { FlexibleTextarea, Textarea } from \"@/components/ui/textarea.tsx\";\nimport Tips from \"@/components/Tips.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Switch } from \"@/components/ui/switch.tsx\";\nimport { MultiCombobox } from \"@/components/ui/multi-combobox.tsx\";\nimport { allGroups } from \"@/utils/groups.ts\";\nimport { useChannelModels } from \"@/admin/hook.tsx\";\nimport { useSelector } from \"react-redux\";\nimport { selectSupportModels } from \"@/store/chat.ts\";\nimport { JSONEditorProvider } from \"@/components/EditorProvider.tsx\";\nimport { Combobox } from \"@/components/ui/combo-box.tsx\";\n\ntype CompProps<T> = {\n  data: T;\n  form: SystemProps;\n  dispatch: (action: any) => void;\n  onChange: (doToast?: boolean) => Promise<void>;\n};\n\nfunction RootDialog() {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState<boolean>(false);\n  const [password, setPassword] = useState<string>(\"\");\n  const [repeat, setRepeat] = useState<string>(\"\");\n\n  const onPost = async () => {\n    const res = await updateRootPassword(password);\n    withNotify(t, res, true);\n    if (res.status) {\n      setPassword(\"\");\n      setRepeat(\"\");\n      setOpen(false);\n\n      setTimeout(() => {\n        window.location.reload();\n      }, 1000);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant={`outline`} size={`sm`}>\n          {t(\"admin.system.updateRoot\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"admin.system.updateRoot\")}</DialogTitle>\n          <DialogDescription>\n            <div className={`mb-4 select-none`}>\n              {t(\"admin.system.updateRootTip\")}\n            </div>\n            <Input\n              className={`mb-2`}\n              type={`password`}\n              placeholder={t(\"admin.system.updateRootPlaceholder\")}\n              value={password}\n              onChange={(e) => setPassword(e.target.value)}\n            />\n            <Input\n              type={`password`}\n              placeholder={t(\"admin.system.updateRootRepeatPlaceholder\")}\n              value={repeat}\n              onChange={(e) => setRepeat(e.target.value)}\n            />\n          </DialogDescription>\n        </DialogHeader>\n        <DialogFooter>\n          <Button\n            variant={`outline`}\n            onClick={() => {\n              setPassword(\"\");\n              setRepeat(\"\");\n              setOpen(false);\n            }}\n          >\n            {t(\"admin.cancel\")}\n          </Button>\n          <Button\n            variant={`default`}\n            loading={true}\n            onClick={onPost}\n            disabled={\n              password.trim().length === 0 || password.trim() !== repeat.trim()\n            }\n          >\n            {t(\"admin.confirm\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nfunction General({ data, dispatch, onChange }: CompProps<GeneralState>) {\n  const { t } = useTranslation();\n\n  return (\n    <Paragraph\n      title={t(\"admin.system.general\")}\n      configParagraph={true}\n      isCollapsed={true}\n    >\n      <ParagraphItem>\n        <Label>{t(\"admin.system.title\")}</Label>\n        <Input\n          value={data.title}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:general.title\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.titleTip\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.docs\")}</Label>\n        <Input\n          value={data.docs}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:general.docs\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.docsTip\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.logo\")}</Label>\n        <Input\n          value={data.logo}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:general.logo\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.logoTip\", {\n            logo: `${window.location.protocol}//${window.location.host}/favicon.ico`,\n          })}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.backend\")}</Label>\n        <Input\n          value={data.backend}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:general.backend\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.backendPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphDescription border>\n        {t(\"admin.system.backendTip\", {\n          backend: `${window.location.protocol}//${window.location.host}/api`,\n        })}\n      </ParagraphDescription>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.file\")}</Label>\n        <Input\n          value={data.file}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:general.file\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.filePlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphDescription border>\n        {t(\"admin.system.fileTip\")}\n      </ParagraphDescription>\n      <ParagraphItem>\n        <Label>PWA Manifest</Label>\n        <JSONEditorProvider\n          value={data.pwa_manifest ?? \"\"}\n          onChange={(value) =>\n            dispatch({ type: \"update:general.pwa_manifest\", value })\n          }\n        >\n          <Button variant={`outline`}>\n            <PencilLine className={`h-4 w-4 mr-1`} />\n            {t(\"edit\")}\n          </Button>\n        </JSONEditorProvider>\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.debugMode\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.debugModeTip\")}\n          />\n        </Label>\n        <Switch\n          checked={data.debug_mode}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:general.debug_mode\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphSpace />\n      <ParagraphFooter>\n        <div className={`grow`} />\n        <RootDialog />\n        <Button\n          size={`sm`}\n          loading={true}\n          onClick={async () => await onChange()}\n        >\n          {t(\"admin.system.save\")}\n        </Button>\n      </ParagraphFooter>\n    </Paragraph>\n  );\n}\n\nfunction Mail({ data, dispatch, onChange }: CompProps<MailState>) {\n  const { t } = useTranslation();\n  const [email, setEmail] = useState<string>(\"\");\n\n  const [mailDialog, setMailDialog] = useState<boolean>(false);\n\n  const valid = useMemo((): boolean => {\n    return (\n      data.host.length > 0 &&\n      data.port > 0 &&\n      data.port < 65535 &&\n      data.username.length > 0 &&\n      data.password.length > 0 &&\n      data.from.length > 0\n    );\n  }, [data]);\n\n  const onTest = async () => {\n    if (!email.trim()) return;\n    await onChange(false);\n    const res = await doVerify(email);\n    withNotify(t, res, true);\n\n    if (res.status) setMailDialog(false);\n  };\n\n  const white_list = useMemo(() => {\n    const raw = data.white_list.custom\n      .split(\",\")\n      .map((item) => item.trim())\n      .filter((item) => item.length > 0);\n\n    return [...commonWhiteList, ...raw];\n  }, [data]);\n\n  return (\n    <Paragraph\n      title={t(\"admin.system.mail\")}\n      configParagraph={true}\n      isCollapsed={true}\n    >\n      {!valid && (\n        <ParagraphDescription border={true}>\n          {t(\"admin.system.mailConfNotValid\")}\n        </ParagraphDescription>\n      )}\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailHost\")}\n        </Label>\n        <Input\n          value={data.host}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:mail.host\",\n              value: e.target.value,\n            })\n          }\n          placeholder={`smtp.qcloudmail.com`}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailProtocol\")}\n        </Label>\n        <Select\n          value={data.protocol ? \"true\" : \"false\"}\n          onValueChange={(value: string) => {\n            dispatch({\n              type: \"update:mail.protocol\",\n              value: value === \"true\",\n            });\n          }}\n        >\n          <SelectTrigger className={`select`}>\n            <SelectValue\n              placeholder={\n                data.protocol\n                  ? t(\"admin.system.mailProtocolTLS\")\n                  : t(\"admin.system.mailProtocolSSL\")\n              }\n            />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectItem value=\"true\">TLS</SelectItem>\n            <SelectItem value=\"false\">SSL</SelectItem>\n          </SelectContent>\n        </Select>\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailPort\")}\n        </Label>\n        <NumberInput\n          value={data.port}\n          onValueChange={(value) =>\n            dispatch({ type: \"update:mail.port\", value })\n          }\n          placeholder={`465`}\n          min={0}\n          max={65535}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailUser\")}\n        </Label>\n        <Input\n          value={data.username}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:mail.username\",\n              value: e.target.value,\n            })\n          }\n          className={cn(\"transition-all duration-300\")}\n          placeholder={t(\"admin.system.mailUser\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailPass\")}\n        </Label>\n        <Input\n          value={data.password}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:mail.password\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.mailPass\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          <Require /> {t(\"admin.system.mailFrom\")}\n        </Label>\n        <Input\n          value={data.from}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:mail.from\",\n              value: e.target.value,\n            })\n          }\n          placeholder={`${t(\"admin.system.mailFrom\")} <${data.username}@${location.hostname}>`}\n          className={cn(\"transition-all duration-300\")}\n        />\n      </ParagraphItem>\n      <ParagraphSpace />\n      <ParagraphItem>\n        <Label>{t(\"admin.system.mailEnableWhitelist\")}</Label>\n        <Switch\n          checked={data.white_list.enabled}\n          onCheckedChange={(value) => {\n            dispatch({\n              type: \"update:mail.white_list.enabled\",\n              value,\n            });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.mailWhitelist\")}</Label>\n        <MultiCombobox\n          value={data.white_list.white_list}\n          list={white_list}\n          disabled={!data.white_list.enabled}\n          onChange={(value) => {\n            dispatch({\n              type: \"update:mail.white_list.white_list\",\n              value,\n            });\n          }}\n          placeholder={t(\"admin.system.mailWhitelistSelected\", {\n            length: data.white_list.white_list.length,\n          })}\n          searchPlaceholder={t(\"admin.system.mailWhitelistSearchPlaceholder\")}\n        />\n      </ParagraphItem>\n      <Input\n        className={`mb-2`}\n        value={data.white_list.custom}\n        onChange={(e) =>\n          dispatch({\n            type: \"update:mail.white_list.custom\",\n            value: e.target.value,\n          })\n        }\n        disabled={!data.white_list.enabled}\n        placeholder={t(\"admin.system.customWhitelistPlaceholder\")}\n      />\n      <ParagraphFooter>\n        <div className={`grow`} />\n        <Dialog open={mailDialog} onOpenChange={setMailDialog}>\n          <DialogTrigger asChild>\n            <Button variant={`outline`} size={`sm`}>\n              {t(\"admin.system.test\")}\n            </Button>\n          </DialogTrigger>\n          <DialogContent>\n            <DialogHeader>\n              <DialogTitle>{t(\"admin.system.test\")}</DialogTitle>\n              <DialogDescription className={`pt-2`}>\n                <Input\n                  placeholder={t(\"auth.email-placeholder\")}\n                  value={email}\n                  onChange={(e) => setEmail(e.target.value)}\n                />\n              </DialogDescription>\n            </DialogHeader>\n            <DialogFooter>\n              <Button\n                variant={`outline`}\n                onClick={() => {\n                  setEmail(\"\");\n                  setMailDialog(false);\n                }}\n              >\n                {t(\"admin.cancel\")}\n              </Button>\n              <Button variant={`default`} loading={true} onClick={onTest}>\n                {t(\"admin.confirm\")}\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n        <Button\n          size={`sm`}\n          loading={true}\n          onClick={async () => await onChange()}\n        >\n          {t(\"admin.system.save\")}\n        </Button>\n      </ParagraphFooter>\n    </Paragraph>\n  );\n}\n\nfunction Site({ data, dispatch, onChange }: CompProps<SiteState>) {\n  const { t } = useTranslation();\n\n  return (\n    <Paragraph\n      title={t(\"admin.system.site\")}\n      configParagraph={true}\n      isCollapsed={true}\n    >\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.closeRegistration\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.closeRegistrationTip\")}\n          />\n        </Label>\n        <Switch\n          checked={data.close_register}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:site.close_register\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.closeRelay\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.closeRelayTip\")}\n          />\n        </Label>\n        <Switch\n          checked={data.close_relay}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:site.close_relay\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.relayPlan\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.relayPlanTip\")}\n          />\n        </Label>\n        <Switch\n          checked={data.relay_plan}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:site.relay_plan\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.quota\")}\n          <Tips content={t(\"admin.system.quotaTip\")} />\n        </Label>\n        <NumberInput\n          value={data.quota}\n          onValueChange={(value) =>\n            dispatch({ type: \"update:site.quota\", value })\n          }\n          placeholder={`5`}\n          min={0}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.buyLink\")}</Label>\n        <Input\n          value={data.buy_link}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:site.buy_link\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.buyLinkPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem rowLayout={true}>\n        <Label>{t(\"admin.system.announcement\")}</Label>\n        <FlexibleTextarea\n          value={data.announcement}\n          rows={12}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:site.announcement\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.announcementPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem rowLayout={true}>\n        <Label>{t(\"admin.system.contact\")}</Label>\n        <FlexibleTextarea\n          value={data.contact}\n          rows={6}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:site.contact\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.contactPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphSpace />\n      <ParagraphItem rowLayout={true}>\n        <Label>{t(\"admin.system.footer\")}</Label>\n        <FlexibleTextarea\n          rows={6}\n          value={data.footer}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:site.footer\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.footerPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.authFooter\")}</Label>\n        <Switch\n          checked={data.auth_footer}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:site.auth_footer\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphFooter>\n        <div className={`grow`} />\n        <Button\n          size={`sm`}\n          loading={true}\n          onClick={async () => await onChange()}\n        >\n          {t(\"admin.system.save\")}\n        </Button>\n      </ParagraphFooter>\n    </Paragraph>\n  );\n}\n\nfunction Common({ form, data, dispatch, onChange }: CompProps<CommonState>) {\n  const { t } = useTranslation();\n\n  const { channelModels } = useChannelModels();\n  const supportModels = useSelector(selectSupportModels);\n\n  return (\n    <Paragraph\n      title={t(\"admin.system.common\")}\n      configParagraph={true}\n      isCollapsed={true}\n    >\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.image_store\")}\n          <Tips content={t(\"admin.system.image_storeTip\")} />\n        </Label>\n        <Switch\n          checked={data.image_store}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:common.image_store\", value });\n          }}\n        />\n      </ParagraphItem>\n      {data.image_store && form.general.backend.length === 0 && (\n        <ParagraphDescription border={true}>\n          {t(\"admin.system.image_storeNoBackend\")}\n        </ParagraphDescription>\n      )}\n      <ParagraphSpace />\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.cache\")}\n          <Tips content={t(\"admin.system.cacheTip\")} />\n        </Label>\n        <MultiCombobox\n          value={data.cache}\n          onChange={(value) => {\n            dispatch({ type: \"update:common.cache\", value });\n          }}\n          list={channelModels}\n          placeholder={t(\"admin.system.cachePlaceholder\", {\n            length: (data.cache ?? []).length,\n          })}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.cacheExpired\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.cacheExpiredTip\")}\n          />\n        </Label>\n        <NumberInput\n          value={data.expire}\n          onValueChange={(value) =>\n            dispatch({ type: \"update:common.expire\", value })\n          }\n          min={0}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>\n          {t(\"admin.system.cacheSize\")}\n          <Tips\n            className={`inline-block`}\n            content={t(\"admin.system.cacheSizeTip\")}\n          />\n        </Label>\n        <NumberInput\n          value={data.size}\n          onValueChange={(value) =>\n            dispatch({ type: \"update:common.size\", value })\n          }\n          min={0}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <div className={`flex flex-row flex-wrap gap-2 ml-auto`}>\n          <Button\n            variant={`outline`}\n            onClick={() => dispatch({ type: \"update:common.cache\", value: [] })}\n          >\n            <Settings2\n              className={`inline-flex h-4 w-4 mr-2 translate-y-[1px]`}\n            />\n            {t(\"admin.system.cacheNone\")}\n          </Button>\n          <Button\n            variant={`outline`}\n            onClick={() =>\n              dispatch({\n                type: \"update:common.cache\",\n                value: supportModels\n                  .filter((item) => item.free)\n                  .map((item) => item.id),\n              })\n            }\n          >\n            <Settings2\n              className={`inline-flex h-4 w-4 mr-2 translate-y-[1px]`}\n            />\n            {t(\"admin.system.cacheFree\")}\n          </Button>\n          <Button\n            variant={`outline`}\n            onClick={() =>\n              dispatch({ type: \"update:common.cache\", value: channelModels })\n            }\n          >\n            <Settings2\n              className={`inline-flex h-4 w-4 mr-2 translate-y-[1px]`}\n            />\n            {t(\"admin.system.cacheAll\")}\n          </Button>\n        </div>\n      </ParagraphItem>\n      <ParagraphSpace />\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.article\")}\n          <Tips content={t(\"admin.system.articleTip\")} />\n        </Label>\n        <MultiCombobox\n          value={data.article}\n          onChange={(value) => {\n            dispatch({ type: \"update:common.article\", value });\n          }}\n          list={allGroups}\n          listTranslate={`admin.channels.groups`}\n          placeholder={t(\"admin.system.groupPlaceholder\", {\n            length: (data.article ?? []).length,\n          })}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.generate\")}\n          <Tips content={t(\"admin.system.generateTip\")} />\n        </Label>\n        <MultiCombobox\n          value={data.generation}\n          onChange={(value) => {\n            dispatch({ type: \"update:common.generation\", value });\n          }}\n          list={allGroups}\n          listTranslate={`admin.channels.groups`}\n          placeholder={t(\"admin.system.groupPlaceholder\", {\n            length: (data.generation ?? []).length,\n          })}\n        />\n      </ParagraphItem>\n      <ParagraphFooter>\n        <div className={`grow`} />\n        <Button\n          size={`sm`}\n          loading={true}\n          onClick={async () => await onChange()}\n        >\n          {t(\"admin.system.save\")}\n        </Button>\n      </ParagraphFooter>\n    </Paragraph>\n  );\n}\n\nfunction Search({ data, dispatch, onChange }: CompProps<SearchState>) {\n  const { t } = useTranslation();\n\n  const [search, setSearch] = useState<string>(\"\");\n  const [searchDialog, setSearchDialog] = useState<boolean>(false);\n  const [searchResult, setSearchResult] = useState<string>(\"\");\n  const [searchLoading, setSearchLoading] = useState<boolean>(false);\n\n  return (\n    <Paragraph\n      title={t(\"admin.system.search\")}\n      configParagraph={true}\n      isCollapsed={true}\n    >\n      <ParagraphDescription border>\n        {t(\"admin.system.searchTip\")}\n      </ParagraphDescription>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.searchEndpoint\")}</Label>\n        <Input\n          value={data.endpoint}\n          onChange={(e) =>\n            dispatch({\n              type: \"update:search.endpoint\",\n              value: e.target.value,\n            })\n          }\n          placeholder={t(\"admin.system.searchPlaceholder\")}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.searchEngines\")}</Label>\n        <MultiCombobox\n          value={data.engines}\n          onChange={(value) => {\n            dispatch({ type: \"update:search.engines\", value });\n          }}\n          list={[\n            \"google\",\n            \"bing\",\n            \"duckduckgo\",\n            \"qwant\",\n            \"brave\",\n            \"mojeek\",\n            \"arxiv\",\n            \"crossref\",\n            \"youtube\",\n            \"bilibili\",\n            \"presearch\",\n            \"yahoo\",\n            \"wiby\",\n            \"seznam\",\n            \"goo\",\n            \"naver\",\n            \"wikidata\",\n            \"wikipedia\",\n            \"wikimini\",\n            \"wikibooks\",\n            \"wikiquote\",\n            \"wikisource\",\n            \"wikispecies\",\n            \"wikiversity\",\n            \"wikivoyage\",\n            \"ask\",\n            \"currency\",\n            \"yep\",\n            \"yacy\",\n            \"genius\",\n            \"github\",\n            \"gitlab\",\n            \"gitea.com\",\n            \"bitbucket\",\n            \"codeberg\",\n            \"mdn\",\n          ]}\n          placeholder={t(\"admin.system.searchEnginesPlaceholder\", {\n            length: (data.engines || []).length,\n          })}\n          searchPlaceholder={t(\"admin.system.searchEnginesSearchPlaceholder\")}\n        />\n      </ParagraphItem>\n      {data.engines.length === 0 && (\n        <ParagraphDescription border>\n          {t(\"admin.system.searchEnginesEmptyTip\")}\n        </ParagraphDescription>\n      )}\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.searchImageProxy\")}\n          <Tips content={t(\"admin.system.searchImageProxyTip\")} />\n        </Label>\n        <Switch\n          checked={data.image_proxy}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:search.image_proxy\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label className={`flex flex-row items-center`}>\n          {t(\"admin.system.searchCrop\")}\n          <Tips content={t(\"admin.system.searchCropTip\")} />\n        </Label>\n        <Switch\n          checked={data.crop}\n          onCheckedChange={(value) => {\n            dispatch({ type: \"update:search.crop\", value });\n          }}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.searchCropLen\")}</Label>\n        <NumberInput\n          value={data.crop_len}\n          onValueChange={(value) =>\n            dispatch({ type: \"update:search.crop_len\", value })\n          }\n          min={1}\n          disabled={!data.crop}\n        />\n      </ParagraphItem>\n      <ParagraphItem>\n        <Label>{t(\"admin.system.searchSafeSearch\")}</Label>\n        <Combobox\n          value={[\"none\", \"moderation\", \"strict\"][data.safe_search] || \"none\"}\n          onChange={(value) => {\n            dispatch({\n              type: \"update:search.safe_search\",\n              value: [\"none\", \"moderation\", \"strict\"].indexOf(value),\n            });\n          }}\n          list={[\"none\", \"moderation\", \"strict\"]}\n          listTranslated={`admin.system.searchSafeSearchModes`}\n          hideSearchBar\n        />\n      </ParagraphItem>\n      <ParagraphFooter>\n        <div className={`grow`} />\n        <Dialog open={searchDialog} onOpenChange={setSearchDialog}>\n          <DialogTrigger asChild>\n            <Button variant={`outline`} size={`sm`}>\n              {t(\"admin.system.searchTest\")}\n            </Button>\n          </DialogTrigger>\n          <DialogContent>\n            <DialogHeader>\n              <DialogTitle>{t(\"admin.system.searchTest\")}</DialogTitle>\n              <FlexibleTextarea\n                placeholder={t(\"admin.system.searchTestTip\")}\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n              />\n              {(searchLoading || searchResult) && (\n                <div\n                  className={`mt-2 border rounded-md p-4 flex items-center justify-center flex-col`}\n                >\n                  {searchLoading ? (\n                    <Loader2 className={`h-4 w-4 animate-spin`} />\n                  ) : (\n                    <>\n                      <p className={`text-sm mb-1`}>SearXNG Result</p>\n                      <Textarea value={searchResult} rows={5} readOnly />\n                    </>\n                  )}\n                </div>\n              )}\n            </DialogHeader>\n            <DialogFooter>\n              <Button\n                variant={`outline`}\n                onClick={() => {\n                  setSearch(\"\");\n                  setSearchDialog(false);\n                }}\n              >\n                {t(\"admin.cancel\")}\n              </Button>\n              <Button\n                variant={`default`}\n                loading={true}\n                onClick={async () => {\n                  await onChange();\n\n                  setSearchResult(\"\");\n                  setSearchLoading(true);\n                  const res = await testWebSearching(search);\n                  if (res.status) setSearchResult(res.result);\n\n                  withNotify(t, res, true);\n                  setSearchLoading(false);\n                }}\n              >\n                {t(\"admin.confirm\")}\n              </Button>\n            </DialogFooter>\n          </DialogContent>\n        </Dialog>\n        <Button\n          size={`sm`}\n          loading={true}\n          onClick={async () => await onChange()}\n        >\n          {t(\"admin.system.save\")}\n        </Button>\n      </ParagraphFooter>\n    </Paragraph>\n  );\n}\n\nfunction System() {\n  const { t } = useTranslation();\n  const [data, setData] = useReducer(\n    formReducer<SystemProps>(),\n    initialSystemState,\n  );\n\n  const [loading, setLoading] = useState<boolean>(false);\n\n  const doSaving = async (doToast?: boolean) => {\n    const res = await setConfig(data);\n\n    if (doToast !== false) withNotify(t, res, true);\n  };\n\n  const doRefresh = async () => {\n    setLoading(true);\n    const res = await getConfig();\n    setLoading(false);\n    withNotify(t, res);\n    if (res.status) {\n      setData({ type: \"set\", value: res.data });\n    }\n  };\n\n  useEffectAsync(doRefresh, []);\n\n  return (\n    <div className={`system`}>\n      <Card className={`admin-card system-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.settings\")}</CardTitle>\n        </CardHeader>\n        <CardContent className={`flex flex-col gap-1`}>\n          <div className={`system-actions flex flex-row`}>\n            <div className={`grow`} />\n            <Button\n              size={`icon`}\n              variant={`outline`}\n              loading={true}\n              className={`mr-2`}\n              onClick={async () => await doRefresh()}\n            >\n              <RotateCw className={cn(loading && `animate-spin`, `h-4 w-4`)} />\n            </Button>\n            <Button\n              size={`icon`}\n              loading={true}\n              onClick={async () => await doSaving()}\n            >\n              <Save className={`h-4 w-4`} />\n            </Button>\n          </div>\n          <General\n            form={data}\n            data={data.general}\n            dispatch={setData}\n            onChange={doSaving}\n          />\n          <Site\n            form={data}\n            data={data.site}\n            dispatch={setData}\n            onChange={doSaving}\n          />\n          <Mail\n            form={data}\n            data={data.mail}\n            dispatch={setData}\n            onChange={doSaving}\n          />\n          <Search\n            form={data}\n            data={data.search}\n            dispatch={setData}\n            onChange={doSaving}\n          />\n          <Common\n            form={data}\n            data={data.common}\n            dispatch={setData}\n            onChange={doSaving}\n          />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default System;\n"
  },
  {
    "path": "app/src/routes/admin/Users.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { useTranslation } from \"react-i18next\";\nimport InvitationTable from \"@/components/admin/InvitationTable.tsx\";\nimport UserTable from \"@/components/admin/UserTable.tsx\";\nimport { mobile } from \"@/utils/device.ts\";\nimport Tips from \"@/components/Tips.tsx\";\nimport RedeemTable from \"@/components/admin/RedeemTable.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\n\nfunction Users() {\n  const { t } = useTranslation();\n\n  return (\n    <div className={cn(\"user-interface\", mobile && \"mobile\")}>\n      <Card className={`admin-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle>{t(\"admin.user\")}</CardTitle>\n        </CardHeader>\n        <CardContent>\n          <UserTable />\n        </CardContent>\n      </Card>\n      <Card className={`admin-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle className={`flex items-center`}>\n            {t(\"admin.invitation-manage\")}\n            <Tips\n              content={t(\"admin.invitation-tips\")}\n              className={`ml-2 h-6 w-6 translate-y-0.5`}\n            />\n          </CardTitle>\n        </CardHeader>\n        <CardContent>\n          <InvitationTable />\n        </CardContent>\n      </Card>\n      <Card className={`admin-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle className={`flex items-center`}>\n            {t(\"admin.invitation\")}\n            <Tips\n              content={t(\"admin.redeem-tips\")}\n              className={`ml-2 h-6 w-6 translate-y-0.5`}\n            />\n          </CardTitle>\n        </CardHeader>\n        <CardContent>\n          <RedeemTable />\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default Users;\n"
  },
  {
    "path": "app/src/routes/admin/common/CommonAdminPage.tsx",
    "content": "import React from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card.tsx\";\nimport { Badge } from \"@/components/ui/badge.tsx\";\n\nexport type CommonAdminPageProps = {\n  title: string;\n  children?: React.ReactNode;\n  pro?: boolean;\n};\n\nfunction CommonAdminPage({ title, children, pro }: CommonAdminPageProps) {\n  const { t } = useTranslation();\n\n  return (\n    <div className={`admin-container`}>\n      <Card className={`admin-card`}>\n        <CardHeader className={`select-none`}>\n          <CardTitle className={`flex flex-row items-center`}>\n            {t(`admin.${title}`)}\n\n            {pro && (\n              <Badge className={`ml-2`} variant={`gold`}>\n                Pro\n              </Badge>\n            )}\n          </CardTitle>\n        </CardHeader>\n        <CardContent>{children}</CardContent>\n      </Card>\n    </div>\n  );\n}\n\nexport default CommonAdminPage;\n"
  },
  {
    "path": "app/src/routes/wallet/AmountItem.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { useRef, useState } from \"react\";\nimport Clickable from \"@/components/ui/clickable.tsx\";\nimport { cn } from \"@/components/ui/lib/utils.ts\";\nimport { Cloud, Check } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useCurrency } from \"@/store/info\";\n\ntype AmountComponentProps = {\n  amount: number;\n  active?: boolean;\n  other?: boolean;\n  onClick?: () => void;\n  onAmountChange?: (amount: number) => void;\n};\n\nfunction QuotaItem({ amount, active, other, onClick }: AmountComponentProps) {\n  const { t } = useTranslation();\n  const { symbol } = useCurrency();\n  const ref = useRef<HTMLInputElement>(null);\n\n  const onClickEvent = () => {\n    onClick?.();\n    ref.current?.focus();\n  };\n\n  return (\n    <Clickable\n      className={cn(\n        \"amount bg-input/10 dark:bg-input/20 shadow-sm h-full\",\n        active && \"active\",\n      )}\n      onClick={onClickEvent}\n      tapScale={0.975}\n    >\n      {active && (\n        <Check\n          className={`h-5 w-5 p-1 bg-primary/5 dark:bg-primary/10 rounded-full absolute top-2 right-2 text-primary`}\n        />\n      )}\n      {!other ? (\n        <>\n          <div className={`amount-title`}>\n            {(amount * 10).toFixed(0)}\n            <Cloud className={`h-4 w-4`} />\n          </div>\n          <div className={`amount-desc text-xs`}>\n            <span className=\"text-2xs\">{symbol}</span>\n            {amount.toFixed(2)}\n          </div>\n        </>\n      ) : (\n        <>\n          <div className={`other my-auto`}>{t(\"buy.other\")}</div>\n        </>\n      )}\n    </Clickable>\n  );\n}\n\ntype QuotaWrapperProps = {\n  current: number;\n  onCurrentChange: (index: number) => void;\n  amount: number;\n  onAmountChange: (amount: number) => void;\n  builtinAmount: number[];\n};\n\nexport default function QuotaWrapper({\n  current,\n  onCurrentChange,\n  onAmountChange,\n  builtinAmount,\n}: QuotaWrapperProps) {\n  const customIndex = builtinAmount.length;\n  const [customAmount, setCustomAmount] = useState(\"\");\n\n  const containerVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        staggerChildren: 0.05,\n      },\n    },\n  };\n\n  const itemVariants = {\n    hidden: { y: 20, opacity: 0 },\n    visible: {\n      y: 0,\n      opacity: 1,\n      transition: {\n        type: \"spring\",\n        stiffness: 300,\n        damping: 20,\n      },\n    },\n  };\n\n  const handleCustomAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    let value = e.target.value.trim() || \"0\";\n    // Remove leading zeros\n    if (value.startsWith(\"0\") && value.length > 1) {\n      value = value.replace(/^0+/, \"\");\n    }\n    if (/^\\d+$/.test(value)) {\n      setCustomAmount(value);\n      onAmountChange(Number(value));\n    }\n  };\n\n  return (\n    <motion.div\n      className={`amount-container`}\n      initial=\"hidden\"\n      animate=\"visible\"\n      variants={containerVariants}\n    >\n      <div className={`amount-wrapper`}>\n        <AnimatePresence>\n          {builtinAmount.map((amount, index) => (\n            <motion.div key={index} variants={itemVariants}>\n              <QuotaItem\n                amount={amount}\n                active={current === index}\n                onClick={() => {\n                  onCurrentChange(index);\n                  onAmountChange(amount * 10);\n                }}\n              />\n            </motion.div>\n          ))}\n          <motion.div variants={itemVariants}>\n            <QuotaItem\n              amount={NaN}\n              other={true}\n              active={current === customIndex}\n              onClick={() => onCurrentChange(customIndex)}\n              onAmountChange={onAmountChange}\n            />\n          </motion.div>\n        </AnimatePresence>\n      </div>\n      {current === customIndex && (\n        <div className=\"mt-0.5 relative w-full\">\n          <Input\n            type=\"number\"\n            className=\"w-full pl-10 text-center\"\n            value={customAmount}\n            onChange={handleCustomAmountChange}\n            maxLength={8}\n          />\n          <Cloud\n            className={`h-4 w-4 absolute left-2.5 top-1/2 -translate-y-1/2`}\n          />\n        </div>\n      )}\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "app/src/routes/wallet/WalletQuotaBox.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { useState } from \"react\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { quotaSelector, refreshQuota } from \"@/store/quota.ts\";\nimport { AppDispatch } from \"@/store\";\nimport { Cloud, ExternalLink, Gift } from \"lucide-react\";\nimport { docsEndpoint } from \"@/conf/env.ts\";\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { toast } from \"sonner\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog.tsx\";\nimport { Input } from \"@/components/ui/input.tsx\";\nimport { useRedeem as redeemCode } from \"@/api/redeem.ts\";\nimport { motion } from \"framer-motion\";\n\nexport default function WalletQuotaBox() {\n  const { t } = useTranslation();\n  const quota = useSelector(quotaSelector);\n  const [redeemOpen, setRedeemOpen] = useState(false);\n\n  const containerVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        duration: 0.1,\n        when: \"beforeChildren\",\n        staggerChildren: 0.1,\n      },\n    },\n  };\n\n  const itemVariants = {\n    hidden: { opacity: 0, y: 20 },\n    visible: {\n      opacity: 1,\n      y: 0,\n      transition: { duration: 0.5 },\n    },\n  };\n\n  return (\n    <motion.div\n      className={`w-full h-fit md:mt-4`}\n      id={`quota`}\n      variants={containerVariants}\n      initial=\"hidden\"\n      animate=\"visible\"\n    >\n      <RedeemComponent open={redeemOpen} onOpenChanged={setRedeemOpen} />\n      <motion.div className={`flex flex-col pb-4`} variants={itemVariants}>\n        <motion.div className={`dialog-wrapper`} variants={itemVariants}>\n          <motion.div className={`buy-interface`} variants={itemVariants}>\n            <motion.div\n              className={`w-full h-fit mt-0 border rounded-lg p-2.5 bg-background flex flex-col md:flex-row`}\n              variants={itemVariants}\n            >\n              <motion.div\n                className=\"flex flex-col w-full md:w-1/2 p-2.5 pb-4 md:pb-2.5 border-b md:border-r md:border-b-0\"\n                variants={itemVariants}\n              >\n                <motion.div\n                  className=\"text-xs text-secondary mb-1\"\n                  variants={itemVariants}\n                >\n                  {t(\"buy.title\")}\n                </motion.div>\n                <motion.div\n                  className=\"text-2xl font-medium mb-1 jetbrains-mono\"\n                  variants={itemVariants}\n                >\n                  <Cloud className={`h-4 w-4 mr-1 inline-block`} />\n                  {quota.toFixed(2)}\n                </motion.div>\n                <motion.div\n                  className={`text-xs text-secondary mt-auto break-all whitespace-pre-wrap`}\n                  variants={itemVariants}\n                >\n                  {t(\"buy.quota-info\")}\n                  <a\n                    href={docsEndpoint}\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"inline-block text-sky-500 hover:text-sky-600\"\n                  >\n                    <ExternalLink className=\"h-3.5 w-3.5 mr-0.5 ml-1 inline-block\" />\n                    {t(\"buy.learn-more\")}\n                  </a>\n                </motion.div>\n              </motion.div>\n\n              <motion.div\n                className=\"flex flex-col w-full md:w-1/2 pt-4 md:pt-0 md:pl-2\"\n                variants={itemVariants}\n              >\n                <motion.div\n                  className={`flex flex-col items-center justify-center h-full`}\n                  variants={itemVariants}\n                >\n                  <motion.div\n                    className=\"flex flex-col space-y-2 w-full px-1\"\n                    variants={itemVariants}\n                  >\n                    <Button\n                      variant=\"outline\"\n                      className=\"w-full transition-all hover:bg-secondary\"\n                      onClick={() => setRedeemOpen(true)}\n                    >\n                      <Gift className=\"h-4 w-4 mr-2\" />\n                      {t(\"buy.redeem-title\")}\n                    </Button>\n                  </motion.div>\n                </motion.div>\n              </motion.div>\n            </motion.div>\n          </motion.div>\n        </motion.div>\n      </motion.div>\n    </motion.div>\n  );\n}\n\ntype RedeemComponentProps = {\n  open: boolean;\n  onOpenChanged: (open: boolean) => void;\n};\n\nfunction RedeemComponent({ open, onOpenChanged }: RedeemComponentProps) {\n  const { t } = useTranslation();\n  const [redeem, setRedeem] = useState(\"\");\n  const dispatch: AppDispatch = useDispatch();\n\n  const doRedeemAction = async () => {\n    if (redeem.trim() === \"\") return;\n    const res = await redeemCode(redeem.trim());\n    if (res.status) {\n      toast.success(t(\"buy.exchange-success\"), {\n        description: t(\"buy.exchange-success-prompt\", {\n          amount: res.quota,\n        }),\n      });\n      setRedeem(\"\");\n      dispatch(refreshQuota());\n      onOpenChanged(false);\n    } else {\n      toast.error(t(\"buy.exchange-failed\"), {\n        description: t(\"buy.exchange-failed-prompt\", {\n          reason: res.error,\n        }),\n      });\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChanged}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"buy.redeem-title\")}</DialogTitle>\n          <DialogDescription>{t(\"buy.redeem-description\")}</DialogDescription>\n        </DialogHeader>\n        <div className={`w-full h-fit relative`}>\n          <Gift\n            className={`h-4 w-4 text-muted-foreground absolute left-3 top-1/2 -translate-y-1/2`}\n          />\n          <Input\n            className={`redeem-input flex-grow text-center pl-10`}\n            placeholder={t(\"buy.redeem-placeholder\")}\n            value={redeem}\n            onChange={(e) => setRedeem(e.target.value)}\n          />\n        </div>\n        <DialogFooter>\n          <Button\n            variant=\"outline\"\n            unClickable\n            onClick={() => onOpenChanged(false)}\n          >\n            {t(\"cancel\")}\n          </Button>\n          <Button\n            unClickable\n            disabled={redeem.trim() === \"\"}\n            loading={true}\n            onClick={doRedeemAction}\n          >\n            {t(\"buy.redeem\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/spinner.tsx",
    "content": "import { NProgress } from \"@tanem/react-nprogress\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { decreaseTask, increaseTask, selectIsTasking } from \"@/store/auth.ts\";\nimport { useEffect } from \"react\";\nimport {\n  closeSpinnerType,\n  openSpinnerType,\n  spinnerEvent,\n} from \"@/events/spinner.ts\";\n\nfunction Spinner() {\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    spinnerEvent.bind((event) => {\n      switch (event.type) {\n        case openSpinnerType:\n          dispatch(increaseTask(event.id));\n          break;\n        case closeSpinnerType:\n          dispatch(decreaseTask(event.id));\n          break;\n      }\n    });\n  }, []);\n\n  const isAnimating = useSelector(selectIsTasking);\n\n  return (\n    <NProgress isAnimating={isAnimating}>\n      {({ animationDuration, isFinished, progress }) => (\n        <div\n          className={`spinner`}\n          style={{\n            opacity: isFinished ? 0 : 1,\n            transitionDuration: `${animationDuration}ms`,\n            width: `${progress * 100}vw`,\n          }}\n        ></div>\n      )}\n    </NProgress>\n  );\n}\n\nexport default Spinner;\n"
  },
  {
    "path": "app/src/store/api.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { getKey, regenerateKey } from \"@/api/addition.ts\";\nimport { AppDispatch, RootState } from \"./index.ts\";\n\nexport const apiSlice = createSlice({\n  name: \"api\",\n  initialState: {\n    key: \"\",\n  },\n  reducers: {\n    setKey: (state, action) => {\n      state.key = action.payload as string;\n    },\n  },\n});\n\nexport const { setKey } = apiSlice.actions;\nexport default apiSlice.reducer;\n\nexport const keySelector = (state: RootState): string => state.api.key;\n\nexport const getApiKey = async (dispatch: AppDispatch, retries?: boolean) => {\n  const response = await getKey();\n  if (response.status) {\n    if (response.key.length === 0 && retries !== false) {\n      await getApiKey(dispatch, false);\n      return;\n    }\n    dispatch(setKey(response.key));\n  }\n};\n\nexport const regenerateApiKey = async (dispatch: AppDispatch) => {\n  const response = await regenerateKey();\n  if (response.status) {\n    dispatch(setKey(response.key));\n  }\n\n  return response;\n};\n"
  },
  {
    "path": "app/src/store/auth.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport axios from \"axios\";\nimport { tokenField } from \"@/conf/bootstrap.ts\";\nimport { AppDispatch, RootState } from \"./index.ts\";\nimport { forgetMemory, setMemory } from \"@/utils/memory.ts\";\nimport { doState } from \"@/api/auth.ts\";\n\nexport const authSlice = createSlice({\n  name: \"auth\",\n  initialState: {\n    token: \"\",\n    init: false,\n    authenticated: false,\n    admin: false,\n    username: \"\",\n    tasks: [] as number[],\n  },\n  reducers: {\n    setToken: (state, action) => {\n      const token = (action.payload as string).trim();\n      state.token = token;\n      axios.defaults.headers.common[\"Authorization\"] = token;\n      if (token.length > 0) setMemory(tokenField, token);\n    },\n    setAuthenticated: (state, action) => {\n      state.authenticated = action.payload as boolean;\n    },\n    setUsername: (state, action) => {\n      state.username = action.payload as string;\n    },\n    setInit: (state, action) => {\n      state.init = action.payload as boolean;\n    },\n    setAdmin: (state, action) => {\n      state.admin = action.payload as boolean;\n    },\n    updateData: (state, action) => {\n      state.init = true;\n      state.authenticated = action.payload.authenticated as boolean;\n      state.username = action.payload.username as string;\n      state.admin = action.payload.admin as boolean;\n    },\n    increaseTask: (state, action) => {\n      state.tasks.push(action.payload as number);\n    },\n    decreaseTask: (state, action) => {\n      state.tasks = state.tasks.filter((v) => v !== (action.payload as number));\n    },\n    clearTask: (state) => {\n      state.tasks = [];\n    },\n    logout: (state) => {\n      state.token = \"\";\n      state.authenticated = false;\n      state.username = \"\";\n      axios.defaults.headers.common[\"Authorization\"] = \"\";\n      forgetMemory(tokenField);\n\n      location.reload();\n    },\n  },\n});\n\nexport function validateToken(\n  dispatch: AppDispatch,\n  token: string,\n  hook?: () => any,\n) {\n  token = token.trim();\n  dispatch(setToken(token));\n\n  if (token.length === 0) {\n    dispatch(\n      updateData({\n        authenticated: false,\n        username: \"\",\n        admin: false,\n      }),\n    );\n\n    return;\n  } else\n    doState()\n      .then((data) => {\n        console.log(\"[auth] user info:\", data);\n        dispatch(\n          updateData({\n            authenticated: data.status,\n            username: data.user,\n            admin: data.admin,\n          }),\n        );\n\n        hook && hook();\n      })\n      .catch((err) => {\n        // keep state\n        console.debug(err);\n      });\n}\n\nexport const selectAuthenticated = (state: RootState) =>\n  state.auth.authenticated;\nexport const selectUsername = (state: RootState) => state.auth.username;\nexport const selectInit = (state: RootState) => state.auth.init;\nexport const selectAdmin = (state: RootState) => state.auth.admin;\nexport const selectTasks = (state: RootState) => state.auth.tasks;\nexport const selectTasksLength = (state: RootState) => state.auth.tasks.length;\nexport const selectIsTasking = (state: RootState) =>\n  state.auth.tasks.length > 0;\n\nexport const {\n  setToken,\n  setAuthenticated,\n  setUsername,\n  logout,\n  setInit,\n  setAdmin,\n  updateData,\n  increaseTask,\n  decreaseTask,\n  clearTask,\n} = authSlice.actions;\nexport default authSlice.reducer;\n"
  },
  {
    "path": "app/src/store/avatar.ts",
    "content": "import { createSlice, PayloadAction } from \"@reduxjs/toolkit\";\n\ninterface AvatarState {\n  avatars: Record<string, Blob | null>;\n}\n\nconst initialState: AvatarState = {\n  avatars: {},\n};\n\nconst avatarSlice = createSlice({\n  name: \"avatar\",\n  initialState,\n  reducers: {\n    setAvatar: (\n      state,\n      action: PayloadAction<{ username: string; blob: Blob | null }>,\n    ) => {\n      state.avatars[action.payload.username] = action.payload.blob;\n    },\n  },\n});\n\nexport const { setAvatar } = avatarSlice.actions;\n\nexport default avatarSlice.reducer;\n"
  },
  {
    "path": "app/src/store/chat.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport {\n  AssistantRole,\n  ConversationInstance,\n  Model,\n  UserRole,\n} from \"@/api/types.tsx\";\nimport { Message } from \"@/api/types.tsx\";\nimport { AppDispatch, RootState } from \"./index.ts\";\nimport {\n  getArrayMemory,\n  getBooleanMemory,\n  getMemory,\n  setArrayMemory,\n  setMemory,\n  setNumberMemory,\n} from \"@/utils/memory.ts\";\nimport {\n  getOfflineModels,\n  loadPreferenceModels,\n  setOfflineModels,\n} from \"@/conf/storage.ts\";\nimport {\n  deleteConversation as doDeleteConversation,\n  deleteAllConversations as doDeleteAllConversations,\n  renameConversation as doRenameConversation,\n  loadConversation,\n  getConversationList,\n} from \"@/api/history.ts\";\nimport { CustomMask, Mask } from \"@/masks/types.ts\";\nimport { listMasks } from \"@/api/mask.ts\";\nimport { useDispatch, useSelector } from \"react-redux\";\nimport { useMemo } from \"react\";\nimport { ConnectionStack, StreamMessage } from \"@/api/connection.ts\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  contextSelector,\n  frequencyPenaltySelector,\n  historySelector,\n  maxTokensSelector,\n  presencePenaltySelector,\n  repetitionPenaltySelector,\n  temperatureSelector,\n  topKSelector,\n  topPSelector,\n} from \"@/store/settings.ts\";\n\nexport type ConversationSerialized = {\n  model?: string;\n  messages: Message[];\n};\n\nexport type ConnectionEvent = {\n  id: number;\n  event: string;\n  index?: number;\n  message?: string;\n};\n\ntype initialStateType = {\n  history: ConversationInstance[];\n  conversations: Record<number, ConversationSerialized>;\n  model: string;\n  web: boolean;\n  current: number;\n  model_list: string[];\n  market: boolean;\n  mask_item: Mask | null;\n  custom_masks: CustomMask[];\n  support_models: Model[];\n};\n\nconst defaultConversation: ConversationSerialized = { messages: [] };\n\nexport function inModel(supportModels: Model[], model: string): boolean {\n  return (\n    model.length > 0 &&\n    supportModels.filter((item: Model) => item.id === model).length > 0\n  );\n}\n\nexport function getModel(\n  supportModels: Model[],\n  model: string | undefined | null,\n): string {\n  if (supportModels.length === 0) return \"\";\n  return model && inModel(supportModels, model) ? model : supportModels[0].id;\n}\n\nexport function getModelList(\n  supportModels: Model[],\n  models: string[],\n  select: string,\n): string[] {\n  const list = models.filter((item) => inModel(supportModels, item));\n  const selection = getModel(supportModels, select);\n  if (!list.includes(selection)) list.push(selection);\n  return list;\n}\n\nexport const stack = new ConnectionStack();\nconst offline = loadPreferenceModels(getOfflineModels());\nconst chatSlice = createSlice({\n  name: \"chat\",\n  initialState: {\n    history: [],\n    messages: [],\n    conversations: {\n      [-1]: { ...defaultConversation },\n    },\n    web: getBooleanMemory(\"web\", false),\n    current: -1,\n    model: getModel(offline, getMemory(\"model\")),\n    model_list: getModelList(\n      offline,\n      getArrayMemory(\"model_mark_list\"),\n      getMemory(\"model\"),\n    ),\n    market: false,\n    mask_item: null,\n    custom_masks: [],\n    support_models: offline,\n  } as initialStateType,\n  reducers: {\n    createMessage: (state, action) => {\n      const { id, role, content } = action.payload as {\n        id: number;\n        role: string;\n        content?: string;\n      };\n\n      const conversation = state.conversations[id];\n      if (!conversation) return;\n\n      conversation.messages.push({\n        role: role ?? AssistantRole,\n        content: content ?? \"\",\n        end: role === AssistantRole ? false : undefined,\n      });\n    },\n    fillMaskItem: (state) => {\n      const conversation = state.conversations[-1];\n\n      if (state.mask_item && conversation.messages.length === 0) {\n        conversation.messages = [...state.mask_item.context];\n        state.mask_item = null;\n      }\n    },\n    updateMessage: (state, action) => {\n      const { id, message } = action.payload as {\n        id: number;\n        message: StreamMessage;\n      };\n      const conversation = state.conversations[id];\n      if (!conversation) return;\n\n      if (conversation.messages.length === 0)\n        conversation.messages.push({\n          role: AssistantRole,\n          content: message.message,\n          keyword: message.keyword,\n          quota: message.quota,\n          end: message.end,\n          plan: message.plan,\n        });\n\n      const instance = conversation.messages[conversation.messages.length - 1];\n      if (message.message.length > 0) instance.content += message.message;\n      if (message.keyword) instance.keyword = message.keyword;\n      if (message.quota) instance.quota = message.quota;\n      if (message.end) instance.end = message.end;\n      instance.plan = message.plan;\n    },\n    removeMessage: (state, action) => {\n      const { id, idx } = action.payload as { id: number; idx: number };\n      const conversation = state.conversations[id];\n      if (!conversation) return;\n\n      conversation.messages.splice(idx, 1);\n    },\n    restartMessage: (state, action) => {\n      const id = action.payload as number;\n      const conversation = state.conversations[id];\n      if (!conversation || conversation.messages.length === 0) return;\n\n      conversation.messages.push({\n        role: AssistantRole,\n        content: \"\",\n        end: false,\n      });\n    },\n    editMessage: (state, action) => {\n      const { id, idx, message } = action.payload as {\n        id: number;\n        idx: number;\n        message: string;\n      };\n      const conversation = state.conversations[id];\n      if (!conversation || conversation.messages.length <= idx) return;\n\n      conversation.messages[idx].content = message;\n    },\n    stopMessage: (state, action) => {\n      const { id } = action.payload as { id: number };\n      const conversation = state.conversations[id];\n      if (!conversation || conversation.messages.length === 0) return;\n\n      conversation.messages[conversation.messages.length - 1].end = true;\n    },\n    raiseConversation: (state, action) => {\n      // raise conversation `-1` to target id\n      const id = action.payload as number;\n      const conversation = state.conversations[-1];\n      if (!conversation || id === -1) return;\n\n      state.conversations[id] = conversation;\n      if (state.current === -1) state.current = id;\n\n      state.conversations[-1] = { ...defaultConversation };\n    },\n    importConversation: (state, action) => {\n      const { conversation, id } = action.payload as {\n        conversation: ConversationSerialized;\n        id: number;\n      };\n\n      if (state.conversations[id]) return;\n      state.conversations[id] = conversation;\n    },\n    deleteConversation: (state, action) => {\n      const id = action.payload as number;\n\n      if (id === -1) return;\n\n      state.history = state.history.filter((item) => item.id !== id);\n\n      if (!state.conversations[id]) return;\n\n      if (state.current === id) state.current = -1;\n      delete state.conversations[id];\n    },\n    deleteAllConversation: (state) => {\n      state.history = [];\n\n      state.conversations = { [-1]: { ...defaultConversation } };\n      state.current = -1;\n    },\n    setHistory: (state, action) => {\n      state.history = action.payload as ConversationInstance[];\n    },\n    preflightHistory: (state, action) => {\n      const name = action.payload as string;\n\n      // add a new history at the beginning\n      state.history = [{ id: -1, name, message: [] }, ...state.history];\n    },\n    renameHistory: (state, action) => {\n      const { id, name } = action.payload as { id: number; name: string };\n      const conversation = state.history.find((item) => item.id === id);\n      if (conversation) conversation.name = name;\n    },\n    setModel: (state, action) => {\n      const model = action.payload as string;\n      if (!model || model === \"\") return;\n      if (!inModel(state.support_models, model)) return;\n\n      // if model is not in model list, add it\n      // if (!state.model_list.includes(model)) {\n      //   console.log(\"[model] auto add model to list:\", model);\n      //   state.model_list.push(model);\n      //   setArrayMemory(\"model_mark_list\", state.model_list);\n      // }\n\n      setMemory(\"model\", model as string);\n      state.model = action.payload as string;\n    },\n    setWeb: (state, action) => {\n      setMemory(\"web\", action.payload ? \"true\" : \"false\");\n      state.web = action.payload as boolean;\n    },\n    toggleWeb: (state) => {\n      const web = !state.web;\n      setMemory(\"web\", web ? \"true\" : \"false\");\n      state.web = web;\n    },\n    setCurrent: (state, action) => {\n      const current = action.payload as number;\n      state.current = current;\n\n      const conversation = state.conversations[current];\n      if (!conversation) return;\n      if (\n        conversation.model &&\n        inModel(state.support_models, conversation.model)\n      ) {\n        state.model = conversation.model;\n      }\n    },\n    setModelList: (state, action) => {\n      const models = action.payload as string[];\n      state.model_list = models.filter((item) =>\n        inModel(state.support_models, item),\n      );\n      setArrayMemory(\"model_mark_list\", models);\n    },\n    addModelList: (state, action) => {\n      const model = action.payload as string;\n      if (\n        inModel(state.support_models, model) &&\n        !state.model_list.includes(model)\n      ) {\n        state.model_list.push(model);\n        setArrayMemory(\"model_mark_list\", state.model_list);\n      }\n    },\n    removeModelList: (state, action) => {\n      const model = action.payload as string;\n      if (\n        inModel(state.support_models, model) &&\n        state.model_list.includes(model)\n      ) {\n        state.model_list = state.model_list.filter((item) => item !== model);\n        setArrayMemory(\"model_mark_list\", state.model_list);\n      }\n    },\n    setMaskItem: (state, action) => {\n      state.mask_item = action.payload as Mask;\n    },\n    clearMaskItem: (state) => {\n      state.mask_item = null;\n    },\n    setCustomMasks: (state, action) => {\n      state.custom_masks = action.payload as CustomMask[];\n    },\n    setSupportModels: (state, action) => {\n      const models = action.payload as Model[];\n\n      state.support_models = models;\n      state.model = getModel(models, getMemory(\"model\"));\n      state.model_list = getModelList(\n        models,\n        getArrayMemory(\"model_mark_list\"),\n        getMemory(\"model\"),\n      );\n\n      setOfflineModels(models);\n    },\n\n  },\n});\n\nexport const {\n  setHistory,\n  renameHistory,\n  setCurrent,\n  setModel,\n  setWeb,\n  toggleWeb,\n  setModelList,\n  addModelList,\n  removeModelList,\n  setCustomMasks,\n  setSupportModels,\n  setMaskItem,\n  clearMaskItem,\n  fillMaskItem,\n  createMessage,\n  updateMessage,\n  removeMessage,\n  restartMessage,\n  editMessage,\n  stopMessage,\n  raiseConversation,\n  importConversation,\n  deleteConversation,\n  deleteAllConversation,\n  preflightHistory,\n} = chatSlice.actions;\nexport const selectHistory = (state: RootState): ConversationInstance[] =>\n  state.chat.history;\nexport const selectConversations = (\n  state: RootState,\n): Record<number, ConversationSerialized> => state.chat.conversations;\nexport const selectModel = (state: RootState): string => state.chat.model;\nexport const selectWeb = (state: RootState): boolean => state.chat.web;\nexport const selectCurrent = (state: RootState): number => state.chat.current;\nexport const selectModelList = (state: RootState): string[] =>\n  state.chat.model_list;\nexport const selectCustomMasks = (state: RootState): CustomMask[] =>\n  state.chat.custom_masks;\nexport const selectSupportModels = (state: RootState): Model[] =>\n  state.chat.support_models;\nexport const selectMaskItem = (state: RootState): Mask | null =>\n  state.chat.mask_item;\n\nexport function useConversation(): ConversationSerialized | undefined {\n  const conversations = useSelector(selectConversations);\n  const current = useSelector(selectCurrent);\n\n  return useMemo(() => conversations[current], [conversations, current]);\n}\n\nexport function useConversationActions() {\n  const dispatch = useDispatch();\n  const conversations = useSelector(selectConversations);\n  const current = useSelector(selectCurrent);\n  const mask = useSelector(selectMaskItem);\n\n  return {\n    toggle: async (id: number) => {\n      const conversation = conversations[id];\n      setNumberMemory(\"history_conversation\", id);\n      if (!conversation) {\n        const data = await loadConversation(id);\n        const props: ConversationSerialized = {\n          model: data.model,\n          messages: data.message,\n        };\n        dispatch(\n          importConversation({\n            conversation: props,\n            id,\n          }),\n        );\n      }\n\n      if (current === -1 && conversations[-1].messages.length === 0) {\n        // current is mask, clear mask\n        mask && dispatch(clearMaskItem());\n      }\n\n      dispatch(setCurrent(id));\n    },\n    rename: async (id: number, name: string) => {\n      const resp = await doRenameConversation(id, name);\n      resp.status && dispatch(renameHistory({ id, name }));\n\n      return resp;\n    },\n    remove: async (id: number) => {\n      const state = await doDeleteConversation(id);\n      state && dispatch(deleteConversation(id));\n\n      return state;\n    },\n    removeAll: async () => {\n      const state = await doDeleteAllConversations();\n      state && dispatch(deleteAllConversation());\n\n      return state;\n    },\n    refresh: async () => {\n      const resp = await getConversationList();\n      dispatch(setHistory(resp));\n\n      return resp;\n    },\n    mask: (mask: Mask) => {\n      dispatch(setMaskItem(mask));\n\n      if (current !== -1) {\n        dispatch(setCurrent(-1));\n      }\n    },\n    selected: (model?: string) => {\n      dispatch(setModel(model ?? \"\"));\n    },\n  };\n}\n\nexport function useMessageActions() {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const { refresh } = useConversationActions();\n  const current = useSelector(selectCurrent);\n  const conversations = useSelector(selectConversations);\n  const mask = useSelector(selectMaskItem);\n\n  const model = useSelector(selectModel);\n  const web = useSelector(selectWeb);\n  const history = useSelector(historySelector);\n  const context = useSelector(contextSelector);\n  const max_tokens = useSelector(maxTokensSelector);\n  const temperature = useSelector(temperatureSelector);\n  const top_p = useSelector(topPSelector);\n  const top_k = useSelector(topKSelector);\n  const presence_penalty = useSelector(presencePenaltySelector);\n  const frequency_penalty = useSelector(frequencyPenaltySelector);\n  const repetition_penalty = useSelector(repetitionPenaltySelector);\n\n  return {\n    send: async (message: string, using_model?: string) => {\n      if (current === -1 && conversations[-1].messages.length === 0) {\n        // preflight history if it's a new conversation\n        dispatch(preflightHistory(message));\n      }\n\n      if (!stack.hasConnection(current)) {\n        const conn = stack.createConnection(current);\n\n        if (current === -1 && mask && mask.context.length > 0) {\n          conn.sendMaskEvent(t, mask);\n          dispatch(fillMaskItem());\n        }\n      }\n\n      const state = stack.send(current, t, {\n        type: \"chat\",\n        message,\n        web,\n        model: using_model || model,\n        context: history,\n        ignore_context: !context,\n        max_tokens,\n        temperature,\n        top_p,\n        top_k,\n        presence_penalty,\n        frequency_penalty,\n        repetition_penalty,\n      });\n      if (!state) return false;\n\n      dispatch(\n        createMessage({ id: current, role: UserRole, content: message }),\n      );\n      dispatch(createMessage({ id: current, role: AssistantRole }));\n\n      return true;\n    },\n    stop: () => {\n      if (!stack.hasConnection(current)) return;\n      stack.sendStopEvent(current, t);\n      dispatch(stopMessage(current));\n    },\n    restart: () => {\n      if (!stack.hasConnection(current)) {\n        stack.createConnection(current);\n      }\n      stack.sendRestartEvent(current, t, {\n        web,\n        model,\n        context: history,\n        ignore_context: !context,\n        max_tokens,\n        temperature,\n        top_p,\n        top_k,\n        presence_penalty,\n        frequency_penalty,\n        repetition_penalty,\n        message: \"\",\n      });\n\n      // remove the last message if it's from assistant and create a new message\n      dispatch(restartMessage(current));\n    },\n    remove: (idx: number) => {\n      if (idx < 0 || idx >= conversations[current].messages.length) return;\n\n      dispatch(removeMessage({ id: current, idx }));\n\n      if (!stack.hasConnection(current)) stack.createConnection(current);\n      stack.sendRemoveEvent(current, t, idx);\n    },\n    edit: (idx: number, message: string) => {\n      if (idx < 0 || idx >= conversations[current].messages.length) return;\n\n      dispatch(editMessage({ id: current, idx, message }));\n      if (!stack.hasConnection(current)) stack.createConnection(current);\n      stack.sendEditEvent(current, t, idx, message);\n    },\n    receive: async (id: number, message: StreamMessage) => {\n      dispatch(updateMessage({ id, message }));\n\n      // raise conversation if it is -1\n      if (id === -1 && message.conversation) {\n        const target: number = message.conversation;\n        dispatch(raiseConversation(target));\n        setNumberMemory(\"history_conversation\", target);\n        stack.raiseConnection(target);\n        await refresh();\n      }\n    },\n  };\n}\n\nexport function listenMessageEvent() {\n  const actions = useMessageActions();\n\n  return (e: ConnectionEvent) => {\n    console.debug(`[conversation] receive event: ${e.event} (id: ${e.id})`);\n\n    switch (e.event) {\n      case \"stop\":\n        actions.stop();\n        break;\n      case \"restart\":\n        actions.restart();\n        break;\n      case \"remove\":\n        actions.remove(e.index ?? -1);\n        break;\n      case \"edit\":\n        actions.edit(e.index ?? -1, e.message ?? \"\");\n        break;\n    }\n  };\n}\n\nexport function useMessages(): Message[] {\n  const conversations = useSelector(selectConversations);\n  const current = useSelector(selectCurrent);\n  const mask = useSelector(selectMaskItem);\n\n  return useMemo(() => {\n    const messages = conversations[current]?.messages || [];\n    const showMask = current === -1 && mask && messages.length === 0;\n    return !showMask ? messages : mask?.context;\n  }, [conversations, current, mask]);\n}\n\nexport function useWorking(): boolean {\n  const messages = useMessages();\n\n  return useMemo(() => {\n    if (messages.length === 0) return false;\n\n    const last = messages[messages.length - 1];\n    if (last.role !== AssistantRole || last.end === undefined) return false;\n    return !last.end;\n  }, [messages]);\n}\n\nexport const updateMasks = async (dispatch: AppDispatch) => {\n  const resp = await listMasks();\n  resp.data && resp.data.length > 0 && dispatch(setCustomMasks(resp.data));\n\n  return resp;\n};\n\nexport const updateSupportModels = (dispatch: AppDispatch, models: Model[]) => {\n  dispatch(setSupportModels(loadPreferenceModels(models)));\n};\n\nexport default chatSlice.reducer;\n"
  },
  {
    "path": "app/src/store/globals.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { Plans } from \"@/api/types.tsx\";\nimport { AppDispatch, RootState } from \"@/store/index.ts\";\nimport { getOfflinePlans, setOfflinePlans } from \"@/conf/storage.ts\";\nimport { getTheme, Theme } from \"@/components/ThemeProvider.tsx\";\n\ntype GlobalState = {\n  theme: Theme;\n  subscription: Plans;\n};\n\nexport const globalSlice = createSlice({\n  name: \"global\",\n  initialState: {\n    theme: getTheme(),\n    subscription: getOfflinePlans(),\n  } as GlobalState,\n  reducers: {\n    setSubscription: (state, action) => {\n      const plans = action.payload as Plans;\n      state.subscription = plans;\n      setOfflinePlans(plans);\n    },\n    setTheme: (state, action) => {\n      state.theme = action.payload;\n    },\n  },\n});\n\nexport const { setSubscription, setTheme } = globalSlice.actions;\n\nexport default globalSlice.reducer;\n\nexport const subscriptionDataSelector = (state: RootState): Plans =>\n  state.global.subscription;\nexport const themeSelector = (state: RootState): Theme => state.global.theme;\n\nexport const dispatchSubscriptionData = (\n  dispatch: AppDispatch,\n  subscription: Plans,\n) => {\n  dispatch(setSubscription(subscription));\n};\n"
  },
  {
    "path": "app/src/store/index.ts",
    "content": "import { configureStore } from \"@reduxjs/toolkit\";\nimport infoReducer from \"./info\";\nimport globalReducer from \"./globals\";\nimport menuReducer from \"./menu\";\nimport authReducer from \"./auth\";\nimport chatReducer from \"./chat\";\nimport quotaReducer from \"./quota\";\nimport packageReducer from \"./package\";\nimport subscriptionReducer from \"./subscription\";\nimport apiReducer from \"./api\";\nimport sharingReducer from \"./sharing\";\nimport settingsReducer from \"./settings\";\nimport recordReducer from \"./record\";\nimport avatarReducer from \"./avatar\";\n\nconst store = configureStore({\n  reducer: {\n    info: infoReducer,\n    global: globalReducer,\n    menu: menuReducer,\n    auth: authReducer,\n    chat: chatReducer,\n    quota: quotaReducer,\n    package: packageReducer,\n    subscription: subscriptionReducer,\n    api: apiReducer,\n    sharing: sharingReducer,\n    settings: settingsReducer,\n    record: recordReducer,\n    avatar: avatarReducer,\n  },\n});\n\ntype RootState = ReturnType<typeof store.getState>;\ntype AppDispatch = typeof store.dispatch;\n\nexport function createCronJob(\n  dispatch: AppDispatch,\n  method: Function,\n  interval: number,\n  runWhenInit?: boolean,\n) {\n  if (runWhenInit) dispatch(method());\n  return setInterval(() => dispatch(method()), interval * 1000);\n}\n\nexport function clearCronJob(job: ReturnType<typeof setInterval>) {\n  clearInterval(job);\n}\n\nexport function clearCronJobs(jobs: ReturnType<typeof setInterval>[]) {\n  jobs.forEach((job) => clearInterval(job));\n}\n\nexport type { RootState, AppDispatch };\nexport default store;\n"
  },
  {
    "path": "app/src/store/info.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { RootState } from \"@/store/index.ts\";\nimport {\n  getArrayMemory,\n  getBooleanMemory,\n  getMemory,\n  setArrayMemory,\n  setBooleanMemory,\n  setMemory,\n} from \"@/utils/memory.ts\";\nimport { SiteInfo } from \"@/admin/api/info.ts\";\nimport { useSelector } from \"react-redux\";\nimport { BroadcastEvent } from \"@/api/broadcast\";\n\ntype Currency = {\n  symbol: string;\n  name: string;\n  id: string;\n};\n\nexport const currencyMap: Record<string, Currency> = {\n  cny: { symbol: \"￥\", name: \"CNY\", id: \"cny\" },\n  jpy: { symbol: \"JP¥\", name: \"JPY\", id: \"jpy\" },\n  hkd: { symbol: \"HK$\", name: \"HKD\", id: \"hkd\" },\n  usd: { symbol: \"$\", name: \"USD\", id: \"usd\" },\n  eur: { symbol: \"€\", name: \"EUR\", id: \"eur\" },\n  gbp: { symbol: \"£\", name: \"GBP\", id: \"gbp\" },\n};\n\nexport const infoSlice = createSlice({\n  name: \"info\",\n  initialState: {\n    mail: getBooleanMemory(\"mail\", false),\n    currency: getMemory(\"currency\"),\n    contact: getMemory(\"contact\"),\n    article: getArrayMemory(\"article\"),\n    generation: getArrayMemory(\"generation\"),\n    footer: getMemory(\"footer\"),\n    auth_footer: getBooleanMemory(\"auth_footer\", false),\n    relay_plan: getBooleanMemory(\"relay_plan\", false),\n    payment: getArrayMemory(\"payment\"),\n    payment_aggregation: getBooleanMemory(\"payment_aggregation\", false),\n    title: getMemory(\"title\"),\n    logo: getMemory(\"logo\"),\n    file: getMemory(\"file\"),\n    docs: getMemory(\"docs\"),\n    announcement: getMemory(\"announcement\"),\n    buy_link: getMemory(\"buy_link\"),\n    hide_key_docs: getBooleanMemory(\"hide_key_docs\", false),\n    backend: getMemory(\"backend\"),\n    group_pricing: {},\n\n    broadcast: getMemory(\"broadcast_data\")\n      ? (JSON.parse(getMemory(\"broadcast_data\")) as BroadcastEvent)\n      : {\n          message: \"\",\n          firstReceived: false,\n        },\n    oauth_providers: {},\n  } as SiteInfo & {\n    broadcast: BroadcastEvent;\n  },\n  reducers: {\n    setForm: (state, action) => {\n      const form = action.payload as SiteInfo;\n      state.mail = form.mail ?? false;\n      state.currency = form.currency ?? \"cny\";\n      state.contact = form.contact ?? \"\";\n      state.article = form.article ?? [];\n      state.generation = form.generation ?? [];\n      state.footer = form.footer ?? \"\";\n      state.auth_footer = form.auth_footer ?? false;\n      state.relay_plan = form.relay_plan ?? false;\n      state.payment = form.payment ?? [];\n      state.title = form.title ?? \"\";\n      state.logo = form.logo ?? \"\";\n      state.file = form.file ?? \"\";\n      state.docs = form.docs ?? \"\";\n      state.announcement = form.announcement ?? \"\";\n      state.buy_link = form.buy_link ?? \"\";\n      state.hide_key_docs = form.hide_key_docs ?? false;\n      state.backend = form.backend ?? state.backend;\n      state.payment_aggregation = form.payment_aggregation ?? false;\n      state.broadcast = form.broadcast ?? {\n        message: \"\",\n        firstReceived: false,\n      };\n      setMemory(\"title\", state.title);\n      setMemory(\"logo\", state.logo);\n      setMemory(\"file\", state.file);\n      setMemory(\"docs\", state.docs);\n      setMemory(\"announcement\", state.announcement);\n      setMemory(\"buy_link\", state.buy_link);\n      setBooleanMemory(\"hide_key_docs\", state.hide_key_docs);\n      if (state.backend) setMemory(\"backend\", state.backend);\n      setMemory(\"currency\", state.currency);\n      setBooleanMemory(\"mail\", state.mail);\n      setMemory(\"contact\", state.contact);\n      setArrayMemory(\"article\", state.article);\n      setArrayMemory(\"generation\", state.generation);\n      setMemory(\"footer\", state.footer);\n      setBooleanMemory(\"auth_footer\", state.auth_footer);\n      setBooleanMemory(\"relay_plan\", state.relay_plan);\n      setArrayMemory(\"payment\", state.payment);\n      setBooleanMemory(\"payment_aggregation\", state.payment_aggregation);\n      setMemory(\"broadcast_data\", JSON.stringify(state.broadcast));\n    },\n  },\n});\n\nexport const { setForm } = infoSlice.actions;\n\nexport default infoSlice.reducer;\n\nexport const infoMailSelector = (state: RootState): boolean => state.info.mail;\nexport const infoContactSelector = (state: RootState): string =>\n  state.info.contact;\nexport const infoArticleSelector = (state: RootState): string[] =>\n  state.info.article;\nexport const infoGenerationSelector = (state: RootState): string[] =>\n  state.info.generation;\nexport const infoFooterSelector = (state: RootState): string =>\n  state.info.footer;\nexport const infoAuthFooterSelector = (state: RootState): boolean =>\n  state.info.auth_footer;\nexport const infoRelayPlanSelector = (state: RootState): boolean =>\n  state.info.relay_plan;\nexport const infoPaymentSelector = (state: RootState): string[] =>\n  state.info.payment;\nexport const isPaymentAggregationSelector = (state: RootState): boolean =>\n  state.info.payment_aggregation;\nexport const infoCurrencySelector = (state: RootState): string =>\n  state.info.currency;\nexport const infoAnnouncementSelector = (state: RootState): string =>\n  state.info.announcement;\nexport const infoHideKeyDocsSelector = (state: RootState): boolean =>\n  (state.info as any).hide_key_docs ?? false;\nexport const infoBackendSelector = (state: RootState): string | undefined =>\n  (state.info as any).backend;\nexport const infoBroadcastSelector = (state: RootState): BroadcastEvent =>\n  state.info.broadcast;\n\nexport const useCurrency = (): Currency => {\n  const currency = useSelector(infoCurrencySelector);\n  return currencyMap[currency] ?? currencyMap.cny;\n};\n"
  },
  {
    "path": "app/src/store/menu.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { mobile } from \"@/utils/device.ts\";\n\nexport const menuSlice = createSlice({\n  name: \"menu\",\n  initialState: {\n    open: !mobile, // mobile: false, desktop: true\n  },\n  reducers: {\n    toggleMenu: (state) => {\n      state.open = !state.open;\n    },\n    closeMenu: (state) => {\n      state.open = false;\n    },\n    openMenu: (state) => {\n      state.open = true;\n    },\n    setMenu: (state, action) => {\n      state.open = action.payload as boolean;\n    },\n  },\n});\n\nexport const { toggleMenu, closeMenu, openMenu, setMenu } = menuSlice.actions;\nexport default menuSlice.reducer;\n\nexport const selectMenu = (state: any) => state.menu.open;\n"
  },
  {
    "path": "app/src/store/package.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { getPackage } from \"@/api/addition.ts\";\nimport { AppDispatch } from \"./index.ts\";\n\nexport const packageSlice = createSlice({\n  name: \"package\",\n  initialState: {\n    cert: false,\n    teenager: false,\n  },\n  reducers: {\n    refreshState: (state, action) => {\n      state.cert = action.payload.cert;\n      state.teenager = action.payload.teenager;\n    },\n  },\n});\n\nexport const { refreshState } = packageSlice.actions;\nexport default packageSlice.reducer;\n\nexport const certSelector = (state: any): boolean => state.package.cert;\nexport const teenagerSelector = (state: any): boolean => state.package.teenager;\n\nexport const refreshPackage = async (dispatch: AppDispatch) => {\n  const response = await getPackage();\n  if (response.status) dispatch(refreshState(response));\n};\n"
  },
  {
    "path": "app/src/store/quota.ts",
    "content": "import { createAsyncThunk, createSlice } from \"@reduxjs/toolkit\";\nimport { RootState } from \"./index.ts\";\nimport { getQuota } from \"@/api/quota.ts\";\n\nexport const quotaSlice = createSlice({\n  name: \"quota\",\n  initialState: {\n    quota: 0,\n  },\n  reducers: {\n    setQuota: (state, action) => {\n      state.quota = action.payload as number;\n    },\n  },\n  extraReducers: (builder) => {\n    builder.addCase(refreshQuota.fulfilled, (state, action) => {\n      console.log(\n        \"[redux] receive task `refreshQuota` event: \",\n        action.payload,\n      );\n      state.quota = action.payload as number;\n    });\n  },\n});\n\nexport const { setQuota } = quotaSlice.actions;\nexport default quotaSlice.reducer;\n\nexport const quotaSelector = (state: RootState): number => state.quota.quota;\n\nexport const refreshQuota = createAsyncThunk(\"quota/refreshQuota\", async () => {\n  return await getQuota();\n});\n"
  },
  {
    "path": "app/src/store/record.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { RootState } from \"@/store/index.ts\";\nimport { RecordData, RecordStats } from \"@/api/record.ts\";\n\ntype RecordProps = {\n  data: RecordData;\n  stats: RecordStats;\n  page: number;\n};\n\nexport const recordSlice = createSlice({\n  name: \"record\",\n  initialState: {\n    data: {\n      total: 0,\n      records: [],\n    },\n    stats: {\n      billing_today: 0,\n      billing_month: 0,\n      request_today: 0,\n      request_month: 0,\n      rpm: 0,\n      tpm: 0,\n    },\n    page: 0,\n  } as RecordProps,\n  reducers: {\n    setData: (state, action) => {\n      state.data = action.payload;\n    },\n    setPage: (state, action) => {\n      state.page = action.payload;\n    },\n    setStats: (state, action) => {\n      state.stats = action.payload;\n    },\n  },\n});\n\nexport const { setData, setPage, setStats } = recordSlice.actions;\nexport default recordSlice.reducer;\n\nexport const dataSelector = (state: RootState) => state.record.data;\nexport const pageSelector = (state: RootState) => state.record.page;\nexport const statsSelector = (state: RootState) => state.record.stats;\n"
  },
  {
    "path": "app/src/store/settings.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport {\n  getBooleanMemory,\n  getNumberMemory,\n  setBooleanMemory,\n  setNumberMemory,\n} from \"@/utils/memory.ts\";\nimport { RootState } from \"@/store/index.ts\";\nimport { isMobile } from \"@/utils/device.ts\";\n\nexport const sendKeys = [\"Ctrl + Enter\", \"Enter\"];\nexport const initialSettings = {\n  context: true,\n  align: false,\n  history: 8,\n  sender: !isMobile(), // default [mobile: Ctrl + Enter, pc: Enter]\n  max_tokens: 2000,\n  temperature: 0.6,\n  top_p: 1,\n  top_k: 5,\n  presence_penalty: 0,\n  frequency_penalty: 0,\n  repetition_penalty: 1,\n  hide_model: false,\n  hide_toolbar: false,\n  hide_toolbar_text: true,\n};\n\nexport const settingsSlice = createSlice({\n  name: \"settings\",\n  initialState: {\n    dialog: false,\n    context: getBooleanMemory(\"context\", true), // keep context\n    align: getBooleanMemory(\"align\", false), // chat textarea align center\n    history: getNumberMemory(\"history_context\", 8), // max history context length\n    sender: getBooleanMemory(\"sender\", !isMobile()), // sender (false: Ctrl + Enter, true: Enter)\n    max_tokens: getNumberMemory(\"max_tokens\", 2000), // max tokens\n    temperature: getNumberMemory(\"temperature\", 0.6), // temperature\n    top_p: getNumberMemory(\"top_p\", 1), // top_p\n    top_k: getNumberMemory(\"top_k\", 5), // top_k\n    presence_penalty: getNumberMemory(\"presence_penalty\", 0), // presence_penalty\n    frequency_penalty: getNumberMemory(\"frequency_penalty\", 0), // frequency_penalty\n    repetition_penalty: getNumberMemory(\"repetition_penalty\", 1), // repetition_penalty\n    hide_model: getBooleanMemory(\"hide_model\", false), // hide model\n    hide_toolbar: getBooleanMemory(\"hide_toolbar\", false), // hide toolbar\n    hide_toolbar_text: getBooleanMemory(\"hide_toolbar_text\", true), // hide toolbar text\n  },\n  reducers: {\n    toggleDialog: (state) => {\n      state.dialog = !state.dialog;\n    },\n    setDialog: (state, action) => {\n      state.dialog = action.payload as boolean;\n    },\n    openDialog: (state) => {\n      state.dialog = true;\n    },\n    closeDialog: (state) => {\n      state.dialog = false;\n    },\n    setContext: (state, action) => {\n      state.context = action.payload as boolean;\n      setBooleanMemory(\"context\", action.payload);\n    },\n    setAlign: (state, action) => {\n      state.align = action.payload as boolean;\n      setBooleanMemory(\"align\", action.payload);\n    },\n    setHistory: (state, action) => {\n      state.history = action.payload as number;\n      setNumberMemory(\"history_context\", action.payload);\n    },\n    setSender: (state, action) => {\n      state.sender = action.payload as boolean;\n      setBooleanMemory(\"sender\", action.payload);\n    },\n    setMaxTokens: (state, action) => {\n      state.max_tokens = action.payload as number;\n      setNumberMemory(\"max_tokens\", action.payload);\n    },\n    setTemperature: (state, action) => {\n      state.temperature = action.payload as number;\n      setNumberMemory(\"temperature\", action.payload);\n    },\n    setTopP: (state, action) => {\n      state.top_p = action.payload as number;\n      setNumberMemory(\"top_p\", action.payload);\n    },\n    setTopK: (state, action) => {\n      state.top_k = action.payload as number;\n      setNumberMemory(\"top_k\", action.payload);\n    },\n    setPresencePenalty: (state, action) => {\n      state.presence_penalty = action.payload as number;\n      setNumberMemory(\"presence_penalty\", action.payload);\n    },\n    setFrequencyPenalty: (state, action) => {\n      state.frequency_penalty = action.payload as number;\n      setNumberMemory(\"frequency_penalty\", action.payload);\n    },\n    setRepetitionPenalty: (state, action) => {\n      state.repetition_penalty = action.payload as number;\n      setNumberMemory(\"repetition_penalty\", action.payload);\n    },\n    setHideModel: (state, action) => {\n      state.hide_model = action.payload as boolean;\n      setBooleanMemory(\"hide_model\", action.payload);\n    },\n    setHideToolbar: (state, action) => {\n      state.hide_toolbar = action.payload as boolean;\n      setBooleanMemory(\"hide_toolbar\", action.payload);\n    },\n    setHideToolbarText: (state, action) => {\n      state.hide_toolbar_text = action.payload as boolean;\n      setBooleanMemory(\"hide_toolbar_text\", action.payload);\n    },\n    resetSettings: (state) => {\n      state.context = initialSettings.context;\n      state.align = initialSettings.align;\n      state.history = initialSettings.history;\n      state.sender = initialSettings.sender;\n      state.max_tokens = initialSettings.max_tokens;\n      state.temperature = initialSettings.temperature;\n      state.top_p = initialSettings.top_p;\n      state.top_k = initialSettings.top_k;\n      state.presence_penalty = initialSettings.presence_penalty;\n      state.frequency_penalty = initialSettings.frequency_penalty;\n      state.repetition_penalty = initialSettings.repetition_penalty;\n      state.hide_model = initialSettings.hide_model;\n      state.hide_toolbar = initialSettings.hide_toolbar;\n      state.hide_toolbar_text = initialSettings.hide_toolbar_text;\n\n      setBooleanMemory(\"context\", initialSettings.context);\n      setBooleanMemory(\"align\", initialSettings.align);\n      setNumberMemory(\"history_context\", initialSettings.history);\n      setBooleanMemory(\"sender\", initialSettings.sender);\n      setNumberMemory(\"max_tokens\", initialSettings.max_tokens);\n      setNumberMemory(\"temperature\", initialSettings.temperature);\n      setNumberMemory(\"top_p\", initialSettings.top_p);\n      setNumberMemory(\"top_k\", initialSettings.top_k);\n      setNumberMemory(\"presence_penalty\", initialSettings.presence_penalty);\n      setNumberMemory(\"frequency_penalty\", initialSettings.frequency_penalty);\n      setNumberMemory(\"repetition_penalty\", initialSettings.repetition_penalty);\n      setBooleanMemory(\"hide_model\", initialSettings.hide_model);\n      setBooleanMemory(\"hide_toolbar\", initialSettings.hide_toolbar);\n      setBooleanMemory(\"hide_toolbar_text\", initialSettings.hide_toolbar_text);\n    },\n  },\n});\n\nexport const {\n  toggleDialog,\n  setDialog,\n  openDialog,\n  closeDialog,\n  setContext,\n  setAlign,\n  setHistory,\n  setSender,\n  setMaxTokens,\n  setTemperature,\n  setTopP,\n  setTopK,\n  setPresencePenalty,\n  setFrequencyPenalty,\n  setRepetitionPenalty,\n  resetSettings,\n  setHideModel,\n  setHideToolbar,\n  setHideToolbarText,\n} = settingsSlice.actions;\nexport default settingsSlice.reducer;\n\nexport const dialogSelector = (state: RootState): boolean =>\n  state.settings.dialog;\nexport const contextSelector = (state: RootState): boolean =>\n  state.settings.context;\nexport const alignSelector = (state: RootState): boolean =>\n  state.settings.align;\nexport const historySelector = (state: RootState): number =>\n  state.settings.history;\nexport const senderSelector = (state: RootState): boolean =>\n  state.settings.sender;\nexport const maxTokensSelector = (state: RootState): number =>\n  state.settings.max_tokens;\nexport const temperatureSelector = (state: RootState): number =>\n  state.settings.temperature;\nexport const topPSelector = (state: RootState): number => state.settings.top_p;\nexport const topKSelector = (state: RootState): number => state.settings.top_k;\nexport const presencePenaltySelector = (state: RootState): number =>\n  state.settings.presence_penalty;\nexport const frequencyPenaltySelector = (state: RootState): number =>\n  state.settings.frequency_penalty;\nexport const repetitionPenaltySelector = (state: RootState): number =>\n  state.settings.repetition_penalty;\nexport const hideModelSelector = (state: RootState): boolean =>\n  state.settings.hide_model;\nexport const hideToolbarSelector = (state: RootState): boolean =>\n  state.settings.hide_toolbar;\nexport const hideToolbarTextSelector = (state: RootState): boolean =>\n  state.settings.hide_toolbar_text;\n"
  },
  {
    "path": "app/src/store/sharing.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\nimport { AppDispatch, RootState } from \"./index.ts\";\nimport {\n  deleteSharing,\n  listSharing,\n  SharingPreviewForm,\n} from \"@/api/sharing.ts\";\n\nexport const sharingSlice = createSlice({\n  name: \"sharing\",\n  initialState: {\n    data: [] as SharingPreviewForm[],\n  },\n  reducers: {\n    setData: (state, action) => {\n      state.data = action.payload as SharingPreviewForm[];\n    },\n    removeData: (state, action) => {\n      const hash = action.payload as string;\n      state.data = state.data.filter((item) => item.hash !== hash);\n    },\n  },\n});\n\nexport const { setData, removeData } = sharingSlice.actions;\nexport default sharingSlice.reducer;\n\nexport const dataSelector = (state: RootState): SharingPreviewForm[] =>\n  state.sharing.data;\n\nexport const syncData = async (dispatch: AppDispatch): Promise<string> => {\n  const response = await listSharing();\n\n  if (response.status) dispatch(setData(response.data));\n\n  return response.status ? \"\" : response.message;\n};\n\nexport const deleteData = async (\n  dispatch: AppDispatch,\n  hash: string,\n): Promise<string> => {\n  const response = await deleteSharing(hash);\n  if (response.status) dispatch(removeData(hash));\n\n  return response.status ? \"\" : response.message;\n};\n"
  },
  {
    "path": "app/src/store/subscription.ts",
    "content": "import { createAsyncThunk, createSlice } from \"@reduxjs/toolkit\";\nimport { getSubscription } from \"@/api/addition.ts\";\n\nexport const subscriptionSlice = createSlice({\n  name: \"subscription\",\n  initialState: {\n    is_subscribed: false,\n    level: 0,\n    enterprise: false,\n    expired: 0,\n    expired_at: \"\",\n    refresh: 0,\n    refresh_at: \"\",\n    usage: {},\n  },\n  reducers: {},\n  extraReducers: (builder) => {\n    builder.addCase(refreshSubscription.fulfilled, (state, action) => {\n      console.log(\n        \"[redux] receive task `refreshSubscription` event: \",\n        action.payload,\n      );\n      if (!action.payload.status) return;\n      state.is_subscribed = action.payload.is_subscribed;\n      state.expired = action.payload.expired;\n      state.usage = action.payload.usage || {};\n      state.enterprise = action.payload.enterprise || false;\n      state.level = action.payload.level;\n      state.expired_at = action.payload.expired_at || \"\";\n      state.refresh = action.payload.refresh || 0;\n      state.refresh_at = action.payload.refresh_at || \"\";\n    });\n  },\n});\n\nexport default subscriptionSlice.reducer;\n\nexport const isSubscribedSelector = (state: any): boolean =>\n  state.subscription.is_subscribed;\nexport const levelSelector = (state: any): number => state.subscription.level;\nexport const expiredSelector = (state: any): number =>\n  state.subscription.expired;\nexport const expiredAtSelector = (state: any): string =>\n  state.subscription.expired_at;\nexport const refreshSelector = (state: any): number =>\n  state.subscription.refresh;\nexport const refreshAtSelector = (state: any): string =>\n  state.subscription.refresh_at;\nexport const usageSelector = (state: any): any => state.subscription.usage;\n\nexport const refreshSubscription = createAsyncThunk(\n  \"subscription/refreshSubscription\",\n  async () => {\n    return await getSubscription();\n  },\n);\n"
  },
  {
    "path": "app/src/store/utils.ts",
    "content": "import { useDispatch, useSelector } from \"react-redux\";\nimport { RootState } from \"./index.ts\";\n\nexport function dispatchWrapper(\n  action: (state: RootState, payload?: any) => any,\n) {\n  return (payload?: any) => {\n    const dispatch = useDispatch();\n    dispatch(action(payload));\n  };\n}\n\nexport function getSelector(reducer: string, key: string) {\n  return useSelector((state: any) => state[reducer][key]);\n}\n"
  },
  {
    "path": "app/src/translator/adapter.ts",
    "content": "// format language code to name/ISO 639-1 code map\nconst languageTranslatorMap: Record<string, string> = {\n  cn: \"zh-CN\",\n  tw: \"zh-TW\",\n  en: \"en\",\n  ru: \"ru\",\n  ja: \"ja\",\n  ko: \"ko\",\n  fr: \"fr\",\n  de: \"de\",\n  es: \"es\",\n  pt: \"pt\",\n  it: \"it\",\n};\n\nexport function getFormattedLanguage(lang: string): string {\n  return languageTranslatorMap[lang.toLowerCase()] || lang;\n}\n\ntype translationResponse = {\n  responseData: {\n    translatedText: string;\n  };\n};\n\nasync function translate(\n  text: string,\n  from: string,\n  to: string,\n): Promise<string> {\n  if (from === to || text.length === 0) return text;\n  const resp = await fetch(\n    `https://api.mymemory.translated.net/get?q=${encodeURIComponent(\n      text,\n    )}&langpair=${from}|${to}`,\n  );\n  const data: translationResponse = await resp.json();\n\n  return data.responseData.translatedText;\n}\n\nexport function doTranslate(\n  content: string,\n  from: string,\n  to: string,\n): Promise<string> {\n  from = getFormattedLanguage(from);\n  to = getFormattedLanguage(to);\n\n  if (content.startsWith(\"!!\")) content = content.substring(2);\n\n  return translate(content, from, to);\n}\n"
  },
  {
    "path": "app/src/translator/index.ts",
    "content": "import { Plugin, ResolvedConfig } from \"vite\";\nimport { processTranslation } from \"./translator\";\n\nexport function createTranslationPlugin(): Plugin {\n  return {\n    name: \"translate-plugin\",\n    apply: \"build\",\n    async configResolved(config: ResolvedConfig) {\n      try {\n        console.info(\"[i18n] start translation process\");\n        await processTranslation(config);\n      } catch (e) {\n        console.warn(`error during translation: ${e}`);\n      } finally {\n        console.info(\"[i18n] translation process finished\");\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "app/src/translator/io.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\n\nexport function readJSON(...paths: string[]): any {\n  return JSON.parse(fs.readFileSync(path.resolve(...paths)).toString());\n}\n\nexport function writeJSON(data: any, ...paths: string[]): void {\n  fs.writeFileSync(path.resolve(...paths), JSON.stringify(data, null, 2));\n}\n\nexport function getMigration(\n  mother: Record<string, any>,\n  data: Record<string, any>,\n  prefix: string,\n): string[] {\n  return Object.keys(mother)\n    .map((key): string[] => {\n      const template = mother[key],\n        translation = data !== undefined && key in data ? data[key] : undefined;\n      const val = [prefix.length === 0 ? key : `${prefix}.${key}`];\n\n      switch (typeof template) {\n        case \"string\":\n          if (typeof translation !== \"string\") return val;\n          else if (template.startsWith(\"!!\")) return val;\n          break;\n        case \"object\":\n          return getMigration(template, translation, val[0]);\n        default:\n          return typeof translation === typeof template ? [] : val;\n      }\n\n      return [];\n    })\n    .flat()\n    .filter((key) => key !== undefined && key.length > 0);\n}\n\nexport function getFields(data: any): number {\n  switch (typeof data) {\n    case \"string\":\n      return 1;\n    case \"object\":\n      if (Array.isArray(data)) return data.length;\n      return Object.keys(data).reduce(\n        (acc, key) => acc + getFields(data[key]),\n        0,\n      );\n    default:\n      return 1;\n  }\n}\n\nexport function getTranslation(data: Record<string, any>, path: string): any {\n  const keys = path.split(\".\");\n  let current = data;\n  for (const key of keys) {\n    if (current[key] === undefined) return undefined;\n    current = current[key];\n  }\n  return current;\n}\n\nexport function setTranslation(\n  data: Record<string, any>,\n  path: string,\n  value: any,\n): void {\n  const keys = path.split(\".\");\n  let current = data;\n  for (let i = 0; i < keys.length - 1; i++) {\n    if (current[keys[i]] === undefined) current[keys[i]] = {};\n    current = current[keys[i]];\n  }\n  current[keys[keys.length - 1]] = value;\n}\n"
  },
  {
    "path": "app/src/translator/translator.ts",
    "content": "import { ResolvedConfig } from \"vite\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport {\n  getFields,\n  getMigration,\n  getTranslation,\n  readJSON,\n  setTranslation,\n  writeJSON,\n} from \"./io\";\nimport { doTranslate } from \"./adapter\";\n\nexport const defaultDevLang = \"cn\";\n\nexport async function processTranslation(\n  config: ResolvedConfig,\n): Promise<void> {\n  const source = path.resolve(config.root, \"src/resources/i18n\");\n  const files = fs.readdirSync(source);\n\n  const motherboard = `${defaultDevLang}.json`;\n\n  if (files.length === 0) {\n    console.warn(\"no translation files found\");\n    return;\n  } else if (!files.includes(motherboard)) {\n    console.warn(`no default translation file found (${defaultDevLang}.json)`);\n    return;\n  }\n\n  const data = readJSON(source, motherboard);\n\n  const target = files.filter((file) => file !== motherboard);\n  for (const file of target) {\n    const lang = file.split(\".\")[0];\n    const translation = { ...readJSON(source, file) };\n\n    const fields = getFields(data);\n    const migration = getMigration(data, translation, \"\");\n    const total = migration.length;\n    let current = 0;\n    for (const key of migration) {\n      const from = getTranslation(data, key);\n      const to =\n        typeof from === \"string\"\n          ? await doTranslate(from, defaultDevLang, lang)\n          : from;\n      current++;\n\n      console.log(\n        `[i18n] successfully translated: ${from} -> ${to} (lang: ${defaultDevLang} -> ${lang}, progress: ${current}/${total})`,\n      );\n      setTranslation(translation, key, to);\n    }\n\n    if (migration.length > 0) {\n      writeJSON(translation, source, file);\n    }\n\n    console.info(\n      `translation file ${file} loaded, ${fields} fields detected, ${migration.length} migration(s) applied`,\n    );\n  }\n}\n"
  },
  {
    "path": "app/src/types/performance.d.ts",
    "content": "declare global {\n  // see https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory\n\n  interface PerformanceMemory {\n    usedJSHeapSize: number;\n    totalJSHeapSize: number;\n    jsHeapSizeLimit: number;\n  }\n\n  interface Performance {\n    memory: PerformanceMemory;\n  }\n\n  interface Window {\n    __TAURI__: Tauri;\n  }\n}\n\nexport declare function getMemoryPerformance(): number;\n"
  },
  {
    "path": "app/src/types/service.d.ts",
    "content": "declare module \"virtual:pwa-register/react\" {\n  // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error\n  // @ts-expect-error ignore when React is not installed\n  import type { Dispatch, SetStateAction } from \"react\";\n  import type { RegisterSWOptions } from \"vite-plugin-pwa/types\";\n\n  export type { RegisterSWOptions };\n\n  export function useRegisterSW(options?: RegisterSWOptions): {\n    needRefresh: [boolean, Dispatch<SetStateAction<boolean>>];\n    offlineReady: [boolean, Dispatch<SetStateAction<boolean>>];\n    updateServiceWorker: (reloadPage?: boolean) => Promise<void>;\n    onRegistered: (registration: ServiceWorkerRegistration) => void;\n  };\n}\n\ninterface BeforeInstallPromptEvent extends Event {\n  readonly platforms: string[];\n  readonly userChoice: Promise<{\n    outcome: \"accepted\" | \"dismissed\";\n    platform: string;\n  }>;\n  prompt(): Promise<void>;\n}\n"
  },
  {
    "path": "app/src/types/ui.d.ts",
    "content": "declare module \"@radix-ui/react-select-area\";\n\ndeclare module \"sonner\" {\n  export interface ToastProps {\n    description?: string | React.ReactNode;\n    action?: {\n      label: string;\n      onClick: () => void;\n    };\n    duration?: number;\n  }\n\n  export const Toaster: React.FC;\n\n  export type ToastFunc = (title: string, options?: ToastProps) => void;\n  export type ToastPromise = (promise: Promise<any>, options?: any) => void;\n\n  export const toast: {\n    (title: string, options?: ToastProps): void;\n\n    info: ToastFunc;\n    success: ToastFunc;\n    error: ToastFunc;\n    loading: ToastFunc;\n    warning: ToastFunc;\n    action: ToastFunc;\n    promise: ToastPromise;\n  };\n}\n"
  },
  {
    "path": "app/src/utils/analytics.ts",
    "content": "import ReactGA from \"react-ga4\";\n\nexport function initGoogleAnalytics(trackingId: string | undefined): void {\n  trackingId && ReactGA.initialize(trackingId);\n}\n"
  },
  {
    "path": "app/src/utils/app.ts",
    "content": "import router from \"@/router.tsx\";\nimport { useDeeptrain } from \"@/conf/env.ts\";\nimport { goDeepLogin } from \"@/conf/deeptrain.tsx\";\n\nexport let event: BeforeInstallPromptEvent | undefined;\n\nwindow.addEventListener(\"beforeinstallprompt\", (e: Event) => {\n  console.debug(`[service] catch event from app install prompt`);\n  event = e as BeforeInstallPromptEvent;\n});\n\nexport function triggerInstallApp() {\n  /**\n   * Trigger install app prompt\n   * Warning: this is a browser experimental feature, it may not work on some browsers\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent\n   *\n   * @example\n   * triggerInstallApp();\n   */\n  if (!event) return;\n  try {\n    event.prompt();\n    event.userChoice.then((choice: any) => {\n      console.debug(`[service] installed app (status: ${choice.outcome})`);\n    });\n  } catch (err) {\n    console.debug(\"[service] install app error\", err);\n  }\n\n  event = undefined;\n}\n\nexport function getMemoryPerformance(): number {\n  /**\n   * Get memory performance\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory\n   *\n   * @example\n   * getMemoryPerformance();\n   */\n\n  if (!performance || !performance.memory) return NaN;\n  return performance.memory.usedJSHeapSize / 1024 / 1024;\n}\n\nexport function navigate(path: string): void {\n  router\n    .navigate(path)\n    .then(() => console.debug(`[service] navigate to ${path}`))\n    .catch((err) => console.debug(`[service] navigate error`, err));\n}\n\nexport function goAuth(): void {\n  useDeeptrain ? goDeepLogin() : navigate(\"/login\");\n}\n"
  },
  {
    "path": "app/src/utils/base.ts",
    "content": "import React from \"react\";\n\nexport function insert<T>(arr: T[], idx: number, value: T): T[] {\n  return [...arr.slice(0, idx), value, ...arr.slice(idx)];\n}\n\nexport function insertStart<T>(arr: T[], value: T): T[] {\n  return [value, ...arr];\n}\n\nexport function remove<T>(arr: T[], idx: number): T[] {\n  return [...arr.slice(0, idx), ...arr.slice(idx + 1)];\n}\n\nexport function replace<T>(arr: T[], idx: number, value: T): T[] {\n  return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)];\n}\n\nexport function move<T>(arr: T[], from: number, to: number): T[] {\n  const value = arr[from];\n  return insert(remove(arr, from), to, value);\n}\n\nexport function asyncCaller<T>(fn: (...args: any[]) => Promise<T>) {\n  let promise: Promise<T> | undefined;\n  return (...args: any[]) => {\n    if (!promise) promise = fn(...args);\n    return promise;\n  };\n}\n\nexport function sum(arr: number[]): number {\n  return arr.reduce((a, b) => a + b, 0);\n}\n\nexport function average(arr: number[]): number {\n  return sum(arr) / arr.length;\n}\n\nexport function getUniqueList<T>(arr: T[]): T[] {\n  return [...new Set(arr)];\n}\n\nexport function getNumber(value: string, supportNegative = true): string {\n  return value.replace(supportNegative ? /[^-0-9.]/g : /[^0-9.]/g, \"\");\n}\n\nexport function parseNumber(value: string): number {\n  return parseFloat(getNumber(value));\n}\n\nexport function splitList(value: string, separators: string[]): string[] {\n  const result: string[] = [];\n  for (const item of value.split(new RegExp(separators.join(\"|\"), \"g\"))) {\n    if (item) result.push(item);\n  }\n  return result;\n}\n\nexport function getErrorMessage(error: any): string {\n  if (error instanceof Error) return error.message;\n  if (typeof error === \"string\") return error;\n  return JSON.stringify(error);\n}\n\nexport function isAsyncFunc(fn: any): boolean {\n  return fn.constructor.name === \"AsyncFunction\";\n}\n\nexport function generateRandomChar(n: number): string {\n  const chars = \"abcdefghijklmnopqrstuvwxyz\";\n  return Array(n)\n    .fill(0)\n    .map(() => chars[Math.floor(Math.random() * chars.length)])\n    .join(\"\");\n}\n\nexport function generateInt(min: number, max: number): number {\n  return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nexport function generateListNumber(n: number): number {\n  return generateInt(Math.pow(10, n - 1), Math.pow(10, n) - 1);\n}\n\nexport function isUrl(value: string): boolean {\n  if (!value) return false;\n\n  value = value.trim();\n  if (!value.length) return false;\n  try {\n    new URL(value);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nexport function isEnter<T extends HTMLElement>(\n  e: React.KeyboardEvent<T> | KeyboardEvent,\n): boolean {\n  return e.key === \"Enter\" && e.keyCode != 229;\n}\n\nexport function withCtrl<T extends HTMLElement>(\n  e: React.KeyboardEvent<T> | KeyboardEvent,\n): boolean {\n  // if platform is Mac, use Command instead of Ctrl\n  return e.ctrlKey || e.metaKey;\n}\n\nexport function withShift<T extends HTMLElement>(\n  e: React.KeyboardEvent<T> | KeyboardEvent,\n): boolean {\n  return e.shiftKey;\n}\n\nexport function resetJsArray<T>(arr: T[], target: T[]): T[] {\n  /**\n   * this function is used to reset an array to another array without changing the *pointer\n   */\n\n  arr.splice(0, arr.length, ...target);\n  return arr;\n}\n\nexport function getSizeUnit(size: number): string {\n  if (size < 1024) return `${size} B`;\n  if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;\n  if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`;\n  return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`;\n}\n\nexport function getHostName(url: string): string {\n  try {\n    return new URL(url).hostname;\n  } catch {\n    return \"\";\n  }\n}\n\nexport function isB64Image(value: string): boolean {\n  return /data:image\\/([^;]+);base64,([a-zA-Z0-9+/=]+)/g.test(value);\n}\n\nexport function trimPrefix(value: string, prefix: string): string {\n  return value.startsWith(prefix) ? value.slice(prefix.length) : value;\n}\n\nexport function trimSuffix(value: string, suffix: string): string {\n  return value.endsWith(suffix) ? value.slice(0, -suffix.length) : value;\n}\n\nexport function addPrefix(value: string, prefix: string): string {\n  return value.startsWith(prefix) ? value : prefix + value;\n}\n\nexport function addSuffix(value: string, suffix: string): string {\n  return value.endsWith(suffix) ? value : value + suffix;\n}\n\nexport function trimPrefixes(value: string, prefixes: string[]): string {\n  for (const prefix of prefixes) {\n    if (value.startsWith(prefix)) return value.slice(prefix.length);\n  }\n  return value;\n}\n\nexport function trimSuffixes(value: string, suffixes: string[]): string {\n  for (const suffix of suffixes) {\n    if (value.endsWith(suffix)) return value.slice(0, -suffix.length);\n  }\n  return value;\n}\n\nexport function getFilenameFromURL(url: string | undefined): string {\n  // e.g. https://example.com/example.png => example.png\n  if (!url) return \"\";\n  return url.split(\"/\").pop() || url;\n}\n\nexport function formatDecimal(value: number): string {\n  if (value === 0) return \"0.000\";\n  \n  if (Number.isInteger(value)) {\n    return value.toFixed(3);\n  }\n  \n  const str = value.toString();\n  if (str.includes('e')) {\n    const [, exp] = str.split('e');\n    const expNum = parseInt(exp);\n    if (expNum < 0) {\n      return value.toFixed(Math.abs(expNum));\n    }\n  }\n  \n  const parts = str.split('.');\n  if (parts.length === 2) {\n    const decimalPlaces = Math.max(3, parts[1].length);\n    return value.toFixed(decimalPlaces).replace(/\\.?0+$/, '');\n  }\n  \n  return value.toFixed(3);\n}\n"
  },
  {
    "path": "app/src/utils/date.ts",
    "content": "export function convertDate(date: Date): string {\n  // result: \"2021-09-01\"\n  return date.toISOString().split(\"T\")[0];\n}\n\nexport function convertDateTime(date: Date): string {\n  // result: \"2021-09-01 12:00:00\"\n  return date.toISOString().replace(\"T\", \" \").split(\".\")[0];\n}\n"
  },
  {
    "path": "app/src/utils/desktop.ts",
    "content": "export function isTauri(): boolean {\n  return window.__TAURI__ !== undefined;\n}\n"
  },
  {
    "path": "app/src/utils/dev.ts",
    "content": "export function inWaiting(duration: number): Promise<void> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, duration);\n  });\n}\n"
  },
  {
    "path": "app/src/utils/device.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport { addEventListeners } from \"@/utils/dom.ts\";\n\nexport let mobile = isMobile();\n\nwindow.addEventListener(\"resize\", () => {\n  mobile = isMobile();\n});\n\nexport function isMobile(): boolean {\n  return (\n    (document.documentElement.clientWidth || window.innerWidth) <= 668 ||\n    (document.documentElement.clientHeight || window.innerHeight) <= 468 ||\n    navigator.userAgent.includes(\"Mobile\") ||\n    navigator.userAgent.includes(\"Android\") ||\n    navigator.userAgent.includes(\"iPhone\") ||\n    navigator.userAgent.includes(\"iPad\") ||\n    navigator.userAgent.includes(\"iPod\") ||\n    navigator.userAgent.includes(\"Watch\")\n  );\n}\n\nexport function isSafari(): boolean {\n  return (\n    navigator.userAgent.includes(\"Safari\") &&\n    !navigator.userAgent.includes(\"Chrome\") &&\n    !navigator.userAgent.includes(\"Android\") &&\n    !navigator.userAgent.includes(\"Edge\")\n  );\n}\n\nexport function useMobile(): boolean {\n  const [mobile, setMobile] = useState<boolean>(isMobile);\n\n  useEffect(() => {\n    const handler = () => setMobile(isMobile);\n\n    return addEventListeners(\n      window,\n      [\n        \"resize\",\n        \"orientationchange\",\n        \"touchstart\",\n        \"touchmove\",\n        \"touchend\",\n        \"touchcancel\",\n        \"gesturestart\",\n        \"gesturechange\",\n        \"gestureend\",\n      ],\n      handler,\n    );\n  }, []);\n\n  return mobile;\n}\n\nexport function openWindow(url: string, target?: string): void {\n  /**\n   * Open a new window with the given URL.\n   * If the device does not support opening a new window, the URL will be opened in the current window.\n   * @param url The URL to open.\n   * @param target The target of the URL.\n   */\n\n  if (mobile) {\n    window.location.href = url;\n  } else {\n    window.open(url, target);\n  }\n}\n\nexport function openPage(url: string): void {\n  window.location.href = url;\n}\n\nexport function openForm(\n  url: string,\n  method: string,\n  params: Record<string, string>,\n): void {\n  /**\n   * Open a new window with a form that submits the given parameters to the given URL.\n   * If the device does not support opening a new window, the form will be submitted in the current window.\n   * @param url The URL to open.\n   * @param method The method of the form.\n   * @param params The parameters of the form.\n   */\n\n  const form = document.createElement(\"form\");\n  form.style.display = \"none\";\n  form.method = method;\n  form.action = url;\n\n  !isSafari() && form.setAttribute(\"target\", \"_blank\");\n\n  for (const key in params) {\n    const input = document.createElement(\"input\");\n    input.type = \"hidden\";\n    input.name = key;\n    input.value = params[key];\n    form.appendChild(input);\n  }\n\n  document.body.appendChild(form);\n  form.submit();\n  document.body.removeChild(form);\n}\n"
  },
  {
    "path": "app/src/utils/dom.ts",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { getErrorMessage } from \"@/utils/base.ts\";\nimport { extractMessage } from \"@/utils/processor.ts\";\nimport { toast } from \"sonner\";\n\nasync function _copyClipboard(text: string) {\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    return await navigator.clipboard.writeText(text);\n  }\n\n  const el = document.createElement(\"textarea\");\n  el.value = text;\n  // android may require editable\n  el.style.position = \"absolute\";\n  el.style.left = \"-9999px\";\n  document.body.appendChild(el);\n  el.focus();\n  el.select();\n  el.setSelectionRange(0, text.length);\n  document.execCommand(\"copy\");\n  document.body.removeChild(el);\n}\n\nexport async function copyClipboard(text: string) {\n  /**\n   * Copy text to clipboard\n   * @param text Text to copy\n   * @example\n   * await copyClipboard(\"Hello world!\");\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText\n   */\n\n  try {\n    await _copyClipboard(text);\n  } catch (e) {\n    console.warn(e);\n  }\n}\n\nexport function useClipboard() {\n  /**\n   * Use clipboard\n   * @example\n   * const copy = useClipboard();\n   * copy(\"Hello world!\");\n   */\n\n  const { t } = useTranslation();\n\n  return async (text: string) => {\n    try {\n      await _copyClipboard(text);\n      toast.success(t(\"copied.success\"), {\n        description: `${t(\"copied.success-description\")}: ${extractMessage(\n          text,\n          32,\n        )}`,\n      });\n    } catch (e) {\n      console.warn(e);\n      toast.error(t(\"copied.failed\"), {\n        description: t(\"copied.failed-description\", {\n          reason: getErrorMessage(e),\n        }),\n      });\n    }\n  };\n}\n\nexport function saveAsFile(filename: string, content: string) {\n  /**\n   * Save text as file\n   * @param filename Filename\n   * @param content File content\n   * @example\n   * saveAsFile(\"hello.txt\", \"Hello world!\");\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Blob\n   */\n\n  const a = document.createElement(\"a\");\n  a.href = URL.createObjectURL(new Blob([content]));\n  a.download = filename;\n  a.click();\n}\n\nexport function saveBlobAsFile(filename: string, blob: Blob) {\n  /**\n   * Save blob as file\n   * @param filename Filename\n   * @param blob Blob\n   * @example\n   * saveBlobAsFile(\"hello.txt\", new Blob([\"Hello world!\"]));\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Blob\n   */\n\n  const a = document.createElement(\"a\");\n  a.href = URL.createObjectURL(blob);\n  a.download = filename;\n  a.click();\n}\n\nexport function saveImageAsFile(filename: string, data_url: string) {\n  /**\n   * Save data url as image file\n   * @param filename Filename\n   * @param data_url Data url\n   * @example\n   * saveImageAsFile(\"hello.png\", \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABwElEQVRIS+2VwQ3CMBBF/4f7B0Qf4B9\n   */\n\n  const a = document.createElement(\"a\");\n  a.href = data_url;\n  a.download = filename;\n  a.click();\n}\n\nexport function getSelectionText(): string {\n  /**\n   * Get selected text\n   * @example\n   * const text = getSelectionText();\n   * console.log(text);\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection\n   */\n\n  if (window.getSelection) {\n    return window.getSelection()?.toString() || \"\";\n  } else if (document.getSelection && document.getSelection()?.toString()) {\n    return document.getSelection()?.toString() || \"\";\n  }\n  return \"\";\n}\n\nexport function getSelectionTextInArea(el: HTMLElement): string {\n  /**\n   * Get selected text in element\n   * @param el Element\n   * @example\n   * const text = getSelectionTextInArea(document.getElementById(\"textarea\"));\n   * console.log(text);\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection\n   */\n\n  const selection = window.getSelection();\n  if (!selection) return \"\";\n  const range = selection.getRangeAt(0);\n  const preSelectionRange = range.cloneRange();\n  preSelectionRange.selectNodeContents(el);\n  preSelectionRange.setEnd(range.startContainer, range.startOffset);\n  const start = preSelectionRange.toString().length;\n  return el.innerText.slice(start, start + range.toString().length);\n}\n\nexport function useDraggableInput(\n  target: HTMLElement,\n  handleChange: (files: File[]) => void,\n) {\n  /**\n   * Make input element draggable\n   */\n\n  const dragOver = (e: DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n  };\n\n  const drop = (e: DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    const files = e.dataTransfer?.files || ([] as File[]);\n    if (!files.length) return;\n    handleChange(Array.from(files));\n  };\n\n  target.addEventListener(\"dragover\", dragOver);\n  target.addEventListener(\"drop\", drop);\n\n  return () => {\n    target.removeEventListener(\"dragover\", dragOver);\n    target.removeEventListener(\"drop\", drop);\n  };\n}\n\nexport function testNumberInputEvent(e: any): boolean {\n  /**\n   * Test if input event is valid for number input\n   * @param e Input event\n   * @example\n   * const handler = (e: any) => {\n   *    if (testNumberInputEvent(e)) {\n   *      // do something\n   *    }\n   *    return;\n   *  }\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n   */\n\n  if (\n    /^[0-9]+$/.test(e.key) ||\n    [\"Backspace\", \"Delete\", \"ArrowLeft\", \"ArrowRight\", \"Tab\"].includes(e.key)\n  ) {\n    return true;\n  }\n  e.preventDefault();\n  return false;\n}\n\nexport function replaceInputValue(\n  input: HTMLInputElement | undefined,\n  value: string,\n) {\n  /**\n   * Replace input value and focus\n   * @param input Input element\n   * @param value New value\n   * @example\n   * const input = document.getElementById(\"input\") as HTMLInputElement;\n   * replaceInputValue(input, \"Hello world!\");\n   */\n\n  return input && (input.value = value);\n}\n\nexport function useInputValue(id: string, value: string) {\n  /**\n   * Replace input value and focus\n   * @param id Input element id\n   * @param value New value\n   * @example\n   * const input = document.getElementById(\"input\") as HTMLInputElement;\n   * useInputValue(\"input\", \"Hello world!\");\n   */\n\n  const input = document.getElementById(id) as HTMLInputElement | undefined;\n  return input && replaceInputValue(input, value) && input.focus();\n}\n\nexport function addEventListener(\n  el: HTMLElement,\n  event: string,\n  handler: EventListenerOrEventListenerObject,\n): () => void {\n  /**\n   * Add event listener to element\n   * @param el Element\n   * @param event Event name\n   * @param handler Event handler\n   * @example\n   * const el = document.getElementById(\"el\");\n   * const handler = () => console.log(\"Hello world!\");\n   * const remove = addEventListener(el, \"click\", handler);\n   * remove();\n   */\n\n  el.addEventListener(event, handler);\n  return () => el.removeEventListener(event, handler);\n}\n\nexport function addEventListeners(\n  el: Window | HTMLElement,\n  events: string[],\n  handler: EventListenerOrEventListenerObject,\n): () => void {\n  /**\n   * Add event listeners to element\n   * @param el Element\n   * @param events Event names\n   * @param handler Event handler\n   * @example\n   * const el = document.getElementById(\"el\");\n   * const handler = () => console.log(\"Hello world!\");\n   * const remove = addEventListeners(el, [\"click\", \"touchstart\"], handler);\n   * remove();\n   */\n\n  events.forEach((event) => el.addEventListener(event, handler));\n  return () =>\n    events.forEach((event) => el.removeEventListener(event, handler));\n}\n\nexport function scrollDown(el: HTMLElement | null) {\n  /**\n   * Scroll to bottom\n   * @param el Element\n   * @example\n   * const el = document.getElementById(\"el\");\n   * scrollDown(el);\n   */\n\n  el &&\n    el.scrollTo({\n      top: el.scrollHeight,\n      behavior: \"smooth\",\n    });\n}\n\nexport function scrollUp(el: HTMLElement | null) {\n  /**\n   * Scroll to top\n   * @param el Element\n   * @example\n   * const el = document.getElementById(\"el\");\n   * scrollUp(el);\n   */\n\n  el &&\n    el.scrollTo({\n      top: 0,\n      behavior: \"smooth\",\n    });\n}\n\nexport function updateFavicon(url?: string) {\n  /**\n   * Update favicon in the link element from head\n   * @param url Favicon url\n   * @example\n   * updateFavicon(\"https://example.com/favicon.ico\");\n   */\n  if (!url || url.trim() === \"\") return;\n  const link = document.querySelector(\"link[rel*='icon']\");\n  return link && link.setAttribute(\"href\", url);\n}\n\nexport function updateDocumentTitle(title?: string) {\n  /**\n   * Update document title\n   * @param title Document title\n   * @example\n   * updateDocumentTitle(\"Hello world!\");\n   */\n  if (!title || title.trim() === \"\") return;\n  document.title = title;\n}\n\nexport function getQuerySelector(query: string): HTMLElement | null {\n  /**\n   * Get element by query selector\n   * @param query Query selector\n   * @example\n   * const el = getQuerySelector(\"#el\");\n   * console.log(el);\n   */\n\n  return document.body.querySelector(query);\n}\n\nexport function isContainDom(\n  el: HTMLElement | undefined | null,\n  target: HTMLElement | undefined | null,\n  notIncludeSelf = false,\n) {\n  /**\n   * Test if element contains target\n   * @param el Element\n   * @param target Target\n   * @example\n   * const el = document.getElementById(\"el\");\n   * const target = document.getElementById(\"target\");\n   * console.log(isContain(el, target));\n   */\n\n  if (!el || !target) return false;\n  if (!notIncludeSelf) {\n    return el.contains(target);\n  }\n  return el === target || el.contains(target);\n}\n\nexport function convertImgToB64(img: HTMLImageElement): string {\n  /**\n   * Convert image to base64\n   * @param img Image element\n   * @example\n   * const img = document.getElementById(\"img\") as HTMLImageElement;\n   * const b64 = convertImgToB64(img);\n   * console.log(b64);\n   */\n\n  if (!img || !img.src) return \"\";\n  if (img.src.startsWith(\"data:image\")) return img.src;\n\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = img.width;\n  canvas.height = img.height;\n  canvas.style.display = \"none\";\n  document.body.appendChild(canvas);\n  const ctx = canvas.getContext(\"2d\");\n  ctx?.drawImage(img, 0, 0);\n\n  const content = canvas.toDataURL();\n  document.body.removeChild(canvas);\n\n  return content;\n}\n"
  },
  {
    "path": "app/src/utils/form.ts",
    "content": "export function setKey<T>(state: T, key: string, value: any): T {\n  const segment = key.split(\".\");\n  if (segment.length === 1) {\n    return { ...state, [key]: value };\n  } else if (segment.length > 1) {\n    const [k, ...v] = segment;\n    return { ...state, [k]: setKey(state[k as keyof T], v.join(\".\"), value) };\n  }\n\n  // segment.length is zero\n  throw new Error(\"invalid key\");\n}\n\nexport const formReducer = <T>() => {\n  return (state: T, action: any): T => {\n    action.payload = action.payload ?? action.value;\n\n    switch (action.type) {\n      case \"update\":\n        return { ...state, ...action.payload } as T;\n      case \"reset\":\n        return { ...action.payload } as T;\n      case \"set\":\n        return action.payload as T;\n      default:\n        if (action.type.startsWith(\"update:\")) {\n          const key = action.type.slice(7);\n          return setKey(state, key, action.payload) as T;\n        }\n\n        return state;\n    }\n  };\n};\n\nexport function isEmailValid(email: string) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) && email.length <= 255;\n}\n\nexport function isInRange(value: number, min: number, max: number) {\n  return value >= min && value <= max;\n}\n\nexport function isTextInRange(value: string, min: number, max: number) {\n  return value.trim().length >= min && value.trim().length <= max;\n}\n"
  },
  {
    "path": "app/src/utils/groups.ts",
    "content": "import { useSelector } from \"react-redux\";\nimport { selectAdmin, selectAuthenticated } from \"@/store/auth.ts\";\nimport { levelSelector } from \"@/store/subscription.ts\";\nimport { useMemo } from \"react\";\n\nexport const AnonymousType = \"anonymous\";\nexport const NormalType = \"normal\";\nexport const BasicType = \"basic\";\nexport const StandardType = \"standard\";\nexport const ProType = \"pro\";\nexport const AdminType = \"admin\";\n\nexport const allGroups: string[] = [\n  AnonymousType,\n  NormalType,\n  BasicType,\n  StandardType,\n  ProType,\n  AdminType,\n];\n\nexport function useGroup(countAdminLevel?: boolean): string {\n  const auth = useSelector(selectAuthenticated);\n  const level = useSelector(levelSelector);\n  const admin = useSelector(selectAdmin);\n\n  return useMemo(() => {\n    if (!auth) return AnonymousType;\n    if (countAdminLevel && admin) return AdminType;\n    switch (level) {\n      case 1:\n        return BasicType;\n      case 2:\n        return StandardType;\n      case 3:\n        return ProType;\n      default:\n        return NormalType;\n    }\n  }, [auth, level, admin]);\n}\n\nexport function hitGroup(group: string[]): boolean {\n  const current = useGroup();\n  const admin = useSelector(selectAdmin);\n\n  return useMemo(() => {\n    if (group.includes(AdminType) && admin) return true;\n    return group.includes(current);\n  }, [group, current, admin]);\n}\n"
  },
  {
    "path": "app/src/utils/hook.ts",
    "content": "import React, { useEffect } from \"react\";\n\nexport function useEffectAsync<T>(effect: () => Promise<T>, deps?: any[]) {\n  /**\n   * useEffect with async/await support\n   *\n   * @example\n   * useEffectAsync(async () => {\n   *    const result = await fetch(\"https://api.example.com\");\n   *    console.debug(result);\n   *  }, []);\n   */\n\n  return useEffect(() => {\n    effect().catch((err) =>\n      console.debug(\"[runtime] error during use effect\", err),\n    );\n  }, deps);\n}\n\nexport function useAnimation(\n  ref: React.MutableRefObject<any>,\n  cls: string,\n  min?: number,\n): (() => number) | undefined {\n  /**\n   * Add animation class to react ref element and remove it after min ms when returned function is called\n   *\n   * @example\n   * const animation = useAnimation(ref, \"animate\", 1000);\n   * axios.get(\"https://api.example.com\")\n   *  .finally(() => animation());\n   */\n  if (!ref.current) return;\n  const target = ref.current as HTMLButtonElement;\n  const stamp = Date.now();\n  target.classList.add(cls);\n\n  return function () {\n    const duration = Date.now() - stamp;\n    const timeout = min ? Math.max(min - duration, 0) : 0;\n    setTimeout(() => target.classList.remove(cls), timeout);\n    return timeout;\n  };\n}\n\nexport function useTemporaryState(interval?: number): {\n  state: boolean;\n  triggerState: () => void;\n} {\n  const [stamp, setStamp] = React.useState<number>(0);\n  const triggerState = () => setStamp(new Date().getTime());\n\n  return {\n    state: Date.now() - stamp < (interval ?? 3000),\n    triggerState,\n  };\n}\n"
  },
  {
    "path": "app/src/utils/loader.tsx",
    "content": "import React from \"react\";\nimport {\n  closeSpinnerType,\n  openSpinnerType,\n  spinnerEvent,\n} from \"@/events/spinner.ts\";\nimport { generateListNumber } from \"@/utils/base.ts\";\n\nexport function lazyFactor<T extends React.ComponentType<any>>(\n  factor: () => Promise<{ default: T }>,\n): React.LazyExoticComponent<T> {\n  /**\n   * Lazy load factor\n   * @see https://reactjs.org/docs/code-splitting.html#reactlazy\n   *\n   * @example\n   * lazyFactor(() => import(\"./factor.tsx\"));\n   */\n\n  return React.lazy(() => {\n    return new Promise((resolve, reject) => {\n      const task = generateListNumber(6);\n      const id = setTimeout(\n        () =>\n          spinnerEvent.emit({\n            id: task,\n            type: openSpinnerType,\n          }),\n        1000,\n      );\n\n      factor()\n        .then((module) => {\n          clearTimeout(id);\n          spinnerEvent.emit({\n            id: task,\n            type: closeSpinnerType,\n          });\n          resolve(module);\n        })\n        .catch((error) => {\n          console.warn(`[factor] cannot load factor: ${error}`);\n          reject(error);\n        });\n    });\n  });\n}\n"
  },
  {
    "path": "app/src/utils/memory.ts",
    "content": "export function setMemory(key: string, value: string) {\n  const data = value.trim();\n  localStorage.setItem(key, data);\n}\n\nexport function setBooleanMemory(key: string, value: boolean) {\n  setMemory(key, String(value));\n}\n\nexport function setNumberMemory(key: string, value: number) {\n  setMemory(key, value.toString());\n}\n\nexport function setArrayMemory(key: string, value: string[]) {\n  setMemory(key, value.join(\",\"));\n}\n\nexport function getMemory(key: string, defaultValue?: string): string {\n  return (localStorage.getItem(key) || (defaultValue ?? \"\")).trim();\n}\n\nexport function getBooleanMemory(key: string, defaultValue: boolean): boolean {\n  const value = getMemory(key);\n  return value ? value === \"true\" : defaultValue;\n}\n\nexport function getNumberMemory(key: string, defaultValue: number): number {\n  const value = getMemory(key);\n  return value ? Number(value) : defaultValue;\n}\n\nexport function getArrayMemory(key: string): string[] {\n  const value = getMemory(key);\n  return value ? value.split(\",\") : [];\n}\n\nexport function forgetMemory(key: string) {\n  localStorage.removeItem(key);\n}\n\nexport function clearMemory() {\n  localStorage.clear();\n}\n\nexport function popMemory(key: string): string {\n  const value = getMemory(key);\n  forgetMemory(key);\n  return value;\n}\n"
  },
  {
    "path": "app/src/utils/path.ts",
    "content": "export function getQueryParams() {\n  /**\n   * Get query params from url\n   *\n   * @example\n   * // https://example.com?foo=bar&baz=qux\n   * getQueryParams();\n   * // { foo: \"bar\", baz: \"qux\" }\n   */\n\n  const params = new URLSearchParams(window.location.search);\n  const obj: Record<string, string> = {};\n  for (const [key, value] of params.entries()) {\n    obj[key] = value;\n  }\n  return obj;\n}\n\nexport function getQueryParam(key: string): string {\n  /**\n   * Get query param from url\n   *\n   * @example\n   * // https://example.com?foo=bar&baz=qux\n   * getQueryParam(\"foo\");\n   * // \"bar\"\n   */\n\n  const params = new URLSearchParams(window.location.search);\n  return params.get(key) || \"\";\n}\n\nexport function replaceHistoryState(\n  state: Record<string, string>,\n  title: string,\n  url: string,\n) {\n  /**\n   * Replace history state\n   *\n   * @example\n   * replaceHistoryState({ foo: \"bar\" }, \"title\", \"/url\");\n   */\n\n  window.history.replaceState(state, title, url);\n}\n\nexport function pushHistoryState(\n  state: Record<string, string>,\n  title: string,\n  url: string,\n) {\n  /**\n   * Push history state\n   *\n   * @example\n   * pushHistoryState({ foo: \"bar\" }, \"title\", \"/url\");\n   */\n\n  window.history.pushState(state, title, url);\n}\n\nexport function getHistoryState(): Record<string, string> {\n  /**\n   * Get history state\n   *\n   * @example\n   * getHistoryState();\n   * // { foo: \"bar\" }\n   */\n\n  return window.history.state;\n}\n\nexport function clearHistoryState() {\n  /**\n   * Clear history state\n   *\n   * @example\n   * clearHistoryState();\n   */\n\n  window.history.replaceState({}, \"\", window.location.pathname);\n}\n"
  },
  {
    "path": "app/src/utils/processor.ts",
    "content": "import { FileArray, FileObject } from \"@/api/file.ts\";\n\nexport function getFile(file: FileObject): string {\n  return `\\`\\`\\`file\n[[${file.name}]]\n${file.content}\n\\`\\`\\``;\n}\n\nexport function formatMessage(files: FileArray, message: string): string {\n  message = message.trim();\n\n  const data = files.map((file) => getFile(file)).join(\"\\n\\n\");\n  return files.length > 0 ? `${data}\\n\\n${message}` : message;\n}\n\nexport function filterMessage(message: string): string {\n  return message.replace(/```file\\n\\[\\[.*]]\\n[\\s\\S]*?\\n```\\n\\n/g, \"\");\n}\n\nexport function extractMessage(\n  message: string,\n  length: number = 50,\n  flow: string = \"...\",\n) {\n  return message.length > length ? message.slice(0, length) + flow : message;\n}\n\nexport function escapeRegExp(str: string): string {\n  // convert \\n to [enter], \\t to [tab], \\r to [return], \\s to [space], \\\" to [quote], \\' to [single-quote]\n  return str\n    .replace(/\\\\n/g, \"\\n\")\n    .replace(/\\\\t/g, \"\\t\")\n    .replace(/\\\\r/g, \"\\r\")\n    .replace(/\\\\s/g, \" \")\n    .replace(/\\\\\"/g, '\"')\n    .replace(/\\\\'/g, \"'\");\n}\n\nexport function handleLine(\n  data: string,\n  max_line: number,\n  end?: boolean,\n): string {\n  const segment = data.split(\"\\n\");\n  const line = segment.length;\n  if (line > max_line) {\n    return end ?? true\n      ? segment.slice(line - max_line).join(\"\\n\")\n      : segment.slice(0, max_line).join(\"\\n\");\n  } else {\n    return data;\n  }\n}\n\nexport function handleGenerationData(data: string): string {\n  data = data\n    .replace(/{\\s*\"result\":\\s*{/g, \"\")\n    .trim()\n    .replace(/}\\s*$/g, \"\");\n  return handleLine(escapeRegExp(data), 6);\n}\n\nexport function getReadableNumber(\n  num: number,\n  fixed?: number,\n  must_k?: boolean,\n): string {\n  if (num >= 1e9) return (num / 1e9).toFixed(fixed) + \"b\";\n  if (num >= 1e6) return (num / 1e6).toFixed(fixed) + \"m\";\n  if (num >= 1e3 || (num !== 0 && must_k))\n    return (num / 1e3).toFixed(fixed) + \"k\";\n  return num.toFixed(0);\n}\n"
  },
  {
    "path": "app/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "app/src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "app/src-tauri/Cargo.toml",
    "content": "[package]\nname = \"app\"\nversion = \"0.1.0\"\ndescription = \"Your Next Powerful AI Chat Platform\"\nauthors = [\"Deeptrain Team\"]\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/coaidev/coai\"\ndefault-run = \"app\"\nedition = \"2021\"\nrust-version = \"1.60\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[build-dependencies]\ntauri-build = { version = \"1.5.0\", features = [] }\n\n[dependencies]\nserde_json = \"1.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\ntauri = { version = \"1.5.2\", features = [ \"updater\", \"system-tray\"] }\n\n[features]\n# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.\n# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.\n# DO NOT REMOVE!!\ncustom-protocol = [ \"tauri/custom-protocol\" ]\n"
  },
  {
    "path": "app/src-tauri/build.rs",
    "content": "fn main() {\n  tauri_build::build()\n}\n"
  },
  {
    "path": "app/src-tauri/src/main.rs",
    "content": "// Prevents additional console window on Windows in release, DO NOT REMOVE!!\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nfn main() {\n  tauri::Builder::default()\n    .run(tauri::generate_context!())\n    .expect(\"error while running tauri application\");\n}\n"
  },
  {
    "path": "app/src-tauri/tauri.conf.json",
    "content": "{\n  \"$schema\": \"../node_modules/@tauri-apps/cli/schema.json\",\n  \"build\": {\n    \"beforeBuildCommand\": \"pnpm build --mode deeptrain\",\n    \"beforeDevCommand\": \"pnpm dev\",\n    \"devPath\": \"http://localhost:5173\",\n    \"distDir\": \"../dist\"\n  },\n  \"package\": {\n    \"productName\": \"coai\",\n    \"version\": \"4.0.0\"\n  },\n  \"tauri\": {\n    \"allowlist\": {\n      \"all\": false\n    },\n    \"bundle\": {\n      \"active\": true,\n      \"category\": \"DeveloperTool\",\n      \"copyright\": \"\",\n      \"deb\": {\n        \"depends\": []\n      },\n      \"externalBin\": [],\n      \"icon\": [\n        \"icons/32x32.png\",\n        \"icons/128x128.png\",\n        \"icons/128x128@2x.png\",\n        \"icons/icon.icns\",\n        \"icons/icon.ico\"\n      ],\n      \"identifier\": \"com.coai.dev\",\n      \"longDescription\": \"\",\n      \"macOS\": {\n        \"entitlements\": null,\n        \"exceptionDomain\": \"\",\n        \"frameworks\": [],\n        \"providerShortName\": null,\n        \"signingIdentity\": null\n      },\n      \"resources\": [],\n      \"shortDescription\": \"\",\n      \"targets\": \"all\",\n      \"windows\": {\n        \"certificateThumbprint\": null,\n        \"digestAlgorithm\": \"sha256\",\n        \"timestampUrl\": \"\"\n      }\n    },\n    \"security\": {\n      \"csp\": null\n    },\n    \"updater\": {\n      \"active\": true,\n      \"endpoints\": [\n        \"https://github.com/coaidev/coai/releases/latest/download/latest.json\"\n      ],\n      \"dialog\": false,\n      \"windows\": {\n        \"installMode\": \"passive\"\n      },\n      \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExREFBNEEwODA0MzIwOTYKUldTV0lFT0FvS1RhRWJSc1VTMFluOVl6VGFVNThyaVhyLyt4TnhrbTlBUHYyRWZWRXgzSDYrWGEK\"\n    },\n    \"windows\": [\n      {\n        \"fullscreen\": false,\n        \"height\": 600,\n        \"resizable\": true,\n        \"title\": \"CoAI\",\n        \"width\": 1000\n      }\n    ],\n    \"systemTray\": {\n      \"iconPath\": \"icons/icon.png\",\n      \"iconAsTemplate\": true\n    }\n  }\n}\n"
  },
  {
    "path": "app/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nimport colors from 'tailwindcss/colors';\n\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    './pages/**/*.{ts,tsx}',\n    './components/**/*.{ts,tsx}',\n    './app/**/*.{ts,tsx}',\n    './src/**/*.{ts,tsx}',\n    \"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}\",\n\t],\n  theme: {\n    transparent: 'transparent',\n    current: 'currentColor',\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      spacing: {\n        '1/2': '50%',\n        '1/3': '33.333333%',\n        '-1/4': '-25%',\n        '-1/2': '-50%',\n        '-3/4': '-75%',\n      },\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        \"background-container\": \"hsl(var(--background-container))\",\n        \"background-hover\": \"hsl(var(--background-hover))\",\n        \"background-active\": \"hsl(var(--background-active))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        unread: {\n          DEFAULT: \"hsl(var(--text-unread))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        gold: {\n          DEFAULT: \"hsl(var(--gold))\",\n          foreground: \"hsl(var(--gold-foreground))\",\n        },\n        success: {\n          DEFAULT: \"hsl(var(--success))\",\n          foreground: \"hsl(var(--success-foreground))\",\n        },\n        failure: {\n          DEFAULT: \"hsl(var(--failure))\",\n          foreground: \"hsl(var(--failure-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n        alipay: {\n          DEFAULT: \"#1668dc\",\n          foreground: \"#FFFFFF\",\n        },\n        wxpay: {\n          DEFAULT: \"#008094\",\n          foreground: \"#FFFFFF\",\n        },\n        qqpay: {\n          DEFAULT: \"#cb0000\",\n          foreground: \"#FFFFFF\",\n        },\n        afdian: {\n          DEFAULT: \"#925cff\",\n          foreground: \"#FFFFFF\",\n        },\n        paypal: {\n          DEFAULT: \"#003087\",\n          foreground: \"#FFFFFF\",\n        },\n        stripe: {\n          DEFAULT: \"#6772e5\",\n          foreground: \"#FFFFFF\",\n        },\n        tremor: {\n          brand: {\n            faint: colors.blue[50],\n            muted: colors.blue[200],\n            subtle: colors.blue[400],\n            DEFAULT: colors.blue[500],\n            emphasis: colors.blue[700],\n            inverted: colors.white,\n          },\n          background: {\n            muted: colors.gray[50],\n            subtle: colors.gray[100],\n            DEFAULT: colors.white,\n            emphasis: colors.gray[700],\n          },\n          border: {\n            DEFAULT: colors.gray[200],\n          },\n          ring: {\n            DEFAULT: colors.gray[200],\n          },\n          content: {\n            subtle: colors.gray[400],\n            DEFAULT: colors.gray[500],\n            emphasis: colors.gray[700],\n            strong: colors.gray[900],\n            inverted: colors.white,\n          },\n        },\n        'dark-tremor': {\n          brand: {\n            faint: '#0B1229',\n            muted: colors.blue[950],\n            subtle: colors.blue[800],\n            DEFAULT: colors.blue[500],\n            emphasis: colors.blue[400],\n            inverted: colors.blue[950],\n          },\n          background: {\n            muted: '#131A2B',\n            subtle: colors.gray[800],\n            DEFAULT: colors.gray[900],\n            emphasis: colors.gray[300],\n          },\n          border: {\n            DEFAULT: colors.gray[800],\n          },\n          ring: {\n            DEFAULT: colors.gray[800],\n          },\n          content: {\n            subtle: colors.gray[600],\n            DEFAULT: colors.gray[500],\n            emphasis: colors.gray[200],\n            strong: colors.gray[50],\n            inverted: colors.gray[950],\n          },\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n        'tremor-small': '0.375rem',\n        'tremor-default': '0.5rem',\n        'tremor-full': '9999px',\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: 0 },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: 0 },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n      boxShadow: {\n        // light\n        'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',\n        'tremor-card':\n          '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',\n        'tremor-dropdown':\n          '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',\n        // dark\n        'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',\n        'dark-tremor-card':\n          '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',\n        'dark-tremor-dropdown':\n          '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',\n      },\n      fontSize: {\n        'tremor-label': ['0.75rem', { lineHeight: '1rem' }],\n        'tremor-default': ['0.875rem', { lineHeight: '1.25rem' }],\n        'tremor-title': ['1.125rem', { lineHeight: '1.75rem' }],\n        'tremor-metric': ['1.875rem', { lineHeight: '2.25rem' }],\n        '2xs': '.625rem',\n        '3xs': '.5rem',\n      },\n    },\n  },\n  safelist: [\n    {\n      pattern:\n        /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: ['hover', 'ui-selected'],\n    },\n    {\n      pattern:\n        /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: ['hover', 'ui-selected'],\n    },\n    {\n      pattern:\n        /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: ['hover', 'ui-selected'],\n    },\n    {\n      pattern:\n        /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n    },\n    {\n      pattern:\n        /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n    },\n    {\n      pattern:\n        /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n    },\n  ],\n  plugins: [\n    require('@headlessui/tailwindcss'),\n    require(\"tailwindcss-animate\"),\n    require('@tailwindcss/forms'),\n],\n}\n"
  },
  {
    "path": "app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n    }\n  },\n  \"include\": [\n    \"src\",\n    \"*.ts\",\n  ],\n  \"baseUrl\": \".\",\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }],\n}\n"
  },
  {
    "path": "app/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "app/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react-swc'\nimport path from \"path\"\nimport { createHtmlPlugin } from 'vite-plugin-html' //@ts-ignore\nimport { createTranslationPlugin } from \"./src/translator\"\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    react(),\n    createHtmlPlugin({\n      minify: true,\n    }),\n    createTranslationPlugin(),\n  ],\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \"./src\"),\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      less: {\n        javascriptEnabled: true,\n      }\n    }\n  },\n  build: {\n    manifest: true,\n    chunkSizeWarningLimit: 2048,\n    rollupOptions: {\n      output: {\n        entryFileNames: `assets/[name].[hash].js`,\n        chunkFileNames: `assets/[name].[hash].js`,\n      },\n    },\n  },\n  server: {\n    proxy: {\n      \"/api\": {\n        target: \"http://localhost:8094\",\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/api/, \"\"),\n        ws: true,\n      },\n      \"/v1\": {\n        target: \"http://localhost:8094\",\n        changeOrigin: true,\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "auth/analysis.go",
    "content": "package auth\n\nimport (\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"time\"\n)\n\nfunc getMonth() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01\")\n}\n\nfunc getDay() string {\n\tdate := time.Now()\n\treturn date.Format(\"2006-01-02\")\n}\n\nfunc getBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc getMonthBillingFormat(t string) string {\n\treturn fmt.Sprintf(\"nio:billing-analysis-%s\", t)\n}\n\nfunc incrBillingRequest(cache *redis.Client, amount int64) {\n\tutils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2)\n\tutils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2)\n}\n"
  },
  {
    "path": "auth/apikey.go",
    "content": "package auth\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nfunc (u *User) CreateApiKey(db *sql.DB) string {\n\tsalt := utils.Sha2Encrypt(fmt.Sprintf(\"%s-%s\", u.Username, utils.GenerateChar(utils.GetRandomInt(720, 1024))))\n\tkey := fmt.Sprintf(\"sk-%s\", salt[:64]) // 64 bytes\n\tif _, err := globals.ExecDb(db, \"INSERT INTO apikey (user_id, api_key) VALUES (?, ?)\", u.GetID(db), key); err != nil {\n\t\treturn \"\"\n\t}\n\treturn key\n}\n\nfunc (u *User) GetApiKey(db *sql.DB) string {\n\tvar key string\n\tif err := globals.QueryRowDb(db, \"SELECT api_key FROM apikey WHERE user_id = ?\", u.GetID(db)).Scan(&key); err != nil {\n\t\treturn u.CreateApiKey(db)\n\t}\n\treturn key\n}\n\nfunc (u *User) ResetApiKey(db *sql.DB) (string, error) {\n\tif _, err := globals.ExecDb(db, \"DELETE FROM apikey WHERE user_id = ?\", u.GetID(db)); err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\treturn \"\", err\n\t}\n\treturn u.CreateApiKey(db), nil\n}\n"
  },
  {
    "path": "auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dgrijalva/jwt-go\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc ParseToken(c *gin.Context, token string) *User {\n\tinstance, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {\n\t\treturn []byte(viper.GetString(\"secret\")), nil\n\t})\n\tif err != nil {\n\t\treturn nil\n\t}\n\tif claims, ok := instance.Claims.(jwt.MapClaims); ok && instance.Valid {\n\t\tif int64(claims[\"exp\"].(float64)) < time.Now().Unix() {\n\t\t\treturn nil\n\t\t}\n\t\tuser := &User{\n\t\t\tUsername: claims[\"username\"].(string),\n\t\t\tPassword: claims[\"password\"].(string),\n\t\t}\n\t\tif !user.Validate(c) {\n\t\t\treturn nil\n\t\t}\n\t\treturn user\n\t}\n\treturn nil\n}\n\nfunc ParseApiKey(c *gin.Context, key string) *User {\n\tdb := utils.GetDBFromContext(c)\n\n\tif len(key) == 0 {\n\t\treturn nil\n\t}\n\n\tvar user User\n\tif err := globals.QueryRowDb(db, `\n\t\t\tSELECT auth.id, auth.username, auth.password FROM auth \n\t\t\tINNER JOIN apikey ON auth.id = apikey.user_id \n\t\t\tWHERE apikey.api_key = ?\n\t\t\t`, key).Scan(&user.ID, &user.Username, &user.Password); err != nil {\n\t\treturn nil\n\t}\n\n\treturn &user\n}\n\nfunc getCode(c *gin.Context, cache *redis.Client, email string) string {\n\tcode, err := cache.Get(c, fmt.Sprintf(\"nio:otp:%s\", email)).Result()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn code\n}\n\nfunc checkCode(c *gin.Context, cache *redis.Client, email, code string) bool {\n\tstorage := getCode(c, cache, email)\n\tif len(storage) == 0 {\n\t\treturn false\n\t}\n\n\tif storage != code {\n\t\treturn false\n\t}\n\n\tcache.Del(c, fmt.Sprintf(\"nio:otp:%s\", email))\n\treturn true\n}\n\nfunc setCode(c *gin.Context, cache *redis.Client, email, code string) {\n\tcache.Set(c, fmt.Sprintf(\"nio:otp:%s\", email), code, 5*time.Minute)\n}\n\nfunc generateCode(c *gin.Context, cache *redis.Client, email string) string {\n\tcode := utils.GenerateCode(6)\n\tsetCode(c, cache, email, code)\n\treturn code\n}\n\nfunc Verify(c *gin.Context, email string, checkout bool) error {\n\tcache := utils.GetCacheFromContext(c)\n\tcode := generateCode(c, cache, email)\n\n\tif checkout {\n\t\tif err := channel.SystemInstance.IsValidMail(email); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn channel.SystemInstance.SendVerifyMail(email, code)\n}\n\nfunc SignUp(c *gin.Context, form RegisterForm) (string, error) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tusername := strings.TrimSpace(form.Username)\n\tpassword := strings.TrimSpace(form.Password)\n\temail := strings.TrimSpace(form.Email)\n\tcode := strings.TrimSpace(form.Code)\n\n\tenableVerify := channel.SystemInstance.IsMailValid()\n\n\tif !utils.All(\n\t\tvalidateUsername(username),\n\t\tvalidatePassword(password),\n\t\tvalidateEmail(email),\n\t\t!enableVerify || validateCode(code),\n\t) {\n\t\treturn \"\", errors.New(\"invalid username/password/email format\")\n\t}\n\n\tif err := channel.SystemInstance.IsValidMail(form.Email); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif IsUserExist(db, username) {\n\t\treturn \"\", fmt.Errorf(\"username is already taken, please try another one username (your current username: %s)\", username)\n\t}\n\n\tif IsEmailExist(db, email) {\n\t\treturn \"\", fmt.Errorf(\"email is already taken, please try another one email (your current email: %s)\", email)\n\t}\n\n\tif enableVerify && !checkCode(c, cache, email, code) {\n\t\treturn \"\", errors.New(\"invalid email verification code\")\n\t}\n\n\thash := utils.Sha2Encrypt(password)\n\n\tuser := &User{\n\t\tUsername: username,\n\t\tPassword: hash,\n\t\tEmail:    email,\n\t\tBindID:   getMaxBindId(db) + 1,\n\t\tToken:    utils.Sha2Encrypt(email + username),\n\t}\n\n\tif _, err := globals.ExecDb(db, `\n\t\t\tINSERT INTO auth (username, password, email, bind_id, token)\n\t\t\tVALUES (?, ?, ?, ?, ?)\n\t\t\t`, user.Username, user.Password, user.Email, user.BindID, user.Token); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tuser.CreateInitialQuota(db)\n\treturn user.GenerateToken()\n}\n\nfunc Login(c *gin.Context, form LoginForm) (string, error) {\n\tdb := utils.GetDBFromContext(c)\n\tusername := strings.TrimSpace(form.Username)\n\tpassword := strings.TrimSpace(form.Password)\n\n\tif !utils.All(\n\t\tvalidateUsernameOrEmail(username),\n\t\tvalidatePassword(password),\n\t) {\n\t\treturn \"\", errors.New(\"invalid username or password format\")\n\t}\n\n\thash := utils.Sha2Encrypt(password)\n\n\t// get user from db by username (or email) and password\n\tvar user User\n\tif err := globals.QueryRowDb(db, `\n\t\t\tSELECT auth.id, auth.username, auth.password FROM auth \n\t\t\tWHERE (auth.username = ? OR auth.email = ?) AND auth.password = ?\n\t\t\t`, username, username, hash).Scan(&user.ID, &user.Username, &user.Password); err != nil {\n\t\treturn \"\", errors.New(\"invalid username or password\")\n\t}\n\n\tif user.IsBanned(db) {\n\t\treturn \"\", errors.New(\"current user is banned\")\n\t}\n\n\treturn user.GenerateToken()\n}\n\nfunc DeepLogin(c *gin.Context, token string) (string, error) {\n\tif !useDeeptrain() {\n\t\treturn \"\", errors.New(\"deeptrain mode is disabled\")\n\t}\n\n\tuser := Validate(token)\n\tif user == nil {\n\t\treturn \"\", errors.New(\"cannot validate access token\")\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif !IsUserExist(db, user.Username) {\n\t\tif globals.CloseRegistration {\n\t\t\treturn \"\", errors.New(\"this site is not open for registration\")\n\t\t}\n\n\t\t// register\n\t\tpassword := utils.GenerateChar(64)\n\n\t\t_, err := globals.QueryDb(db, \"INSERT INTO auth (bind_id, username, token, password) VALUES (?, ?, ?, ?)\",\n\t\t\tuser.ID, user.Username, utils.Extract(token, 255, \"\"), utils.Sha2Encrypt(password))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tu := &User{\n\t\t\tUsername: user.Username,\n\t\t\tPassword: password,\n\t\t}\n\n\t\tu.CreateInitialQuota(db)\n\t\treturn u.GenerateToken()\n\t}\n\n\t// login\n\t_ = globals.QueryRowDb(db, \"UPDATE auth SET token = ? WHERE username = ?\", utils.Extract(token, 255, \"\"), user.Username)\n\tvar password string\n\terr := globals.QueryRowDb(db, \"SELECT password FROM auth WHERE username = ?\", user.Username).Scan(&password)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tu := &User{\n\t\tUsername: user.Username,\n\t\tPassword: password,\n\t}\n\n\tif u.IsBanned(db) {\n\t\treturn \"\", errors.New(\"current user is banned\")\n\t}\n\n\treturn u.GenerateToken()\n}\n\nfunc Reset(c *gin.Context, form ResetForm) error {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\temail := strings.TrimSpace(form.Email)\n\tcode := strings.TrimSpace(form.Code)\n\tpassword := strings.TrimSpace(form.Password)\n\n\tif !utils.All(\n\t\tvalidateEmail(email),\n\t\tvalidateCode(code),\n\t\tvalidatePassword(password),\n\t) {\n\t\treturn errors.New(\"invalid email/code/password format\")\n\t}\n\n\tif !IsEmailExist(db, email) {\n\t\treturn errors.New(\"email is not registered\")\n\t}\n\n\tif !checkCode(c, cache, email, code) {\n\t\treturn errors.New(\"invalid email verification code\")\n\t}\n\n\tuser := GetUserByEmail(db, email)\n\tif user == nil {\n\t\treturn errors.New(\"cannot find user by email\")\n\t}\n\n\tif err := user.UpdatePassword(db, cache, password); err != nil {\n\t\treturn err\n\t}\n\n\tcache.Del(c, fmt.Sprintf(\"nio:otp:%s\", email))\n\n\treturn nil\n}\n\nfunc (u *User) UpdatePassword(db *sql.DB, cache *redis.Client, password string) error {\n\thash := utils.Sha2Encrypt(password)\n\n\tif _, err := globals.ExecDb(db, `\n\t\t\tUPDATE auth SET password = ? WHERE id = ?\n\t\t\t`, hash, u.ID); err != nil {\n\t\treturn err\n\t}\n\n\tcache.Del(context.Background(), fmt.Sprintf(\"nio:user:%s\", u.Username))\n\n\treturn nil\n}\n\nfunc (u *User) Validate(c *gin.Context) bool {\n\tif u.Username == \"\" || u.Password == \"\" {\n\t\treturn false\n\t}\n\tcache := utils.GetCacheFromContext(c)\n\n\tif password, err := cache.Get(c, fmt.Sprintf(\"nio:user:%s\", u.Username)).Result(); err == nil && len(password) > 0 {\n\t\treturn u.Password == password\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tvar count int\n\tif err := globals.QueryRowDb(db, \"SELECT COUNT(*) FROM auth WHERE username = ? AND password = ?\", u.Username, u.Password).Scan(&count); err != nil || count == 0 {\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"validate user error: %s\", err.Error()))\n\t\t}\n\t\treturn false\n\t}\n\n\tif u.IsBanned(db) {\n\t\treturn false\n\t}\n\n\tcache.Set(c, fmt.Sprintf(\"nio:user:%s\", u.Username), u.Password, 30*time.Minute)\n\treturn true\n}\n\nfunc (u *User) GenerateToken() (string, error) {\n\tinstance := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{\n\t\t\"username\": u.Username,\n\t\t\"password\": u.Password,\n\t\t\"exp\":      time.Now().Add(time.Hour * 24 * 30).Unix(),\n\t})\n\ttoken, err := instance.SignedString([]byte(viper.GetString(\"secret\")))\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if token == \"\" {\n\t\treturn \"\", errors.New(\"unable to generate token\")\n\t}\n\treturn token, nil\n}\n\nfunc (u *User) GenerateTokenSafe(db *sql.DB) (string, error) {\n\tif len(u.Username) == 0 {\n\t\tif err := globals.QueryRowDb(db, \"SELECT username FROM auth WHERE id = ?\", u.ID).Scan(&u.Username); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif len(u.Password) == 0 {\n\t\tif err := globals.QueryRowDb(db, \"SELECT password FROM auth WHERE id = ?\", u.ID).Scan(&u.Password); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\treturn u.GenerateToken()\n}\n"
  },
  {
    "path": "auth/call.go",
    "content": "package auth\n\nimport (\n\t\"chat/utils\"\n\t\"github.com/goccy/go-json\"\n\t\"github.com/spf13/viper\"\n)\n\ntype ValidateUserResponse struct {\n\tStatus   bool   `json:\"status\" required:\"true\"`\n\tUsername string `json:\"username\" required:\"true\"`\n\tID       int    `json:\"id\" required:\"true\"`\n}\n\nfunc getDeeptrainApi(path string) string {\n\treturn viper.GetString(\"auth.endpoint\") + path\n}\n\nfunc useDeeptrain() bool {\n\treturn viper.GetBool(\"auth.use_deeptrain\")\n}\n\nfunc Validate(token string) *ValidateUserResponse {\n\tres, err := utils.Post(getDeeptrainApi(\"/app/validate\"), map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}, map[string]interface{}{\n\t\t\"password\": viper.GetString(\"auth.access\"),\n\t\t\"token\":    token,\n\t\t\"hash\":     utils.Sha2Encrypt(token + viper.GetString(\"auth.salt\")),\n\t})\n\n\tif err != nil || res == nil || res.(map[string]interface{})[\"status\"] == false {\n\t\treturn nil\n\t}\n\n\tconverter, _ := json.Marshal(res)\n\tresp, _ := utils.Unmarshal[ValidateUserResponse](converter)\n\treturn &resp\n}\n"
  },
  {
    "path": "auth/cert.go",
    "content": "package auth\n\nimport (\n\t\"chat/utils\"\n\t\"github.com/goccy/go-json\"\n\t\"github.com/spf13/viper\"\n)\n\ntype CertResponse struct {\n\tStatus   bool `json:\"status\" required:\"true\"`\n\tCert     bool `json:\"cert\"`\n\tTeenager bool `json:\"teenager\"`\n}\n\nfunc Cert(username string) *CertResponse {\n\tres, err := utils.Post(getDeeptrainApi(\"/app/cert\"), map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}, map[string]interface{}{\n\t\t\"password\": viper.GetString(\"auth.access\"),\n\t\t\"user\":     username,\n\t\t\"hash\":     utils.Sha2Encrypt(username + viper.GetString(\"auth.salt\")),\n\t})\n\n\tif err != nil || res == nil || res.(map[string]interface{})[\"status\"] == false {\n\t\treturn nil\n\t}\n\n\tconverter, _ := json.Marshal(res)\n\tresp, _ := utils.Unmarshal[CertResponse](converter)\n\treturn &resp\n}\n"
  },
  {
    "path": "auth/controller.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype RegisterForm struct {\n\tUsername string `form:\"username\" binding:\"required\"`\n\tPassword string `form:\"password\" binding:\"required\"`\n\tEmail    string `form:\"email\" binding:\"required\"`\n\tCode     string `form:\"code\"`\n}\n\ntype VerifyForm struct {\n\tEmail    string `form:\"email\" binding:\"required\"`\n\tCheckout bool   `form:\"checkout\"`\n}\n\ntype LoginForm struct {\n\tUsername string `form:\"username\" binding:\"required\"`\n\tPassword string `form:\"password\" binding:\"required\"`\n}\n\ntype DeepLoginForm struct {\n\tToken string `form:\"token\" binding:\"required\"`\n}\n\ntype ResetForm struct {\n\tEmail    string `form:\"email\" binding:\"required\"`\n\tCode     string `form:\"code\" binding:\"required\"`\n\tPassword string `form:\"password\" binding:\"required\"`\n}\n\ntype BuyForm struct {\n\tQuota int `json:\"quota\" binding:\"required\"`\n}\n\ntype SubscribeForm struct {\n\tLevel int `json:\"level\" binding:\"required\"`\n\tMonth int `json:\"month\" binding:\"required\"`\n}\n\nfunc GetUser(c *gin.Context) *User {\n\tif c.GetBool(\"auth\") {\n\t\treturn &User{\n\t\t\tUsername: c.GetString(\"user\"),\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc GetUserByCtx(c *gin.Context) *User {\n\tuser := utils.GetUserFromContext(c)\n\tif len(user) == 0 {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"user not found\",\n\t\t})\n\t\treturn nil\n\t}\n\n\treturn &User{\n\t\tUsername: user,\n\t}\n}\n\nfunc RequireAuth(c *gin.Context) *User {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\tc.Abort()\n\t\treturn nil\n\t}\n\n\treturn user\n}\n\nfunc RequireAdmin(c *gin.Context) *User {\n\tuser := RequireAuth(c)\n\tif user == nil {\n\t\treturn nil\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif !user.IsAdmin(db) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"admin required\",\n\t\t})\n\t\tc.Abort()\n\t\treturn nil\n\t}\n\n\treturn user\n}\n\nfunc RequireSubscription(c *gin.Context) *User {\n\tuser := RequireAuth(c)\n\tif user == nil {\n\t\treturn nil\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif !user.IsSubscribe(db) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"subscription required\",\n\t\t})\n\t\tc.Abort()\n\t\treturn nil\n\t}\n\n\treturn user\n}\n\nfunc RequireEnterprise(c *gin.Context) *User {\n\tuser := RequireAuth(c)\n\tif user == nil {\n\t\treturn nil\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif !user.IsEnterprise(db) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"enterprise required\",\n\t\t})\n\t\tc.Abort()\n\t\treturn nil\n\t}\n\n\treturn user\n}\n\nfunc RegisterAPI(c *gin.Context) {\n\tif useDeeptrain() {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"this api is not available for deeptrain mode\",\n\t\t})\n\t\treturn\n\t}\n\n\tif globals.CloseRegistration {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"this site is not open for registration\",\n\t\t})\n\t\treturn\n\t}\n\n\tvar form RegisterForm\n\tif err := c.ShouldBind(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"bad request\",\n\t\t})\n\t\treturn\n\t}\n\n\ttoken, err := SignUp(c, form)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"token\":  token,\n\t})\n}\n\nfunc LoginAPI(c *gin.Context) {\n\tvar token string\n\tvar err error\n\n\tif useDeeptrain() {\n\t\tvar form DeepLoginForm\n\t\tif err := c.ShouldBind(&form); err != nil {\n\t\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\t\"status\": false,\n\t\t\t\t\"error\":  \"bad request\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\ttoken, err = DeepLogin(c, form.Token)\n\t} else {\n\t\tvar form LoginForm\n\t\tif err := c.ShouldBind(&form); err != nil {\n\t\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\t\"status\": false,\n\t\t\t\t\"error\":  \"bad request\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\ttoken, err = Login(c, form)\n\t}\n\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"token\":  token,\n\t})\n}\n\nfunc VerifyAPI(c *gin.Context) {\n\tvar form VerifyForm\n\tif err := c.ShouldBind(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"bad request\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := Verify(c, form.Email, form.Checkout); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc ResetAPI(c *gin.Context) {\n\tvar form ResetForm\n\tif err := c.ShouldBind(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"bad request\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := Reset(c, form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t})\n}\n\nfunc StateAPI(c *gin.Context) {\n\tusername := utils.GetUserFromContext(c)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": len(username) != 0,\n\t\t\"user\":   username,\n\t\t\"admin\":  utils.GetAdminFromContext(c),\n\t})\n}\n\nfunc UserInfoAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif info, err := user.GetUserInfo(db); err == nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"data\":   info,\n\t\t})\n\t} else {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t}\n}\n\nfunc IndexAPI(c *gin.Context) {\n\tusername := utils.GetUserFromContext(c)\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"auth\":   len(username) != 0,\n\t\t\"method\": c.Request.Method,\n\t})\n}\n\nfunc KeyAPI(c *gin.Context) {\n\tuser := GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"key\":    user.GetApiKey(utils.GetDBFromContext(c)),\n\t})\n}\n\nfunc ResetKeyAPI(c *gin.Context) {\n\tuser := GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tif key, err := user.ResetApiKey(utils.GetDBFromContext(c)); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t} else {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"key\":    key,\n\t\t})\n\t}\n}\n\nfunc PackageAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tc.JSON(200, gin.H{\n\t\t\"status\": true,\n\t\t\"data\":   RefreshPackage(db, user),\n\t})\n}\n\nfunc QuotaAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tc.JSON(200, gin.H{\n\t\t\"status\": true,\n\t\t\"quota\":  user.GetQuota(db),\n\t})\n}\n\nfunc SubscriptionAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tif disableSubscription() {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\":        true,\n\t\t\t\"level\":         0,\n\t\t\t\"is_subscribed\": false,\n\t\t\t\"enterprise\":    false,\n\t\t\t\"expired\":       0,\n\t\t\t\"expired_at\":    \"1970-01-01 00:00:00\",\n\t\t\t\"refresh\":       0,\n\t\t\t\"refresh_at\":    \"1970-01-01 00:00:00\",\n\t\t\t\"usage\":         channel.UsageMap{},\n\t\t})\n\t}\n\n\tc.JSON(200, gin.H{\n\t\t\"status\":        true,\n\t\t\"level\":         user.GetSubscriptionLevel(db),\n\t\t\"is_subscribed\": user.IsSubscribe(db),\n\t\t\"enterprise\":    user.IsEnterprise(db),\n\t\t\"expired\":       user.GetSubscriptionExpiredDay(db),\n\t\t\"expired_at\":    user.GetSubscriptionExpiredAt(db).Format(\"2006-01-02 15:04:05\"),\n\t\t\"refresh\":       user.GetSubscriptionRefreshDay(db, cache),\n\t\t\"refresh_at\":    user.GetSubscriptionRefreshAt(db, cache).Format(\"2006-01-02 15:04:05\"),\n\t\t\"usage\":         user.GetSubscriptionUsage(db, cache),\n\t})\n}\n\nfunc SubscribeAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tvar form SubscribeForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tif form.Month <= 0 || form.Month > 999 {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"invalid month range (1 ~ 999)\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := BuySubscription(db, cache, user, form.Level, form.Month); err == nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"error\":  \"success\",\n\t\t})\n\t} else {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t}\n}\n\nfunc BuyAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tvar form BuyForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tif form.Quota <= 0 || form.Quota > 99999 {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"invalid quota range (1 ~ 99999)\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := BuyQuota(db, cache, user, form.Quota); err == nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"error\":  \"success\",\n\t\t})\n\t} else {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t}\n}\n\nfunc InviteAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcode := strings.TrimSpace(c.Query(\"code\"))\n\tif len(code) == 0 {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"invalid code\",\n\t\t\t\"quota\":  0.,\n\t\t})\n\t\treturn\n\t}\n\n\tif quota, err := user.UseInvitation(db, code); err != nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t\t\"quota\":  0.,\n\t\t})\n\t\treturn\n\t} else {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"error\":  \"success\",\n\t\t\t\"quota\":  quota,\n\t\t})\n\t}\n}\n\nfunc RedeemAPI(c *gin.Context) {\n\tuser := GetUserByCtx(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tcode := strings.TrimSpace(c.Query(\"code\"))\n\tif len(code) == 0 {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  \"invalid code\",\n\t\t\t\"quota\":  0.,\n\t\t})\n\t\treturn\n\t}\n\n\tif quota, err := user.UseRedeem(db, cache, code); err != nil {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t\t\"quota\":  0.,\n\t\t})\n\t\treturn\n\t} else {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": true,\n\t\t\t\"error\":  \"success\",\n\t\t\t\"quota\":  quota,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "auth/invitation.go",
    "content": "package auth\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n)\n\ntype Invitation struct {\n\tId     int64   `json:\"id\"`\n\tCode   string  `json:\"code\"`\n\tQuota  float32 `json:\"quota\"`\n\tType   string  `json:\"type\"`\n\tUsed   bool    `json:\"used\"`\n\tUsedId int64   `json:\"used_id\"`\n}\n\nfunc GenerateInvitations(db *sql.DB, num int, quota float32, t string) ([]string, error) {\n\tarr := make([]string, 0)\n\tidx := 0\n\tfor idx < num {\n\t\tcode := fmt.Sprintf(\"%s-%s\", t, utils.GenerateChar(24))\n\t\tif err := CreateInvitationCode(db, code, quota, t); err != nil {\n\t\t\t// unique constraint\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"failed to generate code: %w\", err)\n\t\t}\n\t\tarr = append(arr, code)\n\t\tidx++\n\t}\n\n\treturn arr, nil\n}\n\nfunc CreateInvitationCode(db *sql.DB, code string, quota float32, t string) error {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO invitation (code, quota, type)\n\t\tVALUES (?, ?, ?)\n\t`, code, quota, t)\n\treturn err\n}\n\nfunc GetInvitation(db *sql.DB, code string) (*Invitation, error) {\n\trow := globals.QueryRowDb(db, `\n\t\tSELECT id, code, quota, type, used, used_id\n\t\tFROM invitation\n\t\tWHERE code = ?\n\t`, code)\n\tvar invitation Invitation\n\tvar id sql.NullInt64\n\terr := row.Scan(&invitation.Id, &invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &id)\n\tif id.Valid {\n\t\tinvitation.UsedId = id.Int64\n\t}\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, fmt.Errorf(\"invitation code not found\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get invitation: %w\", err)\n\t}\n\treturn &invitation, nil\n}\n\nfunc (i *Invitation) IsUsed() bool {\n\treturn i.Used\n}\n\nfunc (i *Invitation) Use(db *sql.DB, userId int64) error {\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE invitation SET used = TRUE, used_id = ? WHERE id = ?\n\t`, userId, i.Id)\n\treturn err\n}\n\nfunc (i *Invitation) GetQuota() float32 {\n\treturn i.Quota\n}\n\nfunc (i *Invitation) UseInvitation(db *sql.DB, user User) error {\n\tif i.IsUsed() {\n\t\treturn fmt.Errorf(\"this invitation has been used\")\n\t}\n\n\tif err := i.Use(db, user.GetID(db)); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn fmt.Errorf(\"invitation code not found\")\n\t\t} else if errors.Is(err, sql.ErrTxDone) {\n\t\t\treturn fmt.Errorf(\"transaction has been closed\")\n\t\t}\n\t\treturn fmt.Errorf(\"failed to use invitation: %w\", err)\n\t}\n\n\tif !user.IncreaseQuota(db, i.GetQuota()) {\n\t\treturn fmt.Errorf(\"failed to increase quota for user\")\n\t}\n\n\treturn nil\n}\n\nfunc (u *User) UseInvitation(db *sql.DB, code string) (float32, error) {\n\tif invitation, err := GetInvitation(db, code); err != nil {\n\t\treturn 0, err\n\t} else {\n\t\tif err := invitation.UseInvitation(db, *u); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to use invitation: %w\", err)\n\t\t}\n\n\t\treturn invitation.GetQuota(), nil\n\t}\n}\n"
  },
  {
    "path": "auth/package.go",
    "content": "package auth\n\nimport (\n\t\"chat/globals\"\n\t\"database/sql\"\n)\n\ntype GiftResponse struct {\n\tCert     bool `json:\"cert\"`\n\tTeenager bool `json:\"teenager\"`\n}\n\nfunc (u *User) HasPackage(db *sql.DB, _t string) bool {\n\tvar count int\n\tif err := globals.QueryRowDb(db, `SELECT COUNT(*) FROM package where user_id = ? AND type = ?`, u.ID, _t).Scan(&count); err != nil {\n\t\treturn false\n\t}\n\n\treturn count > 0\n}\n\nfunc (u *User) HasCertPackage(db *sql.DB) bool {\n\treturn u.HasPackage(db, \"cert\")\n}\n\nfunc (u *User) HasTeenagerPackage(db *sql.DB) bool {\n\treturn u.HasPackage(db, \"teenager\")\n}\n\nfunc NewPackage(db *sql.DB, user *User, _t string) bool {\n\tid := user.GetID(db)\n\n\tvar count int\n\tif err := globals.QueryRowDb(db, `SELECT COUNT(*) FROM package where user_id = ? AND type = ?`, id, _t).Scan(&count); err != nil {\n\t\treturn false\n\t}\n\n\tif count > 0 {\n\t\treturn false\n\t}\n\n\t_ = globals.QueryRowDb(db, `INSERT INTO package (user_id, type) VALUES (?, ?)`, id, _t)\n\treturn true\n}\n\nfunc NewCertPackage(db *sql.DB, user *User) bool {\n\tres := NewPackage(db, user, \"cert\")\n\tif !res {\n\t\treturn false\n\t}\n\n\treturn user.IncreaseQuota(db, 50)\n}\n\nfunc NewTeenagerPackage(db *sql.DB, user *User) bool {\n\tres := NewPackage(db, user, \"teenager\")\n\tif !res {\n\t\treturn false\n\t}\n\n\treturn user.IncreaseQuota(db, 150)\n}\n\nfunc RefreshPackage(db *sql.DB, user *User) *GiftResponse {\n\tif !useDeeptrain() {\n\t\treturn nil\n\t}\n\n\tresp := Cert(user.Username)\n\tif resp == nil || resp.Status == false {\n\t\treturn nil\n\t}\n\n\tif resp.Cert {\n\t\tNewCertPackage(db, user)\n\t}\n\tif resp.Teenager {\n\t\tNewTeenagerPackage(db, user)\n\t}\n\n\treturn &GiftResponse{\n\t\tCert:     resp.Cert,\n\t\tTeenager: resp.Teenager,\n\t}\n}\n"
  },
  {
    "path": "auth/payment.go",
    "content": "package auth\n\nimport (\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/goccy/go-json\"\n\t\"github.com/spf13/viper\"\n)\n\ntype BalanceResponse struct {\n\tStatus  bool    `json:\"status\" required:\"true\"`\n\tBalance float32 `json:\"balance\"`\n}\n\ntype PaymentResponse struct {\n\tStatus bool `json:\"status\" required:\"true\"`\n\tType   bool `json:\"type\"`\n}\n\nfunc GenerateOrder() string {\n\treturn utils.Sha2Encrypt(utils.GenerateChar(32))\n}\n\nfunc GetBalance(username string) float32 {\n\tif !useDeeptrain() {\n\t\treturn 0.\n\t}\n\n\torder := GenerateOrder()\n\tres, err := utils.Post(getDeeptrainApi(\"/app/balance\"), map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}, map[string]interface{}{\n\t\t\"password\": viper.GetString(\"auth.access\"),\n\t\t\"user\":     username,\n\t\t\"hash\":     utils.Sha2Encrypt(username + viper.GetString(\"auth.salt\")),\n\t\t\"order\":    order,\n\t\t\"sign\":     utils.Sha2Encrypt(username + order + viper.GetString(\"auth.sign\")),\n\t})\n\n\tif err != nil || res == nil || res.(map[string]interface{})[\"status\"] == false {\n\t\treturn 0.\n\t}\n\n\tconverter, _ := json.Marshal(res)\n\tresp, _ := utils.Unmarshal[BalanceResponse](converter)\n\treturn resp.Balance\n}\n\nfunc Pay(username string, amount float32) bool {\n\tif !useDeeptrain() {\n\t\treturn false\n\t}\n\n\torder := GenerateOrder()\n\tres, err := utils.Post(getDeeptrainApi(\"/app/payment\"), map[string]string{\n\t\t\"Content-Type\": \"application/json\",\n\t}, map[string]interface{}{\n\t\t\"password\": viper.GetString(\"auth.access\"),\n\t\t\"user\":     username,\n\t\t\"hash\":     utils.Sha2Encrypt(username + viper.GetString(\"auth.salt\")),\n\t\t\"order\":    order,\n\t\t\"amount\":   amount,\n\t\t\"sign\":     utils.Sha2Encrypt(username + order + viper.GetString(\"auth.sign\")),\n\t})\n\n\tif err != nil || res == nil || res.(map[string]interface{})[\"status\"] == false {\n\t\treturn false\n\t}\n\n\tconverter, _ := json.Marshal(res)\n\tresp, _ := utils.Unmarshal[PaymentResponse](converter)\n\treturn resp.Type\n}\n\nfunc (u *User) Pay(db *sql.DB, cache *redis.Client, amount float32) bool {\n\tif useDeeptrain() {\n\t\tstate := Pay(u.Username, amount)\n\t\tif state {\n\t\t\tincrBillingRequest(cache, int64(amount*100))\n\t\t}\n\t\treturn state\n\t}\n\n\treturn u.PayedQuotaAsAmount(db, amount)\n}\n\nfunc BuyQuota(db *sql.DB, cache *redis.Client, user *User, quota int) error {\n\tmoney := float32(quota) * 0.1\n\n\tif !useDeeptrain() {\n\t\treturn errors.New(\"cannot find payment provider\")\n\t}\n\n\tif user.Pay(db, cache, money) {\n\t\tuser.IncreaseQuota(db, float32(quota))\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"do not have enough money\")\n}\n"
  },
  {
    "path": "auth/quota.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"database/sql\"\n)\n\nfunc (u *User) CreateInitialQuota(db *sql.DB) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)\n\t`, u.GetID(db), channel.SystemInstance.GetInitialQuota(), 0.)\n\treturn err == nil\n}\n\nfunc (u *User) GetQuota(db *sql.DB) float32 {\n\tvar quota float32\n\tif err := globals.QueryRowDb(db, \"SELECT quota FROM quota WHERE user_id = ?\", u.GetID(db)).Scan(&quota); err != nil {\n\t\treturn 0.\n\t}\n\treturn quota\n}\n\nfunc (u *User) GetUsedQuota(db *sql.DB) float32 {\n\tvar quota float32\n\tif err := globals.QueryRowDb(db, \"SELECT used FROM quota WHERE user_id = ?\", u.GetID(db)).Scan(&quota); err != nil {\n\t\treturn 0.\n\t}\n\treturn quota\n}\n\nfunc (u *User) SetQuota(db *sql.DB, quota float32) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = ?\n\t`, u.GetID(db), quota, 0., quota)\n\treturn err == nil\n}\n\nfunc (u *User) SetUsedQuota(db *sql.DB, used float32) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE used = ?\n\t`, u.GetID(db), 0., used, used)\n\treturn err == nil\n}\n\nfunc (u *User) IncreaseQuota(db *sql.DB, quota float32) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = quota + ?\n\t`, u.GetID(db), quota, 0., quota)\n\treturn err == nil\n}\n\nfunc (u *User) IncreaseUsedQuota(db *sql.DB, used float32) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE used = used + ?\n\t`, u.GetID(db), 0., used, used)\n\treturn err == nil\n}\n\nfunc (u *User) DecreaseQuota(db *sql.DB, quota float32) bool {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = quota - ?\n\t`, u.GetID(db), quota, 0., quota)\n\treturn err == nil\n}\n\nfunc (u *User) UseQuota(db *sql.DB, quota float32) bool {\n\tif quota == 0 {\n\t\treturn true\n\t}\n\tif !u.DecreaseQuota(db, quota) {\n\t\treturn false\n\t}\n\treturn u.IncreaseUsedQuota(db, quota)\n}\n\nfunc (u *User) PayedQuota(db *sql.DB, quota float32) bool {\n\tif quota == 0 {\n\t\treturn true\n\t}\n\n\tcurrent := u.GetQuota(db)\n\tif quota > current {\n\t\treturn false\n\t}\n\n\tif !u.DecreaseQuota(db, quota) {\n\t\treturn false\n\t}\n\treturn u.IncreaseUsedQuota(db, quota)\n}\n\nfunc (u *User) PayedQuotaAsAmount(db *sql.DB, amount float32) bool {\n\treturn u.PayedQuota(db, amount*10)\n}\n"
  },
  {
    "path": "auth/redeem.go",
    "content": "package auth\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\ntype Redeem struct {\n\tId    int64   `json:\"id\"`\n\tCode  string  `json:\"code\"`\n\tQuota float32 `json:\"quota\"`\n\tUsed  bool    `json:\"used\"`\n}\n\nfunc GenerateRedeemCodes(db *sql.DB, num int, quota float32) ([]string, error) {\n\tarr := make([]string, 0)\n\tidx := 0\n\tfor idx < num {\n\t\tcode := fmt.Sprintf(\"nio-%s\", utils.GenerateChar(32))\n\t\tif err := CreateRedeemCode(db, code, quota); err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"failed to generate code: %w\", err)\n\t\t}\n\t\tarr = append(arr, code)\n\t\tidx++\n\t}\n\n\treturn arr, nil\n}\n\nfunc CreateRedeemCode(db *sql.DB, code string, quota float32) error {\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO redeem (code, quota) VALUES (?, ?)\n\t`, code, quota)\n\treturn err\n}\n\nfunc GetRedeemCode(db *sql.DB, code string) (*Redeem, error) {\n\trow := globals.QueryRowDb(db, `\n\t\tSELECT id, code, quota, used\n\t\tFROM redeem\n\t\tWHERE code = ?\n\t`, code)\n\tvar redeem Redeem\n\terr := row.Scan(&redeem.Id, &redeem.Code, &redeem.Quota, &redeem.Used)\n\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, fmt.Errorf(\"redeem code not found\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get redeem code: %w\", err)\n\t}\n\treturn &redeem, nil\n}\nfunc (r *Redeem) IsUsed() bool {\n\treturn r.Used\n}\n\nfunc (r *Redeem) Use(db *sql.DB) error {\n\t_, err := globals.ExecDb(db, `\n\t\tUPDATE redeem SET used = TRUE WHERE id = ? AND used = FALSE\n\t`, r.Id)\n\treturn err\n}\n\nfunc (r *Redeem) GetQuota() float32 {\n\treturn r.Quota\n}\n\nfunc (r *Redeem) UseRedeem(db *sql.DB, user *User) error {\n\tif r.IsUsed() {\n\t\treturn fmt.Errorf(\"this redeem code has been used\")\n\t}\n\n\tif err := r.Use(db); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn fmt.Errorf(\"redeem code not found\")\n\t\t} else if errors.Is(err, sql.ErrTxDone) {\n\t\t\treturn fmt.Errorf(\"transaction has been closed\")\n\t\t}\n\t\treturn fmt.Errorf(\"failed to use redeem code: %w\", err)\n\t}\n\n\tif !user.IncreaseQuota(db, r.GetQuota()) {\n\t\treturn fmt.Errorf(\"failed to increase quota for user\")\n\t}\n\n\treturn nil\n}\n\nfunc (u *User) UseRedeem(db *sql.DB, cache *redis.Client, code string) (float32, error) {\n\tif useDeeptrain() {\n\t\treturn 0, errors.New(\"redeem code is not available in deeptrain mode\")\n\t}\n\n\tif redeem, err := GetRedeemCode(db, code); err != nil {\n\t\treturn 0, err\n\t} else {\n\t\tif err := redeem.UseRedeem(db, u); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to use redeem code: %w\", err)\n\t\t}\n\n\t\tincrBillingRequest(cache, int64(redeem.GetQuota()*10))\n\t\treturn redeem.GetQuota(), nil\n\t}\n}\n"
  },
  {
    "path": "auth/router.go",
    "content": "package auth\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Register(app *gin.RouterGroup) {\n\tapp.Any(\"/\", IndexAPI)\n\tapp.POST(\"/verify\", VerifyAPI)\n\tapp.POST(\"/reset\", ResetAPI)\n\tapp.POST(\"/register\", RegisterAPI)\n\tapp.POST(\"/login\", LoginAPI)\n\tapp.POST(\"/state\", StateAPI)\n\tapp.GET(\"/apikey\", KeyAPI)\n\tapp.GET(\"/userinfo\", UserInfoAPI)\n\tapp.POST(\"/resetkey\", ResetKeyAPI)\n\tapp.GET(\"/package\", PackageAPI)\n\tapp.GET(\"/quota\", QuotaAPI)\n\tapp.POST(\"/buy\", BuyAPI)\n\tapp.GET(\"/subscription\", SubscriptionAPI)\n\tapp.POST(\"/subscribe\", SubscribeAPI)\n\tapp.GET(\"/invite\", InviteAPI)\n\tapp.GET(\"/redeem\", RedeemAPI)\n}\n"
  },
  {
    "path": "auth/rule.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"chat/globals\"\n\t\"chat/utils\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nconst (\n\tErrNotAuthenticated = \"not authenticated error (model: %s)\"\n\tErrNotSetPrice      = \"the price of the model is not set (model: %s)\"\n\tErrNotEnoughQuota   = \"user quota is not enough error (model: %s, minimum quota: %0.2f, your quota: %0.2f)\"\n\tErrEstimatedCost    = \"estimated cost exceeds user quota (model: %s, estimated cost: %0.2f, your quota: %0.2f)\"\n)\n\n// CanEnableModel returns whether the model can be enabled (without subscription)\nfunc CanEnableModel(db *sql.DB, user *User, model string, messages []globals.Message) error {\n\tisAuth := user != nil\n\tisAdmin := isAuth && user.IsAdmin(db)\n\n\tcharge := channel.ChargeInstance.GetCharge(model)\n\n\tif charge.IsUnsetType() && !isAdmin {\n\t\treturn fmt.Errorf(ErrNotSetPrice, model)\n\t}\n\n\tif !charge.IsBilling() {\n\t\t// return if is the user is authenticated or anonymous is allowed for this model\n\t\tif charge.SupportAnonymous() || isAuth {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(ErrNotAuthenticated, model)\n\t}\n\n\tif !isAuth {\n\t\treturn fmt.Errorf(ErrNotAuthenticated, model)\n\t}\n\n\t// Calculate estimated input cost\n\tinputTokens := utils.NumTokensFromMessages(messages, model, false)\n\testimatedInputCost := float32(inputTokens) / 1000 * charge.GetInput()\n\n\t// Get user's current quota\n\tquota := user.GetQuota(db)\n\tif quota < estimatedInputCost {\n\t\treturn fmt.Errorf(ErrEstimatedCost, model, estimatedInputCost, quota)\n\t}\n\n\treturn nil\n}\n\nfunc CanEnableModelWithSubscription(db *sql.DB, cache *redis.Client, user *User, model string, messages []globals.Message) (canEnable error, usePlan bool) {\n\t// use subscription quota first\n\tif user != nil && HandleSubscriptionUsage(db, cache, user, model) {\n\t\treturn nil, true\n\t}\n\treturn CanEnableModel(db, user, model, messages), false\n}\n"
  },
  {
    "path": "auth/struct.go",
    "content": "package auth\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"time\"\n)\n\ntype User struct {\n\tID           int64      `json:\"id\"`\n\tUsername     string     `json:\"username\"`\n\tEmail        string     `json:\"email\"`\n\tBindID       int64      `json:\"bind_id\"`\n\tPassword     string     `json:\"password\"`\n\tToken        string     `json:\"token\"`\n\tAdmin        bool       `json:\"is_admin\"`\n\tLevel        int        `json:\"level\"`\n\tSubscription *time.Time `json:\"subscription\"`\n\tBanned       bool       `json:\"is_banned\"`\n}\n\ntype UserInfo struct {\n\tID             int64   `json:\"id\"`\n\tRegisterDays   float64 `json:\"register_days\"`\n\tUsedQuota      float64 `json:\"used_quota\"`\n\tPlanTotalMonth int64   `json:\"plan_total_month\"`\n\tEmail          string  `json:\"email\"`\n}\n\nfunc GetUserById(db *sql.DB, id int64) *User {\n\tvar user User\n\tif err := globals.QueryRowDb(db, \"SELECT id, username FROM auth WHERE id = ?\", id).Scan(&user.ID, &user.Username); err != nil {\n\t\treturn nil\n\t}\n\treturn &user\n}\n\nfunc GetUserByName(db *sql.DB, username string) *User {\n\tvar user User\n\tif err := globals.QueryRowDb(db, \"SELECT id, username FROM auth WHERE username = ?\", username).Scan(&user.ID, &user.Username); err != nil {\n\t\treturn nil\n\t}\n\treturn &user\n}\n\nfunc GetUserByEmail(db *sql.DB, email string) *User {\n\tvar user User\n\tif err := globals.QueryRowDb(db, \"SELECT id, username FROM auth WHERE email = ?\", email).Scan(&user.ID, &user.Username); err != nil {\n\t\treturn nil\n\t}\n\treturn &user\n}\n\nfunc GetId(db *sql.DB, user *User) int64 {\n\tif user == nil {\n\t\treturn -1\n\t}\n\treturn user.GetID(db)\n}\n\nfunc (u *User) IsBanned(db *sql.DB) bool {\n\tif u.Banned {\n\t\treturn true\n\t}\n\n\tvar banned sql.NullBool\n\tif err := globals.QueryRowDb(db, \"SELECT is_banned FROM auth WHERE username = ?\", u.Username).Scan(&banned); err != nil {\n\t\treturn false\n\t}\n\tu.Banned = banned.Valid && banned.Bool\n\n\treturn u.Banned\n}\n\nfunc (u *User) IsAdmin(db *sql.DB) bool {\n\tif u.Admin {\n\t\treturn true\n\t}\n\n\tvar admin sql.NullBool\n\tif err := globals.QueryRowDb(db, \"SELECT is_admin FROM auth WHERE username = ?\", u.Username).Scan(&admin); err != nil {\n\t\treturn false\n\t}\n\n\tu.Admin = admin.Valid && admin.Bool\n\treturn u.Admin\n}\n\nfunc (u *User) GetID(db *sql.DB) int64 {\n\tif u.ID > 0 {\n\t\treturn u.ID\n\t}\n\tif err := globals.QueryRowDb(db, \"SELECT id FROM auth WHERE username = ?\", u.Username).Scan(&u.ID); err != nil {\n\t\treturn 0\n\t}\n\treturn u.ID\n}\n\nfunc (u *User) HitID() int64 {\n\treturn u.ID\n}\n\nfunc (u *User) GetEmail(db *sql.DB) string {\n\tif len(u.Email) > 0 {\n\t\treturn u.Email\n\t}\n\n\tvar email sql.NullString\n\tif err := globals.QueryRowDb(db, \"SELECT email FROM auth WHERE username = ?\", u.Username).Scan(&email); err != nil {\n\t\treturn \"\"\n\t}\n\n\tu.Email = email.String\n\treturn u.Email\n}\n\nfunc (u *User) GetUserInfo(db *sql.DB) (*UserInfo, error) {\n\tvar info UserInfo\n\tvar createdAt []uint8\n\n\tvar planMonth sql.NullInt64\n\tvar usedQuota sql.NullFloat64\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT\n\t\t\tauth.id, quota.created_at, quota.used, subscription.total_month\n\t\tFROM auth\n\t\tLEFT JOIN quota ON quota.user_id = auth.id\n\t\tLEFT JOIN subscription ON subscription.user_id = auth.id\n\t\tWHERE auth.username = ?\n\t`, u.Username).Scan(&info.ID, &createdAt, &usedQuota, &planMonth); err != nil {\n\t\treturn nil, err\n\t}\n\n\tt := utils.ConvertTime(createdAt)\n\tif t != nil {\n\t\tinfo.RegisterDays = time.Since(*t).Hours() / 24\n\t}\n\n\tif planMonth.Valid {\n\t\tinfo.PlanTotalMonth = planMonth.Int64\n\t}\n\n\tif usedQuota.Valid {\n\t\tinfo.UsedQuota = usedQuota.Float64\n\t}\n\n\tinfo.Email = u.GetEmail(db)\n\n\treturn &info, nil\n}\n\nfunc IsUserExist(db *sql.DB, username string) bool {\n\tvar count int\n\tif err := globals.QueryRowDb(db, \"SELECT COUNT(*) FROM auth WHERE username = ?\", username).Scan(&count); err != nil {\n\t\treturn false\n\t}\n\treturn count > 0\n}\n\nfunc IsEmailExist(db *sql.DB, email string) bool {\n\tvar count int\n\tif err := globals.QueryRowDb(db, \"SELECT COUNT(*) FROM auth WHERE email = ?\", email).Scan(&count); err != nil {\n\t\treturn false\n\t}\n\treturn count > 0\n}\n\nfunc getMaxBindId(db *sql.DB) int64 {\n\tvar max int64\n\tif err := globals.QueryRowDb(db, \"SELECT MAX(bind_id) FROM auth\").Scan(&max); err != nil {\n\t\treturn 0\n\t}\n\treturn max\n}\n\nfunc GetGroup(db *sql.DB, user *User) string {\n\tif user == nil {\n\t\treturn globals.AnonymousType\n\t}\n\n\tlevel := user.GetSubscriptionLevel(db)\n\tswitch level {\n\tcase 0:\n\t\treturn globals.NormalType\n\tcase 1:\n\t\treturn globals.BasicType\n\tcase 2:\n\t\treturn globals.StandardType\n\tcase 3:\n\t\treturn globals.ProType\n\tdefault:\n\t\treturn globals.NormalType\n\t}\n}\n\nfunc HitGroup(db *sql.DB, user *User, group string) bool {\n\tif group == globals.AdminType {\n\t\treturn user != nil && user.IsAdmin(db)\n\t}\n\n\treturn GetGroup(db, user) == group\n}\n\nfunc GetUsernameString(db *sql.DB, user *User) string {\n\tif user == nil {\n\t\treturn globals.AnonymousType\n\t}\n\n\treturn user.Username\n}\n\nfunc HitGroups(db *sql.DB, user *User, groups []string) bool {\n\tif utils.Contains(globals.AdminType, groups) {\n\t\tif user != nil && user.IsAdmin(db) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tgroup := GetGroup(db, user)\n\treturn utils.Contains(group, groups)\n}\n"
  },
  {
    "path": "auth/subscription.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"math\"\n\t\"time\"\n)\n\nfunc disableSubscription() bool {\n\treturn !channel.PlanInstance.IsEnabled()\n}\n\nfunc (u *User) GetSubscription(db *sql.DB) (time.Time, int) {\n\tif u.Subscription != nil && u.Subscription.Unix() > 0 {\n\t\treturn *u.Subscription, u.Level\n\t}\n\n\tvar expiredAt []uint8\n\tif err := globals.QueryRowDb(db, \"SELECT expired_at, level FROM subscription WHERE user_id = ?\", u.GetID(db)).Scan(&expiredAt, &u.Level); err != nil {\n\t\treturn time.Unix(0, 0), 0\n\t}\n\n\tt := utils.ConvertTime(expiredAt)\n\tif t == nil {\n\t\tt = utils.ToPtr(time.Unix(0, 0))\n\t}\n\n\tu.Subscription = t\n\treturn *u.Subscription, u.Level\n}\n\nfunc (u *User) GetSubscriptionLevel(db *sql.DB) int {\n\t_, level := u.GetSubscription(db)\n\tif !u.IsSubscribe(db) {\n\t\treturn 0\n\t}\n\treturn level\n}\n\nfunc (u *User) GetPlan(db *sql.DB) channel.Plan {\n\treturn channel.PlanInstance.GetPlan(u.GetSubscriptionLevel(db))\n}\n\nfunc (u *User) GetSubscriptionExpiredAt(db *sql.DB) time.Time {\n\tstamp, _ := u.GetSubscription(db)\n\treturn stamp\n}\n\nfunc (u *User) GetSubscriptionTime(db *sql.DB) time.Time {\n\tstamp, _ := u.GetSubscription(db)\n\treturn stamp\n}\n\nfunc (u *User) IsSubscribe(db *sql.DB) bool {\n\tstamp, level := u.GetSubscription(db)\n\treturn stamp.Unix() > time.Now().Unix() && level > 0\n}\n\nfunc (u *User) IsEnterprise(db *sql.DB) bool {\n\tif !u.IsSubscribe(db) {\n\t\treturn false\n\t}\n\n\tvar enterprise sql.NullBool\n\tif err := globals.QueryRowDb(db, \"SELECT enterprise FROM subscription WHERE user_id = ?\", u.GetID(db)).Scan(&enterprise); err != nil {\n\t\treturn false\n\t}\n\n\treturn enterprise.Valid && enterprise.Bool\n}\n\nfunc (u *User) GetSubscriptionExpiredDay(db *sql.DB) int {\n\tstamp := u.GetSubscriptionTime(db).Sub(time.Now())\n\treturn int(math.Round(stamp.Hours() / 24))\n}\n\nfunc (u *User) AddSubscription(db *sql.DB, month int, level int) bool {\n\tcurrent := u.GetSubscriptionTime(db)\n\tif current.Unix() < time.Now().Unix() {\n\t\tcurrent = time.Now()\n\t}\n\texpiredAt := current.AddDate(0, month, 0)\n\tdate := utils.ConvertSqlTime(expiredAt)\n\t_, err := globals.ExecDb(db, `\n\t\tINSERT INTO subscription (user_id, expired_at, total_month, level) VALUES (?, ?, ?, ?)\n\t\tON DUPLICATE KEY UPDATE expired_at = ?, total_month = total_month + ?, level = ?\n\t`, u.GetID(db), date, month, level, date, month, level)\n\treturn err == nil\n}\n\nfunc (u *User) DowngradePlan(db *sql.DB, target int) error {\n\texpired, current := u.GetSubscription(db)\n\tif current == 0 || current == target {\n\t\treturn fmt.Errorf(\"invalid plan level\")\n\t}\n\n\tnow := time.Now()\n\tweight := channel.PlanInstance.GetPlan(current).Price / channel.PlanInstance.GetPlan(target).Price\n\tstamp := float32(expired.Unix()-now.Unix()) * weight\n\n\t// ceil expired time\n\texpiredAt := now.Add(time.Duration(stamp)*time.Second).AddDate(0, 0, -1)\n\tdate := utils.ConvertSqlTime(expiredAt)\n\t_, err := globals.ExecDb(db, \"UPDATE subscription SET level = ?, expired_at = ? WHERE user_id = ?\", target, date, u.GetID(db))\n\n\treturn err\n}\n\nfunc (u *User) CountUpgradePrice(db *sql.DB, target int) float32 {\n\texpired := u.GetSubscriptionExpiredAt(db)\n\tweight := channel.PlanInstance.GetPlan(target).Price - u.GetPlan(db).Price\n\tif weight < 0 {\n\t\treturn 0\n\t}\n\n\tdays := expired.Sub(time.Now()).Hours() / 24\n\treturn float32(days) * weight / 30\n}\n\nfunc (u *User) SetSubscriptionLevel(db *sql.DB, level int) bool {\n\t_, err := globals.ExecDb(db, \"UPDATE subscription SET level = ? WHERE user_id = ?\", level, u.GetID(db))\n\treturn err == nil\n}\n\nfunc CountSubscriptionPrize(level int, month int) float32 {\n\tplan := channel.PlanInstance.GetPlan(level)\n\tbase := plan.Price * float32(month)\n\tif month >= 36 {\n\t\treturn base * 0.7\n\t} else if month >= 12 {\n\t\treturn base * 0.8\n\t} else if month >= 6 {\n\t\treturn base * 0.9\n\t}\n\treturn base\n}\n\nfunc BuySubscription(db *sql.DB, cache *redis.Client, user *User, level int, month int) error {\n\tif disableSubscription() {\n\t\treturn errors.New(\"subscription feature does not enable of this site\")\n\t}\n\n\tif month < 1 || month > 999 || !channel.IsValidPlan(level) {\n\t\treturn errors.New(\"invalid subscription params\")\n\t}\n\n\tbefore := user.GetSubscriptionLevel(db)\n\tif before == 0 || before == level {\n\t\t// buy new subscription or renew subscription\n\t\tmoney := CountSubscriptionPrize(level, month)\n\t\tif user.Pay(db, cache, money) {\n\t\t\t// migrate subscription\n\t\t\tuser.AddSubscription(db, month, level)\n\n\t\t\tif before == 0 {\n\t\t\t\t// new subscription\n\n\t\t\t\tplan := user.GetPlan(db)\n\t\t\t\tfor _, usage := range plan.Items {\n\t\t\t\t\t// create usage\n\t\t\t\t\tusage.CreateUsage(user, cache)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t} else if before > level {\n\t\t// downgrade subscription\n\t\treturn user.DowngradePlan(db, level)\n\t} else {\n\t\t// upgrade subscription\n\t\tmoney := user.CountUpgradePrice(db, level)\n\t\tif user.Pay(db, cache, money) {\n\t\t\tuser.SetSubscriptionLevel(db, level)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errors.New(\"not enough money\")\n}\n\nfunc HandleSubscriptionUsage(db *sql.DB, cache *redis.Client, user *User, model string) bool {\n\tif disableSubscription() {\n\t\treturn false\n\t}\n\tplan := user.GetPlan(db)\n\treturn plan.IncreaseUsage(user, cache, model)\n}\n\nfunc RevertSubscriptionUsage(db *sql.DB, cache *redis.Client, user *User, model string) bool {\n\tif disableSubscription() {\n\t\treturn false\n\t}\n\tplan := user.GetPlan(db)\n\treturn plan.DecreaseUsage(user, cache, model)\n}\n"
  },
  {
    "path": "auth/usage.go",
    "content": "package auth\n\nimport (\n\t\"chat/channel\"\n\t\"database/sql\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc (u *User) GetSubscriptionUsage(db *sql.DB, cache *redis.Client) channel.UsageMap {\n\tplan := u.GetPlan(db)\n\treturn plan.GetUsage(u, db, cache)\n}\n\nfunc (u *User) GetSubscriptionRefreshAt(db *sql.DB, cache *redis.Client) time.Time {\n\tif disableSubscription() || !u.IsSubscribe(db) {\n\t\treturn time.Unix(0, 0)\n\t}\n\n\tplan := u.GetPlan(db)\n\tif len(plan.Items) == 0 {\n\t\treturn time.Unix(0, 0)\n\t}\n\n\tvar next time.Time\n\tfor i, item := range plan.Items {\n\t\t_, offset := channel.GetSubscriptionUsage(cache, u, item.Id)\n\t\tn := offset.AddDate(0, 1, 0)\n\t\tif i == 0 || n.Before(next) {\n\t\t\tnext = n\n\t\t}\n\t}\n\treturn next\n}\n\nfunc (u *User) GetSubscriptionRefreshDay(db *sql.DB, cache *redis.Client) int {\n\tat := u.GetSubscriptionRefreshAt(db, cache)\n\tif at.Unix() <= 0 {\n\t\treturn 0\n\t}\n\tdiff := time.Until(at)\n\treturn int(math.Round(diff.Hours() / 24))\n}\n"
  },
  {
    "path": "auth/validators.go",
    "content": "package auth\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc isInRange(content string, min, max int) bool {\n\tcontent = strings.TrimSpace(content)\n\treturn len(content) >= min && len(content) <= max\n}\n\nfunc validateUsername(username string) bool {\n\treturn isInRange(username, 2, 24)\n}\n\nfunc validateUsernameOrEmail(username string) bool {\n\treturn isInRange(username, 1, 255)\n}\n\nfunc validatePassword(password string) bool {\n\treturn isInRange(password, 6, 36)\n}\n\nfunc validateEmail(email string) bool {\n\tif !isInRange(email, 1, 255) {\n\t\treturn false\n\t}\n\n\texp := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$`)\n\treturn exp.MatchString(email)\n}\n\nfunc validateCode(code string) bool {\n\treturn isInRange(code, 1, 64)\n}\n"
  },
  {
    "path": "channel/channel.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nvar defaultMaxRetries = 1\nvar defaultReplacer = []string{\n\t\"openai_api\", \"anthropic_api\",\n\t\"api2d\", \"closeai_api\",\n\t\"one_api\", \"new_api\", \"shell_api\",\n}\n\nfunc (c *Channel) GetId() int {\n\treturn c.Id\n}\n\nfunc (c *Channel) GetName() string {\n\treturn c.Name\n}\n\nfunc (c *Channel) GetType() string {\n\treturn c.Type\n}\n\nfunc (c *Channel) GetPriority() int {\n\treturn c.Priority\n}\n\nfunc (c *Channel) GetWeight() int {\n\tif c.Weight <= 0 {\n\t\treturn 1\n\t}\n\treturn c.Weight\n}\n\nfunc (c *Channel) GetModels() []string {\n\treturn c.Models\n}\n\nfunc (c *Channel) GetRetry() int {\n\tif c.Retry <= 0 {\n\t\treturn defaultMaxRetries\n\t}\n\treturn c.Retry\n}\n\nfunc (c *Channel) GetSecret() string {\n\treturn c.Secret\n}\n\nfunc (c *Channel) GetCurrentSecret() *string {\n\treturn c.CurrentSecret\n}\n\n// GetRandomSecret returns a random secret from the secret list\nfunc (c *Channel) GetRandomSecret() string {\n\tarr := strings.Split(c.GetSecret(), \"\\n\")\n\tif len(arr) == 0 {\n\t\treturn \"\"\n\t}\n\n\tidx := utils.Intn(len(arr))\n\tsecret := arr[idx]\n\n\tc.CurrentSecret = &secret\n\treturn secret\n}\n\nfunc (c *Channel) GetCurrentSecretValue() string {\n\tif c.CurrentSecret == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *c.CurrentSecret\n}\n\nfunc (c *Channel) SplitRandomSecret(num int) []string {\n\tsecret := c.GetRandomSecret()\n\tarr := strings.Split(secret, \"|\")\n\tif len(arr) == num {\n\t\treturn arr\n\t} else if len(arr) > num {\n\t\treturn arr[:num]\n\t}\n\n\tfor i := len(arr); i < num; i++ {\n\t\tarr = append(arr, \"\")\n\t}\n\n\treturn arr\n}\n\nfunc (c *Channel) GetEndpoint() string {\n\treturn c.Endpoint\n}\n\nfunc (c *Channel) GetDomain() string {\n\tif instance, err := url.Parse(c.GetEndpoint()); err == nil {\n\t\treturn instance.Host\n\t}\n\n\treturn c.GetEndpoint()\n}\n\nfunc (c *Channel) GetMapper() string {\n\treturn c.Mapper\n}\n\nfunc (c *Channel) Load() {\n\treflect := make(map[string]string)\n\texclude := make([]string, 0)\n\tmodels := c.GetModels()\n\n\tarr := strings.Split(c.GetMapper(), \"\\n\")\n\tfor _, item := range arr {\n\t\tpair := strings.Split(item, \">\")\n\t\tif len(pair) != 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfrom, to := pair[0], pair[1]\n\t\tif strings.HasPrefix(from, \"!\") {\n\t\t\tfrom = strings.TrimPrefix(from, \"!\")\n\t\t\texclude = append(exclude, to)\n\t\t}\n\n\t\treflect[from] = to\n\t}\n\n\tc.Reflect = &reflect\n\tc.ExcludeModels = &exclude\n\n\tvar hits []string\n\n\tfor _, model := range models {\n\t\tif !utils.Contains(model, hits) && !utils.Contains(model, exclude) {\n\t\t\thits = append(hits, model)\n\t\t}\n\t}\n\n\tfor model := range reflect {\n\t\tif !utils.Contains(model, hits) && !utils.Contains(model, exclude) {\n\t\t\thits = append(hits, model)\n\t\t}\n\t}\n\n\tc.HitModels = &hits\n}\n\nfunc (c *Channel) GetReflect() map[string]string {\n\treturn *c.Reflect\n}\n\nfunc (c *Channel) GetExcludeModels() []string {\n\treturn *c.ExcludeModels\n}\n\n// GetModelReflect returns the reflection model name if it exists, otherwise returns the original model name\nfunc (c *Channel) GetModelReflect(model string) string {\n\tref := c.GetReflect()\n\tif reflect, ok := ref[model]; ok && len(reflect) > 0 {\n\t\treturn reflect\n\t}\n\n\treturn model\n}\n\nfunc (c *Channel) GetHitModels() []string {\n\treturn *c.HitModels\n}\n\nfunc (c *Channel) GetState() bool {\n\treturn c.State\n}\n\nfunc (c *Channel) GetGroup() []string {\n\treturn c.Group\n}\n\nfunc (c *Channel) GetProxy() globals.ProxyConfig {\n\treturn c.Proxy\n}\n\nfunc (c *Channel) IsHitGroup(group string) bool {\n\tif len(c.GetGroup()) == 0 {\n\t\treturn true\n\t}\n\n\treturn utils.Contains(group, c.GetGroup())\n}\n\nfunc (c *Channel) IsHit(model string) bool {\n\treturn utils.Contains(model, c.GetHitModels())\n}\n\nfunc (c *Channel) ProcessError(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tcontent := err.Error()\n\n\tif strings.Contains(content, c.GetEndpoint()) {\n\t\t// hide the endpoint\n\t\treplacer := fmt.Sprintf(\"channel://%d\", c.GetId())\n\t\tcontent = strings.Replace(content, c.GetEndpoint(), replacer, -1)\n\t}\n\n\tif domain := c.GetDomain(); len(strings.TrimSpace(domain)) > 0 && strings.Contains(content, domain) {\n\t\tcontent = strings.Replace(content, domain, \"channel\", -1)\n\t}\n\n\tfor _, item := range defaultReplacer {\n\t\tcontent = strings.Replace(content, item, \"chatnio_upstream\", -1)\n\t}\n\n\tsecret := c.GetCurrentSecret()\n\tif secret != nil {\n\t\tcontent = strings.Replace(content, *secret, utils.HideSecret(*secret), -1)\n\t}\n\n\treturn errors.New(content)\n}\n"
  },
  {
    "path": "channel/charge.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\n\t\"github.com/spf13/viper\"\n)\n\nfunc NewChargeManager() *ChargeManager {\n\tvar seq ChargeSequence\n\tif err := viper.UnmarshalKey(\"charge\", &seq); err != nil {\n\t\tpanic(err)\n\t}\n\n\tm := &ChargeManager{\n\t\tSequence:         seq,\n\t\tModels:           map[string]*Charge{},\n\t\tNonBillingModels: []string{},\n\t}\n\tm.Load()\n\n\treturn m\n}\n\nfunc (m *ChargeManager) Load() {\n\tseq := make(ChargeSequence, 0)\n\tfor _, charge := range m.Sequence {\n\t\tif charge == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif charge.Id == -1 {\n\t\t\tcharge.Id = m.GetMaxId() + 1\n\t\t}\n\t\tseq = append(seq, charge)\n\t}\n\tm.Sequence = seq\n\n\t// init support models\n\tm.Models = map[string]*Charge{}\n\tfor _, charge := range m.Sequence {\n\t\tfor _, model := range charge.Models {\n\t\t\tif _, ok := m.Models[model]; !ok {\n\t\t\t\tm.Models[model] = charge\n\t\t\t}\n\t\t}\n\t}\n\n\tm.NonBillingModels = []string{}\n\tfor _, charge := range m.Sequence {\n\t\tif !charge.IsBilling() {\n\t\t\tfor _, model := range charge.Models {\n\t\t\t\tm.NonBillingModels = append(m.NonBillingModels, model)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *ChargeManager) GetModels() map[string]*Charge {\n\treturn m.Models\n}\n\nfunc (m *ChargeManager) GetNonBillingModels() []string {\n\treturn m.NonBillingModels\n}\n\nfunc (m *ChargeManager) IsBilling(model string) bool {\n\treturn !utils.Contains(model, m.NonBillingModels)\n}\n\nfunc (m *ChargeManager) GetCharge(model string) *Charge {\n\tif charge, ok := m.Models[model]; ok {\n\t\treturn charge\n\t}\n\treturn &Charge{\n\t\tType:      globals.NonBilling,\n\t\tAnonymous: false,\n\t\tUnset:     true,\n\t}\n}\n\nfunc (m *ChargeManager) SaveConfig() error {\n\treturn utils.SaveConfig(\"charge\", m.Sequence)\n}\n\nfunc (m *ChargeManager) GetMaxId() int {\n\tmax := 0\n\tfor _, charge := range m.Sequence {\n\t\tif charge.Id > max {\n\t\t\tmax = charge.Id\n\t\t}\n\t}\n\treturn max\n}\n\nfunc (m *ChargeManager) AddRawRule(charge *Charge) {\n\tcharge.Id = m.GetMaxId() + 1\n\tm.Sequence = append(m.Sequence, charge)\n}\n\nfunc (m *ChargeManager) AddRule(charge Charge) error {\n\tm.AddRawRule(&charge)\n\treturn m.SaveConfig()\n}\n\nfunc (m *ChargeManager) UpdateRawRule(charge *Charge) {\n\tfor _, item := range m.Sequence {\n\t\tif item.Id == charge.Id {\n\t\t\t*item = *charge\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (m *ChargeManager) UpdateRule(charge Charge) error {\n\tm.UpdateRawRule(&charge)\n\treturn m.SaveConfig()\n}\n\nfunc (m *ChargeManager) SetRawRule(charge *Charge) {\n\tif charge.Id == -1 {\n\t\tm.AddRawRule(charge)\n\t} else {\n\t\tm.UpdateRawRule(charge)\n\t}\n}\n\nfunc (m *ChargeManager) SetRule(charge Charge) error {\n\tm.SetRawRule(&charge)\n\treturn m.SaveConfig()\n}\n\nfunc (m *ChargeManager) DeleteRawRule(id int) {\n\tfor i, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\tm.Sequence = append(m.Sequence[:i], m.Sequence[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (m *ChargeManager) DeleteRule(id int) error {\n\tm.DeleteRawRule(id)\n\treturn m.SaveConfig()\n}\n\nfunc (m *ChargeManager) SyncRules(charge ChargeSequence, overwrite bool) error {\n\tfor _, item := range charge {\n\t\tm.SyncRule(item, overwrite)\n\t}\n\n\treturn m.SaveConfig()\n}\n\nfunc (m *ChargeManager) SyncRule(charge *Charge, overwrite bool) {\n\tif overwrite {\n\t\tm.SyncRuleWithOverwrite(charge)\n\t} else {\n\t\tm.SyncRuleWithoutOverwrite(charge)\n\t}\n}\n\nfunc (m *ChargeManager) SyncRuleWithOverwrite(charge *Charge) {\n\tif len(charge.Models) == 0 {\n\t\treturn\n\t}\n\n\tfor _, model := range charge.GetModels() {\n\t\tif raw := m.GetRuleByModel(model); raw != nil {\n\t\t\tif len(raw.Models) == 1 {\n\t\t\t\t// rule is already exist and only contains this model, just delete it\n\n\t\t\t\tm.DeleteRawRule(raw.Id)\n\t\t\t} else {\n\t\t\t\t// rule is already exist and contains other models, delete this model from it and add a new rule\n\t\t\t\t// delete model from raw rule\n\t\t\t\traw.Models = utils.Filter(raw.Models, func(m string) bool {\n\t\t\t\t\treturn m != model\n\t\t\t\t})\n\t\t\t\tm.UpdateRawRule(raw)\n\t\t\t}\n\t\t}\n\t}\n\n\tinstance := charge.New(\"\")\n\tinstance.Models = charge.Models\n\tm.AddRawRule(instance)\n}\n\nfunc (m *ChargeManager) SyncRuleWithoutOverwrite(charge *Charge) {\n\tmodels := utils.Filter(charge.GetModels(), func(model string) bool {\n\t\treturn !m.Contains(model)\n\t})\n\n\tif len(models) > 0 {\n\t\tcharge.Models = models\n\t\tm.AddRawRule(charge)\n\t}\n}\n\nfunc (m *ChargeManager) ListRules() ChargeSequence {\n\treturn m.Sequence\n}\n\nfunc (m *ChargeManager) Contains(model string) bool {\n\tfor _, item := range m.Sequence {\n\t\tif item.Contains(model) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (m *ChargeManager) GetRule(id int) *Charge {\n\tfor _, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\treturn item\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *ChargeManager) GetRuleByModel(model string) *Charge {\n\tfor _, item := range m.Sequence {\n\t\tif item.Contains(model) {\n\t\t\treturn item\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Charge) IsUnsetType() bool {\n\treturn c.Unset\n}\n\nfunc (c *Charge) GetType() string {\n\tif c.Type == \"\" {\n\t\treturn globals.NonBilling\n\t}\n\treturn c.Type\n}\n\nfunc (c *Charge) GetModels() []string {\n\treturn c.Models\n}\n\nfunc (c *Charge) GetInput() float32 {\n\tif c.Input <= 0 {\n\t\treturn 0\n\t}\n\treturn c.Input\n}\n\nfunc (c *Charge) GetOutput() float32 {\n\tif c.Output <= 0 {\n\t\treturn 0\n\t}\n\treturn c.Output\n}\n\nfunc (c *Charge) SupportAnonymous() bool {\n\treturn c.Anonymous\n}\n\nfunc (c *Charge) IsBilling() bool {\n\treturn c.GetType() != globals.NonBilling\n}\n\nfunc (c *Charge) IsBillingType(t string) bool {\n\treturn c.GetType() == t\n}\n\nfunc (c *Charge) GetLimit() float32 {\n\tswitch c.GetType() {\n\tcase globals.NonBilling:\n\t\treturn 0\n\tcase globals.TimesBilling:\n\t\treturn c.GetOutput()\n\tcase globals.TokenBilling:\n\t\t// 1k input tokens + 1k output tokens\n\t\treturn c.GetInput() + c.GetOutput()\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc (c *Charge) Contains(model string) bool {\n\treturn utils.Contains(model, c.Models)\n}\n\nfunc (c *Charge) New(model string) *Charge {\n\treturn &Charge{\n\t\tType:      c.Type,\n\t\tModels:    []string{model},\n\t\tInput:     c.Input,\n\t\tOutput:    c.Output,\n\t\tAnonymous: c.Anonymous,\n\t}\n}\n"
  },
  {
    "path": "channel/controller.go",
    "content": "package channel\n\nimport (\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n)\n\ntype SyncChargeForm struct {\n\tOverwrite bool           `json:\"overwrite\"`\n\tData      ChargeSequence `json:\"data\"`\n}\n\nfunc GetInfo(c *gin.Context) {\n\tc.JSON(http.StatusOK, SystemInstance.AsInfo())\n}\n\nfunc AttachmentService(c *gin.Context) {\n\t// /attachments/:hash -> ~/storage/attachments/:hash\n\thash := c.Param(\"hash\")\n\tc.File(fmt.Sprintf(\"storage/attachments/%s\", hash))\n}\n\nfunc DeleteChannel(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tstate := ConduitInstance.DeleteChannel(utils.ParseInt(id))\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc ActivateChannel(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tstate := ConduitInstance.ActivateChannel(utils.ParseInt(id))\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc DeactivateChannel(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tstate := ConduitInstance.DeactivateChannel(utils.ParseInt(id))\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc GetChannelList(c *gin.Context) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"data\":   ConduitInstance.Sequence,\n\t})\n}\n\nfunc GetChannel(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tchannel := ConduitInstance.Sequence.GetChannelById(utils.ParseInt(id))\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": channel != nil,\n\t\t\"data\":   channel,\n\t})\n}\n\nfunc CreateChannel(c *gin.Context) {\n\tvar channel Channel\n\tif err := c.ShouldBindJSON(&channel); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tstate := ConduitInstance.CreateChannel(&channel)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc UpdateChannel(c *gin.Context) {\n\tvar channel Channel\n\tif err := c.ShouldBindJSON(&channel); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tid := c.Param(\"id\")\n\tchannel.Id = utils.ParseInt(id)\n\n\tstate := ConduitInstance.UpdateChannel(channel.Id, &channel)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc SetCharge(c *gin.Context) {\n\tvar charge Charge\n\tif err := c.ShouldBindJSON(&charge); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tstate := ChargeInstance.SetRule(charge)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc GetChargeList(c *gin.Context) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"data\":   ChargeInstance.ListRules(),\n\t})\n}\n\nfunc DeleteCharge(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tstate := ChargeInstance.DeleteRule(utils.ParseInt(id))\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc SyncCharge(c *gin.Context) {\n\tvar form SyncChargeForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t}\n\n\tstate := ChargeInstance.SyncRules(form.Data, form.Overwrite)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc GetConfig(c *gin.Context) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": true,\n\t\t\"data\":   SystemInstance,\n\t})\n}\n\nfunc UpdateConfig(c *gin.Context) {\n\tvar config SystemConfig\n\tif err := c.ShouldBindJSON(&config); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tstate := SystemInstance.UpdateConfig(&config)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n\nfunc GetPlanConfig(c *gin.Context) {\n\tc.JSON(http.StatusOK, PlanInstance)\n}\n\nfunc UpdatePlanConfig(c *gin.Context) {\n\tvar config PlanManager\n\tif err := c.ShouldBindJSON(&config); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"status\": false,\n\t\t\t\"error\":  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tstate := PlanInstance.UpdateConfig(&config)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": state == nil,\n\t\t\"error\":  utils.GetError(state),\n\t})\n}\n"
  },
  {
    "path": "channel/manager.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/spf13/viper\"\n)\n\nvar ConduitInstance *Manager\nvar ChargeInstance *ChargeManager\nvar SystemInstance *SystemConfig\nvar PlanInstance *PlanManager\n\nfunc InitManager() {\n\tConduitInstance = NewChannelManager()\n\tChargeInstance = NewChargeManager()\n\tSystemInstance = NewSystemConfig()\n\tPlanInstance = NewPlanManager()\n}\n\nfunc NewChannelManager() *Manager {\n\tvar seq Sequence\n\tif err := viper.UnmarshalKey(\"channel\", &seq); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// sort by priority\n\n\tmanager := &Manager{\n\t\tSequence:          seq,\n\t\tModels:            []string{},\n\t\tPreflightSequence: map[string]Sequence{},\n\t}\n\tmanager.Load()\n\n\treturn manager\n}\n\nfunc (m *Manager) Load() {\n\t// load channels\n\tfor _, channel := range m.Sequence {\n\t\tif channel != nil {\n\t\t\tchannel.Load()\n\t\t}\n\t}\n\n\t// init support models\n\tm.Models = []string{}\n\tfor _, channel := range m.GetActiveSequence() {\n\t\tfor _, model := range channel.GetHitModels() {\n\t\t\tif !utils.Contains(model, m.Models) {\n\t\t\t\tm.Models = append(m.Models, model)\n\t\t\t}\n\t\t}\n\t}\n\n\t// init preflight sequence\n\tm.PreflightSequence = map[string]Sequence{}\n\tfor _, model := range m.Models {\n\t\tvar seq Sequence\n\t\tfor _, channel := range m.GetActiveSequence() {\n\t\t\tif channel.IsHit(model) {\n\t\t\t\tseq = append(seq, channel)\n\t\t\t}\n\t\t}\n\t\tseq.Sort()\n\t\tm.PreflightSequence[model] = seq\n\t}\n\n\tstamp := time.Now().Unix()\n\n\tglobals.SupportModels = m.Models\n\tglobals.V1ListModels = globals.ListModels{\n\t\tObject: \"list\",\n\t\tData: utils.Each(m.Models, func(model string) globals.ListModelsItem {\n\t\t\treturn globals.ListModelsItem{\n\t\t\t\tId:      model,\n\t\t\t\tObject:  \"model\",\n\t\t\t\tCreated: stamp,\n\t\t\t\tOwnedBy: \"system\",\n\t\t\t}\n\t\t}),\n\t}\n}\n\nfunc (m *Manager) GetSequence() Sequence {\n\treturn m.Sequence\n}\n\nfunc (m *Manager) GetActiveSequence() Sequence {\n\tvar seq Sequence\n\tfor _, channel := range m.Sequence {\n\t\tif channel.GetState() {\n\t\t\tseq = append(seq, channel)\n\t\t}\n\t}\n\tseq.Sort()\n\treturn seq\n}\n\nfunc (m *Manager) GetModels() []string {\n\treturn m.Models\n}\n\nfunc (m *Manager) GetPreflightSequence() map[string]Sequence {\n\treturn m.PreflightSequence\n}\n\n// HitSequence returns the preflight sequence of the model\nfunc (m *Manager) HitSequence(model string) Sequence {\n\treturn m.PreflightSequence[model]\n}\n\n// HasChannel returns whether the channel exists\nfunc (m *Manager) HasChannel(model string) bool {\n\treturn utils.Contains(model, m.Models)\n}\n\nfunc (m *Manager) GetTicker(model, group string) *Ticker {\n\tif !m.HasChannel(model) {\n\t\treturn nil\n\t}\n\n\treturn NewTicker(m.HitSequence(model), group)\n}\n\nfunc (m *Manager) Len() int {\n\treturn len(m.Sequence)\n}\n\nfunc (m *Manager) GetMaxId() int {\n\tvar max int\n\tfor _, channel := range m.Sequence {\n\t\tif channel.Id > max {\n\t\t\tmax = channel.Id\n\t\t}\n\t}\n\treturn max\n}\n\nfunc (m *Manager) SaveConfig() error {\n\treturn utils.SaveConfig(\"channel\", m.Sequence)\n}\n\nfunc (m *Manager) CreateChannel(channel *Channel) error {\n\tchannel.Id = m.GetMaxId() + 1\n\tm.Sequence = append(m.Sequence, channel)\n\treturn m.SaveConfig()\n}\n\nfunc (m *Manager) UpdateChannel(id int, channel *Channel) error {\n\tfor i, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\tm.Sequence[i] = channel\n\t\t\treturn m.SaveConfig()\n\t\t}\n\t}\n\treturn errors.New(\"channel not found\")\n}\n\nfunc (m *Manager) DeleteChannel(id int) error {\n\tfor i, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\tm.Sequence = append(m.Sequence[:i], m.Sequence[i+1:]...)\n\t\t\treturn m.SaveConfig()\n\t\t}\n\t}\n\treturn errors.New(\"channel not found\")\n}\n\nfunc (m *Manager) ActivateChannel(id int) error {\n\tfor i, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\tm.Sequence[i].State = true\n\t\t\treturn m.SaveConfig()\n\t\t}\n\t}\n\treturn errors.New(\"channel not found\")\n}\n\nfunc (m *Manager) DeactivateChannel(id int) error {\n\tfor i, item := range m.Sequence {\n\t\tif item.Id == id {\n\t\t\tm.Sequence[i].State = false\n\t\t\treturn m.SaveConfig()\n\t\t}\n\t}\n\treturn errors.New(\"channel not found\")\n}\n"
  },
  {
    "path": "channel/plan.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/spf13/viper\"\n)\n\ntype PlanManager struct {\n\tEnabled bool   `json:\"enabled\" mapstructure:\"enabled\"`\n\tPlans   []Plan `json:\"plans\" mapstructure:\"plans\"`\n}\n\ntype Plan struct {\n\tLevel int        `json:\"level\" mapstructure:\"level\"`\n\tPrice float32    `json:\"price\" mapstructure:\"price\"`\n\tItems []PlanItem `json:\"items\" mapstructure:\"items\"`\n}\n\ntype PlanItem struct {\n\tId     string   `json:\"id\" mapstructure:\"id\"`\n\tName   string   `json:\"name\" mapstructure:\"name\"`\n\tIcon   string   `json:\"icon\" mapstructure:\"icon\"`\n\tValue  int64    `json:\"value\" mapstructure:\"value\"`\n\tModels []string `json:\"models\" mapstructure:\"models\"`\n}\n\ntype Usage struct {\n\tUsed  int64 `json:\"used\" mapstructure:\"used\"`\n\tTotal int64 `json:\"total\" mapstructure:\"total\"`\n}\ntype UsageMap map[string]Usage\n\nvar planExp int64 = 0\n\nfunc NewPlanManager() *PlanManager {\n\tmanager := &PlanManager{}\n\tif err := viper.UnmarshalKey(\"subscription\", manager); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn manager\n}\n\nfunc (c *PlanManager) SaveConfig() error {\n\treturn utils.SaveConfig(\"subscription\", c)\n}\n\nfunc (c *PlanManager) UpdateConfig(data *PlanManager) error {\n\tc.Enabled = data.Enabled\n\tc.Plans = data.Plans\n\treturn c.SaveConfig()\n}\n\nfunc (c *PlanManager) GetPlan(level int) Plan {\n\tfor _, plan := range c.Plans {\n\t\tif plan.Level == level {\n\t\t\treturn plan\n\t\t}\n\t}\n\treturn Plan{}\n}\n\nfunc (c *PlanManager) GetPlans() []Plan {\n\tif c.Enabled {\n\t\treturn c.Plans\n\t}\n\n\treturn []Plan{}\n}\n\nfunc (c *PlanManager) GetRawPlans() []Plan {\n\treturn c.Plans\n}\n\nfunc (c *PlanManager) IsEnabled() bool {\n\treturn c.Enabled\n}\n\nfunc getOffsetFormat(offset time.Time, usage int64) string {\n\treturn fmt.Sprintf(\"%s/%d\", offset.Format(\"2006-01-02:15:04:05\"), usage)\n}\n\nfunc GetSubscriptionUsage(cache *redis.Client, user globals.AuthLike, t string) (usage int64, offset time.Time) {\n\t// example cache value: 2021-09-01:19:00:00/100\n\t// if date is longer than 1 month, reset usage\n\n\toffset = time.Now()\n\n\tkey := globals.GetSubscriptionLimitFormat(t, user.HitID())\n\tv, err := utils.GetCache(cache, key)\n\tif (err != nil && errors.Is(err, redis.Nil)) || len(v) == 0 {\n\t\tusage = 0\n\t}\n\n\tseg := strings.Split(v, \"/\")\n\tif len(seg) != 2 {\n\t\tusage = 0\n\t} else {\n\t\tdate, err := time.Parse(\"2006-01-02:15:04:05\", seg[0])\n\t\tusage = utils.ParseInt64(seg[1])\n\t\tif err != nil {\n\t\t\tusage = 0\n\t\t}\n\n\t\t// check if date is longer than current date after 1 month, if true, reset usage\n\n\t\tif date.AddDate(0, 1, 0).Before(time.Now()) {\n\t\t\t// date is longer than 1 month, reset usage\n\t\t\tusage = 0\n\n\t\t\t// get current date offset (1 month step)\n\t\t\t// example: 2021-09-01:19:00:0/100 -> 2021-10-01:19:00:00/100\n\n\t\t\t// copy date to offset\n\t\t\toffset = date\n\n\t\t\t// example:\n\t\t\t// current time: 2021-09-08:14:00:00\n\t\t\t// offset: 2021-07-01:19:00:00\n\t\t\t// expected offset: 2021-09-01:19:00:00\n\t\t\t// offset is not longer than current date, stop adding 1 month\n\n\t\t\tfor offset.AddDate(0, 1, 0).Before(time.Now()) {\n\t\t\t\toffset = offset.AddDate(0, 1, 0)\n\t\t\t}\n\t\t} else {\n\t\t\t// date is not longer than 1 month, use current date value\n\n\t\t\toffset = date\n\t\t}\n\t}\n\n\t// set new cache value\n\t_ = utils.SetCache(cache, key, getOffsetFormat(offset, usage), planExp)\n\n\treturn\n}\n\nfunc IncreaseSubscriptionUsage(cache *redis.Client, user globals.AuthLike, t string, limit int64) bool {\n\tkey := globals.GetSubscriptionLimitFormat(t, user.HitID())\n\tusage, offset := GetSubscriptionUsage(cache, user, t)\n\n\tusage += 1\n\tif usage > limit {\n\t\treturn false\n\t}\n\n\t// set new cache value\n\terr := utils.SetCache(cache, key, getOffsetFormat(offset, usage), planExp)\n\treturn err == nil\n}\n\nfunc DecreaseSubscriptionUsage(cache *redis.Client, user globals.AuthLike, t string) bool {\n\tkey := globals.GetSubscriptionLimitFormat(t, user.HitID())\n\tusage, offset := GetSubscriptionUsage(cache, user, t)\n\n\tusage -= 1\n\tif usage < 0 {\n\t\treturn true\n\t}\n\n\t// set new cache value\n\terr := utils.SetCache(cache, key, getOffsetFormat(offset, usage), planExp)\n\treturn err == nil\n}\n\nfunc ReleaseSubscriptionUsage(cache *redis.Client, user globals.AuthLike, t string) bool {\n\tkey := globals.GetSubscriptionLimitFormat(t, user.HitID())\n\t_, offset := GetSubscriptionUsage(cache, user, t)\n\n\t// set new cache value\n\terr := utils.SetCache(cache, key, getOffsetFormat(offset, 0), planExp)\n\treturn err == nil\n}\n\nfunc (p *Plan) GetUsage(user globals.AuthLike, db *sql.DB, cache *redis.Client) UsageMap {\n\treturn utils.EachObject[PlanItem, Usage](p.Items, func(usage PlanItem) (string, Usage) {\n\t\treturn usage.Id, usage.GetUsageForm(user, db, cache)\n\t})\n}\n\nfunc (p *PlanItem) GetUsage(user globals.AuthLike, db *sql.DB, cache *redis.Client) int64 {\n\t// preflight check\n\tuser.GetID(db)\n\tusage, _ := GetSubscriptionUsage(cache, user, p.Id)\n\treturn usage\n}\n\nfunc (p *PlanItem) ResetUsage(user globals.AuthLike, cache *redis.Client) bool {\n\tkey := globals.GetSubscriptionLimitFormat(p.Id, user.HitID())\n\t_, offset := GetSubscriptionUsage(cache, user, p.Id)\n\n\terr := utils.SetCache(cache, key, getOffsetFormat(offset, 0), planExp)\n\treturn err == nil\n}\n\nfunc (p *PlanItem) CreateUsage(user globals.AuthLike, cache *redis.Client) bool {\n\tkey := globals.GetSubscriptionLimitFormat(p.Id, user.HitID())\n\n\terr := utils.SetCache(cache, key, getOffsetFormat(time.Now(), 0), planExp)\n\treturn err == nil\n}\n\nfunc (p *PlanItem) GetUsageForm(user globals.AuthLike, db *sql.DB, cache *redis.Client) Usage {\n\treturn Usage{\n\t\tUsed:  p.GetUsage(user, db, cache),\n\t\tTotal: p.Value,\n\t}\n}\n\nfunc (p *PlanItem) IsInfinity() bool {\n\treturn p.Value == -1\n}\n\nfunc (p *PlanItem) IsExceeded(user globals.AuthLike, db *sql.DB, cache *redis.Client) bool {\n\treturn p.IsInfinity() || p.GetUsage(user, db, cache) < p.Value\n}\n\nfunc (p *PlanItem) Increase(user globals.AuthLike, cache *redis.Client) bool {\n\tstate := IncreaseSubscriptionUsage(cache, user, p.Id, p.Value)\n\treturn state || p.IsInfinity()\n}\n\nfunc (p *PlanItem) Decrease(user globals.AuthLike, cache *redis.Client) bool {\n\tif p.Value == -1 {\n\t\treturn true\n\t}\n\treturn DecreaseSubscriptionUsage(cache, user, p.Id)\n}\n\nfunc (p *PlanItem) Release(user globals.AuthLike, cache *redis.Client) bool {\n\treturn ReleaseSubscriptionUsage(cache, user, p.Id)\n}\n\nfunc (p *Plan) IncreaseUsage(user globals.AuthLike, cache *redis.Client, model string) bool {\n\tfor _, usage := range p.Items {\n\t\tif utils.Contains(model, usage.Models) {\n\t\t\treturn usage.Increase(user, cache)\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (p *Plan) DecreaseUsage(user globals.AuthLike, cache *redis.Client, model string) bool {\n\tfor _, usage := range p.Items {\n\t\tif utils.Contains(model, usage.Models) {\n\t\t\treturn usage.Decrease(user, cache)\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (p *Plan) ReleaseUsage(user globals.AuthLike, cache *redis.Client, model string) bool {\n\tfor _, usage := range p.Items {\n\t\tif utils.Contains(model, usage.Models) {\n\t\t\treturn usage.Release(user, cache)\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (p *Plan) ReleaseAll(user globals.AuthLike, cache *redis.Client) bool {\n\tfor _, usage := range p.Items {\n\t\tif !usage.Release(user, cache) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc IsValidPlan(level int) bool {\n\treturn utils.InRange(level, 1, 3)\n}\n"
  },
  {
    "path": "channel/router.go",
    "content": "package channel\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Register(app *gin.RouterGroup) {\n\tapp.GET(\"/info\", GetInfo)\n\tapp.GET(\"/attachments/:hash\", AttachmentService)\n\n\tapp.GET(\"/admin/channel/list\", GetChannelList)\n\tapp.POST(\"/admin/channel/create\", CreateChannel)\n\tapp.GET(\"/admin/channel/get/:id\", GetChannel)\n\tapp.POST(\"/admin/channel/update/:id\", UpdateChannel)\n\tapp.GET(\"/admin/channel/delete/:id\", DeleteChannel)\n\tapp.GET(\"/admin/channel/activate/:id\", ActivateChannel)\n\tapp.GET(\"/admin/channel/deactivate/:id\", DeactivateChannel)\n\n\tapp.GET(\"/admin/charge/list\", GetChargeList)\n\tapp.POST(\"/admin/charge/set\", SetCharge)\n\tapp.GET(\"/admin/charge/delete/:id\", DeleteCharge)\n\tapp.POST(\"/admin/charge/sync\", SyncCharge)\n\n\tapp.GET(\"/admin/config/view\", GetConfig)\n\tapp.POST(\"/admin/config/update\", UpdateConfig)\n\n\tapp.GET(\"/admin/plan/view\", GetPlanConfig)\n\tapp.POST(\"/admin/plan/update\", UpdatePlanConfig)\n}\n"
  },
  {
    "path": "channel/sequence.go",
    "content": "package channel\n\nimport \"sort\"\n\nfunc (s *Sequence) Len() int {\n\treturn len(*s)\n}\n\nfunc (s *Sequence) Less(i, j int) bool {\n\treturn (*s)[i].GetPriority() > (*s)[j].GetPriority()\n}\n\nfunc (s *Sequence) Swap(i, j int) {\n\t(*s)[i], (*s)[j] = (*s)[j], (*s)[i]\n}\n\nfunc (s *Sequence) GetChannelById(id int) *Channel {\n\tfor _, channel := range *s {\n\t\tif channel.Id == id {\n\t\t\treturn channel\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Sequence) Sort() {\n\t// sort by priority\n\tsort.Sort(s)\n}\n"
  },
  {
    "path": "channel/system.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/spf13/viper\"\n)\n\ntype ApiInfo struct {\n\tTitle        string   `json:\"title\"`\n\tLogo         string   `json:\"logo\"`\n\tFile         string   `json:\"file\"`\n\tDocs         string   `json:\"docs\"`\n\tAnnouncement string   `json:\"announcement\"`\n\tBuyLink      string   `json:\"buy_link\"`\n\tContact      string   `json:\"contact\"`\n\tFooter       string   `json:\"footer\"`\n\tAuthFooter   bool     `json:\"auth_footer\"`\n\tMail         bool     `json:\"mail\"`\n\tArticle      []string `json:\"article\"`\n\tGeneration   []string `json:\"generation\"`\n\tRelayPlan    bool     `json:\"relay_plan\"`\n}\n\ntype generalState struct {\n\tTitle       string `json:\"title\" mapstructure:\"title\"`\n\tLogo        string `json:\"logo\" mapstructure:\"logo\"`\n\tBackend     string `json:\"backend\" mapstructure:\"backend\"`\n\tFile        string `json:\"file\" mapstructure:\"file\"`\n\tDocs        string `json:\"docs\" mapstructure:\"docs\"`\n\tPWAManifest string `json:\"pwa_manifest\" mapstructure:\"pwamanifest\"`\n\tDebugMode   bool   `json:\"debug_mode\" mapstructure:\"debugmode\"`\n}\n\ntype siteState struct {\n\tCloseRegister bool    `json:\"close_register\" mapstructure:\"closeregister\"`\n\tCloseRelay    bool    `json:\"close_relay\" mapstructure:\"closerelay\"`\n\tRelayPlan     bool    `json:\"relay_plan\" mapstructure:\"relayplan\"`\n\tQuota         float64 `json:\"quota\" mapstructure:\"quota\"`\n\tBuyLink       string  `json:\"buy_link\" mapstructure:\"buylink\"`\n\tAnnouncement  string  `json:\"announcement\" mapstructure:\"announcement\"`\n\tContact       string  `json:\"contact\" mapstructure:\"contact\"`\n\tFooter        string  `json:\"footer\" mapstructure:\"footer\"`\n\tAuthFooter    bool    `json:\"auth_footer\" mapstructure:\"authfooter\"`\n}\n\ntype whiteList struct {\n\tEnabled   bool     `json:\"enabled\" mapstructure:\"enabled\"`\n\tCustom    string   `json:\"custom\" mapstructure:\"custom\"`\n\tWhiteList []string `json:\"white_list\" mapstructure:\"whitelist\"`\n}\n\ntype mailState struct {\n\tHost      string    `json:\"host\" mapstructure:\"host\"`\n\tProtocol  bool      `json:\"protocol\" mapstructure:\"protocol\"`\n\tPort      int       `json:\"port\" mapstructure:\"port\"`\n\tUsername  string    `json:\"username\" mapstructure:\"username\"`\n\tPassword  string    `json:\"password\" mapstructure:\"password\"`\n\tFrom      string    `json:\"from\" mapstructure:\"from\"`\n\tWhiteList whiteList `json:\"white_list\" mapstructure:\"whitelist\"`\n}\n\ntype SearchState struct {\n\tEndpoint   string   `json:\"endpoint\" mapstructure:\"endpoint\"`\n\tCrop       bool     `json:\"crop\" mapstructure:\"crop\"`\n\tCropLen    int      `json:\"crop_len\" mapstructure:\"croplen\"`\n\tEngines    []string `json:\"engines\" mapstructure:\"engines\"`\n\tImageProxy bool     `json:\"image_proxy\" mapstructure:\"imageproxy\"`\n\tSafeSearch int      `json:\"safe_search\" mapstructure:\"safesearch\"`\n}\n\ntype commonState struct {\n\tArticle     []string `json:\"article\" mapstructure:\"article\"`\n\tGeneration  []string `json:\"generation\" mapstructure:\"generation\"`\n\tCache       []string `json:\"cache\" mapstructure:\"cache\"`\n\tExpire      int64    `json:\"expire\" mapstructure:\"expire\"`\n\tSize        int64    `json:\"size\" mapstructure:\"size\"`\n\tImageStore  bool     `json:\"image_store\" mapstructure:\"imagestore\"`\n\tPromptStore bool     `json:\"prompt_store\" mapstructure:\"promptstore\"`\n}\n\ntype SystemConfig struct {\n\tGeneral generalState `json:\"general\" mapstructure:\"general\"`\n\tSite    siteState    `json:\"site\" mapstructure:\"site\"`\n\tMail    mailState    `json:\"mail\" mapstructure:\"mail\"`\n\tSearch  SearchState  `json:\"search\" mapstructure:\"search\"`\n\tCommon  commonState  `json:\"common\" mapstructure:\"common\"`\n}\n\nfunc NewSystemConfig() *SystemConfig {\n\tconf := &SystemConfig{}\n\tif err := viper.UnmarshalKey(\"system\", conf); err != nil {\n\t\tpanic(err)\n\t}\n\n\tconf.Load()\n\treturn conf\n}\n\nfunc (c *SystemConfig) Load() {\n\tglobals.NotifyUrl = c.GetBackend()\n\tglobals.DebugMode = c.General.DebugMode\n\n\tglobals.CloseRegistration = c.Site.CloseRegister\n\tglobals.CloseRelay = c.Site.CloseRelay\n\n\tglobals.ArticlePermissionGroup = c.Common.Article\n\tglobals.GenerationPermissionGroup = c.Common.Generation\n\tglobals.CacheAcceptedModels = c.Common.Cache\n\n\tglobals.CacheAcceptedExpire = c.GetCacheAcceptedExpire()\n\tglobals.CacheAcceptedSize = c.GetCacheAcceptedSize()\n\tglobals.AcceptImageStore = c.AcceptImageStore()\n\n\tglobals.AcceptPromptStore = c.Common.PromptStore\n\n\tif c.General.PWAManifest == \"\" {\n\t\tc.General.PWAManifest = utils.ReadPWAManifest()\n\t}\n\n\tglobals.SearchEndpoint = c.Search.Endpoint\n\tglobals.SearchCrop = c.Search.Crop\n\tglobals.SearchCropLength = c.GetSearchCropLength()\n\tglobals.SearchEngines = c.GetSearchEngines()\n\tglobals.SearchImageProxy = c.GetImageProxy()\n\tglobals.SearchSafeSearch = c.Search.SafeSearch\n}\n\nfunc (c *SystemConfig) SaveConfig() error {\n\treturn utils.SaveConfig(\"system\", c)\n}\n\nfunc (c *SystemConfig) AsInfo() ApiInfo {\n\treturn ApiInfo{\n\t\tTitle:        c.General.Title,\n\t\tLogo:         c.General.Logo,\n\t\tFile:         c.General.File,\n\t\tDocs:         c.General.Docs,\n\t\tAnnouncement: c.Site.Announcement,\n\t\tContact:      c.Site.Contact,\n\t\tFooter:       c.Site.Footer,\n\t\tAuthFooter:   c.Site.AuthFooter,\n\t\tBuyLink:      c.Site.BuyLink,\n\t\tMail:         c.IsMailValid(),\n\t\tArticle:      c.Common.Article,\n\t\tGeneration:   c.Common.Generation,\n\t\tRelayPlan:    c.Site.RelayPlan,\n\t}\n}\n\nfunc (c *SystemConfig) UpdateConfig(data *SystemConfig) error {\n\tc.General = data.General\n\tc.Site = data.Site\n\tc.Mail = data.Mail\n\tc.Search = data.Search\n\tc.Common = data.Common\n\n\tutils.ApplySeo(c.General.Title, c.General.Logo)\n\tutils.ApplyPWAManifest(c.General.PWAManifest)\n\n\treturn c.SaveConfig()\n}\n\nfunc (c *SystemConfig) GetInitialQuota() float64 {\n\treturn c.Site.Quota\n}\n\nfunc (c *SystemConfig) GetBackend() string {\n\treturn strings.TrimSuffix(c.General.Backend, \"/\")\n}\n\nfunc (c *SystemConfig) GetMail() *utils.SmtpPoster {\n\treturn utils.NewSmtpPoster(\n\t\tc.Mail.Host,\n\t\tc.Mail.Protocol,\n\t\tc.Mail.Port,\n\t\tc.Mail.Username,\n\t\tc.Mail.Password,\n\t\tc.Mail.From,\n\t)\n}\n\nfunc (c *SystemConfig) IsMailValid() bool {\n\treturn c.GetMail().Valid()\n}\n\nfunc (c *SystemConfig) GetMailSuffix() []string {\n\tif c.Mail.WhiteList.Enabled {\n\t\treturn c.Mail.WhiteList.WhiteList\n\t}\n\n\treturn []string{}\n}\n\nfunc (c *SystemConfig) IsValidMailSuffix(suffix string) bool {\n\tif c.Mail.WhiteList.Enabled {\n\t\treturn utils.Contains(suffix, c.Mail.WhiteList.WhiteList)\n\t}\n\n\treturn true\n}\n\nfunc (c *SystemConfig) IsValidMail(email string) error {\n\tsegment := strings.Split(email, \"@\")\n\tif len(segment) != 2 {\n\t\treturn fmt.Errorf(\"invalid email format\")\n\t}\n\n\tif suffix := segment[1]; !c.IsValidMailSuffix(suffix) {\n\t\treturn fmt.Errorf(\"email suffix @%s is not allowed to register\", suffix)\n\t}\n\n\treturn nil\n}\n\nfunc (c *SystemConfig) SendVerifyMail(email string, code string) error {\n\ttype Temp struct {\n\t\tTitle string `json:\"title\"`\n\t\tLogo  string `json:\"logo\"`\n\t\tCode  string `json:\"code\"`\n\t}\n\n\treturn c.GetMail().RenderMail(\n\t\t\"code.html\",\n\t\tTemp{Title: c.GetAppName(), Logo: c.GetAppLogo(), Code: code},\n\t\temail,\n\t\tfmt.Sprintf(\"%s | OTP Verification\", c.GetAppName()),\n\t)\n}\n\nfunc (c *SystemConfig) GetSearchCropLength() int {\n\tif c.Search.CropLen <= 0 {\n\t\treturn 1000\n\t}\n\n\treturn c.Search.CropLen\n}\n\nfunc (c *SystemConfig) GetSearchEngines() string {\n\treturn strings.Join(c.Search.Engines, \",\")\n}\n\nfunc (c *SystemConfig) GetImageProxy() string {\n\t// return \"True\" or \"False\"\n\tif c.Search.ImageProxy {\n\t\treturn \"True\"\n\t}\n\n\treturn \"False\"\n}\n\nfunc (c *SystemConfig) GetAppName() string {\n\ttitle := strings.TrimSpace(c.General.Title)\n\tif len(title) == 0 {\n\t\treturn \"CoAI.Dev\"\n\t}\n\n\treturn title\n}\n\nfunc (c *SystemConfig) GetAppLogo() string {\n\tlogo := strings.TrimSpace(c.General.Logo)\n\tif len(logo) == 0 {\n\t\treturn \"https://chatnio.net/favicon.ico\"\n\t}\n\n\treturn logo\n}\n\nfunc (c *SystemConfig) GetCacheAcceptedModels() []string {\n\treturn c.Common.Cache\n}\n\nfunc (c *SystemConfig) GetCacheAcceptedExpire() int64 {\n\tif c.Common.Expire <= 0 {\n\t\t// default 1 hour\n\t\treturn 3600\n\t}\n\n\treturn c.Common.Expire\n}\n\nfunc (c *SystemConfig) GetCacheAcceptedSize() int64 {\n\tif c.Common.Size < 1 {\n\t\treturn 1\n\t}\n\n\treturn c.Common.Size\n}\n\nfunc (c *SystemConfig) AcceptImageStore() bool {\n\t// if notify url is empty, then image store is not allowed\n\tif len(strings.TrimSpace(globals.NotifyUrl)) == 0 {\n\t\treturn false\n\t}\n\n\treturn c.Common.ImageStore\n}\n\nfunc (c *SystemConfig) SupportRelayPlan() bool {\n\treturn c.Site.RelayPlan\n}\n"
  },
  {
    "path": "channel/ticker.go",
    "content": "package channel\n\nimport \"chat/utils\"\n\nfunc NewTicker(seq Sequence, group string) *Ticker {\n\tstack := make(Sequence, 0)\n\tfor _, channel := range seq {\n\t\tif channel.IsHitGroup(group) {\n\t\t\tstack = append(stack, channel)\n\t\t}\n\t}\n\n\tstack.Sort()\n\n\treturn &Ticker{\n\t\tSequence: stack,\n\t}\n}\n\nfunc (t *Ticker) GetChannelByPriority(priority int) *Channel {\n\tvar stack Sequence\n\n\tfor idx, channel := range t.Sequence {\n\t\tif channel.GetPriority() == priority {\n\t\t\t// get if the next channel has the same priority\n\t\t\tif idx+1 < len(t.Sequence) && t.Sequence[idx+1].GetPriority() == priority {\n\t\t\t\tstack = append(stack, channel)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif len(stack) == 0 {\n\t\t\t\treturn channel\n\t\t\t}\n\n\t\t\t// stack is not empty\n\t\t\tstack = append(stack, channel)\n\n\t\t\t// sort by weight and break the loop\n\t\t\tif idx+1 >= len(t.Sequence) || t.Sequence[idx+1].GetPriority() != priority {\n\t\t\t\tstack.Sort()\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tweight := utils.Each(stack, func(channel *Channel) int {\n\t\treturn channel.GetWeight()\n\t})\n\ttotal := utils.Sum(weight)\n\n\t// get random number\n\tcursor := utils.Intn(total)\n\n\t// get channel by weight\n\tfor _, channel := range stack {\n\t\tcursor -= channel.GetWeight()\n\t\tif cursor < 0 {\n\t\t\treturn channel\n\t\t}\n\t}\n\n\treturn stack[0]\n}\n\nfunc (t *Ticker) Next() *Channel {\n\tif t.Cursor >= len(t.Sequence) {\n\t\t// out of sequence\n\t\treturn nil\n\t}\n\n\tpriority := t.Sequence[t.Cursor].GetPriority()\n\tchannel := t.GetChannelByPriority(priority)\n\tt.SkipPriority(priority)\n\n\treturn channel\n}\n\nfunc (t *Ticker) SkipPriority(priority int) {\n\tfor idx, channel := range t.Sequence {\n\t\tif channel.GetPriority() == priority {\n\t\t\t// get if the next channel does not have the same priority or out of sequence\n\t\t\tif idx+1 >= len(t.Sequence) || t.Sequence[idx+1].GetPriority() != priority {\n\t\t\t\tt.Cursor = idx + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *Ticker) IsDone() bool {\n\treturn t.Cursor >= len(t.Sequence)\n}\n\nfunc (t *Ticker) IsEmpty() bool {\n\treturn len(t.Sequence) == 0\n}\n"
  },
  {
    "path": "channel/types.go",
    "content": "package channel\n\nimport (\n\t\"chat/globals\"\n)\n\ntype Channel struct {\n\tId            int                 `json:\"id\" mapstructure:\"id\"`\n\tName          string              `json:\"name\" mapstructure:\"name\"`\n\tType          string              `json:\"type\" mapstructure:\"type\"`\n\tPriority      int                 `json:\"priority\" mapstructure:\"priority\"`\n\tWeight        int                 `json:\"weight\" mapstructure:\"weight\"`\n\tModels        []string            `json:\"models\" mapstructure:\"models\"`\n\tRetry         int                 `json:\"retry\" mapstructure:\"retry\"`\n\tSecret        string              `json:\"secret\" mapstructure:\"secret\"`\n\tEndpoint      string              `json:\"endpoint\" mapstructure:\"endpoint\"`\n\tMapper        string              `json:\"mapper\" mapstructure:\"mapper\"`\n\tState         bool                `json:\"state\" mapstructure:\"state\"`\n\tGroup         []string            `json:\"group\" mapstructure:\"group\"`\n\tProxy         globals.ProxyConfig `json:\"proxy\" mapstructure:\"proxy\"`\n\tReflect       *map[string]string  `json:\"-\"`\n\tHitModels     *[]string           `json:\"-\"`\n\tExcludeModels *[]string           `json:\"-\"`\n\tCurrentSecret *string             `json:\"-\"`\n}\n\ntype Sequence []*Channel\n\ntype Manager struct {\n\tSequence          Sequence            `json:\"sequence\"`\n\tPreflightSequence map[string]Sequence `json:\"preflight_sequence\"`\n\tModels            []string            `json:\"models\"`\n}\n\ntype Ticker struct {\n\tSequence Sequence `json:\"sequence\"`\n\tCursor   int      `json:\"cursor\"`\n}\n\ntype Charge struct {\n\tId        int      `json:\"id\" mapstructure:\"id\"`\n\tType      string   `json:\"type\" mapstructure:\"type\"`\n\tModels    []string `json:\"models\" mapstructure:\"models\"`\n\tInput     float32  `json:\"input\" mapstructure:\"input\"`\n\tOutput    float32  `json:\"output\" mapstructure:\"output\"`\n\tAnonymous bool     `json:\"anonymous\" mapstructure:\"anonymous\"`\n\tUnset     bool     `json:\"-\" mapstructure:\"-\"`\n}\n\ntype ChargeSequence []*Charge\n\ntype ChargeManager struct {\n\tSequence         ChargeSequence     `json:\"sequence\"`\n\tModels           map[string]*Charge `json:\"models\"`\n\tNonBillingModels []string           `json:\"non_billing_models\"`\n}\n"
  },
  {
    "path": "channel/worker.go",
    "content": "package channel\n\nimport (\n\t\"chat/adapter\"\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc NewChatRequest(group string, props *adaptercommon.ChatProps, hook globals.Hook) error {\n\tticker := ConduitInstance.GetTicker(props.OriginalModel, group)\n\tif ticker == nil || ticker.IsEmpty() {\n\t\treturn fmt.Errorf(\"cannot find channel for model %s\", props.OriginalModel)\n\t}\n\n\tvar err error\n\tfor !ticker.IsDone() {\n\t\tif channel := ticker.Next(); channel != nil {\n\t\t\tprops.MaxRetries = utils.ToPtr(channel.GetRetry())\n\t\t\tif err = adapter.NewChatRequest(channel, props, hook); adapter.IsSkipError(err) {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tglobals.Warn(fmt.Sprintf(\"[channel] caught error %s for model %s at channel %s\", err.Error(), props.OriginalModel, channel.GetName()))\n\t\t}\n\t}\n\n\tglobals.Info(fmt.Sprintf(\"[channel] channels are exhausted for model %s\", props.OriginalModel))\n\n\tif err == nil {\n\t\terr = fmt.Errorf(\"channels are exhausted for model %s\", props.OriginalModel)\n\t}\n\n\treturn err\n}\n\nfunc PreflightCache(cache *redis.Client, model string, hash string, buffer *utils.Buffer, hook globals.Hook) (int64, bool, error) {\n\tif !utils.Contains(model, globals.CacheAcceptedModels) {\n\t\treturn 0, false, nil\n\t}\n\n\tidx := utils.Intn64(globals.CacheAcceptedSize)\n\tkey := fmt.Sprintf(\"chat-cache:%d:%s\", idx, hash)\n\n\traw, err := cache.Get(cache.Context(), key).Result()\n\tif err != nil {\n\t\treturn idx, false, nil\n\t}\n\n\tbuf, err := utils.UnmarshalString[utils.Buffer](raw)\n\tif err != nil {\n\t\treturn idx, false, nil\n\t}\n\n\tdata := buf.Read()\n\tif data == \"\" {\n\t\treturn idx, false, nil\n\t}\n\n\tbuffer.SetInputTokens(buf.CountInputToken())\n\tbuffer.SetToolCalls(buf.GetToolCalls())\n\tbuffer.SetFunctionCall(buf.GetFunctionCall())\n\n\treturn idx, true, hook(&globals.Chunk{\n\t\tContent:      data,\n\t\tFunctionCall: buf.GetFunctionCall(),\n\t\tToolCall:     buf.GetToolCalls(),\n\t})\n}\n\nfunc StoreCache(cache *redis.Client, hash string, index int64, buffer *utils.Buffer) {\n\tkey := fmt.Sprintf(\"chat-cache:%d:%s\", index, hash)\n\traw := utils.Marshal(buffer)\n\texpire := time.Duration(globals.CacheAcceptedExpire) * time.Second\n\n\tcache.Set(cache.Context(), key, raw, expire)\n}\n\nfunc NewChatRequestWithCache(cache *redis.Client, buffer *utils.Buffer, group string, props *adaptercommon.ChatProps, hook globals.Hook) (bool, error) {\n\thash := utils.Md5Encrypt(utils.Marshal(props))\n\n\tif len(props.OriginalModel) == 0 {\n\t\tprops.OriginalModel = props.Model\n\t}\n\n\tidx, hit, err := PreflightCache(cache, props.OriginalModel, hash, buffer, hook)\n\tif hit {\n\t\treturn true, err\n\t}\n\n\tif err = NewChatRequest(group, props, hook); err != nil {\n\t\treturn false, err\n\t}\n\n\tStoreCache(cache, hash, idx, buffer)\n\treturn false, nil\n}\n\nfunc NewVideoRequestWithCache(_ *redis.Client, buffer *utils.Buffer, group string, props *adaptercommon.VideoProps, hook globals.Hook) (bool, error) {\n\t// TODO: Implement video request with cache\n\n\tif len(props.OriginalModel) == 0 {\n\t\tprops.OriginalModel = props.Model\n\t}\n\n\tticker := ConduitInstance.GetTicker(props.OriginalModel, group)\n\tif ticker == nil || ticker.IsEmpty() {\n\t\treturn false, fmt.Errorf(\"cannot find channel for model %s\", props.OriginalModel)\n\t}\n\n\tvar err error\n\tvar times int = 0\n\tfor !ticker.IsDone() {\n\t\tif channel := ticker.Next(); channel != nil {\n\t\t\ttimes++\n\t\t\tprops.MaxRetries = utils.ToPtr(channel.GetRetry())\n\t\t\tif err = adapter.NewVideoRequest(channel, props, hook); adapter.IsSkipError(err) {\n\t\t\t\tglobals.Debug(fmt.Sprintf(\n\t\t\t\t\t\"[channel] calling video request success (channel: %s, user: %s, model: %s, reflected-model: %s, secret: %s)\",\n\t\t\t\t\tchannel.GetName(), props.User, props.OriginalModel, props.Model,\n\t\t\t\t\tutils.HideSecret(channel.GetCurrentSecretValue(), 16),\n\t\t\t\t))\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tglobals.Warn(fmt.Sprintf(\n\t\t\t\t\"[channel] caught error: %s (channel: %s, user: %s, model: %s, reflected-model: %s, secret: %s)\",\n\t\t\t\terr.Error(), channel.GetName(), props.User, props.OriginalModel, props.Model,\n\t\t\t\tutils.HideSecret(channel.GetCurrentSecretValue(), 16),\n\t\t\t))\n\t\t}\n\t}\n\n\tif err == nil {\n\t\terr = fmt.Errorf(\"channels are all used up (model: %s)\", props.OriginalModel)\n\t}\n\n\tif adapter.IsAvailableError(err) {\n\t\tglobals.Info(fmt.Sprintf(\"[channel] request failed: %s (model: %s, user: %s, attempts: %d, all channels are used up)\", err.Error(), props.OriginalModel, props.User, times))\n\t}\n\n\treturn false, err\n}\n"
  },
  {
    "path": "cli/admin.go",
    "content": "package cli\n\nimport (\n\t\"chat/admin\"\n\t\"chat/connection\"\n\t\"errors\"\n)\n\nfunc UpdateRootCommand(args []string) {\n\tdb := connection.ConnectDatabase()\n\tcache := connection.ConnectRedis()\n\n\tif len(args) == 0 {\n\t\toutputError(errors.New(\"invalid arguments, please provide a new root password\"))\n\t\treturn\n\t}\n\n\tpassword := args[0]\n\tif err := admin.UpdateRootPassword(db, cache, password); err != nil {\n\t\toutputError(err)\n\t\treturn\n\t}\n\n\toutputInfo(\"root\", \"root password updated\")\n}\n"
  },
  {
    "path": "cli/exec.go",
    "content": "package cli\n\nfunc Run() bool {\n\targs := GetArgs()\n\tif len(args) == 0 {\n\t\treturn false\n\t}\n\n\tparam := args[1:]\n\tswitch args[0] {\n\tcase \"help\":\n\t\tHelp()\n\tcase \"invite\":\n\t\tCreateInvitationCommand(param)\n\tcase \"token\":\n\t\tCreateTokenCommand(param)\n\tcase \"root\":\n\t\tUpdateRootCommand(param)\n\tdefault:\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "cli/help.go",
    "content": "package cli\n\nimport \"fmt\"\n\nvar Prompt = `\nCommands:\n\t- help\n\t- invite <type> <num> <quota>\n\t- token <user-id>\n\t- root <password>\n`\n\nfunc Help() {\n\tfmt.Println(fmt.Sprintf(\"%s\", Prompt))\n}\n"
  },
  {
    "path": "cli/invite.go",
    "content": "package cli\n\nimport (\n\t\"chat/auth\"\n\t\"chat/connection\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc CreateInvitationCommand(args []string) {\n\tdb := connection.ConnectDatabase()\n\n\tvar (\n\t\tt     = GetArgString(args, 0)\n\t\tnum   = GetArgInt(args, 1)\n\t\tquota = GetArgFloat32(args, 2)\n\t)\n\n\tresp, err := auth.GenerateInvitations(db, num, quota, t)\n\tif err != nil {\n\t\toutputError(err)\n\t\treturn\n\t}\n\n\toutputInfo(\"invite\", fmt.Sprintf(\"%d invitation codes generated\", len(resp)))\n\tfmt.Println(strings.Join(resp, \"\\n\"))\n}\n"
  },
  {
    "path": "cli/parser.go",
    "content": "package cli\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strconv\"\n)\n\nfunc GetArgs() []string {\n\treturn os.Args[1:]\n}\n\nfunc GetArg(args []string, idx int) string {\n\tif len(args) <= idx {\n\t\tlog.Fatalln(fmt.Sprintf(\"not enough arguments: %d\", idx))\n\t}\n\treturn args[idx]\n}\n\nfunc GetArgInt(args []string, idx int) int {\n\ti, err := strconv.Atoi(GetArg(args, idx))\n\tif err != nil {\n\t\tlog.Fatalln(fmt.Sprintf(\"invalid argument: %s\", err.Error()))\n\t}\n\treturn i\n}\n\nfunc GetArgFloat(args []string, idx int, bitSize int) float64 {\n\tf, err := strconv.ParseFloat(GetArg(args, idx), bitSize)\n\tif err != nil {\n\t\tlog.Fatalln(fmt.Sprintf(\"invalid argument: %s\", err.Error()))\n\t}\n\treturn f\n}\n\nfunc GetArgFloat32(args []string, idx int) float32 {\n\treturn float32(GetArgFloat(args, idx, 32))\n}\n\nfunc GetArgFloat64(args []string, idx int) float64 {\n\treturn GetArgFloat(args, idx, 64)\n}\n\nfunc GetArgBool(args []string, idx int) bool {\n\tb, err := strconv.ParseBool(GetArg(args, idx))\n\tif err != nil {\n\t\tlog.Fatalln(fmt.Sprintf(\"invalid argument: %s\", err.Error()))\n\t}\n\treturn b\n}\n\nfunc GetArgInt64(args []string, idx int) int64 {\n\ti, err := strconv.ParseInt(GetArg(args, idx), 10, 64)\n\tif err != nil {\n\t\tlog.Fatalln(fmt.Sprintf(\"invalid argument: %s\", err.Error()))\n\t}\n\treturn i\n}\n\nfunc GetArgString(args []string, idx int) string {\n\treturn GetArg(args, idx)\n}\n\nfunc outputError(err error) {\n\tfmt.Println(fmt.Sprintf(\"\\033[31m[cli] error: %s\\033[0m\", err.Error()))\n}\n\nfunc outputInfo(t, msg string) {\n\tfmt.Println(fmt.Sprintf(\"[cli] %s: %s\", t, msg))\n}\n"
  },
  {
    "path": "cli/token.go",
    "content": "package cli\n\nimport (\n\t\"chat/auth\"\n\t\"chat/connection\"\n\t\"fmt\"\n\t\"strconv\"\n)\n\nfunc CreateTokenCommand(args []string) {\n\tdb := connection.ConnectDatabase()\n\tid, _ := strconv.Atoi(args[0])\n\n\tuser := auth.GetUserById(db, int64(id))\n\ttoken, err := user.GenerateTokenSafe(db)\n\tif err != nil {\n\t\toutputError(err)\n\t\treturn\n\t}\n\n\toutputInfo(\"token\", \"token generated\")\n\tfmt.Println(token)\n}\n"
  },
  {
    "path": "config.example.yaml",
    "content": "mysql:\n  db: chatnio\n  host: localhost\n  password: chatnio123456\n  port: 3306\n  user: root\n  tls: false\n\nredis:\n  host: localhost\n  port: 6379\n  db: 0\n  password: \"\"\n\n\nsecret: secret\nserve_static: true\n\nserver:\n  port: 8094\nsystem:\n  general:\n    backend: \"\"\n  mail:\n    host: \"\"\n    port: 465\n    username: \"\"\n    password: \"\"\n    from: \"\"\n  search:\n    endpoint: https://duckduckgo-api.vercel.app\n    query: 5\n"
  },
  {
    "path": "connection/cache.go",
    "content": "package connection\n\nimport (\n\t\"chat/globals\"\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/spf13/viper\"\n)\n\nvar Cache *redis.Client\n\nfunc InitRedisSafe() *redis.Client {\n\tConnectRedis()\n\n\t// using Cache as a global variable to point to the latest redis connection\n\tRedisWorker(Cache)\n\treturn Cache\n}\n\nfunc ConnectRedis() *redis.Client {\n\t// connect to redis\n\tCache = redis.NewClient(&redis.Options{\n\t\tAddr:     fmt.Sprintf(\"%s:%d\", viper.GetString(\"redis.host\"), viper.GetInt(\"redis.port\")),\n\t\tPassword: viper.GetString(\"redis.password\"),\n\t\tDB:       viper.GetInt(\"redis.db\"),\n\t})\n\n\tif err := pingRedis(Cache); err != nil {\n\t\tglobals.Warn(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"[connection] failed to connect to redis host: %s (message: %s), will retry in 5 seconds\",\n\t\t\t\tviper.GetString(\"redis.host\"),\n\t\t\t\terr.Error(),\n\t\t\t),\n\t\t)\n\t} else {\n\t\tglobals.Debug(fmt.Sprintf(\"[connection] connected to redis (host: %s)\", viper.GetString(\"redis.host\")))\n\t}\n\n\tif viper.GetBool(\"debug\") {\n\t\tCache.FlushAll(context.Background())\n\t\tglobals.Debug(fmt.Sprintf(\"[connection] flush redis cache (host: %s)\", viper.GetString(\"redis.host\")))\n\t}\n\treturn Cache\n}\n"
  },
  {
    "path": "connection/database.go",
    "content": "package connection\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"crypto/tls\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/spf13/viper\"\n)\n\nvar DB *sql.DB\n\nfunc InitMySQLSafe() *sql.DB {\n\tConnectDatabase()\n\n\t// using DB as a global variable to point to the latest db connection\n\tMysqlWorker(DB)\n\treturn DB\n}\n\nfunc getConn() *sql.DB {\n\tif viper.GetString(\"mysql.host\") == \"\" {\n\t\tglobals.SqliteEngine = true\n\t\tglobals.Warn(\"[connection] mysql host is not set, using sqlite (~/db/chatnio.db)\")\n\t\tdb, err := sql.Open(\"sqlite3\", utils.FileSafe(\"./db/chatnio.db\"))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\treturn db\n\t}\n\n\tmysqlUrl := fmt.Sprintf(\n\t\t\"%s:%s@tcp(%s:%d)/%s\",\n\t\tviper.GetString(\"mysql.user\"),\n\t\tviper.GetString(\"mysql.password\"),\n\t\tviper.GetString(\"mysql.host\"),\n\t\tviper.GetInt(\"mysql.port\"),\n\t\tutils.GetStringConfs(\"mysql.database\", \"mysql.db\"),\n\t)\n\tif viper.GetBool(\"mysql.tls\") {\n\t\tmysql.RegisterTLSConfig(\"tls\", &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tServerName: viper.GetString(\"mysql.host\"),\n\t\t})\n\n\t\tmysqlUrl += \"?tls=tls\"\n\t}\n\n\t// connect to MySQL\n\tdb, err := sql.Open(\"mysql\", mysqlUrl)\n\n\tif pingErr := db.Ping(); err != nil || pingErr != nil {\n\t\terrMsg := utils.Multi[string](err != nil, utils.GetError(err), utils.GetError(pingErr)) // err.Error() may contain nil pointer\n\t\tglobals.Warn(\n\t\t\tfmt.Sprintf(\"[connection] failed to connect to mysql server: %s (message: %s), will retry in 5 seconds\",\n\t\t\t\tviper.GetString(\"mysql.host\"), errMsg,\n\t\t\t),\n\t\t)\n\n\t\tutils.Sleep(5000)\n\t\tdb.Close()\n\n\t\treturn getConn()\n\t}\n\n\tglobals.Debug(fmt.Sprintf(\"[connection] connected to mysql server (host: %s)\", viper.GetString(\"mysql.host\")))\n\treturn db\n}\n\nfunc ConnectDatabase() *sql.DB {\n\tdb := getConn()\n\n\tdb.SetMaxOpenConns(512)\n\tdb.SetMaxIdleConns(64)\n\n\tCreateUserTable(db)\n\tCreateConversationTable(db)\n\tCreateMaskTable(db)\n\tCreateSharingTable(db)\n\tCreatePackageTable(db)\n\tCreateQuotaTable(db)\n\tCreateSubscriptionTable(db)\n\tCreateApiKeyTable(db)\n\tCreateInvitationTable(db)\n\tCreateRedeemTable(db)\n\tCreateBroadcastTable(db)\n\n\tif err := doMigration(db); err != nil {\n\t\tfmt.Println(fmt.Sprintf(\"migration error: %s\", err))\n\t}\n\n\tDB = db\n\n\treturn db\n}\n\nfunc InitRootUser(db *sql.DB) {\n\t// create root user if totally empty\n\tvar count int\n\terr := globals.QueryRowDb(db, \"SELECT COUNT(*) FROM auth\").Scan(&count)\n\tif err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to query user count: %s\", err.Error()))\n\t\treturn\n\t}\n\n\tif count == 0 {\n\t\tglobals.Debug(\"[service] no user found, creating root user (username: root, password: chatnio123456, email: root@example.com)\")\n\t\t_, err := globals.ExecDb(db, `\n\t\t\tINSERT INTO auth (username, password, email, is_admin, bind_id, token)\n\t\t\tVALUES (?, ?, ?, ?, ?, ?)\n\t\t`, \"root\", utils.Sha2Encrypt(\"chatnio123456\"), \"root@example.com\", true, 0, \"root\")\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to create root user: %s\", err.Error()))\n\t\t}\n\t} else {\n\t\tglobals.Debug(fmt.Sprintf(\"[service] %d user(s) found, skip creating root user\", count))\n\t}\n}\n\nfunc CreateUserTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS auth (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  bind_id INT UNIQUE,\n\t\t  username VARCHAR(24) UNIQUE,\n\t\t  token VARCHAR(255) NOT NULL,\n\t\t  email VARCHAR(255) UNIQUE,\n\t\t  password VARCHAR(64) NOT NULL,\n\t\t  is_admin BOOLEAN DEFAULT FALSE,\n\t\t  is_banned BOOLEAN DEFAULT FALSE\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\n\tInitRootUser(db)\n}\n\nfunc CreatePackageTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS package (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  user_id INT,\n\t\t  type VARCHAR(255),\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id),\n\t\t  UNIQUE KEY (user_id, type)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateQuotaTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS quota (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  user_id INT UNIQUE,\n\t\t  quota DECIMAL(24, 6),\n\t\t  used DECIMAL(24, 6),\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateConversationTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS conversation (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  user_id INT,\n\t\t  conversation_id INT,\n\t\t  conversation_name VARCHAR(255),\n\t\t  data MEDIUMTEXT,\n\t\t  model VARCHAR(255) NOT NULL DEFAULT 'gpt-3.5-turbo-0613',\n\t\t  task_id VARCHAR(255) NULL,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  UNIQUE KEY (user_id, conversation_id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateMaskTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS mask (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  user_id INT,\n\t\t  avatar VARCHAR(255),\n\t\t  name VARCHAR(255),\n\t\t  description TEXT,\n\t\t  context TEXT,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateSharingTable(db *sql.DB) {\n\t// refs is an array of message id, separated by comma (-1 means all messages)\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS sharing (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  hash CHAR(32) UNIQUE,\n\t\t  user_id INT,\n\t\t  conversation_id INT,\n\t\t  refs TEXT,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateSubscriptionTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS subscription (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  level INT DEFAULT 1,\n\t\t  user_id INT UNIQUE,\n\t\t  expired_at DATETIME,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  total_month INT DEFAULT 0,\n\t\t  enterprise BOOLEAN DEFAULT FALSE,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateApiKeyTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS apikey (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  user_id INT UNIQUE,\n\t\t  api_key VARCHAR(255) UNIQUE,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (user_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateInvitationTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS invitation (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  code VARCHAR(255) UNIQUE,\n\t\t  quota DECIMAL(16, 4),\n\t\t  type VARCHAR(255),\n\t\t  used BOOLEAN DEFAULT FALSE,\n\t\t  used_id INT,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  UNIQUE KEY (used_id, type),\n\t\t  FOREIGN KEY (used_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateRedeemTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS redeem (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  code VARCHAR(255) UNIQUE,\n\t\t  quota DECIMAL(16, 4),\n\t\t  used BOOLEAN DEFAULT FALSE,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n\nfunc CreateBroadcastTable(db *sql.DB) {\n\t_, err := globals.ExecDb(db, `\n\t\tCREATE TABLE IF NOT EXISTS broadcast (\n\t\t  id INT PRIMARY KEY AUTO_INCREMENT,\n\t\t  poster_id INT,\n\t\t  content TEXT,\n\t\t  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n\t\t  FOREIGN KEY (poster_id) REFERENCES auth(id)\n\t\t);\n\t`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n}\n"
  },
  {
    "path": "connection/db_migration.go",
    "content": "package connection\n\nimport (\n\t\"chat/globals\"\n\t\"database/sql\"\n\t\"strings\"\n)\n\nfunc validSqlError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tcontent := err.Error()\n\n\t// Error 1060: Duplicate column name\n\t// Error 1050: Table already exists\n\n\treturn !(strings.Contains(content, \"Error 1060\") || strings.Contains(content, \"Error 1050\"))\n}\n\nfunc checkSqlError(_ sql.Result, err error) error {\n\tif validSqlError(err) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc execSql(db *sql.DB, sql string, args ...interface{}) error {\n\treturn checkSqlError(globals.ExecDb(db, sql, args...))\n}\n\nfunc doMigration(db *sql.DB) error {\n\tif globals.SqliteEngine {\n\t\treturn doSqliteMigration(db)\n\t}\n\n\t// v3.10 migration\n\n\t// update `quota`, `used` field in `quota` table\n\t// migrate `DECIMAL(16, 4)` to `DECIMAL(24, 6)`\n\n\tif err := execSql(db, `\n\t\tALTER TABLE quota\n\t\tMODIFY COLUMN quota DECIMAL(24, 6),\n\t\tMODIFY COLUMN used DECIMAL(24, 6);\n\t`); err != nil {\n\t\treturn err\n\t}\n\n\t// add new field `is_banned` in `auth` table\n\tif err := execSql(db, `\n\t\tALTER TABLE auth\n\t\tADD COLUMN is_banned BOOLEAN DEFAULT FALSE;\n\t`); err != nil {\n\t\treturn err\n\t}\n\n\t// add new field `task_id` in `conversation` table to store task id (e.g., video job id)\n\tif err := execSql(db, `\n\t\tALTER TABLE conversation\n\t\tADD COLUMN task_id VARCHAR(255) NULL;\n\t`); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc doSqliteMigration(db *sql.DB) error {\n\t// v3.10 added sqlite support, no migration needed before this version\n\n\t// v4 migration\n\t// add new field `task_id` in `conversation` table to store task id (e.g., video job id)\n\tif err := execSql(db, `\n\t\tALTER TABLE conversation\n\t\tADD COLUMN task_id VARCHAR(255) NULL;\n\t`); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "connection/worker.go",
    "content": "package connection\n\nimport (\n\t\"database/sql\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"time\"\n)\n\nvar tick time.Duration = 5 * time.Second // tick every 5 second\n\nfunc MysqlWorker(db *sql.DB) {\n\tgo func() {\n\t\tfor {\n\t\t\tif db == nil || db.Ping() != nil {\n\t\t\t\tdb = ConnectDatabase()\n\t\t\t}\n\n\t\t\ttime.Sleep(tick)\n\t\t}\n\t}()\n}\n\nfunc pingRedis(client *redis.Client) error {\n\tif _, err := client.Ping(client.Context()).Result(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc RedisWorker(cache *redis.Client) {\n\tgo func() {\n\t\tfor {\n\t\t\tif cache == nil || pingRedis(cache) != nil {\n\t\t\t\tcache = ConnectRedis()\n\t\t\t}\n\n\t\t\ttime.Sleep(tick)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "docker-compose.stable.yaml",
    "content": "version: \"3\"\nservices:\n  mysql:\n    image: mysql:5.7.44\n    container_name: db\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n      MYSQL_DATABASE: chatnio\n      MYSQL_USER: chatnio\n      MYSQL_PASSWORD: chatnio123456!\n      TZ: Asia/Shanghai\n    expose:\n      - \"3306\"\n    volumes:\n      - ./db:/var/lib/mysql\n    networks:\n      - chatnio-network\n\n  redis:\n    image: redis:8.2.1\n    container_name: redis\n    restart: always\n    expose:\n      - \"6379\"\n    volumes:\n      - ./redis:/data\n    networks:\n      - chatnio-network\n\n  chatnio:\n    image: programzmh/chatnio:stable\n    container_name: chatnio\n    restart: always\n    ports:\n      - \"8000:8094\"\n    depends_on:\n      - mysql\n      - redis\n    links:\n      - mysql\n      - redis\n    ulimits:\n      nofile:\n        soft: 65535\n        hard: 65535\n    environment:\n      MYSQL_HOST: mysql\n      MYSQL_USER: chatnio\n      MYSQL_PASSWORD: chatnio123456!\n      MYSQL_DB: chatnio\n      REDIS_HOST: redis\n      REDIS_PORT: 6379\n      REDIS_PASSWORD: \"\"\n      REDIS_DB: 0\n      SERVE_STATIC: \"true\"\n    volumes:\n      - ./config:/config\n      - ./logs:/logs\n      - ./storage:/storage\n    networks:\n      - chatnio-network\n\nnetworks:\n  chatnio-network:\n    driver: bridge\n"
  },
  {
    "path": "docker-compose.watch.yaml",
    "content": "version: '3'\nservices:\n  mysql:\n    image: mysql:latest\n    container_name: db\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n      MYSQL_DATABASE: chatnio\n      MYSQL_USER: chatnio\n      MYSQL_PASSWORD: chatnio123456!\n      TZ: Asia/Shanghai\n    expose:\n      - \"3306\"\n    volumes:\n      - ./db:/var/lib/mysql\n    networks:\n      - chatnio-network\n\n  redis:\n    image: redis:latest\n    container_name: redis\n    restart: always\n    expose:\n      - \"6379\"\n    volumes:\n      - ./redis:/data\n    networks:\n      - chatnio-network\n\n  chatnio:\n    image: programzmh/chatnio\n    container_name: chatnio\n    restart: always\n    ports:\n      - \"8000:8094\"\n    depends_on:\n      - mysql\n      - redis\n    links:\n      - mysql\n      - redis\n    ulimits:\n      nofile:\n        soft: 65535\n        hard: 65535\n    environment:\n      MYSQL_HOST: mysql\n      MYSQL_USER: chatnio\n      MYSQL_PASSWORD: chatnio123456!\n      MYSQL_DB: chatnio\n      REDIS_HOST: redis\n      REDIS_PORT: 6379\n      REDIS_PASSWORD: \"\"\n      REDIS_DB: 0\n      SERVE_STATIC: \"true\"\n    volumes:\n      - ./config:/config\n      - ./logs:/logs\n      - ./storage:/storage\n    networks:\n      - chatnio-network\n\n  watchtower:\n    image: containrrr/watchtower\n    container_name: watchtower\n    restart: always\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    command: --interval 3600 --stop-timeout 60s\n    networks:\n      - chatnio-network\n\nnetworks:\n  chatnio-network:\n    driver: bridge\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: '3'\nservices:\n  mysql:\n    image: mysql:latest\n    container_name: db\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n      MYSQL_DATABASE: chatnio\n      MYSQL_USER: chatnio\n      MYSQL_PASSWORD: chatnio123456!\n      TZ: Asia/Shanghai\n    expose:\n      - \"3306\"\n    volumes:\n        - ./db:/var/lib/mysql\n    networks:\n      - chatnio-network\n\n  redis:\n    image: redis:latest\n    container_name: redis\n    restart: always\n    expose:\n      - \"6379\"\n    volumes:\n      - ./redis:/data\n    networks:\n      - chatnio-network\n\n  chatnio:\n      image: programzmh/chatnio\n      container_name: chatnio\n      restart: always\n      ports:\n          - \"8000:8094\"\n      depends_on:\n          - mysql\n          - redis\n      links:\n          - mysql\n          - redis\n      ulimits:\n        nofile:\n          soft: 65535\n          hard: 65535\n      environment:\n          MYSQL_HOST: mysql\n          MYSQL_USER: chatnio\n          MYSQL_PASSWORD: chatnio123456!\n          MYSQL_DB: chatnio\n          REDIS_HOST: redis\n          REDIS_PORT: 6379\n          REDIS_PASSWORD: \"\"\n          REDIS_DB: 0\n          SERVE_STATIC: \"true\"\n      volumes:\n        - ./config:/config\n        - ./logs:/logs\n        - ./storage:/storage\n      networks:\n        - chatnio-network\n\nnetworks:\n  chatnio-network:\n    driver: bridge\n"
  },
  {
    "path": "globals/constant.go",
    "content": "package globals\n\nconst (\n\tSystem    = \"system\"\n\tUser      = \"user\"\n\tAssistant = \"assistant\"\n\tTool      = \"tool\"\n\tFunction  = \"function\"\n)\n\nconst (\n\tOpenAIChannelType      = \"openai\"\n\tAzureOpenAIChannelType = \"azure\"\n\tClaudeChannelType      = \"claude\"\n\tSlackChannelType       = \"slack\"\n\tSparkdeskChannelType   = \"sparkdesk\"\n\tChatGLMChannelType     = \"chatglm\"\n\tHunyuanChannelType     = \"hunyuan\"\n\tQwenChannelType        = \"qwen\"\n\tZhinaoChannelType      = \"zhinao\"\n\tBaichuanChannelType    = \"baichuan\"\n\tSkylarkChannelType     = \"skylark\"\n\tBingChannelType        = \"bing\"\n\tPalmChannelType        = \"palm\"\n\tMidjourneyChannelType  = \"midjourney\"\n\tMoonshotChannelType    = \"moonshot\"\n\tGroqChannelType        = \"groq\"\n\tDeepseekChannelType    = \"deepseek\"\n\tDifyChannelType        = \"dify\"\n\tCozeChannelType        = \"coze\"\n)\n\nconst (\n\tNonBilling   = \"non-billing\"\n\tTimesBilling = \"times-billing\"\n\tTokenBilling = \"token-billing\"\n)\n\nconst (\n\tAnonymousType = \"anonymous\"\n\tNormalType    = \"normal\"\n\tBasicType     = \"basic\"    // basic subscription\n\tStandardType  = \"standard\" // standard subscription\n\tProType       = \"pro\"      // pro subscription\n\tAdminType     = \"admin\"\n)\n\nconst (\n\tNoneProxyType = iota\n\tHttpProxyType\n\tHttpsProxyType\n\tSocks5ProxyType\n)\n\nconst (\n\tWebTokenType = \"web\"\n\tApiTokenType = \"api\"\n\tSystemToken  = \"system\"\n)\n"
  },
  {
    "path": "globals/interface.go",
    "content": "package globals\n\nimport \"database/sql\"\n\ntype ChannelConfig interface {\n\tGetType() string\n\tGetModelReflect(model string) string\n\tGetRetry() int\n\tGetRandomSecret() string\n\tSplitRandomSecret(num int) []string\n\tGetEndpoint() string\n\tProcessError(err error) error\n\tGetId() int\n\tGetProxy() ProxyConfig\n}\n\ntype AuthLike interface {\n\tGetID(db *sql.DB) int64\n\tHitID() int64\n}\n"
  },
  {
    "path": "globals/logger.go",
    "content": "package globals\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/natefinch/lumberjack\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/viper\"\n)\n\nconst DefaultLoggerFile = \"chatnio.log\"\n\nvar Logger *logrus.Logger\n\ntype AppLogger struct {\n\t*logrus.Logger\n}\n\nfunc (l *AppLogger) Format(entry *logrus.Entry) ([]byte, error) {\n\tdata := fmt.Sprintf(\n\t\t\"[%s] - [%s] - %s\\n\",\n\t\tstrings.ToUpper(entry.Level.String()),\n\t\tentry.Time.Format(\"2006-01-02 15:04:05\"),\n\t\tentry.Message,\n\t)\n\n\tif !viper.GetBool(\"log.ignore_console\") {\n\t\tfmt.Print(data)\n\t}\n\n\treturn []byte(data), nil\n}\n\nfunc init() {\n\tLogger = logrus.New()\n\tLogger.SetFormatter(&AppLogger{\n\t\tLogger: Logger,\n\t})\n\n\tLogger.SetOutput(&lumberjack.Logger{\n\t\tFilename:   fmt.Sprintf(\"logs/%s\", DefaultLoggerFile),\n\t\tMaxSize:    1,\n\t\tMaxBackups: 500,\n\t\tMaxAge:     21, // 3 weeks\n\t})\n\n\tLogger.SetLevel(logrus.DebugLevel)\n}\n\nfunc Output(args ...interface{}) {\n\tLogger.Println(args...)\n}\n\nfunc Debug(args ...interface{}) {\n\tLogger.Debugln(args...)\n}\n\nfunc Info(args ...interface{}) {\n\tLogger.Infoln(args...)\n}\n\nfunc Warn(args ...interface{}) {\n\tLogger.Warnln(args...)\n}\n\nfunc Error(args ...interface{}) {\n\tLogger.Errorln(args...)\n}\n\nfunc Fatal(args ...interface{}) {\n\tLogger.Fatalln(args...)\n}\n\nfunc Panic(args ...interface{}) {\n\tLogger.Panicln(args...)\n}\n"
  },
  {
    "path": "globals/method.go",
    "content": "package globals\n\nfunc (c *Chunk) IsEmpty() bool {\n\treturn len(c.Content) == 0 && c.ToolCall == nil && c.FunctionCall == nil\n}\n"
  },
  {
    "path": "globals/params.go",
    "content": "package globals\n\nvar V1ListModels ListModels\nvar SupportModels []string\n"
  },
  {
    "path": "globals/sql.go",
    "content": "package globals\n\nimport (\n\t\"database/sql\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar SqliteEngine = false\n\ntype batch struct {\n\tOld   string\n\tNew   string\n\tRegex bool\n}\n\nfunc batchReplace(sql string, batch []batch) string {\n\tfor _, item := range batch {\n\t\tif item.Regex {\n\t\t\tsql = regexp.MustCompile(item.Old).ReplaceAllString(sql, item.New)\n\t\t\tcontinue\n\t\t}\n\n\t\tsql = strings.ReplaceAll(sql, item.Old, item.New)\n\t}\n\treturn sql\n}\n\nfunc PreflightSql(sql string) string {\n\t// this is a simple way to adapt the sql to the sqlite engine\n\t// it's not a common way to use sqlite in production, just as polyfill\n\n\tif SqliteEngine {\n\t\tif strings.Contains(sql, \"DUPLICATE KEY\") {\n\t\t\tsql = batchReplace(sql, []batch{\n\t\t\t\t{\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = ?\",\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET quota = ?\",\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE used = ?\",\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET used = ?\",\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = quota + ?\",\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET quota = quota + ?\",\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE used = used + ?\",\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET used = used + ?\",\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = quota - ?\",\n\t\t\t\t\t\"INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET quota = quota - ?\",\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tsql = batchReplace(sql, []batch{\n\t\t\t// KEYWORD REPLACEMENT\n\t\t\t{`INT `, `INTEGER `, false},\n\t\t\t{` AUTO_INCREMENT`, ` AUTOINCREMENT`, false},\n\t\t\t{`DATETIME`, `TEXT`, false},\n\t\t\t{`DECIMAL`, `REAL`, false},\n\t\t\t{`MEDIUMTEXT`, `TEXT`, false},\n\t\t\t{`VARCHAR`, `TEXT`, false},\n\n\t\t\t// TEXT(65535) -> TEXT, REAL(10,2) -> REAL\n\t\t\t{`TEXT\\(\\d+\\)`, `TEXT`, true},\n\t\t\t{`REAL\\(\\d+,\\d+\\)`, `REAL`, true},\n\n\t\t\t// UNIQUE KEY -> UNIQUE\n\t\t\t{`UNIQUE KEY`, `UNIQUE`, false},\n\t\t})\n\t}\n\n\treturn sql\n}\n\nfunc ExecDb(db *sql.DB, sql string, args ...interface{}) (sql.Result, error) {\n\tsql = PreflightSql(sql)\n\treturn db.Exec(sql, args...)\n}\n\nfunc PrepareDb(db *sql.DB, sql string) (*sql.Stmt, error) {\n\tsql = PreflightSql(sql)\n\treturn db.Prepare(sql)\n}\n\nfunc QueryDb(db *sql.DB, sql string, args ...interface{}) (*sql.Rows, error) {\n\tsql = PreflightSql(sql)\n\treturn db.Query(sql, args...)\n}\n\nfunc QueryRowDb(db *sql.DB, sql string, args ...interface{}) *sql.Row {\n\tsql = PreflightSql(sql)\n\treturn db.QueryRow(sql, args...)\n}\n"
  },
  {
    "path": "globals/tools.go",
    "content": "package globals\n\ntype FunctionTools []ToolObject\ntype ToolObject struct {\n\tType     string       `json:\"type\"`\n\tFunction ToolFunction `json:\"function\"`\n}\n\ntype ToolFunction struct {\n\tName        string         `json:\"name\"`\n\tDescription string         `json:\"description\"`\n\tUrl         *string        `json:\"url,omitempty\"`\n\tParameters  ToolParameters `json:\"parameters\"`\n}\n\ntype ToolParameters struct {\n\tType       string         `json:\"type\"`\n\tProperties ToolProperties `json:\"properties\"`\n\tRequired   *[]string      `json:\"required,omitempty\"`\n}\n\ntype ToolProperties map[string]ToolProperty\n\n// https://github.com/openai/openai-node/blob/6175eca426b15990be5e5cdb0e8497e547f87d8a/src/lib/jsonschema.ts\n\ntype JsonSchemaType any\ntype JSONSchemaDefinition any\ntype ToolProperty map[string]interface{}\ntype DetailToolProperty struct {\n\tType  *string           `json:\"type,omitempty\"`\n\tEnum  *[]JsonSchemaType `json:\"enum,omitempty\"`\n\tConst *JsonSchemaType   `json:\"const,omitempty\"`\n\n\tMultipleOf       *int    `json:\"multipleOf,omitempty\"`\n\tMaximum          *int    `json:\"maximum,omitempty\"`\n\tExclusiveMaximum *int    `json:\"exclusiveMaximum,omitempty\"`\n\tMinimum          *int    `json:\"minimum,omitempty\"`\n\tExclusiveMinimum *int    `json:\"exclusiveMinimum,omitempty\"`\n\tMaxLength        *int    `json:\"maxLength,omitempty\"`\n\tMinLength        *int    `json:\"minLength,omitempty\"`\n\tPattern          *string `json:\"pattern,omitempty\"`\n\n\tItems           *JSONSchemaDefinition `json:\"items,omitempty\"`\n\tAdditionalItems *JSONSchemaDefinition `json:\"additionalItems,omitempty\"`\n\tMaxItems        *int                  `json:\"maxItems,omitempty\"`\n\tMinItems        *int                  `json:\"minItems,omitempty\"`\n\tUniqueItems     *bool                 `json:\"uniqueItems,omitempty\"`\n\tContains        *JSONSchemaDefinition `json:\"contains,omitempty\"`\n\n\tMaxProperties        *int                     `json:\"maxProperties,omitempty\"`\n\tMinProperties        *int                     `json:\"minProperties,omitempty\"`\n\tRequired             *[]string                `json:\"required,omitempty\"`\n\tProperties           *map[string]ToolProperty `json:\"properties,omitempty\"`\n\tPatternProperties    *map[string]ToolProperty `json:\"patternProperties,omitempty\"`\n\tAdditionalProperties *JSONSchemaDefinition    `json:\"additionalProperties,omitempty\"`\n\tPropertyNames        *JSONSchemaDefinition    `json:\"propertyNames,omitempty\"`\n\n\tIf   *JSONSchemaDefinition `json:\"if,omitempty\"`\n\tThen *JSONSchemaDefinition `json:\"then,omitempty\"`\n\tElse *JSONSchemaDefinition `json:\"else,omitempty\"`\n\n\tAllOf *[]JSONSchemaDefinition `json:\"allOf,omitempty\"`\n\tAnyOf *[]JSONSchemaDefinition `json:\"anyOf,omitempty\"`\n\tOneOf *[]JSONSchemaDefinition `json:\"oneOf,omitempty\"`\n\tNot   *JSONSchemaDefinition   `json:\"not,omitempty\"`\n\n\tFormat *string `json:\"format,omitempty\"`\n\n\tTitle       *string         `json:\"title,omitempty\"`\n\tDescription *string         `json:\"description,omitempty\"`\n\tDefault     *string         `json:\"default,omitempty\"`\n\tReadOnly    *bool           `json:\"readOnly,omitempty\"`\n\tWriteOnly   *bool           `json:\"writeOnly,omitempty\"`\n\tExamples    *JsonSchemaType `json:\"examples,omitempty\"`\n}\n\ntype ToolCallFunction struct {\n\tName      string `json:\"name,omitempty\"`\n\tArguments string `json:\"arguments,omitempty\"`\n}\n\ntype ToolCall struct {\n\tIndex    *int             `json:\"index,omitempty\"`\n\tType     string           `json:\"type\"`\n\tId       string           `json:\"id\"`\n\tFunction ToolCallFunction `json:\"function\"`\n}\ntype ToolCalls []ToolCall\n\ntype FunctionCall struct {\n\tName      string `json:\"name\"`\n\tArguments string `json:\"arguments\"`\n}\n"
  },
  {
    "path": "globals/types.go",
    "content": "package globals\n\ntype Hook func(data *Chunk) error\n\ntype Message struct {\n\tRole             string        `json:\"role\"`\n\tContent          string        `json:\"content\"`\n\tName             *string       `json:\"name,omitempty\"`\n\tFunctionCall     *FunctionCall `json:\"function_call,omitempty\"`     // only `function` role\n\tToolCallId       *string       `json:\"tool_call_id,omitempty\"`      // only `tool` role\n\tToolCalls        *ToolCalls    `json:\"tool_calls,omitempty\"`        // only `assistant` role\n\tReasoningContent *string       `json:\"reasoning_content,omitempty\"` // only for deepseek reasoner models\n}\n\ntype Chunk struct {\n\tContent      string        `json:\"content\"`\n\tToolCall     *ToolCalls    `json:\"tool_call,omitempty\"`\n\tFunctionCall *FunctionCall `json:\"function_call,omitempty\"`\n}\n\ntype ChatSegmentResponse struct {\n\tConversation int64   `json:\"conversation\"`\n\tQuota        float32 `json:\"quota\"`\n\tKeyword      string  `json:\"keyword\"`\n\tMessage      string  `json:\"message\"`\n\tEnd          bool    `json:\"end\"`\n\tPlan         bool    `json:\"plan\"`\n}\n\ntype GenerationSegmentResponse struct {\n\tQuota   float32 `json:\"quota\"`\n\tMessage string  `json:\"message\"`\n\tHash    string  `json:\"hash\"`\n\tEnd     bool    `json:\"end\"`\n\tError   string  `json:\"error\"`\n}\n\ntype ListModels struct {\n\tObject string           `json:\"object\"`\n\tData   []ListModelsItem `json:\"data\"`\n}\n\ntype ListModelsItem struct {\n\tId      string `json:\"id\"`\n\tObject  string `json:\"object\"`\n\tCreated int64  `json:\"created\"`\n\tOwnedBy string `json:\"owned_by\"`\n}\n\ntype ProxyConfig struct {\n\tProxyType int    `json:\"proxy_type\" mapstructure:\"proxytype\"`\n\tProxy     string `json:\"proxy\" mapstructure:\"proxy\"`\n\tUsername  string `json:\"username\" mapstructure:\"username\"`\n\tPassword  string `json:\"password\" mapstructure:\"password\"`\n}\n"
  },
  {
    "path": "globals/usage.go",
    "content": "package globals\n\nimport (\n\t\"fmt\"\n)\n\nfunc GetSubscriptionLimitFormat(t string, id int64) string {\n\treturn fmt.Sprintf(\"usage-%s:%d\", t, id)\n}\n"
  },
  {
    "path": "globals/variables.go",
    "content": "package globals\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst ChatMaxThread = 5\nconst AnonymousMaxThread = 1\n\nvar HttpMaxTimeout = 30 * time.Minute\n\nvar AllowedOrigins []string\n\nvar DebugMode bool\nvar NotifyUrl = \"\"\nvar ArticlePermissionGroup []string\nvar GenerationPermissionGroup []string\nvar CacheAcceptedModels []string\nvar CacheAcceptedExpire int64\nvar CacheAcceptedSize int64\nvar AcceptImageStore bool\nvar AcceptPromptStore bool\nvar CloseRegistration bool\nvar CloseRelay bool\n\nvar SearchEndpoint string\nvar SearchCrop bool\nvar SearchCropLength int\nvar SearchEngines string    // e.g. \"google,bing\"\nvar SearchImageProxy string // e.g. \"True\", \"False\"\nvar SearchSafeSearch int    // e.g. 0: None, 1: Moderation, 2: Strict\n\nfunc OriginIsAllowed(uri string) bool {\n\tif len(AllowedOrigins) == 0 {\n\t\t// if allowed origins is empty, allow all origins\n\t\treturn true\n\t}\n\n\tinstance, _ := url.Parse(uri)\n\tif instance == nil {\n\t\treturn false\n\t}\n\n\tif instance.Hostname() == \"localhost\" || instance.Scheme == \"file\" {\n\t\treturn true\n\t}\n\n\tif strings.HasPrefix(instance.Host, \"www.\") {\n\t\tinstance.Host = instance.Host[4:]\n\t}\n\n\treturn in(instance.Host, AllowedOrigins)\n}\n\nfunc OriginIsOpen(c *gin.Context) bool {\n\treturn strings.HasPrefix(c.Request.URL.Path, \"/v1\") || strings.HasPrefix(c.Request.URL.Path, \"/dashboard\") || strings.HasPrefix(c.Request.URL.Path, \"/mj\")\n}\n\nconst (\n\tGPT3Turbo                    = \"gpt-3.5-turbo\"\n\tGPT3TurboInstruct            = \"gpt-3.5-turbo-instruct\"\n\tGPT3Turbo0613                = \"gpt-3.5-turbo-0613\"\n\tGPT3Turbo0301                = \"gpt-3.5-turbo-0301\"\n\tGPT3Turbo1106                = \"gpt-3.5-turbo-1106\"\n\tGPT3Turbo0125                = \"gpt-3.5-turbo-0125\"\n\tGPT3Turbo16k                 = \"gpt-3.5-turbo-16k\"\n\tGPT3Turbo16k0613             = \"gpt-3.5-turbo-16k-0613\"\n\tGPT3Turbo16k0301             = \"gpt-3.5-turbo-16k-0301\"\n\tGPT4                         = \"gpt-4\"\n\tGPT4All                      = \"gpt-4-all\"\n\tGPT4Vision                   = \"gpt-4-v\"\n\tGPT4Dalle                    = \"gpt-4-dalle\"\n\tGPT40314                     = \"gpt-4-0314\"\n\tGPT40613                     = \"gpt-4-0613\"\n\tGPT41106Preview              = \"gpt-4-1106-preview\"\n\tGPT40125Preview              = \"gpt-4-0125-preview\"\n\tGPT4TurboPreview             = \"gpt-4-turbo-preview\"\n\tGPT4VisionPreview            = \"gpt-4-vision-preview\"\n\tGPT4Turbo                    = \"gpt-4-turbo\"\n\tGPT4Turbo20240409            = \"gpt-4-turbo-2024-04-09\"\n\tGPT41106VisionPreview        = \"gpt-4-1106-vision-preview\"\n\tGPT432k                      = \"gpt-4-32k\"\n\tGPT432k0314                  = \"gpt-4-32k-0314\"\n\tGPT432k0613                  = \"gpt-4-32k-0613\"\n\tGPT4O                        = \"gpt-4o\"\n\tGPT4O20240513                = \"gpt-4o-2024-05-13\"\n\tGPTImage1                    = \"gpt-image-1\"\n\tSora2                        = \"sora-2\"\n\tDalle                        = \"dalle\"\n\tDalle2                       = \"dall-e-2\"\n\tDalle3                       = \"dall-e-3\"\n\tClaude1                      = \"claude-1\"\n\tClaude1100k                  = \"claude-1.3\"\n\tClaude2                      = \"claude-1-100k\"\n\tClaude2100k                  = \"claude-2\"\n\tClaude2200k                  = \"claude-2.1\"\n\tClaude3                      = \"claude-3\"\n\tClaudeSlack                  = \"claude-slack\"\n\tSparkDeskLite                = \"spark-desk-lite\"\n\tSparkDeskPro                 = \"spark-desk-pro\"\n\tSparkDeskPro128K             = \"spark-desk-pro-128k\"\n\tSparkDeskMax                 = \"spark-desk-max\"\n\tSparkDeskMax32K              = \"spark-desk-max-32k\"\n\tSparkDeskV4Ultra             = \"spark-desk-4.0-ultra\"\n\tChatBison001                 = \"chat-bison-001\"\n\tGeminiPro                    = \"gemini-pro\"\n\tGeminiProVision              = \"gemini-pro-vision\"\n\tGemini15ProLatest            = \"gemini-1.5-pro-latest\"\n\tGemini15FlashLatest          = \"gemini-1.5-flash-latest\"\n\tGemini20ProExp               = \"gemini-2.0-pro-exp-02-05\"\n\tGemini20Flash                = \"gemini-2.0-flash\"\n\tGemini20FlashExp             = \"gemini-2.0-flash-exp\"\n\tGemini20Flash001             = \"gemini-2.0-flash-001\"\n\tGemini20FlashThinkingExp     = \"gemini-2.0-flash-thinking-exp-01-21\"\n\tGemini20FlashLitePreview     = \"gemini-2.0-flash-lite-preview-02-05\"\n\tGemini20FlashThinkingExp1219 = \"gemini-2.0-flash-thinking-exp-1219\"\n\tGeminiExp1206                = \"gemini-exp-1206\"\n\tGoogleImagen002              = \"imagen-3.0-generate-002\"\n\tBingCreative                 = \"bing-creative\"\n\tBingBalanced                 = \"bing-balanced\"\n\tBingPrecise                  = \"bing-precise\"\n\tZhiPuChatGLM4                = \"glm-4\"\n\tZhiPuChatGLM4Vision          = \"glm-4v\"\n\tZhiPuChatGLM3Turbo           = \"glm-3-turbo\"\n\tZhiPuChatGLMTurbo            = \"zhipu-chatglm-turbo\"\n\tZhiPuChatGLMPro              = \"zhipu-chatglm-pro\"\n\tZhiPuChatGLMStd              = \"zhipu-chatglm-std\"\n\tZhiPuChatGLMLite             = \"zhipu-chatglm-lite\"\n\tQwenTurbo                    = \"qwen-turbo\"\n\tQwenPlus                     = \"qwen-plus\"\n\tQwenTurboNet                 = \"qwen-turbo-net\"\n\tQwenPlusNet                  = \"qwen-plus-net\"\n\tMidjourney                   = \"midjourney\"\n\tMidjourneyFast               = \"midjourney-fast\"\n\tMidjourneyTurbo              = \"midjourney-turbo\"\n\tHunyuan                      = \"hunyuan\"\n\tGPT360V9                     = \"360-gpt-v9\"\n\tBaichuan53B                  = \"baichuan-53b\"\n\tSkylarkLite                  = \"skylark-lite-public\"\n\tSkylarkPlus                  = \"skylark-plus-public\"\n\tSkylarkPro                   = \"skylark-pro-public\"\n\tSkylarkChat                  = \"skylark-chat\"\n\tDeepseekV3                   = \"deepseek-chat\"\n\tDeepseekR1                   = \"deepseek-reasoner\"\n)\n\nvar OpenAIDalleModels = []string{\n\tDalle, Dalle2, Dalle3, GPTImage1,\n}\n\nvar GoogleImagenModels = []string{\n\tGoogleImagen002,\n}\n\nvar VisionModels = []string{\n\tGPT4VisionPreview, GPT41106VisionPreview, GPT4Turbo, GPT4Turbo20240409, GPT4O, GPT4O20240513, // openai\n\tGeminiProVision, Gemini15ProLatest, Gemini15FlashLatest, // gemini\n\tClaude3,             // anthropic\n\tZhiPuChatGLM4Vision, // chatglm\n}\n\nvar VisionSkipModels = []string{\n\tGPT4TurboPreview,\n}\n\nvar VideoModels = []string{\n\tSora2,\n}\n\nfunc in(value string, slice []string) bool {\n\tfor _, item := range slice {\n\t\tif item == value || strings.Contains(value, item) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc IsOpenAIDalleModel(model string) bool {\n\t// using image generation api if model is in dalle models\n\treturn in(model, OpenAIDalleModels) && !strings.Contains(model, \"gpt-4-dalle\")\n}\n\nfunc IsGoogleImagenModel(model string) bool {\n\t// using image generation api if model is in imagen models\n\treturn in(model, GoogleImagenModels)\n}\n\nfunc IsVisionModel(model string) bool {\n\treturn in(model, VisionModels) && !in(model, VisionSkipModels)\n}\n\nfunc IsVideoModel(model string) bool {\n\treturn in(model, VideoModels)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module chat\n\ngo 1.20\n\nrequire (\n\tgithub.com/bincooo/claude-api v1.0.2\n\tgithub.com/chai2010/webp v1.1.1\n\tgithub.com/dgrijalva/jwt-go v3.2.0+incompatible\n\tgithub.com/gin-contrib/static v0.0.1\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/go-redis/redis/v8 v8.11.5\n\tgithub.com/go-sql-driver/mysql v1.7.1\n\tgithub.com/goccy/go-json v0.10.2\n\tgithub.com/golang/protobuf v1.5.3\n\tgithub.com/google/uuid v1.3.1\n\tgithub.com/gorilla/websocket v1.5.0\n\tgithub.com/lukasjarosch/go-docx v0.4.7\n\tgithub.com/mattn/go-sqlite3 v1.14.22\n\tgithub.com/natefinch/lumberjack v2.0.0+incompatible\n\tgithub.com/pkoukk/tiktoken-go v0.1.6\n\tgithub.com/russross/blackfriday/v2 v2.1.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/viper v1.16.0\n\tgithub.com/volcengine/volc-sdk-golang v1.0.127\n\tgithub.com/volcengine/volcengine-go-sdk v1.0.180\n\tgolang.org/x/net v0.15.0\n\tgopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.0.5 // indirect\n\tgithub.com/bincooo/requests v0.0.0-20230720064210-7eae5d6c9d1e // indirect\n\tgithub.com/bitly/go-simplejson v0.5.0 // indirect\n\tgithub.com/bytedance/sonic v1.10.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.1.2 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect\n\tgithub.com/chenzhuoyu/iasm v0.9.0 // indirect\n\tgithub.com/cloudwego/hertz/cmd/hz v0.7.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dlclark/regexp2 v1.10.0 // indirect\n\tgithub.com/fsnotify/fsnotify v1.6.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.2 // indirect\n\tgithub.com/gaukas/godicttls v0.0.3 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.15.4 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/joho/godotenv v1.5.1 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.15.15 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.5 // indirect\n\tgithub.com/leodido/go-urn v1.2.4 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mattn/go-isatty v0.0.19 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.1.0 // indirect\n\tgithub.com/refraction-networking/utls v1.3.2 // indirect\n\tgithub.com/spf13/afero v1.10.0 // indirect\n\tgithub.com/spf13/cast v1.5.1 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.11 // indirect\n\tgithub.com/wangluozhe/fhttp v0.0.0-20230512135433-5c2ebfb4868a // indirect\n\tgolang.org/x/arch v0.5.0 // indirect\n\tgolang.org/x/crypto v0.13.0 // indirect\n\tgolang.org/x/sys v0.12.0 // indirect\n\tgolang.org/x/text v0.13.0 // indirect\n\tgoogle.golang.org/protobuf v1.31.0 // indirect\n\tgopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/mail.v2 v2.3.1 // indirect\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=\ngithub.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=\ngithub.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=\ngithub.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=\ngithub.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=\ngithub.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=\ngithub.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bincooo/claude-api v1.0.2 h1:qhN+s5vpMRU7e7IN+K+uyCmUDV/jtnihgfST8IC0G1o=\ngithub.com/bincooo/claude-api v1.0.2/go.mod h1:qoD2FHwGq2+Ohl5xi+jFFYjAGxP0V1ImeIg6R05uqPQ=\ngithub.com/bincooo/requests v0.0.0-20230720064210-7eae5d6c9d1e h1:38ztKJW0K6qQGitqAlcJs8O2nal60qYoS9oNZnpkZWE=\ngithub.com/bincooo/requests v0.0.0-20230720064210-7eae5d6c9d1e/go.mod h1:0WuzYU+4cQL/hVbjoncY5TACMTbD9I+pLCdnPjfItp0=\ngithub.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=\ngithub.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=\ngithub.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=\ngithub.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=\ngithub.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=\ngithub.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=\ngithub.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=\ngithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=\ngithub.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=\ngithub.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/hertz/cmd/hz v0.7.0 h1:5FTw/HrOst5DcrTPrm/bMGD1UlpQ224fRKUXHPoRmvA=\ngithub.com/cloudwego/hertz/cmd/hz v0.7.0/go.mod h1:6SroAwvZkyL54CiPANDkTR3YoX2MY4ZOW1+gtmWhRJE=\ngithub.com/cloudwego/thriftgo v0.1.7/go.mod h1:LzeafuLSiHA9JTiWC8TIMIq64iadeObgRUhmVG1OC/w=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/demdxx/gocast v1.2.0/go.mod h1:RTyqNS6BdIq/19jJX96PlVhfqG31tldKMnpVJnPa3pw=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=\ngithub.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=\ngithub.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=\ngithub.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=\ngithub.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=\ngithub.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=\ngithub.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=\ngithub.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=\ngithub.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=\ngithub.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=\ngithub.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=\ngithub.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=\ngithub.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=\ngithub.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=\ngithub.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=\ngithub.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=\ngithub.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=\ngithub.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=\ngithub.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=\ngithub.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=\ngithub.com/lukasjarosch/go-docx v0.4.7 h1:+yXUfj8ZJatMjL88MC0MEQQ5HSHzmZNyuWBAQxh6bmA=\ngithub.com/lukasjarosch/go-docx v0.4.7/go.mod h1:ka/NZgDIJId48vMvcfWfduVTY7uV0/f8EgsmCjuS9X0=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=\ngithub.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=\ngithub.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=\ngithub.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=\ngithub.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY=\ngithub.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g=\ngithub.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=\ngithub.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=\ngithub.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=\ngithub.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=\ngithub.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=\ngithub.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=\ngithub.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=\ngithub.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=\ngithub.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=\ngithub.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=\ngithub.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=\ngithub.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=\ngithub.com/volcengine/volc-sdk-golang v1.0.127 h1:gQtXzSyzQ9Fj5mQz5HHvjzS2BtOXQV+ONisMZSmQ4Bg=\ngithub.com/volcengine/volc-sdk-golang v1.0.127/go.mod h1:4g6M0qA4IvkSjEkNHe8+bpdYhdwI/A0c8q1I3lgzoNE=\ngithub.com/volcengine/volcengine-go-sdk v1.0.180 h1:lzcNlaxeGIUdXgDuVH7KJwZYZjIZzaCAYPDh91htU6U=\ngithub.com/volcengine/volcengine-go-sdk v1.0.180/go.mod h1:gfEDc1s7SYaGoY+WH2dRrS3qiuDJMkwqyfXWCa7+7oA=\ngithub.com/wangluozhe/fhttp v0.0.0-20230512135433-5c2ebfb4868a h1:nFqhBDkWfNrI5h8nAOv4orMHi0w3qMrd7GoBFXXZGmc=\ngithub.com/wangluozhe/fhttp v0.0.0-20230512135433-5c2ebfb4868a/go.mod h1:kAK+x1U0Wmy/htOSEeV31JyFBAVndp/orqVJZTq9FxM=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=\ngolang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=\ngolang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=\ngolang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=\ngolang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=\ngopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"chat/adapter\"\n\t\"chat/addition\"\n\t\"chat/admin\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/cli\"\n\t\"chat/globals\"\n\t\"chat/manager\"\n\t\"chat/manager/conversation\"\n\t\"chat/middleware\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n\t\"net/url\"\n)\n\nfunc readCorsOrigins() {\n\torigins := viper.GetStringSlice(\"allow_origins\")\n\tif len(origins) > 0 {\n\t\tglobals.AllowedOrigins = utils.Each(origins, func(origin string) string {\n\t\t\t// remove protocol and trailing slash\n\t\t\t// e.g. https://chatnio.net/ -> chatnio.net\n\n\t\t\tif host, err := url.Parse(origin); err == nil {\n\t\t\t\treturn host.Host\n\t\t\t}\n\n\t\t\treturn origin\n\t\t})\n\t}\n}\n\nfunc registerApiRouter(engine *gin.Engine) {\n\tvar app *gin.RouterGroup\n\tif !viper.GetBool(\"serve_static\") {\n\t\tapp = engine.Group(\"\")\n\t} else {\n\t\tapp = engine.Group(\"/api\")\n\t}\n\n\t{\n\t\tauth.Register(app)\n\t\tadmin.Register(app)\n\t\tadapter.Register(app)\n\t\tmanager.Register(app)\n\t\taddition.Register(app)\n\t\tconversation.Register(app)\n\t}\n}\n\nfunc main() {\n\tutils.ReadConf()\n\tadmin.InitInstance()\n\tchannel.InitManager()\n\n\tif cli.Run() {\n\t\treturn\n\t}\n\n\tapp := utils.NewEngine()\n\tworker := middleware.RegisterMiddleware(app)\n\tdefer worker()\n\n\tutils.RegisterStaticRoute(app)\n\tregisterApiRouter(app)\n\treadCorsOrigins()\n\n\tif err := app.Run(fmt.Sprintf(\":%s\", viper.GetString(\"server.port\"))); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "manager/broadcast/controller.go",
    "content": "package broadcast\n\nimport (\n\t\"chat/auth\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n)\n\nfunc ViewBroadcastAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, getLatestBroadcast(c))\n}\n\nfunc CreateBroadcastAPI(c *gin.Context) {\n\tuser := auth.RequireAdmin(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tvar form createRequest\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, createResponse{\n\t\t\tStatus: false,\n\t\t\tError:  err.Error(),\n\t\t})\n\t}\n\n\terr := createBroadcast(c, user, form.Content)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, createResponse{\n\t\t\tStatus: false,\n\t\t\tError:  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, createResponse{\n\t\tStatus: true,\n\t})\n}\n\nfunc GetBroadcastListAPI(c *gin.Context) {\n\tuser := auth.RequireAdmin(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdata, err := getBroadcastList(c)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, listResponse{\n\t\t\tData: []Info{},\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, listResponse{\n\t\tData: data,\n\t})\n}\n"
  },
  {
    "path": "manager/broadcast/manage.go",
    "content": "package broadcast\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"context\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc createBroadcast(c *gin.Context, user *auth.User, content string) error {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tif _, err := globals.ExecDb(db, `INSERT INTO broadcast (poster_id, content) VALUES (?, ?)`, user.GetID(db), content); err != nil {\n\t\treturn err\n\t}\n\n\tcache.Del(context.Background(), \":broadcast\")\n\n\treturn nil\n}\n\nfunc getBroadcastList(c *gin.Context) ([]Info, error) {\n\tdb := utils.GetDBFromContext(c)\n\n\tvar broadcastList []Info\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT broadcast.id, broadcast.content, auth.username, broadcast.created_at\n\t\tFROM broadcast\n\t\tINNER JOIN auth ON broadcast.poster_id = auth.id\n\t\tORDER BY broadcast.id DESC\n\t`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor rows.Next() {\n\t\tvar broadcast Info\n\t\tvar createdAt []uint8\n\t\tif err := rows.Scan(&broadcast.Index, &broadcast.Content, &broadcast.Poster, &createdAt); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbroadcast.CreatedAt = utils.ConvertTime(createdAt).Format(\"2006-01-02 15:04:05\")\n\t\tbroadcastList = append(broadcastList, broadcast)\n\t}\n\n\treturn broadcastList, nil\n}\n"
  },
  {
    "path": "manager/broadcast/router.go",
    "content": "package broadcast\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Register(app *gin.RouterGroup) {\n\tapp.GET(\"/broadcast/view\", ViewBroadcastAPI)\n\tapp.GET(\"/broadcast/list\", GetBroadcastListAPI)\n\tapp.POST(\"/broadcast/create\", CreateBroadcastAPI)\n}\n"
  },
  {
    "path": "manager/broadcast/types.go",
    "content": "package broadcast\n\ntype Broadcast struct {\n\tIndex   int    `json:\"index\"`\n\tContent string `json:\"content\"`\n}\n\ntype Info struct {\n\tIndex     int    `json:\"index\"`\n\tContent   string `json:\"content\"`\n\tPoster    string `json:\"poster\"`\n\tCreatedAt string `json:\"created_at\"`\n}\n\ntype listResponse struct {\n\tData []Info `json:\"data\"`\n}\n\ntype createRequest struct {\n\tContent string `json:\"content\"`\n}\n\ntype createResponse struct {\n\tStatus bool   `json:\"status\"`\n\tError  string `json:\"error\"`\n}\n"
  },
  {
    "path": "manager/broadcast/view.go",
    "content": "package broadcast\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"context\"\n\t\"github.com/gin-gonic/gin\"\n\t\"time\"\n)\n\nfunc getLatestBroadcast(c *gin.Context) *Broadcast {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tif data, err := cache.Get(context.Background(), \":broadcast\").Result(); err == nil {\n\t\tif broadcast := utils.UnmarshalForm[Broadcast](data); broadcast != nil {\n\t\t\treturn broadcast\n\t\t}\n\t}\n\n\tvar broadcast Broadcast\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT id, content FROM broadcast ORDER BY id DESC LIMIT 1;\n\t`).Scan(&broadcast.Index, &broadcast.Content); err != nil {\n\t\treturn nil\n\t}\n\n\tcache.Set(context.Background(), \":broadcast\", utils.Marshal(broadcast), 10*time.Minute)\n\treturn &broadcast\n}\n"
  },
  {
    "path": "manager/chat.go",
    "content": "package manager\n\nimport (\n\t\"chat/adapter\"\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/addition/web\"\n\t\"chat/admin\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/manager/conversation\"\n\t\"chat/utils\"\n\t\"time\"\n\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\nconst defaultMessage = \"empty response\"\nconst interruptMessage = \"interrupted\"\n\nfunc CollectQuota(c *gin.Context, user *auth.User, buffer *utils.Buffer, uncountable bool, err error) {\n\tdb := utils.GetDBFromContext(c)\n\tquota := buffer.GetQuota()\n\n\tif user == nil || quota <= 0 {\n\t\treturn\n\t}\n\n\tif buffer.IsEmpty() || err != nil {\n\t\treturn\n\t}\n\n\tif !uncountable {\n\t\tuser.UseQuota(db, quota)\n\t}\n}\n\ntype partialChunk struct {\n\tChunk *globals.Chunk\n\tEnd   bool\n\tHit   bool\n\tError error\n}\n\nfunc createStopSignal(conn *Connection) chan bool {\n\tstopSignal := make(chan bool, 1)\n\tgo func(conn *Connection, stopSignal chan bool) {\n\t\tticker := time.NewTicker(100 * time.Millisecond)\n\t\tdefer func() {\n\t\t\tticker.Stop()\n\t\t\tif r := recover(); r != nil && !strings.Contains(fmt.Sprintf(\"%s\", r), \"closed channel\") {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from stop signal: %s\\n%s\", r, stack))\n\t\t\t}\n\t\t}()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tstate := conn.PeekStop() != nil // check the stop state\n\t\t\t\tstopSignal <- state\n\n\t\t\t\tif state {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}(conn, stopSignal)\n\n\treturn stopSignal\n}\n\nfunc createChatTask(\n\tconn *Connection, user *auth.User, buffer *utils.Buffer, db *sql.DB, cache *redis.Client,\n\tmodel string, instance *conversation.Conversation, segment []globals.Message, plan bool,\n) (hit bool, err error) {\n\tchunkChan := make(chan partialChunk, 24) // the channel to send the chunk data\n\tinterruptSignal := make(chan error, 1)   // the signal to interrupt the chat task routine\n\tstopSignal := createStopSignal(conn)     // the signal to stop from the client\n\n\tdefer func() {\n\t\t// close all channels\n\t\tclose(interruptSignal)\n\t\tclose(stopSignal)\n\t\tclose(chunkChan)\n\t}()\n\n\t// create a new chat request routine\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil && !strings.Contains(fmt.Sprintf(\"%s\", r), \"closed channel\") {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from chat request: %s\\n%s\", r, stack))\n\t\t\t}\n\t\t}()\n\n\t\tif globals.IsVideoModel(model) {\n\t\t\tprops := adaptercommon.CreateVideoProps(&adaptercommon.VideoProps{\n\t\t\t\tModel:  model,\n\t\t\t\tPrompt: segment[len(segment)-1].Content,\n\t\t\t})\n\t\t\tprops.User = auth.GetUsernameString(db, user)\n\n\t\t\tvar finalJobJson string\n\t\t\thit, err := channel.NewVideoRequestWithCache(\n\t\t\t\tcache, buffer,\n\t\t\t\tauth.GetGroup(db, user),\n\t\t\t\tprops,\n\t\t\t\tfunc(data *globals.Chunk) error {\n\t\t\t\t\tif data != nil && data.Content != \"\" {\n\t\t\t\t\t\tif strings.HasPrefix(data.Content, \"{\") && strings.Contains(data.Content, \"\\\"id\\\"\") && strings.Contains(data.Content, \"\\\"status\\\"\") {\n\t\t\t\t\t\t\tfinalJobJson = data.Content\n\n\t\t\t\t\t\t\tjob, err := utils.UnmarshalString[RelayVideoJob](data.Content)\n\t\t\t\t\t\t\tif err == nil && job.Id != \"\" && job.Status == \"completed\" {\n\t\t\t\t\t\t\t\tbackendUrl := channel.SystemInstance.GetBackend()\n\t\t\t\t\t\t\t\tvideoUrl := fmt.Sprintf(\"%s/v1/videos/%s/content\", backendUrl, job.Id)\n\t\t\t\t\t\t\t\tvideoMarkdown := utils.GetVideoMarkdown(videoUrl, \"video\")\n\n\t\t\t\t\t\t\t\tchunkChan <- partialChunk{Chunk: &globals.Chunk{Content: videoMarkdown}, End: false, Hit: false, Error: nil}\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Send original content for progress updates and other messages\n\t\t\t\t\tchunkChan <- partialChunk{Chunk: data, End: false, Hit: false, Error: nil}\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t)\n\n\t\t\tif instance != nil && finalJobJson != \"\" {\n\t\t\t\tjob, err := utils.UnmarshalString[RelayVideoJob](finalJobJson)\n\t\t\t\tif err != nil {\n\t\t\t\t\tglobals.Warn(fmt.Sprintf(\"[video] failed to parse job JSON: %s, finalJobJson: %s\", err.Error(), finalJobJson))\n\t\t\t\t} else if job.Id == \"\" {\n\t\t\t\t\tglobals.Warn(fmt.Sprintf(\"[video] job.Id is empty after parsing, finalJobJson: %s\", finalJobJson))\n\t\t\t\t} else {\n\t\t\t\t\tglobals.Debug(fmt.Sprintf(\"[video] saving task_id %s to conversation %d\", job.Id, instance.GetId()))\n\t\t\t\t\tinstance.SetTaskID(job.Id)\n\t\t\t\t\tif !instance.SaveConversation(db) {\n\t\t\t\t\t\tglobals.Warn(fmt.Sprintf(\"[video] failed to save conversation with task_id %s\", job.Id))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tglobals.Debug(fmt.Sprintf(\"[video] successfully saved task_id %s to conversation %d\", job.Id, instance.GetId()))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif instance == nil {\n\t\t\t\t\tglobals.Warn(\"[video] instance is nil, cannot save task_id\")\n\t\t\t\t} else if finalJobJson == \"\" {\n\t\t\t\t\tglobals.Warn(\"[video] finalJobJson is empty, cannot save task_id\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchunkChan <- partialChunk{Chunk: nil, End: true, Hit: hit, Error: err}\n\t\t\treturn\n\t\t}\n\n\t\thit, err := channel.NewChatRequestWithCache(\n\t\t\tcache, buffer,\n\t\t\tauth.GetGroup(db, user),\n\t\t\tadaptercommon.CreateChatProps(&adaptercommon.ChatProps{\n\t\t\t\tModel:             model,\n\t\t\t\tMessage:           segment,\n\t\t\t\tMaxTokens:         instance.GetMaxTokens(),\n\t\t\t\tTemperature:       instance.GetTemperature(),\n\t\t\t\tTopP:              instance.GetTopP(),\n\t\t\t\tTopK:              instance.GetTopK(),\n\t\t\t\tPresencePenalty:   instance.GetPresencePenalty(),\n\t\t\t\tFrequencyPenalty:  instance.GetFrequencyPenalty(),\n\t\t\t\tRepetitionPenalty: instance.GetRepetitionPenalty(),\n\t\t\t}, buffer),\n\n\t\t\t// the function to handle the chunk data\n\t\t\tfunc(data *globals.Chunk) error {\n\t\t\t\t// if interrupt signal is received\n\t\t\t\tif len(interruptSignal) > 0 {\n\t\t\t\t\treturn errors.New(interruptMessage)\n\t\t\t\t}\n\n\t\t\t\t// send the chunk data to the channel\n\t\t\t\tchunkChan <- partialChunk{\n\t\t\t\t\tChunk: data,\n\t\t\t\t\tEnd:   false,\n\t\t\t\t\tHit:   false,\n\t\t\t\t\tError: nil,\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\t// chat request routine is done\n\t\tchunkChan <- partialChunk{\n\t\t\tChunk: nil,\n\t\t\tEnd:   true,\n\t\t\tHit:   hit,\n\t\t\tError: err,\n\t\t}\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase data := <-chunkChan:\n\t\t\tif data.Error != nil && data.Error.Error() == interruptMessage {\n\t\t\t\t// skip the interrupt message\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thit = data.Hit\n\t\t\terr = data.Error\n\n\t\t\tif data.End {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := conn.SendClient(globals.ChatSegmentResponse{\n\t\t\t\tMessage: buffer.WriteChunk(data.Chunk),\n\t\t\t\tQuota:   buffer.GetQuota(),\n\t\t\t\tEnd:     false,\n\t\t\t\tPlan:    plan,\n\t\t\t}); err != nil {\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"failed to send message to client: %s\", err.Error()))\n\t\t\t\tinterruptSignal <- err\n\t\t\t\treturn hit, nil\n\t\t\t}\n\n\t\tcase signal := <-stopSignal:\n\t\t\t// if stop signal is received\n\t\t\tif signal {\n\t\t\t\tglobals.Info(fmt.Sprintf(\"client stopped the chat request (model: %s, client: %s)\", model, conn.GetCtx().ClientIP()))\n\t\t\t\t_ = conn.SendClient(globals.ChatSegmentResponse{\n\t\t\t\t\tQuota: buffer.GetQuota(),\n\t\t\t\t\tEnd:   true,\n\t\t\t\t\tPlan:  plan,\n\t\t\t\t})\n\t\t\t\tinterruptSignal <- errors.New(\"signal\")\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc ChatHandler(conn *Connection, user *auth.User, instance *conversation.Conversation, restart bool) string {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from chat handler: %s (instance: %s, client: %s)\\n%s\",\n\t\t\t\terr, instance.GetModel(), conn.GetCtx().ClientIP(), stack,\n\t\t\t))\n\t\t}\n\t}()\n\n\tdb := conn.GetDB()\n\tcache := conn.GetCache()\n\n\tmodel := instance.GetModel()\n\tsegment := adapter.ClearMessages(model, web.ToChatSearched(instance, restart))\n\n\tcheck, plan := auth.CanEnableModelWithSubscription(db, cache, user, model, segment)\n\tconn.Send(globals.ChatSegmentResponse{\n\t\tConversation: instance.GetId(),\n\t})\n\n\tif check != nil {\n\t\tmessage := check.Error()\n\t\tconn.Send(globals.ChatSegmentResponse{\n\t\t\tMessage: message,\n\t\t\tQuota:   0,\n\t\t\tEnd:     true,\n\t\t})\n\t\treturn message\n\t}\n\n\tbuffer := utils.NewBuffer(model, segment, channel.ChargeInstance.GetCharge(model))\n\thit, err := createChatTask(conn, user, buffer, db, cache, model, instance, segment, plan)\n\n\tadmin.AnalyseRequest(model, buffer, err)\n\tif adapter.IsAvailableError(err) {\n\t\tglobals.Warn(fmt.Sprintf(\"%s (model: %s, client: %s)\", err, model, conn.GetCtx().ClientIP()))\n\n\t\tauth.RevertSubscriptionUsage(db, cache, user, model)\n\t\tconn.Send(globals.ChatSegmentResponse{\n\t\t\tMessage: err.Error(),\n\t\t\tEnd:     true,\n\t\t})\n\t\treturn err.Error()\n\t}\n\n\tif !hit {\n\t\tCollectQuota(conn.GetCtx(), user, buffer, plan, err)\n\t}\n\n\tif buffer.IsEmpty() {\n\t\tconn.Send(globals.ChatSegmentResponse{\n\t\t\tMessage: defaultMessage,\n\t\t\tEnd:     true,\n\t\t})\n\t\treturn defaultMessage\n\t}\n\n\tconn.Send(globals.ChatSegmentResponse{\n\t\tEnd:   true,\n\t\tQuota: buffer.GetQuota(),\n\t\tPlan:  plan,\n\t})\n\n\treturn buffer.ReadWithDefault(defaultMessage)\n}\n"
  },
  {
    "path": "manager/chat_completions.go",
    "content": "package manager\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/addition/web\"\n\t\"chat/admin\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\nconst (\n\tReasonStop      = \"stop\"\n\tReasonToolCalls = \"tool_calls\"\n)\n\nfunc supportRelayPlan() bool {\n\treturn channel.SystemInstance.SupportRelayPlan()\n}\n\nfunc checkEnableState(db *sql.DB, cache *redis.Client, user *auth.User, model string, messages []globals.Message) (state error, plan bool) {\n\tif supportRelayPlan() {\n\t\treturn auth.CanEnableModelWithSubscription(db, cache, user, model, messages)\n\t}\n\n\treturn auth.CanEnableModel(db, user, model, messages), false\n}\n\nfunc ChatRelayAPI(c *gin.Context) {\n\tif globals.CloseRelay {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"relay api is denied of access\"), \"access_denied_error\")\n\t\treturn\n\t}\n\n\tusername := utils.GetUserFromContext(c)\n\tif username == \"\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid api key\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tif utils.GetAgentFromContext(c) != \"api\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid agent\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tvar form RelayForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"invalid request body: %s\", err.Error()), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\tid := utils.Md5Encrypt(username + form.Model + time.Now().String())\n\tcreated := time.Now().Unix()\n\n\tmessages := transform(form.Messages)\n\tif strings.HasPrefix(form.Model, \"web-\") {\n\t\tsuffix := strings.TrimPrefix(form.Model, \"web-\")\n\n\t\tform.Model = suffix\n\t\tmessages = web.ToSearched(true, messages)\n\t}\n\n\tif strings.HasSuffix(form.Model, \"-official\") {\n\t\tform.Model = strings.TrimSuffix(form.Model, \"-official\")\n\t\tform.Official = true\n\t}\n\n\tcheck, plan := checkEnableState(db, cache, user, form.Model, messages)\n\tif check != nil {\n\t\tsendErrorResponse(c, check, \"quota_exceeded_error\")\n\t\treturn\n\t}\n\n\tif form.Stream {\n\t\tsendStreamTranshipmentResponse(c, form, messages, id, created, user, plan)\n\t} else {\n\t\tsendTranshipmentResponse(c, form, messages, id, created, user, plan)\n\t}\n}\n\nfunc getChatProps(form RelayForm, messages []globals.Message, buffer *utils.Buffer) *adaptercommon.ChatProps {\n\treturn adaptercommon.CreateChatProps(&adaptercommon.ChatProps{\n\t\tModel:             form.Model,\n\t\tMessage:           messages,\n\t\tMaxTokens:         form.MaxTokens,\n\t\tPresencePenalty:   form.PresencePenalty,\n\t\tFrequencyPenalty:  form.FrequencyPenalty,\n\t\tRepetitionPenalty: form.RepetitionPenalty,\n\t\tTemperature:       form.Temperature,\n\t\tTopP:              form.TopP,\n\t\tTopK:              form.TopK,\n\t\tTools:             form.Tools,\n\t\tToolChoice:        form.ToolChoice,\n\t}, buffer)\n}\n\nfunc sendTranshipmentResponse(c *gin.Context, form RelayForm, messages []globals.Message, id string, created int64, user *auth.User, plan bool) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tbuffer := utils.NewBuffer(form.Model, messages, channel.ChargeInstance.GetCharge(form.Model))\n\thit, err := channel.NewChatRequestWithCache(cache, buffer, auth.GetGroup(db, user), getChatProps(form, messages, buffer), func(data *globals.Chunk) error {\n\t\tbuffer.WriteChunk(data)\n\t\treturn nil\n\t})\n\n\tadmin.AnalyseRequest(form.Model, buffer, err)\n\tif err != nil {\n\t\tauth.RevertSubscriptionUsage(db, cache, user, form.Model)\n\t\tglobals.Warn(fmt.Sprintf(\"error from chat request api: %s (instance: %s, client: %s)\", err, form.Model, c.ClientIP()))\n\n\t\tsendErrorResponse(c, err)\n\t\treturn\n\t}\n\n\tif !hit {\n\t\tCollectQuota(c, user, buffer, plan, err)\n\t}\n\n\ttools := buffer.GetToolCalls()\n\n\tc.JSON(http.StatusOK, RelayResponse{\n\t\tId:      fmt.Sprintf(\"chatcmpl-%s\", id),\n\t\tObject:  \"chat.completion\",\n\t\tCreated: created,\n\t\tModel:   form.Model,\n\t\tChoices: []Choice{\n\t\t\t{\n\t\t\t\tIndex: 0,\n\t\t\t\tMessage: globals.Message{\n\t\t\t\t\tRole:         globals.Assistant,\n\t\t\t\t\tContent:      buffer.Read(),\n\t\t\t\t\tToolCalls:    tools,\n\t\t\t\t\tFunctionCall: buffer.GetFunctionCall(),\n\t\t\t\t},\n\t\t\t\tFinishReason: utils.Multi(tools != nil, ReasonToolCalls, ReasonStop),\n\t\t\t},\n\t\t},\n\t\tUsage: Usage{\n\t\t\tPromptTokens:     buffer.CountInputToken(),\n\t\t\tCompletionTokens: buffer.CountOutputToken(false),\n\t\t\tTotalTokens:      buffer.CountToken(),\n\t\t},\n\t\tQuota: utils.Multi[*float32](form.Official, nil, utils.ToPtr(buffer.GetQuota())),\n\t})\n}\n\nfunc getFinishReason(buffer *utils.Buffer, end bool) interface{} {\n\tif !end {\n\t\treturn nil\n\t}\n\n\tif buffer.IsFunctionCalling() {\n\t\treturn ReasonToolCalls\n\t}\n\n\treturn ReasonStop\n}\n\nfunc getRole(data *globals.Chunk) string {\n\tif data.Content != \"\" {\n\t\treturn globals.Assistant\n\t} else if data.ToolCall != nil {\n\t\treturn globals.Tool\n\t} else if data.FunctionCall != nil {\n\t\treturn globals.Function\n\t}\n\n\treturn \"\"\n}\n\nfunc getStreamTranshipmentForm(id string, created int64, form RelayForm, data *globals.Chunk, buffer *utils.Buffer, end bool, err error) RelayStreamResponse {\n\treturn RelayStreamResponse{\n\t\tId:      fmt.Sprintf(\"chatcmpl-%s\", id),\n\t\tObject:  \"chat.completion.chunk\",\n\t\tCreated: created,\n\t\tModel:   form.Model,\n\t\tChoices: []ChoiceDelta{\n\t\t\t{\n\t\t\t\tIndex: 0,\n\t\t\t\tDelta: Message{\n\t\t\t\t\tRole:         getRole(data),\n\t\t\t\t\tContent:      data.Content,\n\t\t\t\t\tToolCalls:    data.ToolCall,\n\t\t\t\t\tFunctionCall: data.FunctionCall,\n\t\t\t\t},\n\t\t\t\tFinishReason: getFinishReason(buffer, end),\n\t\t\t},\n\t\t},\n\t\tUsage: Usage{\n\t\t\tPromptTokens:     buffer.CountInputToken(),\n\t\t\tCompletionTokens: buffer.CountOutputToken(true),\n\t\t\tTotalTokens:      buffer.CountToken(),\n\t\t},\n\t\tQuota: utils.Multi[*float32](form.Official, nil, utils.ToPtr(buffer.GetQuota())),\n\t\tError: err,\n\t}\n}\n\nfunc sendStreamTranshipmentResponse(c *gin.Context, form RelayForm, messages []globals.Message, id string, created int64, user *auth.User, plan bool) {\n\tpartial := make(chan RelayStreamResponse)\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tgroup := auth.GetGroup(db, user)\n\tcharge := channel.ChargeInstance.GetCharge(form.Model)\n\n\tgo func() {\n\t\tbuffer := utils.NewBuffer(form.Model, messages, charge)\n\t\thit, err := channel.NewChatRequestWithCache(\n\t\t\tcache, buffer, group, getChatProps(form, messages, buffer),\n\t\t\tfunc(data *globals.Chunk) error {\n\t\t\t\tbuffer.WriteChunk(data)\n\n\t\t\t\tif !data.IsEmpty() {\n\t\t\t\t\tpartial <- getStreamTranshipmentForm(id, created, form, data, buffer, false, nil)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\tadmin.AnalyseRequest(form.Model, buffer, err)\n\t\tif err != nil {\n\t\t\tauth.RevertSubscriptionUsage(db, cache, user, form.Model)\n\t\t\tglobals.Warn(fmt.Sprintf(\"error from chat request api: %s (instance: %s, client: %s)\", err.Error(), form.Model, c.ClientIP()))\n\t\t\tpartial <- getStreamTranshipmentForm(id, created, form, &globals.Chunk{Content: err.Error()}, buffer, true, err)\n\t\t\tclose(partial)\n\t\t\treturn\n\t\t}\n\n\t\tpartial <- getStreamTranshipmentForm(id, created, form, &globals.Chunk{Content: \"\"}, buffer, true, nil)\n\n\t\tif !hit {\n\t\t\tCollectQuota(c, user, buffer, plan, err)\n\t\t}\n\n\t\tclose(partial)\n\t\treturn\n\t}()\n\n\tc.Stream(func(w io.Writer) bool {\n\t\tif resp, ok := <-partial; ok {\n\t\t\tif resp.Error != nil {\n\t\t\t\tsendErrorResponse(c, resp.Error)\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tc.Render(-1, utils.NewEvent(resp))\n\t\t\treturn true\n\t\t}\n\n\t\tc.Render(-1, utils.NewEndEvent())\n\t\treturn false\n\t})\n}\n"
  },
  {
    "path": "manager/completions.go",
    "content": "package manager\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/addition/web\"\n\t\"chat/admin\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc NativeChatHandler(c *gin.Context, user *auth.User, model string, message []globals.Message, enableWeb bool) (string, float32) {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from chat handler: %s (instance: %s, client: %s)\\n%s\",\n\t\t\t\terr, model, c.ClientIP(), stack,\n\t\t\t))\n\t\t}\n\t}()\n\n\tsegment := web.ToSearched(enableWeb, message)\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tcheck, plan := auth.CanEnableModelWithSubscription(db, cache, user, model, segment)\n\n\tif check != nil {\n\t\treturn check.Error(), 0\n\t}\n\n\tbuffer := utils.NewBuffer(model, segment, channel.ChargeInstance.GetCharge(model))\n\thit, err := channel.NewChatRequestWithCache(\n\t\tcache, buffer,\n\t\tauth.GetGroup(db, user),\n\t\tadaptercommon.CreateChatProps(&adaptercommon.ChatProps{\n\t\t\tModel:   model,\n\t\t\tMessage: segment,\n\t\t}, buffer),\n\t\tfunc(resp *globals.Chunk) error {\n\t\t\tbuffer.WriteChunk(resp)\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tadmin.AnalyseRequest(model, buffer, err)\n\tif err != nil {\n\t\tauth.RevertSubscriptionUsage(db, cache, user, model)\n\t\treturn err.Error(), 0\n\t}\n\n\tif !hit {\n\t\tCollectQuota(c, user, buffer, plan, err)\n\t}\n\n\treturn buffer.ReadWithDefault(defaultMessage), buffer.GetQuota()\n}\n"
  },
  {
    "path": "manager/connection.go",
    "content": "package manager\n\nimport (\n\t\"chat/globals\"\n\t\"chat/manager/conversation\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\nconst (\n\tChatType    = \"chat\"\n\tStopType    = \"stop\"\n\tRestartType = \"restart\"\n\tShareType   = \"share\"\n\tMaskType    = \"mask\"\n\tEditType    = \"edit\"\n\tRemoveType  = \"remove\"\n)\n\ntype Stack chan *conversation.FormMessage\n\ntype Connection struct {\n\tconn  *utils.WebSocket\n\tstack Stack\n\tauth  bool\n\thash  string\n}\n\nfunc NewConnection(conn *utils.WebSocket, auth bool, hash string, bufferSize int) *Connection {\n\treturn &Connection{\n\t\tconn:  conn,\n\t\tauth:  auth,\n\t\thash:  hash,\n\t\tstack: make(Stack, bufferSize),\n\t}\n}\n\nfunc (c *Connection) GetConn() *utils.WebSocket {\n\treturn c.conn\n}\n\nfunc (c *Connection) GetCtx() *gin.Context {\n\treturn c.conn.GetCtx()\n}\n\nfunc (c *Connection) GetStack() Stack {\n\treturn c.stack\n}\n\nfunc (c *Connection) ReadWorker() {\n\tfor {\n\t\tif c.IsClosed() {\n\t\t\tbreak\n\t\t}\n\n\t\tform, err := utils.ReadForm[conversation.FormMessage](c.conn)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif form.Type == \"\" {\n\t\t\tform.Type = ChatType\n\t\t}\n\n\t\tc.Write(form)\n\t}\n\n\tc.Stop()\n}\n\nfunc (c *Connection) Write(data *conversation.FormMessage) {\n\tif len(c.stack) == cap(c.stack) {\n\t\tc.Skip()\n\t}\n\tc.stack <- data\n}\n\nfunc (c *Connection) IsClosed() bool {\n\treturn c.conn.IsClosed()\n}\n\nfunc (c *Connection) Stop() {\n\tc.Write(nil)\n}\n\nfunc (c *Connection) Read() *conversation.FormMessage {\n\tform := <-c.stack\n\treturn form\n}\n\nfunc (c *Connection) Peek() *conversation.FormMessage {\n\tselect {\n\tcase form := <-c.stack:\n\t\tutils.InsertChannel(c.stack, form, 0)\n\t\treturn form\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (c *Connection) PeekWithType(t string) *conversation.FormMessage {\n\t// skip if type is matched\n\n\tif form := c.Peek(); form != nil {\n\t\tif form.Type == t {\n\t\t\tc.Skip()\n\t\t\treturn form\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Connection) PeekWithTypes(types ...string) *conversation.FormMessage {\n\t// skip if type is matched\n\n\tif form := c.Peek(); form != nil {\n\t\tfor _, t := range types {\n\t\t\tif form.Type == t {\n\t\t\t\tc.Skip()\n\t\t\t\treturn form\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Connection) PeekStop() *conversation.FormMessage {\n\treturn c.PeekWithTypes(StopType, RemoveType)\n}\n\nfunc (c *Connection) Skip() {\n\t<-c.stack\n}\n\nfunc (c *Connection) GetDB() *sql.DB {\n\treturn c.conn.GetDB()\n}\n\nfunc (c *Connection) GetCache() *redis.Client {\n\treturn c.conn.GetCache()\n}\n\nfunc (c *Connection) Send(message globals.ChatSegmentResponse) {\n\tc.conn.Send(message)\n}\n\nfunc (c *Connection) SendClient(message globals.ChatSegmentResponse) error {\n\treturn c.conn.SendJSON(message)\n}\n\nfunc (c *Connection) Process(handler func(*conversation.FormMessage) error) {\n\tfor {\n\t\tif form := c.Read(); form != nil {\n\t\t\tif err := handler(form); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *Connection) Handle(handler func(*conversation.FormMessage) error) {\n\tgo c.Process(handler)\n\tc.ReadWorker()\n}\n\nfunc (c *Connection) Lock() bool {\n\tstate := c.conn.IncrRateWithLimit(\n\t\tc.hash,\n\t\tutils.Multi[int64](c.auth, globals.ChatMaxThread, globals.AnonymousMaxThread),\n\t\t60,\n\t)\n\n\tif !state {\n\t\tc.conn.Send(globals.ChatSegmentResponse{\n\t\t\tMessage: fmt.Sprintf(\"You have reached the maximum number of threads (%d) the same time. Please wait for a while.\", globals.ChatMaxThread),\n\t\t\tEnd:     true,\n\t\t})\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (c *Connection) Release() {\n\tc.conn.DecrRate(c.hash)\n}\n"
  },
  {
    "path": "manager/conversation/api.go",
    "content": "package conversation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/utils\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype ShareForm struct {\n\tId   int64 `json:\"id\"`\n\tRefs []int `json:\"refs\"`\n}\n\ntype RenameConversationForm struct {\n\tId   int64  `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype DeleteMaskForm struct {\n\tId int `json:\"id\" binding:\"required\"`\n}\n\ntype LoadMaskResponse struct {\n\tStatus bool   `json:\"status\"`\n\tData   []Mask `json:\"data\"`\n\tError  string `json:\"error\"`\n}\n\ntype CommonMaskResponse struct {\n\tStatus bool   `json:\"status\"`\n\tError  string `json:\"error\"`\n}\n\nfunc ListAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tconversations := LoadConversationList(db, user.GetID(db))\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t\t\"data\":    conversations,\n\t})\n}\n\nfunc LoadAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tid, err := strconv.ParseInt(c.Query(\"id\"), 10, 64)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid id\",\n\t\t})\n\t\treturn\n\t}\n\tconversation := LoadConversation(db, user.GetID(db), id)\n\tif conversation == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"conversation not found\",\n\t\t})\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t\t\"data\":    conversation,\n\t})\n}\n\nfunc DeleteAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tid, err := strconv.ParseInt(c.Query(\"id\"), 10, 64)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid id\",\n\t\t})\n\t\treturn\n\t}\n\tconversation := LoadConversation(db, user.GetID(db), id)\n\tif conversation == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"conversation not found\",\n\t\t})\n\t\treturn\n\t}\n\tconversation.DeleteConversation(db)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t})\n}\n\nfunc RenameAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tvar form RenameConversationForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid form\",\n\t\t})\n\t\treturn\n\t}\n\n\tconversation := LoadConversation(db, user.GetID(db), form.Id)\n\tif conversation == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"conversation not found\",\n\t\t})\n\t\treturn\n\t}\n\tconversation.RenameConversation(db, form.Name)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t})\n}\n\nfunc CleanAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tif err := DeleteAllConversations(db, *user); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t})\n}\n\nfunc ShareAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tvar form ShareForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid form\",\n\t\t})\n\t\treturn\n\t}\n\n\tif hash, err := ShareConversation(db, user, form.Id, form.Refs); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t} else {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  true,\n\t\t\t\"message\": \"\",\n\t\t\t\"data\":    hash,\n\t\t})\n\t}\n}\n\nfunc ViewAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tif hash == \"\" {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid hash\",\n\t\t})\n\t\treturn\n\t}\n\n\tshared, err := GetSharedConversation(db, hash)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t\t\"data\":    shared,\n\t})\n}\n\nfunc ListSharingAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tdata := ListSharedConversation(db, user)\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t\t\"data\":    data,\n\t})\n}\n\nfunc DeleteSharingAPI(c *gin.Context) {\n\tuser := auth.GetUser(c)\n\tif user == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"user not found\",\n\t\t})\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\thash := strings.TrimSpace(c.Query(\"hash\"))\n\tif hash == \"\" {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"invalid hash\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := DeleteSharedConversation(db, user, hash); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\":  true,\n\t\t\"message\": \"\",\n\t})\n}\n\nfunc LoadMaskAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tusername := utils.GetUserFromContext(c)\n\n\tif username == \"\" {\n\t\tc.JSON(http.StatusOK, LoadMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  \"authentication_error\",\n\t\t})\n\t\treturn\n\t}\n\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\n\tmasks, err := LoadMask(db, user)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, LoadMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, LoadMaskResponse{\n\t\tStatus: true,\n\t\tData:   masks,\n\t})\n}\n\nfunc DeleteMaskAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tusername := utils.GetUserFromContext(c)\n\n\tif username == \"\" {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  \"authentication_error\",\n\t\t})\n\t\treturn\n\t}\n\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\n\tvar form DeleteMaskForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  \"invalid_request_error\",\n\t\t})\n\t\treturn\n\t}\n\n\tmask := Mask{\n\t\tId: form.Id,\n\t}\n\n\terr := mask.Delete(db, user)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\tStatus: true,\n\t})\n}\n\nfunc SaveMaskAPI(c *gin.Context) {\n\tdb := utils.GetDBFromContext(c)\n\tusername := utils.GetUserFromContext(c)\n\n\tif username == \"\" {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  \"authentication_error\",\n\t\t})\n\t\treturn\n\t}\n\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\n\tvar mask Mask\n\tif err := c.ShouldBindJSON(&mask); err != nil {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  \"invalid_request_error\",\n\t\t})\n\t\treturn\n\t}\n\n\terr := mask.Save(db, user)\n\tif err != nil {\n\t\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\t\tStatus: false,\n\t\t\tError:  err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, CommonMaskResponse{\n\t\tStatus: true,\n\t})\n}\n"
  },
  {
    "path": "manager/conversation/conversation.go",
    "content": "package conversation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n)\n\nconst defaultConversationName = \"new chat\"\nconst defaultConversationContext = 8\n\ntype Conversation struct {\n\tAuth      bool              `json:\"auth\"`\n\tUserID    int64             `json:\"user_id\"`\n\tId        int64             `json:\"id\"`\n\tName      string            `json:\"name\"`\n\tMessage   []globals.Message `json:\"message\"`\n\tModel     string            `json:\"model\"`\n\tTaskID    string            `json:\"task_id,omitempty\"`\n\tEnableWeb bool              `json:\"enable_web\"`\n\tShared    bool              `json:\"shared\"`\n\tContext   int               `json:\"context\"`\n\n\tMaxTokens         *int     `json:\"max_tokens,omitempty\"`\n\tTemperature       *float32 `json:\"temperature,omitempty\"`\n\tTopP              *float32 `json:\"top_p,omitempty\"`\n\tTopK              *int     `json:\"top_k,omitempty\"`\n\tPresencePenalty   *float32 `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty  *float32 `json:\"frequency_penalty,omitempty\"`\n\tRepetitionPenalty *float32 `json:\"repetition_penalty,omitempty\"`\n}\n\ntype FormMessage struct {\n\tType          string `json:\"type\"`\n\tMessage       string `json:\"message\"`\n\tWeb           bool   `json:\"web\"`\n\tModel         string `json:\"model\"`\n\tIgnoreContext bool   `json:\"ignore_context\"`\n\tContext       int    `json:\"context\"`\n\n\t// request params\n\tMaxTokens         *int     `json:\"max_tokens,omitempty\"`\n\tTemperature       *float32 `json:\"temperature,omitempty\"`\n\tTopP              *float32 `json:\"top_p,omitempty\"`\n\tTopK              *int     `json:\"top_k,omitempty\"`\n\tPresencePenalty   *float32 `json:\"presence_penalty,omitempty\"`\n\tFrequencyPenalty  *float32 `json:\"frequency_penalty,omitempty\"`\n\tRepetitionPenalty *float32 `json:\"repetition_penalty,omitempty\"`\n}\n\nfunc NewAnonymousConversation() *Conversation {\n\treturn &Conversation{\n\t\tAuth:    false,\n\t\tUserID:  -1,\n\t\tId:      -1,\n\t\tName:    defaultConversationName,\n\t\tMessage: []globals.Message{},\n\t\tModel:   globals.GPT3Turbo,\n\t\tContext: defaultConversationContext,\n\t}\n}\n\nfunc NewConversation(db *sql.DB, id int64) *Conversation {\n\treturn &Conversation{\n\t\tAuth:    true,\n\t\tUserID:  id,\n\t\tId:      GetConversationLengthByUserID(db, id) + 1,\n\t\tName:    defaultConversationName,\n\t\tMessage: []globals.Message{},\n\t\tModel:   globals.GPT3Turbo,\n\t}\n}\n\nfunc ExtractConversation(db *sql.DB, user *auth.User, id int64, ref string) *Conversation {\n\tif ref != \"\" {\n\t\tif instance := UseSharedConversation(db, user, ref); instance != nil {\n\t\t\treturn instance\n\t\t}\n\t}\n\n\tif user == nil {\n\t\treturn NewAnonymousConversation()\n\t}\n\n\tif id == -1 {\n\t\t// create new conversation\n\t\treturn NewConversation(db, user.GetID(db))\n\t}\n\n\t// load conversation\n\tif instance := LoadConversation(db, user.GetID(db), id); instance != nil {\n\t\treturn instance\n\t} else {\n\t\treturn NewConversation(db, user.GetID(db))\n\t}\n}\n\nfunc (c *Conversation) GetModel() string {\n\tif len(c.Model) == 0 {\n\t\treturn globals.GPT3Turbo\n\t}\n\treturn c.Model\n}\n\nfunc (c *Conversation) IsEnableWeb() bool {\n\treturn c.EnableWeb\n}\n\nfunc (c *Conversation) GetContextLength() int {\n\tif c.Context <= 0 {\n\t\treturn defaultConversationContext\n\t}\n\n\treturn c.Context\n}\n\nfunc (c *Conversation) SetModel(model string) {\n\tif len(model) == 0 {\n\t\tmodel = globals.GPT3Turbo\n\t}\n\tc.Model = model\n}\n\nfunc (c *Conversation) SetEnableWeb(enable bool) {\n\tc.EnableWeb = enable\n}\n\nfunc (c *Conversation) GetTemperature() *float32 {\n\treturn c.Temperature\n}\n\nfunc (c *Conversation) SetTemperature(temperature *float32) {\n\tc.Temperature = temperature\n}\n\nfunc (c *Conversation) GetTopP() *float32 {\n\treturn c.TopP\n}\n\nfunc (c *Conversation) SetTopP(topP *float32) {\n\tc.TopP = topP\n}\n\nfunc (c *Conversation) GetTopK() *int {\n\treturn c.TopK\n}\n\nfunc (c *Conversation) SetTopK(topK *int) {\n\tc.TopK = topK\n}\n\nfunc (c *Conversation) GetPresencePenalty() *float32 {\n\treturn c.PresencePenalty\n}\n\nfunc (c *Conversation) SetPresencePenalty(presencePenalty *float32) {\n\tc.PresencePenalty = presencePenalty\n}\n\nfunc (c *Conversation) GetFrequencyPenalty() *float32 {\n\treturn c.FrequencyPenalty\n}\n\nfunc (c *Conversation) SetFrequencyPenalty(frequencyPenalty *float32) {\n\tc.FrequencyPenalty = frequencyPenalty\n}\n\nfunc (c *Conversation) GetRepetitionPenalty() *float32 {\n\treturn c.RepetitionPenalty\n}\n\nfunc (c *Conversation) SetRepetitionPenalty(repetitionPenalty *float32) {\n\tc.RepetitionPenalty = repetitionPenalty\n}\n\nfunc (c *Conversation) GetMaxTokens() *int {\n\treturn c.MaxTokens\n}\n\nfunc (c *Conversation) SetMaxTokens(maxTokens *int) {\n\tc.MaxTokens = maxTokens\n}\n\nfunc (c *Conversation) SetContextLength(context int, ignore bool) {\n\tif ignore {\n\t\tcontext = 1\n\t} else if context <= 0 {\n\t\tcontext = defaultConversationContext\n\t}\n\n\tc.Context = context\n}\n\nfunc (c *Conversation) GetName() string {\n\treturn c.Name\n}\n\nfunc (c *Conversation) SetName(db *sql.DB, name string) {\n\tc.Name = utils.Extract(name, 50, \"...\")\n\tc.SaveConversation(db)\n}\n\nfunc (c *Conversation) GetId() int64 {\n\treturn c.Id\n}\n\nfunc (c *Conversation) GetUserID() int64 {\n\treturn c.UserID\n}\n\nfunc (c *Conversation) SetId(id int64) {\n\tc.Id = id\n}\n\nfunc (c *Conversation) GetMessage() []globals.Message {\n\treturn c.Message\n}\n\nfunc (c *Conversation) GetMessageById(id int) globals.Message {\n\treturn c.Message[id]\n}\n\nfunc (c *Conversation) GetMessageLength() int {\n\treturn len(c.Message)\n}\n\nfunc (c *Conversation) GetMessageSegment(length int) []globals.Message {\n\tif length > len(c.Message) {\n\t\treturn c.Message\n\t}\n\treturn c.Message[len(c.Message)-length:]\n}\n\nfunc (c *Conversation) GetChatMessage(restart bool) []globals.Message {\n\tif restart {\n\t\t// remove all last `assistant` role message\n\t\tcp := CopyMessage(c.Message)\n\n\t\tvar index int\n\t\tfor index = len(cp) - 1; index >= 0; index-- {\n\t\t\tif cp[index].Role != globals.Assistant {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif index >= 0 {\n\t\t\tcp = cp[:index+1]\n\t\t}\n\n\t\tif c.GetContextLength() > len(cp) {\n\t\t\treturn cp\n\t\t}\n\n\t\treturn cp[len(cp)-c.GetContextLength():]\n\t}\n\n\treturn c.GetMessageSegment(c.GetContextLength())\n}\n\nfunc CopyMessage(message []globals.Message) []globals.Message {\n\treturn utils.DeepCopy[[]globals.Message](message) // deep copy\n}\n\nfunc (c *Conversation) GetLastMessage() globals.Message {\n\treturn c.Message[len(c.Message)-1]\n}\n\nfunc (c *Conversation) AddMessage(message globals.Message) {\n\tc.Message = append(c.Message, message)\n}\n\nfunc (c *Conversation) AddMessages(messages []globals.Message) {\n\tc.Message = append(c.Message, messages...)\n}\n\nfunc (c *Conversation) InsertMessage(message globals.Message, index int) {\n\tc.Message = append(c.Message[:index], append([]globals.Message{message}, c.Message[index:]...)...)\n}\n\nfunc (c *Conversation) InsertMessages(messages []globals.Message, index int) {\n\tc.Message = append(c.Message[:index], append(messages, c.Message[index:]...)...)\n}\n\nfunc (c *Conversation) AddMessageFromUser(message string) {\n\tc.AddMessage(globals.Message{\n\t\tRole:    globals.User,\n\t\tContent: message,\n\t})\n}\n\nfunc (c *Conversation) AddMessageFromAssistant(message string) {\n\tc.AddMessage(globals.Message{\n\t\tRole:    globals.Assistant,\n\t\tContent: message,\n\t})\n}\n\nfunc (c *Conversation) AddMessageFromSystem(message string) {\n\tc.AddMessage(globals.Message{\n\t\tRole:    globals.System,\n\t\tContent: message,\n\t})\n}\n\nfunc GetMessage(data []byte) (string, error) {\n\tform, err := utils.Unmarshal[FormMessage](data)\n\tform.Message = strings.TrimSpace(form.Message)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(form.Message) == 0 {\n\t\treturn \"\", errors.New(\"message is empty\")\n\t}\n\treturn form.Message, nil\n}\n\nfunc (c *Conversation) ApplyParam(form *FormMessage) {\n\tc.SetModel(form.Model)\n\tc.SetEnableWeb(form.Web)\n\tc.SetContextLength(form.Context, form.IgnoreContext)\n\n\tc.SetMaxTokens(form.MaxTokens)\n\tc.SetTemperature(form.Temperature)\n\tc.SetTopP(form.TopP)\n\tc.SetTopK(form.TopK)\n\tc.SetPresencePenalty(form.PresencePenalty)\n\tc.SetFrequencyPenalty(form.FrequencyPenalty)\n\tc.SetRepetitionPenalty(form.RepetitionPenalty)\n}\n\nfunc (c *Conversation) AddMessageFromByte(data []byte) (string, error) {\n\tform, err := utils.Unmarshal[FormMessage](data)\n\tif err != nil {\n\t\treturn \"\", err\n\t} else if len(form.Message) == 0 {\n\t\treturn \"\", errors.New(\"message is empty\")\n\t}\n\n\tc.AddMessageFromUser(form.Message)\n\tc.ApplyParam(&form)\n\n\treturn form.Message, nil\n}\n\nfunc (c *Conversation) AddMessageFromForm(form *FormMessage) error {\n\tif len(form.Message) == 0 {\n\t\treturn errors.New(\"message is empty\")\n\t}\n\n\tc.AddMessageFromUser(form.Message)\n\tc.ApplyParam(form)\n\n\treturn nil\n}\n\nfunc (c *Conversation) HandleMessage(db *sql.DB, form *FormMessage) bool {\n\thead := len(c.Message) == 0 || c.Name == defaultConversationName\n\tif err := c.AddMessageFromForm(form); err != nil {\n\t\treturn false\n\t}\n\tif head {\n\t\tc.SetName(db, form.Message)\n\t}\n\tc.SaveConversation(db)\n\treturn true\n}\n\nfunc (c *Conversation) HandleMessageFromByte(db *sql.DB, data []byte) bool {\n\thead := len(c.Message) == 0\n\tmsg, err := c.AddMessageFromByte(data)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif head {\n\t\tc.SetName(db, msg)\n\t}\n\tc.SaveConversation(db)\n\treturn true\n}\n\nfunc (c *Conversation) GetLatestMessage() string {\n\treturn c.Message[len(c.Message)-1].Content\n}\n\nfunc (c *Conversation) SaveResponse(db *sql.DB, message string) {\n\tc.AddMessageFromAssistant(message)\n\tc.SaveConversation(db)\n}\n\nfunc (c *Conversation) RemoveMessage(index int) globals.Message {\n\tif index < 0 || index >= len(c.Message) {\n\t\treturn globals.Message{}\n\t}\n\tmessage := c.Message[index]\n\tc.Message = append(c.Message[:index], c.Message[index+1:]...)\n\treturn message\n}\n\nfunc (c *Conversation) RemoveLatestMessage() globals.Message {\n\treturn c.RemoveMessage(len(c.Message) - 1)\n}\n\nfunc (c *Conversation) RemoveLatestMessageWithRole(role string) globals.Message {\n\tif len(c.Message) == 0 {\n\t\treturn globals.Message{}\n\t}\n\n\tmessage := c.Message[len(c.Message)-1]\n\tif message.Role == role {\n\t\treturn c.RemoveLatestMessage()\n\t}\n\n\treturn globals.Message{}\n}\n\nfunc (c *Conversation) EditMessage(index int, message string) {\n\tif index < 0 || index >= len(c.Message) {\n\t\treturn\n\t}\n\tc.Message[index].Content = message\n}\n\nfunc (c *Conversation) DeleteMessage(index int) {\n\tif index < 0 || index >= len(c.Message) {\n\t\treturn\n\t}\n\tc.Message = append(c.Message[:index], c.Message[index+1:]...)\n}\n\nfunc (c *Conversation) GetTaskID() string {\n\treturn c.TaskID\n}\n\nfunc (c *Conversation) SetTaskID(taskID string) {\n\tc.TaskID = taskID\n}\n"
  },
  {
    "path": "manager/conversation/mask.go",
    "content": "package conversation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n)\n\ntype Mask struct {\n\tId          int               `json:\"id\"`\n\tAvatar      string            `json:\"avatar\"`\n\tName        string            `json:\"name\"`\n\tDescription string            `json:\"description\"`\n\tContext     []globals.Message `json:\"context\"`\n}\n\nfunc (c *Conversation) LoadMask(data string) {\n\tmessage := utils.UnmarshalForm[[]globals.Message](data)\n\tif message != nil && len(*message) > 0 {\n\t\tc.InsertMessages(*message, 0)\n\t}\n}\n\nfunc (m *Mask) Save(db *sql.DB, user *auth.User) error {\n\tuserId := user.GetID(db)\n\n\tif m.Id == -1 {\n\t\t_, err := globals.ExecDb(db,\n\t\t\t\"INSERT INTO mask (mask.user_id, avatar, name, description, context) VALUES (?, ?, ?, ?, ?)\",\n\t\t\tuserId, m.Avatar, m.Name, m.Description, utils.Marshal(m.Context),\n\t\t)\n\t\treturn err\n\t}\n\n\t_, err := globals.ExecDb(db,\n\t\t\"UPDATE mask SET avatar = ?, name = ?, description = ?, context = ? WHERE id = ? AND user_id = ?\",\n\t\tm.Avatar, m.Name, m.Description, utils.Marshal(m.Context), m.Id, userId,\n\t)\n\treturn err\n}\n\nfunc (m *Mask) Delete(db *sql.DB, user *auth.User) error {\n\t_, err := globals.ExecDb(db, \"DELETE FROM mask WHERE id = ? AND user_id = ?\", m.Id, user.GetID(db))\n\treturn err\n}\n\nfunc LoadMask(db *sql.DB, user *auth.User) ([]Mask, error) {\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT id, avatar, name, description, context \n\t\tFROM mask WHERE user_id = ?\n\t\tORDER BY id DESC\n\t`, user.GetID(db))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func(rows *sql.Rows) {\n\t\terr := rows.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(err.Error())\n\t\t}\n\t}(rows)\n\n\tmasks := make([]Mask, 0)\n\tfor rows.Next() {\n\t\tvar mask Mask\n\t\tvar context string\n\n\t\terr = rows.Scan(&mask.Id, &mask.Avatar, &mask.Name, &mask.Description, &context)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdata, err := utils.UnmarshalString[[]globals.Message](context)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmask.Context = data\n\n\t\tmasks = append(masks, mask)\n\t}\n\n\treturn masks, nil\n}\n"
  },
  {
    "path": "manager/conversation/router.go",
    "content": "package conversation\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Register(app *gin.RouterGroup) {\n\trouter := app.Group(\"/conversation\")\n\t{\n\t\trouter.GET(\"/list\", ListAPI)\n\t\trouter.GET(\"/load\", LoadAPI)\n\t\trouter.POST(\"/rename\", RenameAPI)\n\t\trouter.GET(\"/delete\", DeleteAPI)\n\t\trouter.GET(\"/clean\", CleanAPI)\n\n\t\t// share\n\t\trouter.POST(\"/share\", ShareAPI)\n\t\trouter.GET(\"/view\", ViewAPI)\n\t\trouter.GET(\"/share/list\", ListSharingAPI)\n\t\trouter.GET(\"/share/delete\", DeleteSharingAPI)\n\n\t\trouter.GET(\"/mask/view\", LoadMaskAPI)\n\t\trouter.POST(\"/mask/save\", SaveMaskAPI)\n\t\trouter.POST(\"/mask/delete\", DeleteMaskAPI)\n\t}\n}\n"
  },
  {
    "path": "manager/conversation/shared.go",
    "content": "package conversation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype SharedPreviewForm struct {\n\tName           string    `json:\"name\"`\n\tConversationId int64     `json:\"conversation_id\"`\n\tTime           time.Time `json:\"time\"`\n\tHash           string    `json:\"hash\"`\n}\n\ntype SharedForm struct {\n\tUsername string            `json:\"username\"`\n\tName     string            `json:\"name\"`\n\tMessages []globals.Message `json:\"messages\"`\n\tModel    string            `json:\"model\"`\n\tTime     time.Time         `json:\"time\"`\n}\n\ntype SharedHashForm struct {\n\tId             int64 `json:\"id\"`\n\tConversationId int64 `json:\"conversation_id\"`\n\tRefs           []int `json:\"refs\"`\n}\n\nfunc GetRef(refs []int) (result string) {\n\tfor _, v := range refs {\n\t\tresult += strconv.Itoa(v) + \",\"\n\t}\n\treturn strings.TrimSuffix(result, \",\")\n}\n\nfunc ShareConversation(db *sql.DB, user *auth.User, id int64, refs []int) (string, error) {\n\tif id < 0 || user == nil {\n\t\treturn \"\", nil\n\t}\n\n\tref := GetRef(refs)\n\thash := utils.Md5EncryptForm(SharedHashForm{\n\t\tId:             user.GetID(db),\n\t\tConversationId: id,\n\t\tRefs:           refs,\n\t})\n\n\tif _, err := globals.ExecDb(db, `\n\t\tINSERT INTO sharing (hash, user_id, conversation_id, refs) VALUES (?, ?, ?, ?)\n\t\tON DUPLICATE KEY UPDATE refs = ?\n\t`, hash, user.GetID(db), id, ref, ref); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn hash, nil\n}\n\nfunc GetSharedMessages(db *sql.DB, userId int64, conversationId int64, refs []string) []globals.Message {\n\tconversation := LoadConversation(db, userId, conversationId)\n\tif conversation == nil {\n\t\treturn nil\n\t}\n\n\tmessages := make([]globals.Message, 0)\n\tfor _, v := range refs {\n\t\tif v == \"-1\" {\n\t\t\treturn conversation.GetMessage()\n\t\t} else {\n\t\t\tid, err := strconv.Atoi(v)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmessages = append(messages, conversation.GetMessageById(id))\n\t\t}\n\t}\n\treturn messages\n}\n\nfunc ListSharedConversation(db *sql.DB, user *auth.User) []SharedPreviewForm {\n\tif user == nil {\n\t\treturn nil\n\t}\n\n\tid := user.GetID(db)\n\trows, err := globals.QueryDb(db, `\n\t\tSELECT conversation.conversation_name, conversation.conversation_id, sharing.updated_at, sharing.hash\n\t\tFROM sharing\n\t\tINNER JOIN conversation \n\t\t    ON conversation.conversation_id = sharing.conversation_id \n\t\t    AND conversation.user_id = sharing.user_id\n\t\tWHERE sharing.user_id = ?\n\t\tORDER BY sharing.updated_at DESC\n\t\tLIMIT 100\n\t`, id)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]SharedPreviewForm, 0)\n\tfor rows.Next() {\n\t\tvar updated []uint8\n\t\tvar form SharedPreviewForm\n\t\tif err := rows.Scan(&form.Name, &form.ConversationId, &updated, &form.Hash); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tform.Time = *utils.ConvertTime(updated)\n\t\tresult = append(result, form)\n\t}\n\treturn result\n}\n\nfunc DeleteSharedConversation(db *sql.DB, user *auth.User, hash string) error {\n\tif user == nil {\n\t\treturn nil\n\t}\n\n\tid := user.GetID(db)\n\tif _, err := globals.ExecDb(db, `\n\t\tDELETE FROM sharing WHERE user_id = ? AND hash = ?\n\t`, id, hash); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc GetSharedConversation(db *sql.DB, hash string) (*SharedForm, error) {\n\tvar shared SharedForm\n\tvar (\n\t\tuid     int64\n\t\tcid     int64\n\t\tref     string\n\t\tupdated []uint8\n\t)\n\tif err := globals.QueryRowDb(db, `\n\t\tSELECT auth.username, sharing.refs, sharing.updated_at, conversation.conversation_name,\n\t\t       sharing.user_id, sharing.conversation_id, conversation.model\n\t\tFROM sharing\n\t\tINNER JOIN auth ON auth.id = sharing.user_id\n\t\tINNER JOIN conversation ON conversation.conversation_id = sharing.conversation_id AND conversation.user_id = sharing.user_id\n\t\tWHERE sharing.hash = ?\n\t`, hash).Scan(&shared.Username, &ref, &updated, &shared.Name, &uid, &cid, &shared.Model); err != nil {\n\t\treturn nil, err\n\t}\n\n\tshared.Time = *utils.ConvertTime(updated)\n\trefs := strings.Split(ref, \",\")\n\tshared.Messages = GetSharedMessages(db, uid, cid, refs)\n\n\treturn &shared, nil\n}\n\nfunc UseSharedConversation(db *sql.DB, user *auth.User, hash string) *Conversation {\n\tshared, err := GetSharedConversation(db, hash)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif user == nil {\n\t\t// anonymous\n\t\treturn &Conversation{\n\t\t\tAuth:    false,\n\t\t\tUserID:  -1,\n\t\t\tId:      -1,\n\t\t\tName:    shared.Name,\n\t\t\tMessage: shared.Messages,\n\t\t\tModel:   globals.GPT3Turbo,\n\t\t}\n\t}\n\n\t// create new conversation\n\tid := user.GetID(db)\n\treturn &Conversation{\n\t\tAuth:    true,\n\t\tId:      GetConversationLengthByUserID(db, id) + 1,\n\t\tUserID:  id,\n\t\tName:    shared.Name,\n\t\tModel:   globals.GPT3Turbo,\n\t\tMessage: shared.Messages,\n\t}\n}\n\nfunc (c *Conversation) LoadSharing(db *sql.DB, hash string) {\n\tif strings.TrimSpace(hash) == \"\" || c.Shared == true {\n\t\treturn\n\t}\n\n\tshared, err := GetSharedConversation(db, hash)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tc.InsertMessages(shared.Messages, 0)\n\tc.SetName(db, shared.Name)\n\tc.Shared = true\n}\n"
  },
  {
    "path": "manager/conversation/storage.go",
    "content": "package conversation\n\nimport (\n\t\"chat/auth\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"database/sql\"\n\t\"fmt\"\n)\n\nfunc (c *Conversation) SaveConversation(db *sql.DB) bool {\n\tif c.UserID == -1 {\n\t\t// anonymous request\n\t\treturn true\n\t}\n\n\tdata := utils.ToJson(c.GetMessage())\n\tquery := `\n\t\tINSERT INTO conversation (user_id, conversation_id, conversation_name, data, model, task_id) VALUES (?, ?, ?, ?, ?, ?)\n\t\tON DUPLICATE KEY UPDATE conversation_name = VALUES(conversation_name), data = VALUES(data), task_id = VALUES(task_id)\n\t`\n\n\tstmt, err := globals.PrepareDb(db, query)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer func(stmt *sql.Stmt) {\n\t\terr := stmt.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(err)\n\t\t}\n\t}(stmt)\n\n\tvar taskID sql.NullString\n\tif c.TaskID != \"\" {\n\t\ttaskID = sql.NullString{String: c.TaskID, Valid: true}\n\t}\n\n\t_, err = stmt.Exec(c.UserID, c.Id, c.Name, data, c.Model, taskID)\n\tif err != nil {\n\t\tglobals.Info(fmt.Sprintf(\"execute error during save conversation: %s\", err.Error()))\n\t\treturn false\n\t}\n\treturn true\n}\nfunc GetConversationLengthByUserID(db *sql.DB, userId int64) int64 {\n\tvar length int64\n\terr := globals.QueryRowDb(db, \"SELECT MAX(conversation_id) FROM conversation WHERE user_id = ?\", userId).Scan(&length)\n\tif err != nil || length < 0 {\n\t\treturn 0\n\t}\n\treturn length\n}\n\nfunc LoadConversation(db *sql.DB, userId int64, conversationId int64) *Conversation {\n\tconversation := Conversation{\n\t\tUserID: userId,\n\t\tId:     conversationId,\n\t}\n\n\tvar (\n\t\tdata   string\n\t\tmodel  interface{}\n\t\ttaskID sql.NullString\n\t)\n\terr := globals.QueryRowDb(db, `\n\t\tSELECT conversation_name, model, data, task_id FROM conversation\n\t\tWHERE user_id = ? AND conversation_id = ?\n\t\t`, userId, conversationId).Scan(&conversation.Name, &model, &data, &taskID)\n\tif value, ok := model.([]byte); ok {\n\t\tconversation.Model = string(value)\n\t} else {\n\t\tconversation.Model = globals.GPT3Turbo\n\t}\n\n\tif taskID.Valid {\n\t\tconversation.TaskID = taskID.String\n\t}\n\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tconversation.Message, err = utils.Unmarshal[[]globals.Message]([]byte(data))\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &conversation\n}\n\nfunc LoadConversationList(db *sql.DB, userId int64) []Conversation {\n\tvar conversationList []Conversation\n\trows, err := globals.QueryDb(db, `\n\t\t\tSELECT conversation_id, conversation_name FROM conversation WHERE user_id = ? \n\t\t\tORDER BY conversation_id DESC LIMIT 100\n\t`, userId)\n\tif err != nil {\n\t\treturn conversationList\n\t}\n\tdefer func(rows *sql.Rows) {\n\t\terr := rows.Close()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}(rows)\n\n\tfor rows.Next() {\n\t\tvar conversation Conversation\n\t\terr := rows.Scan(&conversation.Id, &conversation.Name)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tconversationList = append(conversationList, conversation)\n\t}\n\n\treturn conversationList\n}\n\nfunc (c *Conversation) DeleteConversation(db *sql.DB) bool {\n\t_, err := globals.ExecDb(db, \"DELETE FROM conversation WHERE user_id = ? AND conversation_id = ?\", c.UserID, c.Id)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (c *Conversation) RenameConversation(db *sql.DB, name string) bool {\n\t_, err := globals.ExecDb(db, \"UPDATE conversation SET conversation_name = ? WHERE user_id = ? AND conversation_id = ?\", name, c.UserID, c.Id)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc DeleteAllConversations(db *sql.DB, user auth.User) error {\n\t_, err := globals.ExecDb(db, \"DELETE FROM conversation WHERE user_id = ?\", user.GetID(db))\n\treturn err\n}\n"
  },
  {
    "path": "manager/images.go",
    "content": "package manager\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/admin\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc ImagesRelayAPI(c *gin.Context) {\n\tif globals.CloseRelay {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"relay api is denied of access\"), \"access_denied_error\")\n\t\treturn\n\t}\n\n\tusername := utils.GetUserFromContext(c)\n\tif username == \"\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid api key\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tif utils.GetAgentFromContext(c) != \"api\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid agent\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tvar form RelayImageForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"invalid request body: %s\", err.Error()), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tprompt := strings.TrimSpace(form.Prompt)\n\tif prompt == \"\" {\n\t\tsendErrorResponse(c, fmt.Errorf(\"prompt is required\"), \"invalid_request_error\")\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\n\tcreated := time.Now().Unix()\n\n\tif strings.HasSuffix(form.Model, \"-official\") {\n\t\tform.Model = strings.TrimSuffix(form.Model, \"-official\")\n\t}\n\n\tcheck := auth.CanEnableModel(db, user, form.Model, []globals.Message{})\n\tif check != nil {\n\t\tsendErrorResponse(c, check, \"quota_exceeded_error\")\n\t\treturn\n\t}\n\n\tcreateRelayImageObject(c, form, prompt, created, user, supportRelayPlan())\n}\n\nfunc getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer) *adaptercommon.ChatProps {\n\treturn adaptercommon.CreateChatProps(&adaptercommon.ChatProps{\n\t\tModel:     form.Model,\n\t\tMessage:   messages,\n\t\tMaxTokens: utils.ToPtr(-1),\n\t}, buffer)\n}\n\nfunc getImageDataFromBuffer(buffer *utils.Buffer) (string, string) {\n\tcontent := buffer.Read()\n\n\turls := utils.ExtractImagesFromMarkdown(content)\n\tif len(urls) > 0 {\n\t\treturn urls[len(urls)-1], \"\"\n\t}\n\n\tbase64Data := utils.ExtractBase64FromMarkdown(content)\n\tif len(base64Data) > 0 {\n\t\treturn \"\", base64Data[len(base64Data)-1]\n\t}\n\n\treturn \"\", \"\"\n}\n\nfunc createRelayImageObject(c *gin.Context, form RelayImageForm, prompt string, created int64, user *auth.User, plan bool) {\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\n\tmessages := []globals.Message{\n\t\t{\n\t\t\tRole:    globals.User,\n\t\t\tContent: prompt,\n\t\t},\n\t}\n\n\tbuffer := utils.NewBuffer(form.Model, messages, channel.ChargeInstance.GetCharge(form.Model))\n\thit, err := channel.NewChatRequestWithCache(cache, buffer, auth.GetGroup(db, user), getImageProps(form, messages, buffer), func(data *globals.Chunk) error {\n\t\tbuffer.WriteChunk(data)\n\t\treturn nil\n\t})\n\n\tadmin.AnalyseRequest(form.Model, buffer, err)\n\tif err != nil {\n\t\tauth.RevertSubscriptionUsage(db, cache, user, form.Model)\n\t\tglobals.Warn(fmt.Sprintf(\"error from chat request api: %s (instance: %s, client: %s)\", err, form.Model, c.ClientIP()))\n\n\t\tsendErrorResponse(c, err)\n\t\treturn\n\t}\n\n\tif !hit {\n\t\tCollectQuota(c, user, buffer, plan, err)\n\t}\n\n\turl, b64Json := getImageDataFromBuffer(buffer)\n\tif url == \"\" && b64Json == \"\" {\n\t\tsendErrorResponse(c, fmt.Errorf(\"no image generated\"), \"image_generation_error\")\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, RelayImageResponse{\n\t\tCreated: created,\n\t\tData: []RelayImageData{\n\t\t\t{\n\t\t\t\tUrl:     url,\n\t\t\t\tB64Json: b64Json,\n\t\t\t},\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "manager/manager.go",
    "content": "package manager\n\nimport (\n\t\"chat/auth\"\n\t\"chat/manager/conversation\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype WebsocketAuthForm struct {\n\tToken string `json:\"token\" binding:\"required\"`\n\tId    int64  `json:\"id\" binding:\"required\"`\n\tRef   string `json:\"ref\"`\n}\n\nfunc ParseAuth(c *gin.Context, token string) *auth.User {\n\ttoken = strings.TrimSpace(token)\n\tif token == \"\" {\n\t\treturn nil\n\t}\n\n\tif strings.HasPrefix(token, \"Bearer \") {\n\t\ttoken = token[7:]\n\t}\n\n\tif strings.HasPrefix(token, \"sk-\") {\n\t\treturn auth.ParseApiKey(c, token)\n\t}\n\n\treturn auth.ParseToken(c, token)\n}\n\nfunc splitMessage(message string) (int, string, error) {\n\tparts := strings.SplitN(message, \":\", 2)\n\tif len(parts) == 2 {\n\t\tif id, err := strconv.Atoi(parts[0]); err == nil {\n\t\t\treturn id, parts[1], nil\n\t\t}\n\t}\n\n\treturn 0, message, fmt.Errorf(\"message type error\")\n}\n\nfunc getId(message string) (int, error) {\n\tif id, err := strconv.Atoi(message); err == nil {\n\t\treturn id, nil\n\t}\n\n\treturn 0, fmt.Errorf(\"message type error\")\n}\n\nfunc ChatAPI(c *gin.Context) {\n\tvar conn *utils.WebSocket\n\tif conn = utils.NewWebsocket(c, false); conn == nil {\n\t\treturn\n\t}\n\tdefer conn.DeferClose()\n\n\tdb := utils.GetDBFromContext(c)\n\n\tform, err := utils.ReadForm[WebsocketAuthForm](conn)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tuser := ParseAuth(c, form.Token)\n\tauthenticated := user != nil\n\n\tid := auth.GetId(db, user)\n\n\tinstance := conversation.ExtractConversation(db, user, form.Id, form.Ref)\n\thash := fmt.Sprintf(\":chatthread:%s\", utils.Md5Encrypt(utils.Multi(\n\t\tauthenticated,\n\t\tstrconv.FormatInt(id, 10),\n\t\tc.ClientIP(),\n\t)))\n\n\tbuf := NewConnection(conn, authenticated, hash, 10)\n\tbuf.Handle(func(form *conversation.FormMessage) error {\n\t\tswitch form.Type {\n\t\tcase ChatType:\n\t\t\tif instance.HandleMessage(db, form) {\n\t\t\t\tresponse := ChatHandler(buf, user, instance, false)\n\t\t\t\tinstance.SaveResponse(db, response)\n\t\t\t}\n\t\tcase StopType:\n\t\t\tbreak\n\t\tcase ShareType:\n\t\t\tinstance.LoadSharing(db, form.Message)\n\t\tcase RestartType:\n\t\t\t// reset the params if set\n\t\t\tinstance.ApplyParam(form)\n\n\t\t\tresponse := ChatHandler(buf, user, instance, true)\n\t\t\tinstance.SaveResponse(db, response)\n\t\tcase MaskType:\n\t\t\tinstance.LoadMask(form.Message)\n\t\tcase EditType:\n\t\t\tif id, message, err := splitMessage(form.Message); err == nil {\n\t\t\t\tinstance.EditMessage(id, message)\n\t\t\t\tinstance.SaveConversation(db)\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase RemoveType:\n\t\t\tid, err := getId(form.Message)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tinstance.RemoveMessage(id)\n\t\t\tinstance.SaveConversation(db)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "manager/relay.go",
    "content": "package manager\n\nimport (\n\t\"chat/admin\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n)\n\nfunc ModelAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, globals.V1ListModels)\n}\n\nfunc MarketAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, admin.MarketInstance.GetModels())\n}\n\nfunc ChargeAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, channel.ChargeInstance.ListRules())\n}\n\nfunc PlanAPI(c *gin.Context) {\n\tc.JSON(http.StatusOK, channel.PlanInstance.GetPlans())\n}\n\nfunc sendErrorResponse(c *gin.Context, err error, types ...string) {\n\tvar errType string\n\tif len(types) > 0 {\n\t\terrType = types[0]\n\t} else {\n\t\terrType = \"chatnio_api_error\"\n\t}\n\n\tc.JSON(http.StatusServiceUnavailable, RelayErrorResponse{\n\t\tError: TranshipmentError{\n\t\t\tMessage: err.Error(),\n\t\t\tType:    errType,\n\t\t},\n\t})\n}\n\nfunc abortWithErrorResponse(c *gin.Context, err error, types ...string) {\n\tsendErrorResponse(c, err, types...)\n\tc.Abort()\n}\n"
  },
  {
    "path": "manager/router.go",
    "content": "package manager\n\nimport (\n\t\"chat/manager/broadcast\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc Register(app *gin.RouterGroup) {\n\tapp.GET(\"/chat\", ChatAPI)\n\tapp.GET(\"/v1/models\", ModelAPI)\n\tapp.GET(\"/v1/market\", MarketAPI)\n\tapp.GET(\"/v1/charge\", ChargeAPI)\n\tapp.GET(\"/v1/plans\", PlanAPI)\n\tapp.GET(\"/dashboard/billing/usage\", GetBillingUsage)\n\tapp.GET(\"/dashboard/billing/subscription\", GetSubscription)\n\tapp.POST(\"/v1/chat/completions\", ChatRelayAPI)\n\tapp.POST(\"/v1/images/generations\", ImagesRelayAPI)\n\tapp.POST(\"/v1/videos\", VideosRelayAPI)\n\tapp.GET(\"/v1/videos/:id/content\", VideosContentRelayAPI)\n\n\tbroadcast.Register(app)\n}\n"
  },
  {
    "path": "manager/types.go",
    "content": "package manager\n\nimport (\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n)\n\ntype Message struct {\n\tRole         string                `json:\"role,omitempty\"`\n\tContent      interface{}           `json:\"content\"`\n\tName         *string               `json:\"name,omitempty\"`\n\tFunctionCall *globals.FunctionCall `json:\"function_call,omitempty\"` // only `function` role\n\tToolCallId   *string               `json:\"tool_call_id,omitempty\"`  // only `tool` role\n\tToolCalls    *globals.ToolCalls    `json:\"tool_calls,omitempty\"`    // only `assistant` role\n}\n\ntype ImageUrl struct {\n\tUrl    string  `json:\"url\"`\n\tDetail *string `json:\"detail,omitempty\"`\n}\n\ntype MessageContent struct {\n\tType     string    `json:\"type\"`\n\tText     *string   `json:\"text,omitempty\"`\n\tImageUrl *ImageUrl `json:\"image_url,omitempty\"`\n}\n\ntype MessageContents []MessageContent\n\ntype RelayForm struct {\n\tModel             string    `json:\"model\" binding:\"required\"`\n\tMessages          []Message `json:\"messages\" binding:\"required\"`\n\tStream            bool      `json:\"stream\"`\n\tMaxTokens         *int      `json:\"max_tokens\"`\n\tPresencePenalty   *float32  `json:\"presence_penalty\"`\n\tFrequencyPenalty  *float32  `json:\"frequency_penalty\"`\n\tRepetitionPenalty *float32  `json:\"repetition_penalty\"`\n\tTemperature       *float32  `json:\"temperature\"`\n\tTopP              *float32  `json:\"top_p\"`\n\tTopK              *int      `json:\"top_k\"`\n\tTools             *globals.FunctionTools\n\tToolChoice        *interface{}\n\tOfficial          bool `json:\"official\"`\n}\n\ntype Choice struct {\n\tIndex        int             `json:\"index\"`\n\tMessage      globals.Message `json:\"message\"`\n\tFinishReason string          `json:\"finish_reason\"`\n}\n\ntype StreamMessage struct {\n\tRole         *string               `json:\"role\"`\n\tContent      string                `json:\"content\"`\n\tName         *string               `json:\"name,omitempty\"`\n\tFunctionCall *globals.FunctionCall `json:\"function_call,omitempty\"` // only `function` role\n\tToolCallId   *string               `json:\"tool_call_id,omitempty\"`  // only `tool` role\n\tToolCalls    *globals.ToolCalls    `json:\"tool_calls,omitempty\"`    // only `assistant` role\n}\n\ntype Usage struct {\n\tPromptTokens     int `json:\"prompt_tokens\"`\n\tCompletionTokens int `json:\"completion_tokens\"`\n\tTotalTokens      int `json:\"total_tokens\"`\n}\n\ntype RelayResponse struct {\n\tId      string   `json:\"id\"`\n\tObject  string   `json:\"object\"`\n\tCreated int64    `json:\"created\"`\n\tModel   string   `json:\"model\"`\n\tChoices []Choice `json:\"choices\"`\n\tUsage   Usage    `json:\"usage\"`\n\tQuota   *float32 `json:\"quota,omitempty\"`\n}\n\ntype ChoiceDelta struct {\n\tIndex        int         `json:\"index\"`\n\tDelta        Message     `json:\"delta\"`\n\tFinishReason interface{} `json:\"finish_reason\"`\n}\n\ntype RelayStreamResponse struct {\n\tId      string        `json:\"id\"`\n\tObject  string        `json:\"object\"`\n\tCreated int64         `json:\"created\"`\n\tModel   string        `json:\"model\"`\n\tChoices []ChoiceDelta `json:\"choices\"`\n\tUsage   Usage         `json:\"usage\"`\n\tQuota   *float32      `json:\"quota,omitempty\"`\n\tError   error         `json:\"error,omitempty\"`\n}\n\ntype RelayErrorResponse struct {\n\tError TranshipmentError `json:\"error\"`\n}\n\ntype TranshipmentError struct {\n\tMessage string `json:\"message\"`\n\tType    string `json:\"type\"`\n}\n\ntype RelayImageForm struct {\n\tModel  string `json:\"model\"`\n\tPrompt string `json:\"prompt\"`\n\tN      *int   `json:\"n,omitempty\"`\n}\n\ntype RelayImageData struct {\n\tUrl     string `json:\"url,omitempty\"`\n\tB64Json string `json:\"b64_json,omitempty\"`\n}\n\ntype RelayImageResponse struct {\n\tCreated int64            `json:\"created\"`\n\tData    []RelayImageData `json:\"data\"`\n}\n\ntype RelayVideoForm struct {\n\tModel          string  `json:\"model\"`\n\tPrompt         string  `json:\"prompt\" binding:\"required\"`\n\tSeconds        *string `json:\"seconds,omitempty\"`\n\tSize           *string `json:\"size,omitempty\"`\n\tInputReference *string `json:\"input_reference,omitempty\"`\n}\n\ntype RelayVideoError struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype RelayVideoJob struct {\n\tCompletedAt        *int64           `json:\"completed_at,omitempty\"`\n\tCreatedAt          int64            `json:\"created_at\"`\n\tError              *RelayVideoError `json:\"error,omitempty\"`\n\tExpiresAt          *int64           `json:\"expires_at,omitempty\"`\n\tId                 string           `json:\"id\"`\n\tModel              string           `json:\"model\"`\n\tObject             string           `json:\"object\"`\n\tProgress           *int             `json:\"progress,omitempty\"`\n\tPrompt             string           `json:\"prompt\"`\n\tRemixedFromVideoId *string          `json:\"remixed_from_video_id,omitempty\"`\n\tSeconds            string           `json:\"seconds\"`\n\tSize               string           `json:\"size\"`\n\tStatus             string           `json:\"status\"`\n}\n\nfunc transformContent(content interface{}) string {\n\tswitch v := content.(type) {\n\tcase string:\n\t\treturn v\n\tdefault:\n\t\tvar result string\n\t\tdata := utils.MapToStruct[MessageContents](v)\n\t\tif data == nil || len(*data) == 0 {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tfor _, v := range *data {\n\t\t\tif v.Text != nil {\n\t\t\t\tresult += *v.Text\n\t\t\t}\n\n\t\t\tif v.ImageUrl != nil {\n\t\t\t\tresult += fmt.Sprintf(\" %s \", v.ImageUrl.Url)\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n}\n\nfunc transform(m []Message) []globals.Message {\n\tvar messages []globals.Message\n\tfor _, v := range m {\n\t\tmessages = append(messages, globals.Message{\n\t\t\tRole:         v.Role,\n\t\t\tContent:      transformContent(v.Content),\n\t\t\tName:         v.Name,\n\t\t\tFunctionCall: v.FunctionCall,\n\t\t\tToolCallId:   v.ToolCallId,\n\t\t\tToolCalls:    v.ToolCalls,\n\t\t})\n\t}\n\treturn messages\n}\n"
  },
  {
    "path": "manager/usage.go",
    "content": "package manager\n\nimport (\n\t\"chat/auth\"\n\t\"chat/utils\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n)\n\ntype BillingResponse struct {\n\tObject     string  `json:\"object\"`\n\tTotalUsage float32 `json:\"total_usage\"`\n}\n\ntype SubscriptionResponse struct {\n\tObject             string  `json:\"object\"`\n\tSoftLimit          int64   `json:\"soft_limit\"`\n\tHardLimit          int64   `json:\"hard_limit\"`\n\tSystemHardLimit    int64   `json:\"system_hard_limit\"`\n\tSoftLimitUSD       float32 `json:\"soft_limit_usd\"`\n\tHardLimitUSD       float32 `json:\"hard_limit_usd\"`\n\tSystemHardLimitUSD float32 `json:\"system_hard_limit_usd\"`\n}\n\nfunc GetBillingUsage(c *gin.Context) {\n\tuser := auth.RequireAuth(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tusage := user.GetUsedQuota(db)\n\n\tc.JSON(http.StatusOK, BillingResponse{\n\t\tObject:     \"list\",\n\t\tTotalUsage: usage,\n\t})\n}\n\nfunc GetSubscription(c *gin.Context) {\n\tuser := auth.RequireAuth(c)\n\tif user == nil {\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tquota := user.GetQuota(db)\n\tused := user.GetUsedQuota(db)\n\ttotal := quota + used\n\n\tc.JSON(http.StatusOK, SubscriptionResponse{\n\t\tObject:             \"billing_subscription\",\n\t\tSoftLimit:          int64(quota * 100),\n\t\tHardLimit:          int64(total * 100),\n\t\tSystemHardLimit:    100000000,\n\t\tSoftLimitUSD:       quota / 7.3 / 10,\n\t\tHardLimitUSD:       total / 7.3 / 10,\n\t\tSystemHardLimitUSD: 1000000,\n\t})\n}\n"
  },
  {
    "path": "manager/videos.go",
    "content": "package manager\n\nimport (\n\tadaptercommon \"chat/adapter/common\"\n\t\"chat/admin/analysis\"\n\t\"chat/auth\"\n\t\"chat/channel\"\n\t\"chat/globals\"\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc VideosRelayAPI(c *gin.Context) {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from chat completions api: %s (client: %s)\\n%s\",\n\t\t\t\terr, c.ClientIP(), stack,\n\t\t\t))\n\t\t}\n\t}()\n\n\tif globals.CloseRelay {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"relay api is denied of access\"), \"access_denied_error\")\n\t\treturn\n\t}\n\n\tusername := utils.GetUserFromContext(c)\n\tif username == \"\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid api key\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tif utils.GetAgentFromContext(c) != \"api\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid agent\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tvar form RelayVideoForm\n\tif err := c.ShouldBindJSON(&form); err != nil {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"invalid request body: %s\", err.Error()), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tprompt := strings.TrimSpace(form.Prompt)\n\tif prompt == \"\" {\n\t\tsendErrorResponse(c, fmt.Errorf(\"prompt is required\"), \"invalid_request_error\")\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\tcache := utils.GetCacheFromContext(c)\n\tuser := &auth.User{\n\t\tUsername: username,\n\t}\n\n\tform.Model = strings.TrimSuffix(form.Model, \"-official\")\n\n\tif form.Model == \"\" {\n\t\tform.Model = globals.Sora2\n\t}\n\n\tmessages := []globals.Message{\n\t\t{Role: globals.User, Content: prompt},\n\t}\n\tcheck, plan := checkEnableState(db, cache, user, form.Model, messages)\n\tif check != nil {\n\t\tsendErrorResponse(c, check, \"quota_exceeded_error\")\n\t\treturn\n\t}\n\n\tbuffer := utils.NewBuffer(form.Model, messages, channel.ChargeInstance.GetCharge(form.Model))\n\tbuffer.SetTokenName(globals.ApiTokenType)\n\n\tprops := adaptercommon.CreateVideoProps(&adaptercommon.VideoProps{\n\t\tModel:          form.Model,\n\t\tPrompt:         prompt,\n\t\tSeconds:        form.Seconds,\n\t\tSize:           form.Size,\n\t\tInputReference: form.InputReference,\n\t})\n\tprops.User = auth.GetUsernameString(db, user)\n\n\tgroup := auth.GetGroup(db, user)\n\n\tvar jobJson string\n\thit, err := channel.NewVideoRequestWithCache(cache, buffer, group, props, func(data *globals.Chunk) error {\n\t\tif data != nil {\n\t\t\tjobJson = data.Content\n\t\t}\n\t\treturn nil\n\t})\n\n\tanalysis.AnalyseRequest(form.Model, props.User, buffer, err)\n\tif err != nil {\n\t\tauth.RevertSubscriptionUsage(db, cache, user, form.Model)\n\t\tglobals.Warn(fmt.Sprintf(\"error from video request api: %s (instance: %s, client: %s)\", err, form.Model, c.ClientIP()))\n\t\tsendErrorResponse(c, err)\n\t\treturn\n\t}\n\n\tif !hit {\n\t\tCollectQuota(c, user, buffer, plan, err)\n\t}\n\n\tjob, jerr := utils.UnmarshalString[RelayVideoJob](jobJson)\n\tif jerr != nil {\n\t\tsendErrorResponse(c, fmt.Errorf(\"invalid job response: %s\", jerr.Error()))\n\t\treturn\n\t}\n\n\tif job.Id != \"\" {\n\t\tuserId := user.GetID(db)\n\t\tvar conversationId int64\n\t\terr := globals.QueryRowDb(db, `\n\t\t\tSELECT conversation_id\n\t\t\tFROM conversation\n\t\t\tWHERE user_id = ? AND model = ? AND (task_id IS NULL OR task_id = '')\n\t\t\tORDER BY updated_at DESC\n\t\t\tLIMIT 1\n\t\t`, userId, form.Model).Scan(&conversationId)\n\t\tif err == nil && conversationId > 0 {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[video] saving task_id %s to conversation %d for user %d\", job.Id, conversationId, userId))\n\t\t\t_, err := globals.ExecDb(db, `\n\t\t\t\tUPDATE conversation\n\t\t\t\tSET task_id = ?\n\t\t\t\tWHERE user_id = ? AND conversation_id = ?\n\t\t\t`, job.Id, userId, conversationId)\n\t\t\tif err != nil {\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"failed to save task_id to conversation: %s\", err.Error()))\n\t\t\t} else {\n\t\t\t\tglobals.Debug(fmt.Sprintf(\"[video] successfully saved task_id %s to conversation %d\", job.Id, conversationId))\n\t\t\t}\n\t\t} else {\n\t\t\tif err != nil {\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"[video] failed to find conversation to update task_id: %s\", err.Error()))\n\t\t\t} else {\n\t\t\t\tglobals.Warn(fmt.Sprintf(\"[video] conversation_id is 0 or invalid, cannot update task_id\"))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tglobals.Warn(fmt.Sprintf(\"[video] job.Id is empty, cannot save task_id\"))\n\t}\n\n\tc.JSON(http.StatusOK, job)\n}\n\nfunc VideosContentRelayAPI(c *gin.Context) {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"caught panic from videos content api: %s (client: %s)\\n%s\",\n\t\t\t\terr, c.ClientIP(), stack,\n\t\t\t))\n\t\t}\n\t}()\n\n\tif globals.CloseRelay {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"relay api is denied of access\"), \"access_denied_error\")\n\t\treturn\n\t}\n\n\tdb := utils.GetDBFromContext(c)\n\n\tusername := utils.GetUserFromContext(c)\n\tif username == \"\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid api key\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tagent := utils.GetAgentFromContext(c)\n\tif agent != \"api\" && agent != \"token\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"access denied for invalid agent\"), \"authentication_error\")\n\t\treturn\n\t}\n\n\tid := strings.TrimSpace(c.Param(\"id\"))\n\tif id == \"\" {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"video id is required\"), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tuser := &auth.User{Username: username}\n\tuserId := user.GetID(db)\n\tvar jobModel interface{}\n\n\terr := globals.QueryRowDb(db, `\n\t\tSELECT model\n\t\tFROM conversation\n\t\tWHERE user_id = ? AND task_id = ?\n\t\tLIMIT 1\n\t`, userId, id).Scan(&jobModel)\n\n\tif err != nil {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"cannot find video job for video id %s\", id), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tvar model string\n\tif value, ok := jobModel.([]byte); ok {\n\t\tmodel = string(value)\n\t} else if str, ok := jobModel.(string); ok {\n\t\tmodel = str\n\t} else {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"cannot parse model from conversation for video id %s\", id), \"invalid_request_error\")\n\t\treturn\n\t}\n\tgroup := auth.GetGroup(db, user)\n\tticker := channel.ConduitInstance.GetTicker(model, group)\n\tif ticker == nil || ticker.IsEmpty() {\n\t\tabortWithErrorResponse(c, fmt.Errorf(\"cannot find channel for model %s\", model), \"invalid_request_error\")\n\t\treturn\n\t}\n\n\tvar lastErr error\n\tfor !ticker.IsDone() {\n\t\tch := ticker.Next()\n\t\tif ch == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tendpoint := ch.GetEndpoint()\n\t\tsecret := ch.GetRandomSecret()\n\t\turi := fmt.Sprintf(\"%s/v1/videos/%s/content\", endpoint, id)\n\n\t\theaders := map[string]string{\n\t\t\t\"Authorization\": fmt.Sprintf(\"Bearer %s\", secret),\n\t\t}\n\n\t\tdata, err := utils.HttpRaw(uri, http.MethodGet, headers, nil, []globals.ProxyConfig{ch.GetProxy()})\n\t\tif err != nil || data == nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tcontentType := \"video/mp4\"\n\t\tc.Data(http.StatusOK, contentType, data)\n\t\treturn\n\t}\n\n\tif lastErr == nil {\n\t\tlastErr = fmt.Errorf(\"failed to fetch video content\")\n\t}\n\tsendErrorResponse(c, lastErr)\n}\n"
  },
  {
    "path": "middleware/auth.go",
    "content": "package middleware\n\nimport (\n\t\"chat/auth\"\n\t\"chat/utils\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc ProcessToken(c *gin.Context, token string) *auth.User {\n\tif user := auth.ParseToken(c, token); user != nil {\n\t\tc.Set(\"auth\", true)\n\t\tc.Set(\"user\", user.Username)\n\t\tc.Set(\"agent\", \"token\")\n\t\treturn user\n\t}\n\n\tc.Set(\"auth\", false)\n\tc.Set(\"user\", \"\")\n\tc.Set(\"agent\", \"\")\n\treturn nil\n}\n\nfunc ProcessKey(c *gin.Context, key string) *auth.User {\n\taddr := c.ClientIP()\n\tcache := utils.GetCacheFromContext(c)\n\n\tif utils.IsInBlackList(cache, addr) {\n\t\tc.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\n\t\t\t\"code\":    403,\n\t\t\t\"message\": \"ip in black list\",\n\t\t})\n\t\treturn nil\n\t}\n\n\tif user := auth.ParseApiKey(c, key); user != nil {\n\t\tc.Set(\"auth\", true)\n\t\tc.Set(\"user\", user.Username)\n\t\tc.Set(\"agent\", \"api\")\n\t\treturn user\n\t}\n\n\tutils.IncrIP(cache, addr)\n\tc.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\n\t\t\"code\":    401,\n\t\t\"message\": \"Access denied. Please provide correct api key.\",\n\t})\n\treturn nil\n}\n\nfunc ProcessAuthorization(c *gin.Context) *auth.User {\n\tk := strings.TrimSpace(c.GetHeader(\"Authorization\"))\n\tif k != \"\" {\n\t\tif strings.HasPrefix(k, \"Bearer \") {\n\t\t\tk = strings.TrimPrefix(k, \"Bearer \")\n\t\t}\n\n\t\tif strings.HasPrefix(k, \"sk-\") {\n\t\t\t// api agent\n\t\t\treturn ProcessKey(c, k)\n\t\t} else {\n\t\t\t// token agent\n\t\t\treturn ProcessToken(c, k)\n\t\t}\n\t}\n\n\tc.Set(\"auth\", false)\n\tc.Set(\"user\", \"\")\n\tc.Set(\"agent\", \"\")\n\treturn nil\n}\n\nfunc AuthMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tpath := c.Request.URL.Path\n\t\tinstance := ProcessAuthorization(c)\n\n\t\tif viper.GetBool(\"serve_static\") {\n\t\t\tif !strings.HasPrefix(path, \"/api\") {\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tpath = strings.TrimPrefix(path, \"/api\")\n\t\t\t}\n\t\t}\n\n\t\tdb := utils.GetDBFromContext(c)\n\n\t\tadmin := instance != nil && instance.IsAdmin(db)\n\t\tc.Set(\"admin\", admin)\n\t\tif strings.HasPrefix(path, \"/admin\") {\n\t\t\tif !admin {\n\t\t\t\tc.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\n\t\t\t\t\t\"code\":    401,\n\t\t\t\t\t\"message\": \"Access denied.\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "middleware/builtins.go",
    "content": "package middleware\n\nimport (\n\t\"database/sql\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc BuiltinMiddleWare(db *sql.DB, cache *redis.Client) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Set(\"db\", db)\n\t\tc.Set(\"cache\", cache)\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "middleware/cors.go",
    "content": "package middleware\n\nimport (\n\t\"chat/globals\"\n\t\"github.com/gin-gonic/gin\"\n\t\"net/http\"\n)\n\nfunc CORSMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\torigin := c.Request.Header.Get(\"Origin\")\n\t\tif globals.OriginIsOpen(c) || globals.OriginIsAllowed(origin) {\n\t\t\tc.Writer.Header().Set(\"Access-Control-Allow-Origin\", origin)\n\t\t\tc.Writer.Header().Set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\")\n\t\t\tc.Writer.Header().Set(\"Access-Control-Allow-Headers\", \"Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Auth-Token, X-Requested-With, X-Forwarded-For, X-Real-IP, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port\")\n\t\t\tc.Writer.Header().Set(\"Access-Control-Allow-Credentials\", \"true\")\n\n\t\t\tif c.Request.Method == \"OPTIONS\" {\n\t\t\t\tc.Writer.Header().Set(\"Access-Control-Max-Age\", \"7200\")\n\t\t\t\tc.AbortWithStatus(http.StatusOK)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "middleware/middleware.go",
    "content": "package middleware\n\nimport (\n\t\"chat/connection\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc RegisterMiddleware(app *gin.Engine) func() {\n\tdb := connection.InitMySQLSafe()\n\tcache := connection.InitRedisSafe()\n\n\tapp.Use(CORSMiddleware())\n\tapp.Use(BuiltinMiddleWare(db, cache))\n\tapp.Use(ThrottleMiddleware())\n\tapp.Use(AuthMiddleware())\n\n\treturn func() {\n\t\tdb.Close()\n\t\tcache.Close()\n\t}\n}\n"
  },
  {
    "path": "middleware/throttle.go",
    "content": "package middleware\n\nimport (\n\t\"chat/utils\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/spf13/viper\"\n\t\"strings\"\n)\n\ntype Limiter struct {\n\tDuration int\n\tCount    int64\n}\n\nfunc (l *Limiter) RateLimit(client *redis.Client, ip string, path string) (bool, error) {\n\tkey := fmt.Sprintf(\"rate:%s:%s\", path, ip)\n\trate, err := utils.IncrWithLimit(client, key, 1, l.Count, int64(l.Duration))\n\treturn !rate, err\n}\n\nvar limits = map[string]Limiter{\n\t\"/login\":        {Duration: 10, Count: 20},\n\t\"/register\":     {Duration: 120, Count: 10},\n\t\"/verify\":       {Duration: 120, Count: 10},\n\t\"/reset\":        {Duration: 120, Count: 10},\n\t\"/apikey\":       {Duration: 1, Count: 2},\n\t\"/resetkey\":     {Duration: 3600, Count: 3},\n\t\"/package\":      {Duration: 1, Count: 2},\n\t\"/quota\":        {Duration: 1, Count: 2},\n\t\"/buy\":          {Duration: 1, Count: 2},\n\t\"/subscribe\":    {Duration: 1, Count: 2},\n\t\"/subscription\": {Duration: 1, Count: 2},\n\t\"/chat\":         {Duration: 1, Count: 5},\n\t\"/conversation\": {Duration: 1, Count: 5},\n\t\"/invite\":       {Duration: 7200, Count: 20},\n\t\"/redeem\":       {Duration: 1200, Count: 60},\n\t\"/dashboard\":    {Duration: 1, Count: 5},\n\t\"/card\":         {Duration: 1, Count: 5},\n\t\"/generation\":   {Duration: 1, Count: 5},\n\t\"/article\":      {Duration: 1, Count: 5},\n\t\"/broadcast\":    {Duration: 1, Count: 2},\n}\n\nfunc GetPrefixMap[T comparable](s string, p map[string]T) *T {\n\tif viper.GetBool(\"serve_static\") {\n\t\ts = strings.TrimPrefix(s, \"/api\")\n\t}\n\n\tfor k, v := range p {\n\t\tif strings.HasPrefix(s, k) {\n\t\t\treturn &v\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ThrottleMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tip := c.ClientIP()\n\t\tpath := c.Request.URL.Path\n\t\tcache := utils.GetCacheFromContext(c)\n\n\t\tlimiter := GetPrefixMap[Limiter](path, limits)\n\t\tif limiter != nil {\n\t\t\trate, err := limiter.RateLimit(cache, ip, path)\n\n\t\t\tif err != nil {\n\t\t\t\tc.JSON(200, gin.H{\n\t\t\t\t\t\"status\": false,\n\t\t\t\t\t\"reason\": err.Error(),\n\t\t\t\t\t\"error\":  err.Error(),\n\t\t\t\t})\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif rate {\n\t\t\t\tc.JSON(200, gin.H{\n\t\t\t\t\t\"status\": false,\n\t\t\t\t\t\"reason\": \"You have sent too many requests. Please try again later.\",\n\t\t\t\t\t\"error\":  \"request_throttled\",\n\t\t\t\t})\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "migration/3.6.sql",
    "content": "DELIMITER $$\n\nCREATE PROCEDURE Migration()\nBEGIN\n    DECLARE _count INT;\n\n    SELECT COUNT(*) INTO _count\n    FROM INFORMATION_SCHEMA.COLUMNS\n    WHERE TABLE_SCHEMA = DATABASE()\n      AND TABLE_NAME = 'subscription'\n      AND COLUMN_NAME = 'level';\n\n    IF _count = 0 THEN\n        ALTER TABLE subscription ADD COLUMN level INT DEFAULT 1;\n        UPDATE subscription SET level = 3;\n    END IF;\n\nEND $$\n\nDELIMITER ;\n\nCALL Migration();\n"
  },
  {
    "path": "migration/3.8.sql",
    "content": "ALTER TABLE auth\n    ADD COLUMN email VARCHAR(255) UNIQUE,\n    ADD COLUMN is_banned BOOLEAN DEFAULT FALSE;\n"
  },
  {
    "path": "nginx.conf",
    "content": "server\n{\n    # this is a sample configuration for nginx\n    listen 80;\n\n    location ~ ^/(\\.user.ini|\\.htaccess|\\.git|\\.svn|\\.project|LICENSE|README.md|package.json|package-lock.json|\\.env) {\n        return 404;\n    }\n\n    if ( $uri ~ \"^/\\.well-known/.*\\.(php|jsp|py|js|css|lua|ts|go|zip|tar\\.gz|rar|7z|sql|bak)$\" ) {\n        return 403;\n    }\n\n    location ~ /purge(/.*) {\n        proxy_cache_purge cache_one 127.0.0.1$request_uri$is_args$args;\n    }\n\n    location / {\n        # if you are using compile deployment mode, please use the http://localhost:8094 instead\n        proxy_pass http://127.0.0.1:8000;\n        proxy_set_header Host 127.0.0.1:$server_port;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header REMOTE-HOST $remote_addr;\n        add_header X-Cache $upstream_cache_status;\n        proxy_set_header X-Host $host:$server_port;\n        proxy_set_header X-Scheme $scheme;\n        proxy_connect_timeout 30s;\n        proxy_read_timeout 86400s;\n        proxy_send_timeout 30s;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n    }\n\n    access_log  /www/wwwlogs/chatnio.log;\n    error_log  /www/wwwlogs/chatnio.error.log;\n}\n"
  },
  {
    "path": "utils/base.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/goccy/go-json\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc Intn(n int) int {\n\tsource := rand.NewSource(time.Now().UnixNano())\n\tr := rand.New(source)\n\treturn r.Intn(n)\n}\n\nfunc Intn64(n int64) int64 {\n\tsource := rand.NewSource(time.Now().UnixNano())\n\tr := rand.New(source)\n\treturn r.Int63n(n)\n}\n\nfunc IntnSeed(n int, seed int) int {\n\t// unix nano is the same if called in the same nanosecond, so we need to add another random seed\n\tsource := rand.NewSource(time.Now().UnixNano() + int64(seed))\n\tr := rand.New(source)\n\treturn r.Intn(n)\n}\n\nfunc IntnSeq(n int, len int) (res []int) {\n\tfor i := 0; i < len; i++ {\n\t\tres = append(res, IntnSeed(n, i))\n\t}\n\n\treturn res\n}\n\nfunc Sum[T int | int64 | float32 | float64](arr []T) T {\n\tvar res T\n\tfor _, v := range arr {\n\t\tres += v\n\t}\n\treturn res\n}\n\nfunc Contains[T comparable](value T, slice []T) bool {\n\tfor _, item := range slice {\n\t\tif item == value {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc ToPtr[T any](value T) *T {\n\treturn &value\n}\n\nfunc TryGet[T any](arr []T, index int) T {\n\tif index >= len(arr) {\n\t\treturn arr[0]\n\t}\n\treturn arr[index]\n}\n\nfunc Debug[T any](v T) T {\n\tfmt.Println(v)\n\treturn v\n}\n\nfunc Insert[T any](arr []T, index int, value T) []T {\n\tarr = append(arr, value)\n\tcopy(arr[index+1:], arr[index:])\n\tarr[index] = value\n\treturn arr\n}\n\nfunc InsertSlice[T any](arr []T, index int, value []T) []T {\n\tarr = append(arr, value...)\n\tcopy(arr[index+len(value):], arr[index:])\n\tcopy(arr[index:], value)\n\treturn arr\n}\n\nfunc Collect[T any](arr ...[]T) []T {\n\tres := make([]T, 0)\n\n\tfor _, v := range arr {\n\t\tres = append(res, v...)\n\t}\n\treturn res\n}\n\nfunc Append[T any](arr []T, value T) []T {\n\treturn append(arr, value)\n}\n\nfunc AppendSlice[T any](arr []T, value []T) []T {\n\treturn append(arr, value...)\n}\n\nfunc Prepend[T any](arr []T, value T) []T {\n\treturn append([]T{value}, arr...)\n}\n\nfunc PrependSlice[T any](arr []T, value []T) []T {\n\treturn append(value, arr...)\n}\n\nfunc Remove[T any](arr []T, index int) []T {\n\treturn append(arr[:index], arr[index+1:]...)\n}\n\nfunc RemoveSlice[T any](arr []T, index int, length int) []T {\n\treturn append(arr[:index], arr[index+length:]...)\n}\n\nfunc ToJson(value interface{}) string {\n\tif res, err := json.Marshal(value); err == nil {\n\t\treturn string(res)\n\t} else {\n\t\treturn \"{}\"\n\t}\n}\n\nfunc UnmarshalJson[T any](value string) T {\n\tvar res T\n\tif err := json.Unmarshal([]byte(value), &res); err == nil {\n\t\treturn res\n\t} else {\n\t\treturn res\n\t}\n}\n\nfunc DeepCopy[T any](value T) T {\n\treturn UnmarshalJson[T](ToJson(value))\n}\n\nfunc GetSegment[T any](arr []T, length int) []T {\n\tif length > len(arr) {\n\t\treturn arr\n\t}\n\treturn arr[:length]\n}\n\nfunc GetSegmentString(arr string, length int) string {\n\tif length > len(arr) {\n\t\treturn arr\n\t}\n\treturn arr[:length]\n}\n\nfunc GetLatestSegment[T any](arr []T, length int) []T {\n\tif length > len(arr) {\n\t\treturn arr\n\t}\n\treturn arr[len(arr)-length:]\n}\n\nfunc Reverse[T any](arr []T) []T {\n\tfor i := 0; i < len(arr)/2; i++ {\n\t\tarr[i], arr[len(arr)-i-1] = arr[len(arr)-i-1], arr[i]\n\t}\n\treturn arr\n}\n\nfunc Multi[T comparable](condition bool, tval, fval T) T {\n\tif condition {\n\t\treturn tval\n\t} else {\n\t\treturn fval\n\t}\n}\n\nfunc MultiF[T comparable](condition bool, tval func() T, fval T) T {\n\tif condition {\n\t\treturn tval()\n\t} else {\n\t\treturn fval\n\t}\n}\n\nfunc InsertChannel[T any](ch chan T, value T, index int) {\n\tvar arr []T\n\tfor i := 0; i < len(ch); i++ {\n\t\tarr = append(arr, <-ch)\n\t}\n\tarr = Insert(arr, index, value)\n\tfor _, v := range arr {\n\t\tch <- v\n\t}\n}\n\nfunc Sort[T any](arr []T, compare func(a, b T) bool) []T {\n\tif len(arr) <= 1 {\n\t\treturn arr\n\t}\n\n\tvar result []T\n\tvar temp []T\n\tvar hasFirst bool\n\tvar first T\n\n\tfor _, item := range arr {\n\t\tif !hasFirst {\n\t\t\tfirst = item\n\t\t\thasFirst = true\n\t\t\tcontinue\n\t\t}\n\n\t\tif compare(item, first) {\n\t\t\ttemp = append(temp, item)\n\t\t} else {\n\t\t\tresult = append(result, first)\n\t\t\tresult = append(result, Sort(temp, compare)...)\n\t\t\tfirst = item\n\t\t\ttemp = []T{}\n\t\t}\n\t}\n\n\tif len(temp) > 0 {\n\t\tresult = append(result, first)\n\t\tresult = append(result, Sort(temp, compare)...)\n\t} else if hasFirst {\n\t\tresult = append(result, first)\n\t}\n\n\treturn result\n}\n\nfunc Each[T any, U any](arr []T, f func(T) U) []U {\n\tvar res []U\n\tfor _, v := range arr {\n\t\tres = append(res, f(v))\n\t}\n\treturn res\n}\n\nfunc EachObject[T any, V any](arr []T, f func(T) (string, V)) map[string]V {\n\tres := make(map[string]V)\n\tfor _, v := range arr {\n\t\tkey, val := f(v)\n\t\tres[key] = val\n\t}\n\treturn res\n}\n\nfunc EachNotNil[T any, U any](arr []T, f func(T) *U) []U {\n\tvar res []U\n\tfor _, v := range arr {\n\t\tif val := f(v); val != nil {\n\t\t\tres = append(res, *val)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc Filter[T any](arr []T, f func(T) bool) []T {\n\tvar res []T\n\tfor _, v := range arr {\n\t\tif f(v) {\n\t\t\tres = append(res, v)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc Sleep(ms int) {\n\ttime.Sleep(time.Duration(ms) * time.Millisecond)\n}\n\nfunc GetPtrVal[T any](ptr *T, def T) T {\n\tif ptr == nil {\n\t\treturn def\n\t}\n\treturn *ptr\n}\n\nfunc LimitMax[T int | int64 | float32 | float64](value T, max T) T {\n\tif value > max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc LimitMin[T int | int64 | float32 | float64](value T, min T) T {\n\tif value < min {\n\t\treturn min\n\t}\n\treturn value\n}\n\nfunc InRange[T int | int64 | float32 | float64](value T, min T, max T) bool {\n\treturn value >= min && value <= max\n}\n\nfunc GetError(err error) string {\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn \"\"\n}\n\nfunc GetIndexSafe[T any](arr []T, index int) *T {\n\tif index >= len(arr) {\n\t\treturn nil\n\t}\n\treturn &arr[index]\n}\n\nfunc All(arr ...bool) bool {\n\tfor _, v := range arr {\n\t\tif !v {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc Any(arr ...bool) bool {\n\tfor _, v := range arr {\n\t\tif v {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc Range(start int, end int) []int {\n\tvar res []int\n\tfor i := start; i < end; i++ {\n\t\tres = append(res, i)\n\t}\n\treturn res\n}\n\nfunc GetStringConfs(key ...string) string {\n\tfor _, k := range key {\n\t\tif v := viper.GetString(k); len(v) > 0 {\n\t\t\treturn v\n\t\t}\n\t}\n\n\treturn \"\"\n}"
  },
  {
    "path": "utils/bootstrap.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc ReadConf() {\n\tviper.SetConfigFile(configFile)\n\n\tif !IsFileExist(configFile) {\n\t\tfmt.Printf(\"[service] config.yaml not found, creating one from template: %s\\n\", configExampleFile)\n\t\tif err := CopyFile(configExampleFile, configFile); err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tviper.AutomaticEnv()\n\tviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\n\tsecret := viper.GetString(\"secret\")\n\tif len(secret) < 32 {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] invalid secret length: got %d, expected at least 32 bytes; starting in 10 seconds, please set a stronger `secret` in config or environment; future versions may panic on weak secrets\", len(secret)))\n\t\ttime.Sleep(10 * time.Second)\n\t}\n\n\tif timeout := viper.GetInt(\"max_timeout\"); timeout > 0 {\n\t\tglobals.HttpMaxTimeout = time.Second * time.Duration(timeout)\n\t\tglobals.Debug(fmt.Sprintf(\"[service] http client timeout set to %ds from env\", timeout))\n\t}\n}\n\nfunc NewEngine() *gin.Engine {\n\tif viper.GetBool(\"debug\") {\n\t\treturn gin.Default()\n\t}\n\n\tgin.SetMode(gin.ReleaseMode)\n\n\tengine := gin.New()\n\tengine.Use(gin.Recovery())\n\treturn engine\n}\n"
  },
  {
    "path": "utils/buffer.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Charge interface {\n\tGetType() string\n\tGetModels() []string\n\tGetInput() float32\n\tGetOutput() float32\n\tSupportAnonymous() bool\n\tIsBilling() bool\n\tIsBillingType(t string) bool\n\tGetLimit() float32\n}\n\ntype Buffer struct {\n\tModel           string                `json:\"model\"`\n\tQuota           float32               `json:\"quota\"`\n\tData            string                `json:\"data\"`\n\tLatest          string                `json:\"latest\"`\n\tCursor          int                   `json:\"cursor\"`\n\tTimes           int                   `json:\"times\"`\n\tInputTokens     int                   `json:\"input_tokens\"`\n\tImages          Images                `json:\"images\"`\n\tToolCalls       *globals.ToolCalls    `json:\"tool_calls\"`\n\tToolCallsCursor int                   `json:\"tool_calls_cursor\"`\n\tFunctionCall    *globals.FunctionCall `json:\"function_call\"`\n\tStartTime       *time.Time            `json:\"-\"`\n\tPrompts         string                `json:\"prompts\"`\n\tTokenName       string                `json:\"-\"`\n\tCharge          Charge                `json:\"-\"`\n\tVisionRecall    bool                  `json:\"-\"`\n}\n\nfunc initInputToken(model string, history []globals.Message) int {\n\tif globals.IsVisionModel(model) {\n\t\tfor _, message := range history {\n\t\t\tif message.Role == globals.User {\n\t\t\t\tcontent, _ := ExtractImages(message.Content, true)\n\t\t\t\tmessage.Content = content\n\t\t\t}\n\t\t}\n\n\t\thistory = Each(history, func(message globals.Message) globals.Message {\n\t\t\tif message.Role == globals.User {\n\t\t\t\traw, _ := ExtractImages(message.Content, true)\n\t\t\t\treturn globals.Message{\n\t\t\t\t\tRole:         message.Role,\n\t\t\t\t\tContent:      raw,\n\t\t\t\t\tName:         message.Name,\n\t\t\t\t\tFunctionCall: message.FunctionCall,\n\t\t\t\t\tToolCalls:    message.ToolCalls,\n\t\t\t\t\tToolCallId:   message.ToolCallId,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn message\n\t\t})\n\t}\n\n\treturn NumTokensFromMessages(history, model, false)\n}\n\nfunc NewBuffer(model string, history []globals.Message, charge Charge) *Buffer {\n\ttoken := initInputToken(model, history)\n\n\treturn &Buffer{\n\t\tModel:           model,\n\t\tQuota:           CountInputQuota(charge, token),\n\t\tInputTokens:     token,\n\t\tCharge:          charge,\n\t\tFunctionCall:    nil,\n\t\tToolCalls:       nil,\n\t\tToolCallsCursor: 0,\n\t\tStartTime:       ToPtr(time.Now()),\n\t}\n}\n\nfunc (b *Buffer) GetCursor() int {\n\treturn b.Cursor\n}\n\nfunc (b *Buffer) GetQuota() float32 {\n\treturn b.Quota + CountOutputToken(b.Charge, b.CountOutputToken(true))\n}\n\nfunc (b *Buffer) GetRecordQuota() float32 {\n\t// end of the buffer, the output token is counted using the times\n\treturn b.Quota + CountOutputToken(b.Charge, b.CountOutputToken(false))\n}\n\nfunc (b *Buffer) Write(data string) string {\n\tb.Data += data\n\tb.Cursor += len(data)\n\tb.Times++\n\tb.Latest = data\n\treturn data\n}\n\nfunc (b *Buffer) WriteChunk(data *globals.Chunk) string {\n\tif data == nil {\n\t\treturn \"\"\n\t}\n\n\tb.Write(data.Content)\n\tb.AddToolCalls(data.ToolCall)\n\tb.SetFunctionCall(data.FunctionCall)\n\n\treturn data.Content\n}\n\nfunc (b *Buffer) GetChunk() string {\n\treturn b.Latest\n}\n\nfunc (b *Buffer) InitVisionRecall() {\n\t// set the vision recall flag to true to prevent the buffer from adding more images of retrying the channel\n\tb.VisionRecall = true\n}\n\nfunc (b *Buffer) AddImage(image *Image) {\n\tif image == nil || b.VisionRecall {\n\t\treturn\n\t}\n\n\tb.Images = append(b.Images, *image)\n\n\ttokens := image.CountTokens(b.Model)\n\tb.InputTokens += tokens\n\n\tif b.Charge.IsBillingType(globals.TokenBilling) {\n\t\tb.Quota += float32(tokens) / 1000 * b.Charge.GetInput()\n\t}\n}\n\nfunc (b *Buffer) GetImages() Images {\n\treturn b.Images\n}\n\nfunc (b *Buffer) SetToolCalls(toolCalls *globals.ToolCalls) {\n\tb.ToolCalls = toolCalls\n}\n\nfunc hitTool(tool globals.ToolCall, tools globals.ToolCalls) (int, *globals.ToolCall) {\n\tfor i, t := range tools {\n\t\tif t.Id == tool.Id {\n\t\t\treturn i, &t\n\t\t}\n\t}\n\n\tif len(tool.Type) == 0 && len(tool.Id) == 0 {\n\t\tlength := len(tools)\n\n\t\tif length > 0 {\n\t\t\t// if the tool is empty, return the last tool as the hit\n\t\t\treturn length - 1, &tools[length-1]\n\t\t}\n\t}\n\n\treturn 0, nil\n}\n\nfunc appendTool(tool globals.ToolCall, chunk globals.ToolCall) string {\n\tfrom := ToString(tool.Function.Arguments)\n\tto := ToString(chunk.Function.Arguments)\n\n\treturn from + to\n}\n\nfunc mixTools(source *globals.ToolCalls, target *globals.ToolCalls) *globals.ToolCalls {\n\tif source == nil {\n\t\treturn target\n\t}\n\n\ttools := make(globals.ToolCalls, 0)\n\tarr := Collect[globals.ToolCall](*source, *target)\n\n\tfor _, tool := range arr {\n\t\tidx, hit := hitTool(tool, tools)\n\n\t\tif hit != nil {\n\t\t\ttools[idx].Function.Arguments = appendTool(tools[idx], tool)\n\t\t} else {\n\t\t\ttools = append(tools, tool)\n\t\t}\n\t}\n\n\treturn &tools\n}\n\nfunc (b *Buffer) AddToolCalls(toolCalls *globals.ToolCalls) {\n\tif toolCalls == nil {\n\t\treturn\n\t}\n\n\tb.ToolCalls = mixTools(b.ToolCalls, toolCalls)\n}\n\nfunc (b *Buffer) SetFunctionCall(functionCall *globals.FunctionCall) {\n\tif functionCall == nil {\n\t\treturn\n\t}\n\n\tb.FunctionCall = functionCall\n}\n\nfunc (b *Buffer) GetToolCalls() *globals.ToolCalls {\n\tcalls := b.ToolCalls\n\tb.ToolCalls = nil\n\n\treturn calls\n}\n\nfunc (b *Buffer) GetFunctionCall() *globals.FunctionCall {\n\treturn b.FunctionCall\n}\n\nfunc (b *Buffer) IsFunctionCalling() bool {\n\treturn b.FunctionCall != nil || b.ToolCalls != nil\n}\n\nfunc (b *Buffer) IsEmpty() bool {\n\treturn b.Cursor == 0 && !b.IsFunctionCalling()\n}\n\nfunc (b *Buffer) GetModel() string {\n\treturn b.Model\n}\n\nfunc (b *Buffer) GetCharge() Charge {\n\treturn b.Charge\n}\n\nfunc (b *Buffer) ToChargeInfo() string {\n\tswitch b.Charge.GetType() {\n\tcase globals.TokenBilling:\n\t\treturn fmt.Sprintf(\n\t\t\t\"input tokens: %0.4f quota / 1k tokens\\n\"+\n\t\t\t\t\"output tokens: %0.4f quota / 1k tokens\\n\",\n\t\t\tb.Charge.GetInput(), b.Charge.GetOutput(),\n\t\t)\n\tcase globals.TimesBilling:\n\t\treturn fmt.Sprintf(\"%f quota per request\\n\", b.Charge.GetLimit())\n\tcase globals.NonBilling:\n\t\treturn \"no cost\"\n\t}\n\n\treturn \"\"\n}\n\nfunc (b *Buffer) SetPrompts(prompts interface{}) {\n\tb.Prompts = ToString(prompts)\n}\n\nfunc (b *Buffer) Read() string {\n\treturn b.Data\n}\n\nfunc (b *Buffer) ReadBytes() []byte {\n\treturn []byte(b.Data)\n}\n\nfunc (b *Buffer) ReadWithDefault(_default string) string {\n\tif b.IsEmpty() || (len(strings.TrimSpace(b.Data)) == 0 && !b.IsFunctionCalling()) {\n\t\treturn _default\n\t}\n\treturn b.Data\n}\n\nfunc (b *Buffer) ReadTimes() int {\n\treturn b.Times\n}\n\nfunc (b *Buffer) SetInputTokens(tokens int) {\n\tb.InputTokens = tokens\n}\n\nfunc (b *Buffer) CountInputToken() int {\n\treturn b.InputTokens\n}\n\nfunc (b *Buffer) CountOutputToken(running bool) int {\n\tif running {\n\t\t// performance optimization:\n\t\t// if the buffer is still running, the output token counted using the times instead\n\t\treturn b.Times\n\t}\n\n\treturn NumTokensFromResponse(b.Read(), b.Model)\n}\n\nfunc (b *Buffer) CountToken() int {\n\treturn b.CountInputToken() + b.CountOutputToken(true)\n}\n\nfunc (b *Buffer) GetDuration() float32 {\n\tif b.StartTime == nil {\n\t\treturn 0\n\t}\n\n\treturn float32(time.Since(*b.StartTime).Seconds())\n}\n\nfunc (b *Buffer) GetStartTime() *time.Time {\n\treturn b.StartTime\n}\n\nfunc (b *Buffer) GetPrompts() string {\n\treturn b.Prompts\n}\n\nfunc (b *Buffer) GetTokenName() string {\n\tif len(b.TokenName) == 0 {\n\t\treturn globals.WebTokenType\n\t}\n\n\treturn b.TokenName\n}\n\nfunc (b *Buffer) SetTokenName(tokenName string) {\n\tb.TokenName = tokenName\n}\n\nfunc (b *Buffer) GetRecordPrompts() string {\n\tif !globals.AcceptPromptStore {\n\t\treturn \"\"\n\t}\n\n\treturn b.GetPrompts()\n}\n\nfunc (b *Buffer) GetRecordResponsePrompts() string {\n\tif !globals.AcceptPromptStore {\n\t\treturn \"\"\n\t}\n\n\treturn b.Read()\n}\n"
  },
  {
    "path": "utils/cache.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"time\"\n)\n\nfunc Incr(cache *redis.Client, key string, delta int64) (int64, error) {\n\treturn cache.IncrBy(context.Background(), key, delta).Result()\n}\n\nfunc Decr(cache *redis.Client, key string, delta int64) (int64, error) {\n\treturn cache.DecrBy(context.Background(), key, delta).Result()\n}\n\nfunc GetInt(cache *redis.Client, key string) (int64, error) {\n\treturn cache.Get(context.Background(), key).Int64()\n}\n\nfunc MustInt(cache *redis.Client, key string) int64 {\n\tval, err := cache.Get(context.Background(), key).Int64()\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn val\n}\n\nfunc SetInt(cache *redis.Client, key string, value int64, expiration int64) error {\n\treturn cache.Set(context.Background(), key, value, time.Duration(expiration)*time.Second).Err()\n}\n\nfunc SetJson(cache *redis.Client, key string, value interface{}, expiration int64) error {\n\terr := cache.Set(context.Background(), key, Marshal(value), time.Duration(expiration)*time.Second).Err()\n\treturn err\n}\n\nfunc GetCacheStore[T any](cache *redis.Client, key string) *T {\n\tval, err := cache.Get(context.Background(), key).Result()\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn UnmarshalForm[T](val)\n}\n\nfunc GetCache(cache *redis.Client, key string) (string, error) {\n\treturn cache.Get(context.Background(), key).Result()\n}\n\nfunc SetCache(cache *redis.Client, key string, value string, expiration int64) error {\n\treturn cache.Set(context.Background(), key, value, time.Duration(expiration)*time.Second).Err()\n}\n\nfunc IncrWithLimit(cache *redis.Client, key string, delta int64, limit int64, expiration int64) (bool, error) {\n\t// not exist\n\tif _, err := cache.Get(context.Background(), key).Result(); err != nil {\n\t\tif errors.Is(err, redis.Nil) {\n\t\t\tcache.Set(context.Background(), key, delta, time.Duration(expiration)*time.Second)\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, err\n\t}\n\tres, err := Incr(cache, key, delta)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif res > limit {\n\t\t// reset\n\t\tcache.Set(context.Background(), key, limit, time.Duration(expiration)*time.Second)\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\nfunc DecrInt(cache *redis.Client, key string, delta int64) bool {\n\t_, err := Decr(cache, key, delta)\n\treturn err == nil\n}\n\nfunc IncrIP(cache *redis.Client, ip string) int64 {\n\tkey := fmt.Sprintf(\":ip-rate:%s\", ip)\n\tval, err := Incr(cache, key, 1)\n\tif err != nil && errors.Is(err, redis.Nil) {\n\t\tcache.Set(context.Background(), key, 1, time.Minute*20)\n\t\treturn 1\n\t}\n\n\tcache.Expire(context.Background(), key, time.Minute*20)\n\treturn val\n}\n\nfunc IncrWithExpire(cache *redis.Client, key string, delta int64, expiration time.Duration) {\n\t_, err := Incr(cache, key, delta)\n\tif err != nil && errors.Is(err, redis.Nil) {\n\t\tcache.Set(context.Background(), key, delta, expiration)\n\t}\n}\n\nfunc IncrOnce(cache *redis.Client, key string, expiration time.Duration) {\n\tIncrWithExpire(cache, key, 1, expiration)\n}\n\nfunc IsInBlackList(cache *redis.Client, ip string) bool {\n\tval, err := GetInt(cache, fmt.Sprintf(\":ip-rate:%s\", ip))\n\treturn err == nil && val > 50\n}\n"
  },
  {
    "path": "utils/char.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/goccy/go-json\"\n)\n\nfunc GetRandomInt(min int, max int) int {\n\treturn Intn(max-min) + min\n}\n\nfunc GenerateCode(length int) string {\n\tseq := IntnSeq(10, length)\n\n\tvar code string\n\tfor i := 0; i < length; i++ {\n\t\tcode += strconv.Itoa(seq[i])\n\t}\n\treturn code\n}\n\nfunc GenerateChar(length int) string {\n\tconst charset = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n\tseq := IntnSeq(len(charset), length)\n\n\tresult := make([]byte, length)\n\tfor i := 0; i < length; i++ {\n\t\tresult[i] = charset[seq[i]]\n\t}\n\treturn string(result)\n}\n\nfunc ConvertTime(t []uint8) *time.Time {\n\tval, err := time.Parse(\"2006-01-02 15:04:05\", string(t))\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn &val\n}\n\nfunc Unmarshal[T interface{}](data []byte) (form T, err error) {\n\terr = json.Unmarshal(data, &form)\n\treturn form, err\n}\n\nfunc UnmarshalString[T interface{}](data string) (form T, err error) {\n\terr = json.Unmarshal([]byte(data), &form)\n\treturn form, err\n}\n\nfunc UnmarshalForm[T interface{}](data string) *T {\n\tform, err := UnmarshalString[T](data)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn &form\n}\n\nfunc Marshal[T interface{}](data T) string {\n\tres, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(res)\n}\n\nfunc MarshalWithIndent[T interface{}](data T, length ...int) string {\n\tvar indent string\n\tif len(length) > 0 {\n\t\tindent = strings.Repeat(\" \", length[0])\n\t} else {\n\t\tindent = \"  \"\n\t}\n\n\tres, err := json.MarshalIndent(data, \"\", indent)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(res)\n}\n\nfunc MapToStruct[T any](data interface{}) *T {\n\tval := Marshal(data)\n\tif form, err := Unmarshal[T]([]byte(val)); err == nil {\n\t\treturn &form\n\t} else {\n\t\treturn nil\n\t}\n}\n\nfunc MapToRawStruct[T any](data interface{}) (*T, error) {\n\tval, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tform, err := Unmarshal[T](val)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &form, nil\n}\n\nfunc ParseInt(value string) int {\n\tif res, err := strconv.Atoi(value); err == nil {\n\t\treturn res\n\t} else {\n\t\treturn 0\n\t}\n}\n\nfunc ParseInt64(value string) int64 {\n\tif res, err := strconv.ParseInt(value, 10, 64); err == nil {\n\t\treturn res\n\t} else {\n\t\treturn 0\n\t}\n}\n\nfunc ParseFloat32(value string) float32 {\n\tif res, err := strconv.ParseFloat(value, 32); err == nil {\n\t\treturn float32(res)\n\t} else {\n\t\treturn 0\n\t}\n}\n\nfunc ParseBool(value string) bool {\n\tif res, err := strconv.ParseBool(value); err == nil {\n\t\treturn res\n\t} else {\n\t\treturn false\n\t}\n}\n\nfunc ConvertSqlTime(t time.Time) string {\n\treturn t.Format(\"2006-01-02 15:04:05\")\n}\n\nfunc GetImageMarkdown(url string) string {\n\treturn fmt.Sprintf(\"![image](%s)\", url)\n}\n\nfunc GetBase64ImageMarkdown(b64 string, _desc ...string) string {\n\t// Extracts the image type from base64 string (e.g., \"data:image/png;base64,...\") or defaults to png\n\tvar imageType = \"png\"\n\tif strings.HasPrefix(b64, \"data:image/\") {\n\t\tparts := strings.Split(b64[11:], \";\")\n\t\tif len(parts) > 0 {\n\t\t\timageType = parts[0]\n\t\t}\n\t}\n\n\tdesc := \"image\"\n\tif len(_desc) > 0 && _desc[0] != \"\" {\n\t\tdesc = _desc[0]\n\t}\n\n\treturn fmt.Sprintf(\"![%s](data:image/%s;base64,%s)\", desc, imageType, b64)\n}\n\n// SplitItem is the split function for strings.Split\n// e.g.\n// SplitItem(\"a,b,c\", \",\") => [\"a,\", \"b,\", \"c\"]\nfunc SplitItem(data string, sep string) []string {\n\tif data == \"\" {\n\t\treturn []string{}\n\t}\n\n\tresult := strings.Split(data, sep)\n\tlength := len(result)\n\tfor i, item := range result {\n\t\tif i == length-1 {\n\t\t\tbreak\n\t\t}\n\t\tresult[i] = item + sep\n\t}\n\treturn result\n}\n\nfunc SplitItems(data string, seps []string) []string {\n\tif len(seps) == 0 {\n\t\treturn []string{}\n\t}\n\n\tresult := []string{data}\n\tfor _, sep := range seps {\n\t\tvar temp []string\n\t\tfor _, item := range result {\n\t\t\ttemp = append(temp, SplitItem(item, sep)...)\n\t\t}\n\t\tresult = temp\n\t}\n\treturn result\n}\n\nfunc SplitLangItems(data string) []string {\n\treturn SplitItems(data, []string{\",\", \"，\", \" \", \"\\n\"})\n}\n\nfunc Extract(data string, length int, flow_ ...string) string {\n\tflow := \"\"\n\tif len(flow_) > 0 {\n\t\tflow = flow_[0]\n\t}\n\n\tvalue := []rune(data)\n\tif len(value) > length {\n\t\treturn string(value[:length]) + flow\n\t} else {\n\t\treturn data\n\t}\n}\n\nfunc ExtractUrls(data string) []string {\n\tre := regexp.MustCompile(`(https?://\\S+)`)\n\treturn re.FindAllString(data, -1)\n}\n\nfunc ExtractImages(data string, includeBase64 bool) (content string, images []string) {\n\text := ExtractExternalImages(data)\n\tif includeBase64 {\n\t\timages = append(ext, ExtractBase64Images(data)...)\n\t} else {\n\t\timages = ext\n\t}\n\n\tcontent = data\n\tfor _, image := range images {\n\t\tcontent = strings.ReplaceAll(content, image, \"\")\n\t}\n\n\treturn content, images\n}\n\nfunc ExtractImagesFromMarkdown(data string) (images []string) {\n\t// extract images like `![image](https://xxx.com/xxx?xxx=xxx&xxx=xxx)` and return urls\n\tre := regexp.MustCompile(`!\\[.*\\]\\((https?://\\S+)\\)`)\n\tmatches := re.FindAllStringSubmatch(data, -1)\n\n\tfor _, match := range matches {\n\t\timages = append(images, match[1])\n\t}\n\n\treturn images\n}\n\nfunc GetVideoMarkdown(url string, _desc ...string) string {\n\tdesc := \"video\"\n\tif len(_desc) > 0 && _desc[0] != \"\" {\n\t\tdesc = _desc[0]\n\t}\n\n\treturn fmt.Sprintf(\"![%s](%s)\", desc, url)\n}\n\nfunc ExtractBase64FromMarkdown(data string) (images []string) {\n\t// extract base64 images like `![image](data:image/png;base64,xxxxxx)`\n\tre := regexp.MustCompile(`!\\[.*?\\]\\((data:image/\\w+;base64,[\\w+/=]+)\\)`)\n\tmatches := re.FindAllStringSubmatch(data, -1)\n\n\tfor _, match := range matches {\n\t\t// We only need the base64 data part\n\t\tif len(match) > 1 {\n\t\t\timages = append(images, match[1])\n\t\t}\n\t}\n\n\treturn images\n}\n\nfunc ExtractBase64Images(data string) []string {\n\t// get base64 images from data (data:image/png;base64,xxxxxx) (\\n \\\\n [space] \\\\t \\\\r \\\\v \\\\f break the base64 string)\n\tre := regexp.MustCompile(`(data:image/\\w+;base64,[\\w+/=]+)`)\n\treturn re.FindAllString(data, -1)\n}\n\nfunc ExtractExternalImages(data string) []string {\n\t// https://platform.openai.com/docs/guides/vision/what-type-of-files-can-i-upload\n\n\tre := regexp.MustCompile(`(https?://\\S+\\.(?:png|jpg|jpeg|gif|webp|heif|heic|bmp|svg|ico)(?:\\?\\S+)?)`)\n\treturn re.FindAllString(data, -1)\n}\n\nfunc ContainUnicode(data string) bool {\n\t// like `hi\\\\u2019s` => true\n\tre := regexp.MustCompile(`\\\\u([0-9a-fA-F]{4})`)\n\treturn re.MatchString(data)\n}\n\nfunc DecodeUnicode(data string) string {\n\t// like `hi\\\\u2019s` => `hi's`\n\tre := regexp.MustCompile(`\\\\u([0-9a-fA-F]{4})`)\n\treturn re.ReplaceAllStringFunc(data, func(s string) string {\n\t\tunicode, err := strconv.ParseInt(s[2:], 16, 32)\n\t\tif err != nil {\n\t\t\treturn s\n\t\t}\n\n\t\treturn string(rune(unicode))\n\t})\n}\n\nfunc EscapeChar(data string) string {\n\t// like `\\\\n` => `\\n`, `\\\\t` => `\\t` and so on\n\tre := regexp.MustCompile(`\\\\([nrtvfb])`)\n\n\tmapper := map[string]string{\n\t\t\"n\": \"\\n\",\n\t\t\"r\": \"\\r\",\n\t\t\"t\": \"\\t\",\n\t\t\"v\": \"\\v\",\n\t\t\"f\": \"\\f\",\n\t\t\"b\": \"\\b\",\n\t}\n\n\treturn re.ReplaceAllStringFunc(data, func(s string) string {\n\t\treturn mapper[s[1:]]\n\t})\n}\n\nfunc ProcessRobustnessChar(data string) string {\n\t// like `hi\\\\u2019s` => `hi's`\n\tif ContainUnicode(data) {\n\t\tdata = DecodeUnicode(data)\n\t}\n\n\t// like `\\\\n` => `\\n`, `\\\\t` => `\\t` and so on\n\treturn EscapeChar(data)\n}\n\nfunc SortString(arr []string) []string {\n\t// sort string array by first char\n\t// e.g. [\"a\", \"b\", \"c\", \"ab\", \"ac\", \"bc\"] => [\"a\", \"ab\", \"ac\", \"b\", \"bc\", \"c\"]\n\n\tif len(arr) <= 1 {\n\t\treturn arr\n\t}\n\n\tvar result []string\n\tvar temp []string\n\tvar first string\n\n\tfor _, item := range arr {\n\t\tif first == \"\" {\n\t\t\tfirst = item\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(item, first) {\n\t\t\ttemp = append(temp, item)\n\t\t} else {\n\t\t\tresult = append(result, first)\n\t\t\tresult = append(result, SortString(temp)...)\n\t\t\tfirst = item\n\t\t\ttemp = []string{}\n\t\t}\n\t}\n\n\tif len(temp) > 0 {\n\t\tresult = append(result, first)\n\t\tresult = append(result, SortString(temp)...)\n\t} else {\n\t\tresult = append(result, first)\n\t}\n\n\treturn result\n}\n\nfunc SafeSplit(data string, sep string, seglen int) (res []string) {\n\t// split string by sep, and each segment has seglen length\n\t// e.g. SafeSplit(\"abc,def,ghi\", \",\", 2) => [\"abc\", \"def,ghi\"]\n\n\tif data == \"\" {\n\t\tfor i := 0; i < seglen; i++ {\n\t\t\tres = append(res, \"\")\n\t\t}\n\t\treturn res\n\t}\n\n\tarr := strings.Split(data, sep)\n\tlength := len(arr)\n\n\tif length == seglen {\n\t\treturn arr\n\t}\n\n\tif length < seglen {\n\t\tfor i := 0; i < seglen-length; i++ {\n\t\t\tarr = append(arr, \"\")\n\t\t}\n\t\treturn arr\n\t} else {\n\t\toffset := length - seglen\n\t\tfor i := 0; i < offset; i++ {\n\t\t\tarr[seglen-1] += sep + arr[seglen+i]\n\t\t}\n\t\treturn arr[:seglen]\n\t}\n}\n\nfunc HideSecret(raw string, _flowLength ...int) string {\n\t// like `axVbeixvN` => `axVb*****`\n\n\tflowLength := 4\n\tif len(_flowLength) > 0 {\n\t\tflowLength = _flowLength[0]\n\t}\n\n\tdata := []rune(raw)\n\tlength := len(data)\n\n\tif length < flowLength {\n\t\treturn \"****\"\n\t} else {\n\t\tsuffix := len(data) - flowLength\n\t\treturn string(data[:flowLength]) + strings.Repeat(\"*\", suffix)\n\t}\n}\n\nfunc ToMarkdownCode(lang string, code string) string {\n\treturn fmt.Sprintf(\"```%s\\n%s\\n```\", lang, code)\n}\n\nfunc ToMarkdownError(err error, body string) error {\n\treturn fmt.Errorf(\"%s\\n%s\", err.Error(), ToMarkdownCode(\"html\", body))\n}\n"
  },
  {
    "path": "utils/compress.go",
    "content": "package utils\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc GenerateCompressTask(hash string, base, path, replacer string) (string, string, error) {\n\tbase = handlePath(base)\n\treplacer = handlePath(replacer)\n\n\tCreateFolder(base)\n\tzipPath := fmt.Sprintf(\"%s/%s.zip\", base, hash)\n\tgzipPath := fmt.Sprintf(\"%s/%s.tar.gz\", base, hash)\n\n\tfiles := Walk(path)\n\n\tif err := CreateZipObject(zipPath, files, replacer); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tif err := CreateGzipObject(gzipPath, files, replacer); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn zipPath, gzipPath, nil\n}\n\nfunc GenerateCompressTaskAsync(hash string, base, path, replacer string) (string, string) {\n\tzipPath := fmt.Sprintf(\"%s/%s.zip\", base, hash)\n\tgzipPath := fmt.Sprintf(\"%s/%s.tar.gz\", base, hash)\n\n\tfiles := Walk(path)\n\n\tgo func() {\n\t\tif err := CreateZipObject(zipPath, files, replacer); err != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := CreateGzipObject(gzipPath, files, replacer); err != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\treturn zipPath, gzipPath\n}\n\nfunc handlePath(path string) string {\n\treturn strings.Replace(path, \"\\\\\", \"/\", -1)\n}\n\nfunc CreateZipObject(output string, files []string, replacer string) error {\n\tFileDirSafe(output)\n\tfile, err := os.Create(output)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\twriter := zip.NewWriter(file)\n\tdefer writer.Close()\n\n\tfor _, file := range files {\n\t\terr := addFileToZip(writer, file, replacer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc addFileToZip(zipWriter *zip.Writer, path string, replacer string) error {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tinfo, err := file.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theader, err := zip.FileInfoHeader(info)\n\tif err != nil {\n\t\treturn err\n\t}\n\theader.Name = strings.Trim(strings.Replace(path, replacer, \"\", 1), \"/\")\n\twriter, err := zipWriter.CreateHeader(header)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(writer, file)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc CreateGzipObject(output string, files []string, replacer string) error {\n\tFileDirSafe(output)\n\ttarFile, err := os.Create(output)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tarFile.Close()\n\n\tgzWriter := gzip.NewWriter(tarFile)\n\tdefer gzWriter.Close()\n\n\ttarWriter := tar.NewWriter(gzWriter)\n\tdefer tarWriter.Close()\n\n\tfor _, file := range files {\n\t\terr := addFileToTar(tarWriter, file, replacer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// tar gzip\nfunc addFileToTar(tarWriter *tar.Writer, filePath string, replacer string) error {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tinfo, err := file.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theader, err := tar.FileInfoHeader(info, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theader.Name = strings.Trim(strings.Replace(filePath, replacer, \"\", 1), \"/\")\n\n\terr = tarWriter.WriteHeader(header)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = io.Copy(tarWriter, file)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "utils/config.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gin-contrib/static\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/spf13/viper\"\n)\n\nvar configFile = \"config/config.yaml\"\nvar configTmpFile = \"config/config.tmp.yaml\"\nvar configBackupFile = \"config/config.bak.yaml\"\nvar configExampleFile = \"config.example.yaml\"\nvar configMutex sync.Mutex\n\nvar redirectRoutes = []string{\n\t\"/v1\",\n\t\"/mj\",\n\t\"/attachments\",\n}\n\nfunc SaveConfig(key string, value interface{}) error {\n\t// save config to file with mutex lock\n\tconfigMutex.Lock()\n\tdefer configMutex.Unlock()\n\n\tif err := viper.WriteConfigAs(configBackupFile); err != nil {\n\t\treturn err\n\t}\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\treturn err\n\t}\n\n\tcurrentConfig := viper.AllSettings()\n\n\tcurrentConfig[key] = value\n\n\tfor k, v := range currentConfig {\n\t\tviper.Set(k, v)\n\t}\n\n\tif err := viper.WriteConfigAs(configTmpFile); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := os.Stat(configFile); err == nil {\n\t\tif removeErr := os.Remove(configFile); removeErr != nil {\n\t\t\treturn removeErr\n\t\t}\n\t}\n\n\tif err := os.Rename(configTmpFile, configFile); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc ApplySeo(title, icon string) {\n\t// seo optimization\n\n\tif !viper.GetBool(\"serve_static\") {\n\t\treturn\n\t}\n\n\tcontent, err := ReadFile(\"./app/dist/index.html\")\n\tif err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to read index.html: %s\", err.Error()))\n\t\treturn\n\t}\n\n\tif len(title) > 0 {\n\t\tcontent = strings.ReplaceAll(content, \"CoAI.Dev\", title)\n\t\tcontent = strings.ReplaceAll(content, \"chatnio\", strings.ToLower(title))\n\t}\n\n\tif len(icon) > 0 {\n\t\tcontent = strings.ReplaceAll(content, \"/favicon.ico\", icon)\n\t}\n\n\tif err := WriteFile(\"./app/dist/index.cache.html\", content, true); err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to write index.cache.html: %s\", err.Error()))\n\t}\n\n\tglobals.Info(\"[service] seo optimization applied to index.cache.html\")\n}\n\nfunc ApplyPWAManifest(content string) {\n\t// pwa manifest rewrite (site.webmanifest -> site.cache.webmanifest)\n\n\tif !viper.GetBool(\"serve_static\") {\n\t\treturn\n\t}\n\n\tif len(content) == 0 {\n\t\t// read from site.webmanifest if not provided\n\n\t\tvar err error\n\t\tcontent, err = ReadFile(\"./app/dist/site.webmanifest\")\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to read site.webmanifest: %s\", err.Error()))\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := WriteFile(\"./app/dist/site.cache.webmanifest\", content, true); err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to write site.cache.webmanifest: %s\", err.Error()))\n\t}\n\n\tglobals.Info(\"[service] pwa manifest applied to site.cache.webmanifest\")\n}\n\nfunc ReadPWAManifest() (content string) {\n\t// read site.cache.webmanifest content or site.webmanifest if not found\n\n\tif !viper.GetBool(\"serve_static\") {\n\t\treturn\n\t}\n\n\tif text, err := ReadFile(\"./app/dist/site.cache.webmanifest\"); err == nil && len(text) > 0 {\n\t\treturn text\n\t}\n\n\tif text, err := ReadFile(\"./app/dist/site.webmanifest\"); err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[service] failed to read site.webmanifest: %s\", err.Error()))\n\t} else {\n\t\tcontent = text\n\t}\n\n\treturn\n}\n\nfunc RegisterStaticRoute(engine *gin.Engine) {\n\t// static files are in ~/app/dist\n\n\tif !viper.GetBool(\"serve_static\") {\n\t\tengine.NoRoute(func(c *gin.Context) {\n\t\t\tc.JSON(404, gin.H{\"status\": false, \"message\": \"not found or method not allowed\"})\n\t\t})\n\t\treturn\n\t}\n\n\tif !IsFileExist(\"./app/dist\") {\n\t\tfmt.Println(\"[service] app/dist not found, please run `npm run build`\")\n\t\treturn\n\t}\n\n\tApplySeo(viper.GetString(\"system.general.title\"), viper.GetString(\"system.general.logo\"))\n\tApplyPWAManifest(viper.GetString(\"system.general.pwamanifest\"))\n\n\tengine.GET(\"/\", func(c *gin.Context) {\n\t\tc.File(\"./app/dist/index.cache.html\")\n\t})\n\n\tengine.GET(\"/site.webmanifest\", func(c *gin.Context) {\n\t\tc.File(\"./app/dist/site.cache.webmanifest\")\n\t})\n\n\tengine.Use(static.Serve(\"/\", static.LocalFile(\"./app/dist\", true)))\n\tengine.NoRoute(func(c *gin.Context) {\n\t\tc.File(\"./app/dist/index.cache.html\")\n\t})\n\n\tfor _, route := range redirectRoutes {\n\t\tengine.Any(fmt.Sprintf(\"%s/*path\", route), func(c *gin.Context) {\n\t\t\tc.Request.URL.Path = \"/api\" + c.Request.URL.Path\n\t\t\tfmt.Println(c.Request.URL.Path)\n\t\t\tengine.HandleContext(c)\n\t\t})\n\t}\n\n\tfmt.Println(`[service] start serving static files from ~/app/dist`)\n}\n"
  },
  {
    "path": "utils/ctx.go",
    "content": "package utils\n\nimport (\n\t\"database/sql\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n)\n\nfunc GetDBFromContext(c *gin.Context) *sql.DB {\n\treturn c.MustGet(\"db\").(*sql.DB)\n}\n\nfunc GetCacheFromContext(c *gin.Context) *redis.Client {\n\treturn c.MustGet(\"cache\").(*redis.Client)\n}\n\nfunc GetUserFromContext(c *gin.Context) string {\n\treturn c.MustGet(\"user\").(string)\n}\n\nfunc GetAdminFromContext(c *gin.Context) bool {\n\treturn c.MustGet(\"admin\").(bool)\n}\n\nfunc GetAgentFromContext(c *gin.Context) string {\n\treturn c.MustGet(\"agent\").(string)\n}\n"
  },
  {
    "path": "utils/encrypt.go",
    "content": "package utils\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/md5\"\n\tcrand \"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"io\"\n)\n\nfunc Sha2Encrypt(raw string) string {\n\t// return 64-bit hash\n\thash := sha256.Sum256([]byte(raw))\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc Sha2EncryptForm(form interface{}) string {\n\t// return 64-bit hash\n\thash := sha256.Sum256([]byte(ToJson(form)))\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc Base64Encode(raw string) string {\n\treturn base64.StdEncoding.EncodeToString([]byte(raw))\n}\n\nfunc Base64EncodeBytes(raw []byte) string {\n\treturn base64.StdEncoding.EncodeToString(raw)\n}\n\nfunc Base64Decode(raw string) ([]byte, error) {\n\treturn base64.StdEncoding.DecodeString(raw)\n}\n\nfunc Base64DecodeBytes(raw string) []byte {\n\tif data, err := base64.StdEncoding.DecodeString(raw); err == nil {\n\t\treturn data\n\t} else {\n\t\treturn []byte{}\n\t}\n}\n\nfunc Md5Encrypt(raw string) string {\n\t// return 32-bit hash\n\thash := md5.Sum([]byte(raw))\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc Md5EncryptForm(form interface{}) string {\n\t// return 32-bit hash\n\thash := md5.Sum([]byte(ToJson(form)))\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc AES256Encrypt(key string, data string) (string, error) {\n\ttext := []byte(data)\n\tblock, err := aes.NewCipher([]byte(key))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tiv := make([]byte, aes.BlockSize)\n\tif _, err := io.ReadFull(crand.Reader, iv); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tencryptor := cipher.NewCFBEncrypter(block, iv)\n\n\tciphertext := make([]byte, len(text))\n\tencryptor.XORKeyStream(ciphertext, text)\n\treturn hex.EncodeToString(ciphertext), nil\n}\n\nfunc AES256Decrypt(key string, data string) (string, error) {\n\tciphertext, err := hex.DecodeString(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tblock, err := aes.NewCipher([]byte(key))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tiv := ciphertext[:aes.BlockSize]\n\tciphertext = ciphertext[aes.BlockSize:]\n\n\tdecryptor := cipher.NewCFBDecrypter(block, iv)\n\tplaintext := make([]byte, len(ciphertext))\n\tdecryptor.XORKeyStream(plaintext, ciphertext)\n\n\treturn string(plaintext), nil\n}\n"
  },
  {
    "path": "utils/fs.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"chat/globals\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc CreateFolder(path string) bool {\n\tif err := os.MkdirAll(path, os.ModePerm); err != nil && !os.IsExist(err) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc Exists(path string) bool {\n\terr := os.Mkdir(path, os.ModePerm)\n\treturn err != nil && os.IsExist(err)\n}\n\nfunc DirSafe(path string) string {\n\tCreateFolder(path)\n\treturn path\n}\n\nfunc FileDirSafe(file string) string {\n\tif strings.LastIndex(file, \"/\") == -1 {\n\t\treturn file\n\t}\n\n\treturn DirSafe(file[:strings.LastIndex(file, \"/\")])\n}\n\nfunc FileSafe(file string) string {\n\tFileDirSafe(file)\n\treturn file\n}\n\nfunc WriteFile(path string, data string, folderSafe bool) error {\n\tif folderSafe {\n\t\tFileDirSafe(path)\n\t}\n\n\tfile, err := os.Create(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), path))\n\t\t}\n\t}(file)\n\n\tif _, err := file.WriteString(data); err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"[utils] write file error: %s (path: %s, bytes len: %d)\", err.Error(), path, len(data)))\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc ReadFile(path string) (string, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), path))\n\t\t}\n\t}(file)\n\n\tdata, err := io.ReadAll(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(data), nil\n}\n\nfunc Walk(path string) []string {\n\tvar files []string\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif !info.IsDir() {\n\t\t\tfiles = append(files, handlePath(path))\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn files\n}\n\nfunc GetFileSize(path string) int64 {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), path))\n\t\t}\n\t}(file)\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn stat.Size()\n}\n\nfunc GetFileCreated(path string) string {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), path))\n\t\t}\n\t}(file)\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn stat.ModTime().String()\n}\n\nfunc IsFileExist(path string) bool {\n\t_, err := os.Stat(path)\n\treturn err == nil || os.IsExist(err)\n}\n\nfunc CopyFile(src string, dst string) error {\n\tin, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(in *os.File) {\n\t\terr := in.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), src))\n\t\t}\n\t}(in)\n\n\tFileDirSafe(dst)\n\tout, err := os.Create(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(out *os.File) {\n\t\terr := out.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), dst))\n\t\t}\n\t}(out)\n\n\t_, err = io.Copy(out, in)\n\treturn err\n}\n\nfunc DeleteFile(path string) error {\n\treturn os.Remove(path)\n}\n\nfunc ReadFileLatestLines(path string, length int) (string, error) {\n\tif length <= 0 {\n\t\treturn \"\", errors.New(\"length must be greater than 0\")\n\t}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] close file error: %s (path: %s)\", err.Error(), path))\n\t\t}\n\t}(file)\n\n\tscanner := bufio.NewScanner(file)\n\tscanner.Split(bufio.ScanLines)\n\n\tvar lines []string\n\tfor scanner.Scan() {\n\t\tlines = append(lines, scanner.Text())\n\t}\n\n\tif len(lines) < length {\n\t\tlength = len(lines)\n\t}\n\n\treturn strings.Join(lines[len(lines)-length:], \"\\n\"), nil\n}\n"
  },
  {
    "path": "utils/image.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/gif\"\n\t\"image/jpeg\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/chai2010/webp\"\n)\n\ntype Image struct {\n\tObject  image.Image\n\tContent string\n}\ntype Images []Image\n\nfunc NewImage(url string) (*Image, error) {\n\tif strings.HasPrefix(url, \"data:image/\") {\n\t\tdata := SafeSplit(url, \",\", 2)\n\t\tif data[1] == \"\" {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tdecoded, err := Base64Decode(data[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\timg, _, err := image.Decode(strings.NewReader(string(decoded)))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &Image{Object: img, Content: url}, nil\n\t}\n\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer res.Body.Close()\n\n\tvar img image.Image\n\tsuffix := strings.ToLower(path.Ext(url))\n\tswitch suffix {\n\tcase \".png\":\n\t\tif img, _, err = image.Decode(res.Body); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase \".jpg\", \".jpeg\":\n\t\tif img, err = jpeg.Decode(res.Body); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase \"webp\":\n\t\tif img, err = webp.Decode(res.Body); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase \"gif\":\n\t\tticks, err := gif.DecodeAll(res.Body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\timg = ticks.Image[0]\n\t}\n\n\treturn &Image{Object: img, Content: url}, nil\n}\n\nfunc NewImageContent(content string) *Image {\n\treturn &Image{Content: content}\n}\n\nfunc ConvertToBase64(url string) (string, error) {\n\tif strings.HasPrefix(url, \"data:image/\") {\n\t\tdata := strings.Split(url, \",\")\n\t\tif len(data) != 2 {\n\t\t\treturn \"\", nil\n\t\t}\n\t\treturn data[1], nil\n\t}\n\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdefer res.Body.Close()\n\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn Base64EncodeBytes(data), nil\n}\n\nfunc (i *Image) GetWidth() int {\n\treturn i.Object.Bounds().Max.X\n}\n\nfunc (i *Image) GetHeight() int {\n\treturn i.Object.Bounds().Max.Y\n}\n\nfunc (i *Image) GetPixel(x int, y int) (uint32, uint32, uint32, uint32) {\n\treturn i.Object.At(x, y).RGBA()\n}\n\nfunc (i *Image) GetPixelColor(x int, y int) (int, int, int) {\n\tr, g, b, _ := i.GetPixel(x, y)\n\treturn int(r), int(g), int(b)\n}\n\nfunc (i *Image) CountTokens(model string) int {\n\tif globals.IsVisionModel(model) {\n\t\t// tile size is 512x512\n\t\t// the max size of image is 2048x2048\n\t\t// the image that is larger than 2048x2048 will be resized in 16 tiles\n\n\t\tx := LimitMax(math.Ceil(float64(i.GetWidth())/512), 4)\n\t\ty := LimitMax(math.Ceil(float64(i.GetHeight())/512), 4)\n\t\ttiles := int(x) * int(y)\n\n\t\treturn 85 + 170*tiles\n\t}\n\n\treturn 0\n}\n\nfunc (i *Image) IsBase64() bool {\n\treturn strings.HasPrefix(i.Content, \"data:image/\")\n}\n\nfunc (i *Image) GetType() string {\n\t// example: image/jpeg, image/png, image/gif\n\n\tif i.IsBase64() {\n\t\tt := SafeSplit(i.Content, \";\", 2)[0]\n\t\treturn strings.ReplaceAll(t, \"data:\", \"\")\n\t}\n\n\t// example: .jpg, .png, .gif to image/jpeg, image/png, image/gif\n\tswitch strings.ToLower(path.Ext(i.Content)) {\n\tcase \".jpg\", \".jpeg\":\n\t\treturn \"image/jpeg\"\n\tcase \".png\":\n\t\treturn \"image/png\"\n\tcase \".gif\":\n\t\treturn \"image/gif\"\n\tcase \".webp\":\n\t\treturn \"image/webp\"\n\tcase \".bmp\":\n\t\treturn \"image/bmp\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (i *Image) ToBase64() string {\n\tif i.IsBase64() {\n\t\treturn i.Content\n\t}\n\n\t// get url content and convert to base64\n\tdata, err := ConvertToBase64(i.Content)\n\tif err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"cannot convert image to base64: %s\", err.Error()))\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"data:%s;base64,%s\", i.GetType(), data)\n}\n\nfunc (i *Image) ToRawBase64() string {\n\t// example: return /9j/...\n\tif i.IsBase64() {\n\t\treturn SafeSplit(i.Content, \",\", 2)[1]\n\t}\n\n\t// get url content and convert to base64\n\tdata, err := ConvertToBase64(i.Content)\n\tif err != nil {\n\t\tglobals.Warn(fmt.Sprintf(\"cannot convert image to base64: %s\", err.Error()))\n\t\treturn \"\"\n\t}\n\n\treturn data\n}\n\nfunc DownloadImage(url string, path string) error {\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func(Body io.ReadCloser) {\n\t\terr := Body.Close()\n\t\tif err != nil {\n\t\t\tglobals.Debug(\"[utils] close file error: %s (path: %s)\", err.Error(), path)\n\t\t}\n\t}(res.Body)\n\n\tfile, err := os.Create(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func(file *os.File) {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tglobals.Debug(\"[utils] close file error: %s (path: %s)\", err.Error(), path)\n\t\t}\n\t}(file)\n\n\t_, err = io.Copy(file, res.Body)\n\treturn err\n}\n\nfunc StoreImage(url string) string {\n\tif globals.AcceptImageStore {\n\t\thash := Md5Encrypt(url) + path.Ext(url)\n\n\t\tif err := DownloadImage(url, fmt.Sprintf(\"storage/attachments/%s\", hash)); err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"[utils] save image error: %s\", err.Error()))\n\t\t\treturn url\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%s/attachments/%s\", globals.NotifyUrl, hash)\n\t}\n\n\treturn url\n}\n"
  },
  {
    "path": "utils/net.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"chat/globals\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/goccy/go-json\"\n\t\"golang.org/x/net/proxy\"\n)\n\nfunc newClient(c []globals.ProxyConfig) *http.Client {\n\tclient := &http.Client{\n\t\tTimeout: globals.HttpMaxTimeout,\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t},\n\t}\n\n\tif len(c) == 0 {\n\t\treturn client\n\t}\n\n\tconfig := c[0]\n\tif config.ProxyType == globals.NoneProxyType {\n\t\treturn client\n\t}\n\n\tif config.ProxyType == globals.HttpProxyType || config.ProxyType == globals.HttpsProxyType {\n\t\tproxyUrl, err := url.Parse(config.Proxy)\n\t\tif len(config.Username) > 0 || len(config.Password) > 0 {\n\t\t\tproxyUrl.User = url.UserPassword(config.Username, config.Password)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"failed to parse proxy url: %s\", err))\n\t\t\treturn client\n\t\t}\n\t\tclient.Transport = &http.Transport{\n\t\t\tProxy:           http.ProxyURL(proxyUrl),\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t} else if config.ProxyType == globals.Socks5ProxyType {\n\t\tvar auth *proxy.Auth\n\t\tif len(config.Username) > 0 || len(config.Password) > 0 {\n\t\t\tauth = &proxy.Auth{\n\t\t\t\tUser:     config.Username,\n\t\t\t\tPassword: config.Password,\n\t\t\t}\n\t\t}\n\n\t\tdialer, err := proxy.SOCKS5(\"tcp\", config.Proxy, auth, proxy.Direct)\n\t\tif err != nil {\n\t\t\tglobals.Warn(fmt.Sprintf(\"failed to create socks5 proxy: %s\", err))\n\t\t\treturn client\n\t\t}\n\n\t\tdialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn dialer.Dial(network, addr)\n\t\t}\n\n\t\tclient.Transport = &http.Transport{\n\t\t\tDialContext:     dialContext,\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t}\n\n\tglobals.Debug(fmt.Sprintf(\"[proxy] configured proxy: %s\", config.Proxy))\n\treturn client\n}\n\nfunc fillHeaders(req *http.Request, headers map[string]string) {\n\tfor key, value := range headers {\n\t\treq.Header.Set(key, value)\n\t}\n}\n\nfunc formatBodyForLog(data []byte, contentType string) string {\n\tif len(data) == 0 {\n\t\treturn \"\"\n\t}\n\n\tisBinary := false\n\tif contentType != \"\" {\n\t\tcontentType = strings.ToLower(contentType)\n\t\tbinaryTypes := []string{\n\t\t\t\"video/\", \"image/\", \"audio/\",\n\t\t\t\"application/octet-stream\",\n\t\t\t\"application/pdf\",\n\t\t\t\"application/zip\",\n\t\t\t\"application/x-\",\n\t\t}\n\t\tfor _, bt := range binaryTypes {\n\t\t\tif strings.HasPrefix(contentType, bt) {\n\t\t\t\tisBinary = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif !isBinary {\n\t\tif !utf8.Valid(data) {\n\t\t\tisBinary = true\n\t\t} else {\n\t\t\tnonPrintableCount := 0\n\t\t\tfor _, b := range data {\n\t\t\t\tif b < 32 && b != 9 && b != 10 && b != 13 {\n\t\t\t\t\tnonPrintableCount++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(data) > 0 && float64(nonPrintableCount)/float64(len(data)) > 0.05 {\n\t\t\t\tisBinary = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif isBinary {\n\t\tdetectedType := contentType\n\t\tif detectedType == \"\" {\n\t\t\tdetectedType = http.DetectContentType(data)\n\t\t}\n\t\tsize := len(data)\n\t\tsizeStr := formatSize(size)\n\t\treturn fmt.Sprintf(\"[Binary Content] Type: %s, Size: %s (%d bytes)\", detectedType, sizeStr, size)\n\t}\n\n\treturn string(data)\n}\n\nfunc formatSize(bytes int) string {\n\tconst unit = 1024\n\tif bytes < unit {\n\t\treturn fmt.Sprintf(\"%d B\", bytes)\n\t}\n\tdiv, exp := int64(unit), 0\n\tfor n := bytes / unit; n >= unit; n /= unit {\n\t\tdiv *= unit\n\t\texp++\n\t}\n\treturn fmt.Sprintf(\"%.1f %cB\", float64(bytes)/float64(div), \"KMGTPE\"[exp])\n}\n\nfunc Http(uri string, method string, ptr interface{}, headers map[string]string, body io.Reader, config []globals.ProxyConfig) (err error) {\n\tvar requestBody io.Reader = body\n\tformattedRequestBody := \"\"\n\tif globals.DebugMode {\n\t\tif body != nil {\n\t\t\tif data, readErr := io.ReadAll(body); readErr == nil {\n\t\t\t\tformattedRequestBody = formatBodyForLog(data, \"\")\n\t\t\t\trequestBody = bytes.NewReader(data)\n\t\t\t} else {\n\t\t\t\tformattedRequestBody = fmt.Sprintf(\"[Body Read Error] %s\", readErr)\n\t\t\t}\n\t\t}\n\t\tglobals.Debug(fmt.Sprintf(\"[http] %s %s\\nheaders: \\n%s\\nbody: \\n%s\", method, uri, Marshal(headers), formattedRequestBody))\n\t}\n\n\treq, err := http.NewRequest(method, uri, requestBody)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to create request: %s\", err))\n\t\t}\n\n\t\treturn err\n\t}\n\tfillHeaders(req, headers)\n\n\tclient := newClient(config)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to send request: %s\", err))\n\t\t}\n\n\t\treturn err\n\t}\n\n\tdefer resp.Body.Close()\n\n\trespData, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tcontentType := resp.Header.Get(\"Content-Type\")\n\t\t\tformattedBody := formatBodyForLog(respData, contentType)\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to read response: %s\\nresponse: %s\", err, formattedBody))\n\t\t}\n\t\treturn err\n\t}\n\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\n\tif globals.DebugMode {\n\t\tformattedBody := formatBodyForLog(respData, contentType)\n\t\tglobals.Debug(fmt.Sprintf(\"[http] response: %s\", formattedBody))\n\t}\n\n\tif err = json.Unmarshal(respData, ptr); err != nil {\n\t\tif globals.DebugMode {\n\t\t\tformattedBody := formatBodyForLog(respData, contentType)\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to decode response: %s\\nresponse: %s\", err, formattedBody))\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc HttpRaw(uri string, method string, headers map[string]string, body io.Reader, config []globals.ProxyConfig) (data []byte, err error) {\n\tif globals.DebugMode {\n\t\tformattedBody := \"\"\n\t\tif body != nil {\n\t\t\tif content, readErr := io.ReadAll(body); readErr == nil {\n\t\t\t\tformattedBody = formatBodyForLog(content, \"\")\n\t\t\t\tbody = bytes.NewReader(content)\n\t\t\t} else {\n\t\t\t\tformattedBody = fmt.Sprintf(\"[Body Read Error] %s\", readErr)\n\t\t\t}\n\t\t}\n\t\tglobals.Debug(fmt.Sprintf(\"[http] %s %s\\nheaders: \\n%s\\nbody: \\n%s\", method, uri, Marshal(headers), formattedBody))\n\t}\n\n\treq, err := http.NewRequest(method, uri, body)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to create request: %s\", err))\n\t\t}\n\n\t\treturn nil, err\n\t}\n\tfillHeaders(req, headers)\n\n\tclient := newClient(config)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to send request: %s\", err))\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif data, err = io.ReadAll(resp.Body); err != nil {\n\t\tif globals.DebugMode {\n\t\t\tcontentType := resp.Header.Get(\"Content-Type\")\n\t\t\tformattedBody := formatBodyForLog(data, contentType)\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http] failed to read response: %s\\nresponse: %s\", err, formattedBody))\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tif globals.DebugMode {\n\t\tcontentType := resp.Header.Get(\"Content-Type\")\n\t\tformattedBody := formatBodyForLog(data, contentType)\n\t\tglobals.Debug(fmt.Sprintf(\"[http] response: %s\", formattedBody))\n\t}\n\treturn data, nil\n}\n\nfunc Get(uri string, headers map[string]string, config ...globals.ProxyConfig) (data interface{}, err error) {\n\terr = Http(uri, http.MethodGet, &data, headers, nil, config)\n\treturn data, err\n}\n\nfunc GetRaw(uri string, headers map[string]string, config ...globals.ProxyConfig) (data string, err error) {\n\tbuffer, err := HttpRaw(uri, http.MethodGet, headers, nil, config)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(buffer), nil\n}\n\nfunc Post(uri string, headers map[string]string, body interface{}, config ...globals.ProxyConfig) (data interface{}, err error) {\n\terr = Http(uri, http.MethodPost, &data, headers, ConvertBody(body), config)\n\treturn data, err\n}\n\nfunc ToString(data interface{}) string {\n\tswitch v := data.(type) {\n\tcase string:\n\t\treturn v\n\tcase int, int8, int16, int32, int64:\n\t\treturn fmt.Sprintf(\"%d\", v)\n\tcase uint, uint8, uint16, uint32, uint64:\n\t\treturn fmt.Sprintf(\"%d\", v)\n\tcase float32, float64:\n\t\treturn fmt.Sprintf(\"%f\", v)\n\tcase bool:\n\t\treturn fmt.Sprintf(\"%t\", v)\n\tdefault:\n\t\tdata := Marshal(data)\n\t\tif len(data) > 0 {\n\t\t\treturn data\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%v\", data)\n\t}\n}\n\nfunc PostRaw(uri string, headers map[string]string, body interface{}, config ...globals.ProxyConfig) (data string, err error) {\n\tbuffer, err := HttpRaw(uri, http.MethodPost, headers, ConvertBody(body), config)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(buffer), nil\n}\n\nfunc ConvertBody(body interface{}) (form io.Reader) {\n\tif buffer, err := json.Marshal(body); err == nil {\n\t\tform = bytes.NewBuffer(buffer)\n\t}\n\treturn form\n}\n\nfunc EventSource(method string, uri string, headers map[string]string, body interface{}, callback func(string) error, config ...globals.ProxyConfig) error {\n\t// panic recovery\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"event source panic: %s (uri: %s, method: %s)\\n%s\", err, uri, method, stack))\n\t\t}\n\t}()\n\n\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\n\tif globals.DebugMode {\n\t\tglobals.Debug(fmt.Sprintf(\"[http-stream] %s %s\\nheaders: \\n%s\\nbody: \\n%s\", method, uri, Marshal(headers), Marshal(body)))\n\t}\n\n\tclient := newClient(config)\n\treq, err := http.NewRequest(method, uri, ConvertBody(body))\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http-stream] failed to create request: %s\", err))\n\t\t}\n\n\t\treturn err\n\t}\n\n\tfillHeaders(req, headers)\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http-stream] failed to send request: %s\", err))\n\t\t}\n\n\t\treturn err\n\t}\n\n\tdefer res.Body.Close()\n\n\tif res.StatusCode >= 400 {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[http-stream] request failed with status: %s\\nresponse: %s\", res.Status, res.Body))\n\t\t}\n\n\t\tif content, err := io.ReadAll(res.Body); err == nil {\n\t\t\tif form, err := Unmarshal[map[string]interface{}](content); err == nil {\n\t\t\t\tdata := MarshalWithIndent(form, 2)\n\t\t\t\treturn fmt.Errorf(\"request failed with status: %s\\n```json\\n%s\\n```\", res.Status, data)\n\t\t\t}\n\t\t}\n\n\t\treturn fmt.Errorf(\"request failed with status: %s\", res.Status)\n\t}\n\n\tfor {\n\t\tbuf := make([]byte, 20480)\n\t\tn, err := res.Body.Read(buf)\n\n\t\tif err == io.EOF {\n\t\t\treturn nil\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdata := string(buf[:n])\n\t\tfor _, item := range strings.Split(data, \"\\n\") {\n\t\t\tif globals.DebugMode {\n\t\t\t\tglobals.Debug(fmt.Sprintf(\"[http-stream] response: %s\", item))\n\t\t\t}\n\n\t\t\tsegment := strings.TrimSpace(item)\n\t\t\tif len(segment) > 0 {\n\t\t\t\tif err := callback(segment); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "utils/scanner.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"runtime/debug\"\n\t\"strings\"\n)\n\ntype EventScannerProps struct {\n\tMethod   string\n\tUri      string\n\tHeaders  map[string]string\n\tBody     interface{}\n\tCallback func(string) error\n\tFullSSE  bool\n}\n\ntype EventScannerError struct {\n\tError error\n\tBody  string\n}\n\nfunc getErrorBody(resp *http.Response) string {\n\tif resp == nil {\n\t\treturn \"\"\n\t}\n\n\tif content, err := io.ReadAll(resp.Body); err == nil {\n\t\treturn string(content)\n\t}\n\n\treturn \"\"\n}\n\nfunc EventScanner(props *EventScannerProps, config ...globals.ProxyConfig) *EventScannerError {\n\t// panic recovery\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tglobals.Warn(fmt.Sprintf(\"event source panic: %s (uri: %s, method: %s)\\n%s\", r, props.Uri, props.Method, stack))\n\t\t}\n\t}()\n\n\tif globals.DebugMode {\n\t\tglobals.Debug(fmt.Sprintf(\"[sse] event source: %s %s\\nheaders: %v\\nbody: %v\", props.Method, props.Uri, Marshal(props.Headers), Marshal(props.Body)))\n\t}\n\n\tclient := newClient(config)\n\treq, err := http.NewRequest(props.Method, props.Uri, ConvertBody(props.Body))\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] failed to create request: %s\", err))\n\t\t}\n\n\t\treturn &EventScannerError{Error: err}\n\t}\n\n\tfillHeaders(req, props.Headers)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] failed to send request: %s\", err))\n\t\t}\n\n\t\treturn &EventScannerError{Error: err}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\t// for error response\n\t\tbody := getErrorBody(resp)\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] request failed with status: %s\\nresponse: %s\", resp.Status, body))\n\t\t}\n\n\t\treturn &EventScannerError{\n\t\t\tError: fmt.Errorf(\"request failed with status code: %d\", resp.StatusCode),\n\t\t\tBody:  body,\n\t\t}\n\t}\n\n\tif props.FullSSE {\n\t\treturn processFullSSE(resp.Body, props.Callback)\n\t}\n\n\treturn processLegacySSE(resp.Body, props.Callback)\n}\n\nfunc processFullSSE(body io.ReadCloser, callback func(string) error) *EventScannerError {\n\tscanner := bufio.NewScanner(body)\n\tvar eventType, eventData string\n\tvar buffer strings.Builder\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif len(strings.TrimSpace(line)) == 0 {\n\t\t\tif eventData != \"\" {\n\t\t\t\tif eventType != \"\" {\n\t\t\t\t\tbuffer.WriteString(\"event: \")\n\t\t\t\t\tbuffer.WriteString(eventType)\n\t\t\t\t\tbuffer.WriteString(\"\\n\")\n\t\t\t\t}\n\t\t\t\tbuffer.WriteString(\"data: \")\n\t\t\t\tbuffer.WriteString(eventData)\n\n\t\t\t\teventStr := buffer.String()\n\t\t\t\tif globals.DebugMode {\n\t\t\t\t\tglobals.Debug(fmt.Sprintf(\"[sse-full] event: %s\", eventStr))\n\t\t\t\t}\n\n\t\t\t\tif err := callback(eventStr); err != nil {\n\t\t\t\t\terr := body.Close()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] event source close error: %s\", err.Error()))\n\t\t\t\t\t}\n\t\t\t\t\treturn &EventScannerError{Error: err}\n\t\t\t\t}\n\n\t\t\t\teventType = \"\"\n\t\t\t\teventData = \"\"\n\t\t\t\tbuffer.Reset()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"event:\") {\n\t\t\teventType = strings.TrimSpace(strings.TrimPrefix(line, \"event:\"))\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"data:\") {\n\t\t\teventData = strings.TrimSpace(strings.TrimPrefix(line, \"data:\"))\n\n\t\t\tif eventData == \"[DONE]\" || strings.HasPrefix(eventData, \"[DONE]\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\tif eventData != \"\" {\n\t\tif eventType != \"\" {\n\t\t\tbuffer.WriteString(\"event: \")\n\t\t\tbuffer.WriteString(eventType)\n\t\t\tbuffer.WriteString(\"\\n\")\n\t\t}\n\t\tbuffer.WriteString(\"data: \")\n\t\tbuffer.WriteString(eventData)\n\n\t\teventStr := buffer.String()\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[sse-full] last event: %s\", eventStr))\n\t\t}\n\n\t\tif err := callback(eventStr); err != nil {\n\t\t\treturn &EventScannerError{Error: err}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc processLegacySSE(body io.ReadCloser, callback func(string) error) *EventScannerError {\n\tscanner := bufio.NewScanner(body)\n\tscanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {\n\t\tif atEOF && len(data) == 0 {\n\t\t\t// when EOF and empty data\n\t\t\treturn 0, nil, nil\n\t\t}\n\n\t\tif idx := bytes.Index(data, []byte(\"\\n\")); idx >= 0 {\n\t\t\t// when found new line\n\t\t\treturn idx + 1, data[:idx], nil\n\t\t}\n\n\t\tif atEOF {\n\t\t\t// when EOF and no new line\n\t\t\treturn len(data), data, nil\n\t\t}\n\n\t\t// when need more data\n\t\treturn 0, nil, nil\n\t})\n\n\tfor scanner.Scan() {\n\t\traw := scanner.Text()\n\n\t\tif len(raw) <= 5 || !strings.HasPrefix(raw, \"data:\") {\n\t\t\t// for only `data:` partial raw or unexpected chunk\n\t\t\tcontinue\n\t\t}\n\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] chunk: %s\", raw))\n\t\t}\n\n\t\tchunk := strings.TrimSpace(strings.TrimPrefix(raw, \"data:\"))\n\t\tif chunk == \"[DONE]\" || strings.HasPrefix(chunk, \"[DONE]\") {\n\t\t\t// for done signal\n\t\t\tcontinue\n\t\t}\n\n\t\t// callback chunk\n\t\tif err := callback(chunk); err != nil {\n\t\t\t// break connection on callback error\n\t\t\terr := body.Close()\n\t\t\tif err != nil {\n\t\t\t\tglobals.Debug(fmt.Sprintf(\"[sse] event source close error: %s\", err.Error()))\n\t\t\t}\n\n\t\t\treturn &EventScannerError{Error: err}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "utils/smtp.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"text/template\"\n\n\t\"gopkg.in/mail.v2\"\n)\n\ntype SmtpPoster struct {\n\tHost     string\n\tProtocol bool\n\tPort     int\n\tUsername string\n\tPassword string\n\tFrom     string\n}\n\nfunc NewSmtpPoster(host string, protocol bool, port int, username string, password string, from string) *SmtpPoster {\n\treturn &SmtpPoster{\n\t\tHost:     host,\n\t\tProtocol: protocol,\n\t\tPort:     port,\n\t\tUsername: username,\n\t\tPassword: password,\n\t\tFrom:     from,\n\t}\n}\n\nfunc (s *SmtpPoster) Valid() bool {\n\treturn s.Host != \"\" && s.Port > 0 && s.Port <= 65535 && s.Username != \"\" && s.Password != \"\" && s.From != \"\"\n}\n\nfunc (s *SmtpPoster) SendMail(to string, subject string, body string) error {\n\tif !s.Valid() {\n\t\treturn fmt.Errorf(\"smtp not configured properly\")\n\t}\n\n\tdialer := mail.NewDialer(s.Host, s.Port, s.Username, s.Password)\n\tfrom := s.From\n\n\tmessage := mail.NewMessage()\n\tmessage.SetHeader(\"From\", from)\n\tmessage.SetHeader(\"To\", to)\n\tmessage.SetHeader(\"Subject\", subject)\n\tmessage.SetBody(\"text/html\", body)\n\n\tif s.Protocol {\n\t\tdialer.StartTLSPolicy = mail.MandatoryStartTLS\n\t} else {\n\t\tdialer.StartTLSPolicy = mail.NoStartTLS\n\t}\n\n\tif err := dialer.DialAndSend(message); err != nil {\n\t\treturn fmt.Errorf(\"sent mail failed: %s\", err.Error())\n\t}\n\n\treturn nil\n}\n\nfunc (s *SmtpPoster) RenderTemplate(filename string, data interface{}) (string, error) {\n\ttmpl, err := template.New(filename).ParseFiles(fmt.Sprintf(\"utils/templates/%s\", filename))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := tmpl.ExecuteTemplate(&buf, filename, data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n\nfunc (s *SmtpPoster) RenderMail(filename string, data interface{}, to string, subject string) error {\n\tbody, err := s.RenderTemplate(filename, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn s.SendMail(to, subject, body)\n}\n"
  },
  {
    "path": "utils/sse.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nvar dataReplacer = strings.NewReplacer(\n\t\"\\n\", \"\\ndata:\",\n\t\"\\r\", \"\\\\r\",\n)\n\ntype StreamEvent struct {\n\tEvent string      `json:\"event\"`\n\tId    string      `json:\"id\"`\n\tData  interface{} `json:\"data\"`\n}\n\ntype stringWriter interface {\n\tio.Writer\n\twriteString(string) (int, error)\n}\n\ntype stringWrapper struct {\n\tio.Writer\n}\n\nfunc (w stringWrapper) writeString(str string) (int, error) {\n\treturn w.Writer.Write([]byte(str))\n}\n\nfunc checkWriter(writer io.Writer) stringWriter {\n\tif w, ok := writer.(stringWriter); ok {\n\t\treturn w\n\t} else {\n\t\treturn stringWrapper{writer}\n\t}\n}\n\nfunc encode(writer io.Writer, event StreamEvent) error {\n\tw := checkWriter(writer)\n\treturn writeData(w, event.Data)\n}\n\nfunc writeData(w stringWriter, data interface{}) error {\n\tdataReplacer.WriteString(w, fmt.Sprint(data))\n\tif strings.HasPrefix(data.(string), \"data\") {\n\t\tw.writeString(\"\\n\\n\")\n\t}\n\treturn nil\n}\n\nfunc (r StreamEvent) Render(w http.ResponseWriter) error {\n\tr.WriteContentType(w)\n\treturn encode(w, r)\n}\n\nfunc (r StreamEvent) WriteContentType(w http.ResponseWriter) {\n\theader := w.Header()\n\theader[\"Content-Type\"] = []string{\"text/event-stream\"}\n\n\tif _, exist := header[\"Cache-Control\"]; !exist {\n\t\theader[\"Cache-Control\"] = []string{\"no-cache\"}\n\t}\n}\n\nfunc NewEvent(data interface{}) StreamEvent {\n\tchunk := Marshal(data)\n\treturn StreamEvent{\n\t\tData: fmt.Sprintf(\"data: %s\", chunk),\n\t}\n}\n\nfunc NewEndEvent() StreamEvent {\n\treturn StreamEvent{\n\t\tData: \"data: [DONE]\",\n\t}\n}\n"
  },
  {
    "path": "utils/templates/code.html",
    "content": "<link href=\"https://fonts.googlefonts.cn/css?family=Open+Sans\" rel=\"stylesheet\">\n<style>\n  * {\n    font-family: \"Open Sans\", Ubuntu, Verdana, Nunito, monospace, Consolas, Monospace, sans-serif;\n  }\n  .im {  /* gmail adapter */\n    color: inherit;\n  }\n  .main {\n    width: max-content;\n    padding: 60px 35px;\n    border: 1px solid lightgray;\n    border-radius: 10px;\n    margin: 10px auto;\n  }\n  .column {\n    text-align: center;\n  }\n  h1 {\n    margin-top: 4px;\n  }\n  a {\n    text-decoration: none;\n    transition: .5s;\n    color: #009efd;\n  }\n  a:active, a:hover {\n    color: #0d64fd;\n  }\n  img {\n    width: 64px;\n    height: 64px;\n  }\n  .code {\n    color: #58a6ff;\n    font-size: large;\n  }\n</style>\n<body>\n<div class=\"main\">\n  <div class=\"column\"><img src=\"{{.Logo}}\" alt=\"\"><h1>{{.Title}}</h1></div>\n  <div class=\"column\"><p>Your One-Time Password is <strong class=\"code\">{{.Code}}</strong></p></div>\n  <div class=\"column\" style=\"color:gray\">\n    <span>The code will expire in <strong>10 minutes</strong>.</span><br>\n    <span>If it is not operated by yourself, please ignore it.</span><br>\n  </div>\n  <br>\n  <div class=\"column\">\n    <a href=\"\">&copy; {{.Title}}</a>\n  </div>\n</div>\n</body>"
  },
  {
    "path": "utils/tokenizer.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkoukk/tiktoken-go\"\n)\n\n//   Using https://github.com/pkoukk/tiktoken-go\n//   To count number of tokens of openai chat messages\n//   OpenAI Cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb\n\nfunc GetWeightByModel(model string) int {\n\tswitch model {\n\tcase globals.GPT3TurboInstruct,\n\t\tglobals.Claude1, globals.Claude1100k,\n\t\tglobals.Claude2, globals.Claude2100k, globals.Claude2200k:\n\t\treturn 2\n\tcase globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo1106, globals.GPT3Turbo0125,\n\t\tglobals.GPT3Turbo16k, globals.GPT3Turbo16k0613,\n\t\tglobals.GPT4, globals.GPT40314, globals.GPT40613,\n\t\tglobals.GPT41106Preview, globals.GPT4TurboPreview, globals.GPT40125Preview,\n\t\tglobals.GPT4VisionPreview, globals.GPT41106VisionPreview,\n\t\tglobals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:\n\t\treturn 3\n\tcase globals.GPT3Turbo0301, globals.GPT3Turbo16k0301:\n\t\treturn 4 // every message follows <|start|>{role/name}\\n{content}<|end|>\\n\n\tdefault:\n\t\tif strings.Contains(model, globals.GPT3Turbo) {\n\t\t\t// warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.\n\t\t\treturn GetWeightByModel(globals.GPT3Turbo0613)\n\t\t} else if strings.Contains(model, globals.GPT4) {\n\t\t\t// warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.\n\t\t\treturn GetWeightByModel(globals.GPT40613)\n\t\t} else if strings.Contains(model, globals.Claude1) {\n\t\t\t// warning: claude-1 may update over time. Returning num tokens assuming claude-1-100k.\n\t\t\treturn GetWeightByModel(globals.Claude1100k)\n\t\t} else if strings.Contains(model, globals.Claude2) {\n\t\t\t// warning: claude-2 may update over time. Returning num tokens assuming claude-2-100k.\n\t\t\treturn GetWeightByModel(globals.Claude2100k)\n\t\t} else {\n\t\t\t// not implemented: See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens\n\t\t\treturn 3\n\t\t}\n\t}\n}\nfunc NumTokensFromMessages(messages []globals.Message, model string, responseType bool) (tokens int) {\n\ttokensPerMessage := GetWeightByModel(model)\n\ttkm, err := tiktoken.EncodingForModel(model)\n\n\tif err != nil {\n\t\t// the method above was deprecated, use the recall method instead\n\t\t// can not encode messages, use length of messages as a proxy for number of tokens\n\t\t// using rune instead of byte to account for unicode characters (e.g. emojis, non-english characters)\n\t\t// data := Marshal(messages)\n\t\t// return len([]rune(data)) * weight\n\n\t\t// use the recall method instead (default encoder model is gpt-3.5-turbo-0613)\n\t\tif globals.DebugMode {\n\t\t\tglobals.Debug(fmt.Sprintf(\"[tiktoken] error encoding messages: %s (model: %s), using default model instead\", err, model))\n\t\t}\n\t\treturn NumTokensFromMessages(messages, globals.GPT3Turbo0613, responseType)\n\t}\n\n\tfor _, message := range messages {\n\t\ttokens += len(tkm.Encode(message.Content, nil, nil))\n\n\t\tif !responseType {\n\t\t\ttokens += len(tkm.Encode(message.Role, nil, nil)) + tokensPerMessage\n\t\t}\n\t}\n\n\tif !responseType {\n\t\ttokens += 3 // every reply is primed with <|start|>assistant<|message|>\n\t}\n\n\tif globals.DebugMode {\n\t\tglobals.Debug(fmt.Sprintf(\"[tiktoken] num tokens from messages: %d (tokens per message: %d, model: %s)\", tokens, tokensPerMessage, model))\n\t}\n\treturn tokens\n}\n\nfunc NumTokensFromResponse(response string, model string) int {\n\tif len(response) == 0 {\n\t\treturn 0\n\t}\n\n\treturn NumTokensFromMessages([]globals.Message{{Content: response}}, model, true)\n}\n\nfunc CountInputQuota(charge Charge, token int) float32 {\n\tif charge.GetType() == globals.TokenBilling {\n\t\treturn float32(token) / 1000 * charge.GetInput()\n\t}\n\n\treturn 0\n}\n\nfunc CountOutputToken(charge Charge, token int) float32 {\n\tswitch charge.GetType() {\n\tcase globals.TokenBilling:\n\t\treturn float32(token) / 1000 * charge.GetOutput()\n\tcase globals.TimesBilling:\n\t\treturn charge.GetOutput()\n\tdefault:\n\t\treturn 0\n\t}\n}\n"
  },
  {
    "path": "utils/websocket.go",
    "content": "package utils\n\nimport (\n\t\"chat/globals\"\n\t\"database/sql\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/gorilla/websocket\"\n)\n\nvar websocketConns int64\nvar websocketConnMutex *sync.Mutex\n\nfunc init() {\n\twebsocketConns = 0\n\twebsocketConnMutex = &sync.Mutex{}\n}\n\nfunc increaseConns() {\n\tgo func() {\n\t\twebsocketConnMutex.Lock()\n\t\twebsocketConns++\n\t\tif globals.DebugMode {\n\t\t\tglobals.Logger.Infof(\"[monitor] alive ws connections: %d [+increase]\", websocketConns)\n\t\t}\n\t\twebsocketConnMutex.Unlock()\n\t}()\n}\n\nfunc decreaseConns() {\n\tgo func() {\n\t\twebsocketConnMutex.Lock()\n\t\twebsocketConns--\n\t\tif globals.DebugMode {\n\t\t\tglobals.Logger.Infof(\"[monitor] alive ws connections: %d [-decrease]\", websocketConns)\n\t\t}\n\t\twebsocketConnMutex.Unlock()\n\t}()\n}\n\nfunc GetConns() int64 {\n\treturn websocketConns\n}\n\ntype WebSocket struct {\n\tCtx        *gin.Context\n\tConn       *websocket.Conn\n\tMaxTimeout time.Duration\n\tClosed     bool\n}\n\nvar defaultMaxTimeout = 15 * time.Minute\n\nfunc CheckUpgrader(c *gin.Context, strict bool) *websocket.Upgrader {\n\treturn &websocket.Upgrader{\n\t\tCheckOrigin: func(r *http.Request) bool {\n\t\t\tif !strict {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\torigin := c.Request.Header.Get(\"Origin\")\n\t\t\tif globals.OriginIsAllowed(origin) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}\n}\n\nfunc NewWebsocket(c *gin.Context, strict bool) *WebSocket {\n\tupgrader := CheckUpgrader(c, strict)\n\tif conn, err := upgrader.Upgrade(c.Writer, c.Request, nil); err != nil {\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"status\":  false,\n\t\t\t\"message\": \"\",\n\t\t\t\"reason\":  err.Error(),\n\t\t})\n\t\treturn nil\n\t} else {\n\t\tinstance := &WebSocket{\n\t\t\tCtx:  c,\n\t\t\tConn: conn,\n\t\t}\n\t\tinstance.Init()\n\t\treturn instance\n\t}\n}\n\nfunc NewWebsocketClient(url string) *WebSocket {\n\tif conn, _, err := websocket.DefaultDialer.Dial(url, nil); err != nil {\n\t\treturn nil\n\t} else {\n\t\tinstance := &WebSocket{\n\t\t\tConn: conn,\n\t\t}\n\t\tinstance.Init()\n\t\treturn instance\n\t}\n}\n\nfunc (w *WebSocket) Init() {\n\tincreaseConns()\n\tw.Closed = false\n\n\tw.Conn.SetCloseHandler(func(code int, text string) error {\n\t\tw.Closed = true\n\t\treturn nil\n\t})\n\n\tw.Conn.SetPongHandler(func(appData string) error {\n\t\treturn w.Conn.SetReadDeadline(time.Now().Add(w.GetMaxTimeout()))\n\t})\n}\n\nfunc (w *WebSocket) SetMaxTimeout(timeout time.Duration) {\n\tw.MaxTimeout = timeout\n}\n\nfunc (w *WebSocket) GetMaxTimeout() time.Duration {\n\tif w.MaxTimeout <= 0 {\n\t\treturn defaultMaxTimeout\n\t}\n\treturn w.MaxTimeout\n}\n\nfunc (w *WebSocket) Read() (int, []byte, error) {\n\treturn w.Conn.ReadMessage()\n}\n\nfunc (w *WebSocket) Write(messageType int, data []byte) error {\n\treturn w.Conn.WriteMessage(messageType, data)\n}\n\nfunc (w *WebSocket) Close() error {\n\treturn w.Conn.Close()\n}\n\nfunc (w *WebSocket) DeferClose() {\n\tdecreaseConns()\n\tif err := w.Close(); err != nil {\n\t\treturn\n\t}\n}\n\nfunc (w *WebSocket) NextWriter(messageType int) (io.WriteCloser, error) {\n\treturn w.Conn.NextWriter(messageType)\n}\n\nfunc (w *WebSocket) ReadJSON(v interface{}) error {\n\treturn w.Conn.ReadJSON(v)\n}\n\nfunc (w *WebSocket) SendJSON(v interface{}) error {\n\treturn w.Conn.WriteJSON(v)\n}\n\nfunc (w *WebSocket) Send(v interface{}) bool {\n\treturn w.SendJSON(v) == nil\n}\n\nfunc (w *WebSocket) Receive(v interface{}) bool {\n\treturn w.ReadJSON(v) == nil\n}\n\nfunc (w *WebSocket) SendText(message string) bool {\n\treturn w.Write(websocket.TextMessage, []byte(message)) == nil\n}\n\nfunc (w *WebSocket) DecrRate(key string) bool {\n\tcache := w.GetCache()\n\treturn DecrInt(cache, key, 1)\n}\n\nfunc (w *WebSocket) IncrRate(key string) bool {\n\tcache := w.GetCache()\n\t_, err := Incr(cache, key, 1)\n\treturn err == nil\n}\n\nfunc (w *WebSocket) IncrRateWithLimit(key string, limit int64, expiration int64) bool {\n\tcache := w.GetCache()\n\tstate, err := IncrWithLimit(cache, key, 1, limit, expiration)\n\treturn state && err == nil\n}\n\nfunc (w *WebSocket) GetCtx() *gin.Context {\n\treturn w.Ctx\n}\n\nfunc (w *WebSocket) GetDB() *sql.DB {\n\treturn GetDBFromContext(w.Ctx)\n}\n\nfunc (w *WebSocket) GetCache() *redis.Client {\n\treturn GetCacheFromContext(w.Ctx)\n}\n\nfunc (w *WebSocket) GetConn() *websocket.Conn {\n\treturn w.Conn\n}\n\nfunc (w *WebSocket) IsClosed() bool {\n\treturn w.Closed\n}\n\nfunc ReadForm[T interface{}](w *WebSocket) (*T, error) {\n\t// golang cannot use generic type in class-like struct\n\t// except ping\n\t_, message, err := w.Read()\n\tif err != nil {\n\t\treturn nil, err\n\t} else if string(message) == \"{\\\"type\\\":\\\"ping\\\"}\" {\n\t\treturn ReadForm[T](w)\n\t}\n\n\tform, err := Unmarshal[T](message)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &form, nil\n}\n"
  },
  {
    "path": "zeabur.yaml",
    "content": "apiVersion: zeabur.com/v1\nkind: Template\nmetadata:\n  name: ChatNio\nspec:\n  description: 🥳 Next Generation AI One-Stop Solution\n  icon: https://coai.dev/logo.png\n  coverImage: https://github.com/coaidev/coai/raw/main/screenshot/coai.png\n  variables:\n    - key: PUBLIC_DOMAIN\n      type: DOMAIN\n      name: Domain\n      description: What is the domain you want for your CoAI.Dev?\n  tags:\n    - ChatGPT\n    - API\n    - Website\n  readme: |-\n    # [🥳 CoAI.Dev](https://coai.dev)\n\n    #### 🚀 下一代 AIGC 一站式商业解决方案\n    #### *“ CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api) ”*\n\n\n    [English](./README.md) · 简体中文 · [官网](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [部署文档](https://coai.dev/docs/deploy)\n\n\n    ## 📝 功能\n    1. 🤖️ **丰富模型支持**: 多模型服务商支持 (OpenAI / Anthropic / Gemini / Midjourney 等十余种格式兼容 & 私有化 LLM 支持)\n    2. 🤯 **美观 UI 设计**: UI 兼容 PC / Pad / 移动三端，遵循 [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) 设计规范，丰富美观的界面设计和后台仪表盘\n    3. 🎃 **完整 Markdown 支持**: 支持 **LaTeX 公式** / **Mermaid 思维导图** / 表格渲染 / 代码高亮 / 图表绘制 / 进度条等进阶 Markdown 语法支持\n    4. 👀 **多主题支持**: 支持多种主题切换，包含亮色主题的**明亮模式**和暗色主题的**深色模式**。 👉 [自定义配色](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)\n    5. 📚 **国际化支持**: 支持国际化，支持多语言切换 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 欢迎贡献翻译 [Pull Request](https://github.com/coaidev/coai/pulls) \n    6. 🎨 **文生图支持**: 支持多种文生图模型: **OpenAI DALL-E**✅ & **Midjourney** (支持 **U/V/R** 操作)✅ & Stable Diffusion✅ 等\n    7. 📡 **强大对话同步**: **用户 0 成本对话跨端同步支持**，支持**对话分享** (支持链接分享 & 保存为图片 & 分享管理), **无需 WebDav / WebRTC 等依赖和复杂学习成本**\n    8. 🎈 **模型市场 & 预设系统**: 支持后台可自定义的模型市场, 可提供模型介绍、标签等参数, 站长可根据情况自定义模型简介。同时支持预设系统，包含 **自定义预设** 和 **云端同步** 功能。\n    9. 📖 **丰富文件解析**: **开箱即用**, 支持**所有模型**的文件解析 (PDF / Docx / Pptx / Excel / 图片等格式解析), **支持更多云端图片存储方案** (S3 / R2 / MinIO 等), **支持 OCR 图片识别** 👉 详情参见项目 [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) (支持 Vercel / Docker 一键部署)\n    10. 🌏 **全模型联网搜索**: 基于 [SearXNG](https://github.com/searxng/searxng) 开源引擎, 支持 Google / Bing / DuckDuckGo / Yahoo / WikiPedia / Arxiv / Qwant 等丰富搜索引擎搜索, 支持安全搜索模式, 内容截断, 图片代理, 测试搜索可用性等功能。\n    11. 💕 **渐进式 Web 应用 (PWA)**: 支持 PWA 应用 & 支持桌面端 (桌面端基于 [Tauri](https://github.com/tauri-apps/tauri))\n    12. 🤩 **齐全后台管理**: 支持美观丰富的仪表盘, 公告 & 通知管理, 用户管理, 订阅管理, 礼品码 & 兑换码管理, 价格设定, 订阅设定, 自定义模型市场, 自定义站点名称 & Logo, SMTP 发件设置等功能\n    13. 🤑 **多种计费方式**: 支持 💴 **订阅制** 和 💴 **弹性计费** 两种计费方式, 弹性计费支持 次数计费 / Token 计费 / 不计费 / 可匿名调用 和 **最小请求点数** 检测等强大功能\n    14. 🎉 **创新模型缓存**: 支持开启模型缓存：即同一个请求入参 Hash 下, 如果之前已请求过, 将直接返回缓存结果 (击中缓存将不计费), 减少请求次数。可自行自定义是否缓存的模型、缓存时间、多种缓存结果数等高级缓存设置\n    15. 🥪 **附加功能** (停止支持): 🍎 **AI 项目生成器功能** / 📂 **批量文章生成功能** / 🥪 **AI 卡片功能** (已废弃)\n    16. 😎 **优秀渠道管理**: 自写优秀渠道算法, 支持⚡ **多渠道管理**, 支持🥳**优先级**设置渠道的调用顺序, 支持🥳**权重**设置同一优先级下的渠道均衡负载分配概率, 支持🥳**用户分组**, 🥳**失败自动重试**, 🥳**模型重定向**, 🥳**内置上游隐藏**, 🥳**渠道状态管理**等强大**企业级功能**\n    17. ⭐ **OpenAI API 分发 & 中转系统**: 支持以 **OpenAI API** 标准格式调用各种大模型, 集成强大的渠道管理功能, 仅需部署一个站点即可实现同时发展 B/C 端业务💖\n    18. 👌 **快速同步上游**: 渠道设置、模型市场、价格设定等设置都可快速同步上游站点，以此基础修改自己的站点配置，快速搭建自己的站点，省时省力，一键同步，快速上线\n    19. 👋 **SEO 优化**: 支持 SEO 优化，支持自定义站点名称、站点 Logo 等 SEO 优化设设置使搜索引擎更快的爬取，你的站点与众不同👋\n    20. 🎫 **多种兑换码体系**: 支持多种兑换码体系，支持礼品码和兑换码，支持批量生成，礼品码适合宣传分发，兑换码适合发卡销售，礼品码一个类型的多个码一个用户仅能兑换一个码，在宣传中一定程度上减少一个用户兑换多次的情况😀\n    21. 🥰 **商用友好协议**: 采用 **Apache-2.0** 开源协议, 商用二开 & 分发友好 (也请遵守 Apache-2.0 协议的规定, 请勿用于违法用途)\n\n    ## 🔨 支持模型\n    1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*\n    2. Anthropic Claude *(✅ Vision ✅ Function Calling)*\n    3. Google Gemini & PaLM2 *(✅ Vision)*\n    4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*\n    5. 讯飞星火 SparkDesk *(✅ Vision ✅ Function Calling)*\n    6. 智谱清言 ChatGLM *(✅ Vision)*\n    7. 通义千问 Tongyi Qwen\n    8. 腾讯混元 Tencent Hunyuan\n    9. 百川大模型 Baichuan AI\n    10. 月之暗面 Moonshot AI (👉 OpenAI)\n    11. 深度求索 DeepSeek AI (👉 OpenAI)\n    12. 字节云雀 ByteDance Skylark *(✅ Function Calling)*\n    13. Groq Cloud AI\n    14. OpenRouter (👉 OpenAI)\n    15. 360 GPT\n    16. LocalAI / Ollama (👉 OpenAI)\n\n    ## 👻 中转 OpenAI 兼容 API\n       - [x] Chat Completions _(/v1/chat/completions)_\n       - [x] Image Generation _(/v1/images)_\n       - [x] Model List _(/v1/models)_\n       - [x] Dashboard Billing _(/v1/billing)_\n\n    ## 🧪 部署\n    1. 填入你想要绑定的域名，并点击部署。\n    2. 部署成功后，默认管理用户名为 `root`，管理密码为 `chatnio123456`，请根据提示尽快更换初始密码。\n\n    更多更新功能和部署手册请查看 👉 [CoAI.Dev (Github)](https://github.com/coaidev/coai)\n  services:\n    - name: Redis\n      icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/redis.svg\n      template: PREBUILT\n      spec:\n        source:\n          image: redis/redis-stack-server:latest\n        ports:\n          - id: database\n            port: 6379\n            type: TCP\n        volumes:\n          - id: data\n            dir: /data\n        instructions:\n          - type: TEXT\n            title: Command to connect to your Redis\n            content: redis-cli -h ${PORT_FORWARDED_HOSTNAME} -p ${DATABASE_PORT_FORWARDED_PORT} -a ${REDIS_PASSWORD}\n          - type: TEXT\n            title: Redis Connection String\n            content: redis://:${REDIS_PASSWORD}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}\n          - type: PASSWORD\n            title: Redis password\n            content: ${REDIS_PASSWORD}\n            category: Credentials\n          - type: TEXT\n            title: Redis host\n            content: ${PORT_FORWARDED_HOSTNAME}\n            category: Hostname & Port\n          - type: TEXT\n            title: Redis port\n            content: ${DATABASE_PORT_FORWARDED_PORT}\n            category: Hostname & Port\n        env:\n          REDIS_ARGS:\n            default: --requirepass ${REDIS_PASSWORD}\n          REDIS_CONNECTION_STRING:\n            default: redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}\n            expose: true\n            readonly: true\n          REDIS_HOST:\n            default: ${CONTAINER_HOSTNAME}\n            expose: true\n            readonly: true\n          REDIS_PASSWORD:\n            default: ${PASSWORD}\n            expose: true\n          REDIS_PORT:\n            default: ${DATABASE_PORT}\n            expose: true\n            readonly: true\n          REDIS_URI:\n            default: ${REDIS_CONNECTION_STRING}\n            expose: true\n            readonly: true\n    - name: MySQL\n      icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/mysql.svg\n      template: PREBUILT\n      spec:\n        source:\n          image: mysql:8.0.33\n        ports:\n          - id: database\n            port: 3306\n            type: TCP\n        volumes:\n          - id: data\n            dir: /var/lib/mysql\n        instructions:\n          - type: TEXT\n            title: Command to connect to your MySQL\n            content: mysqlsh --sql --host=${PORT_FORWARDED_HOSTNAME} --port=${DATABASE_PORT_FORWARDED_PORT} --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --schema=${MYSQL_DATABASE}\n          - type: TEXT\n            title: MySQL username\n            content: ${MYSQL_USERNAME}\n            category: Credentials\n          - type: PASSWORD\n            title: MySQL password\n            content: ${MYSQL_PASSWORD}\n            category: Credentials\n          - type: TEXT\n            title: MySQL database\n            content: ${MYSQL_DATABASE}\n            category: Credentials\n          - type: TEXT\n            title: MySQL host\n            content: ${PORT_FORWARDED_HOSTNAME}\n            category: Hostname & Port\n          - type: TEXT\n            title: MySQL port\n            content: ${DATABASE_PORT_FORWARDED_PORT}\n            category: Hostname & Port\n        env:\n          MYSQL_DATABASE:\n            default: zeabur\n            expose: true\n          MYSQL_HOST:\n            default: ${CONTAINER_HOSTNAME}\n            expose: true\n            readonly: true\n          MYSQL_PASSWORD:\n            default: ${MYSQL_ROOT_PASSWORD}\n            expose: true\n            readonly: true\n          MYSQL_PORT:\n            default: ${DATABASE_PORT}\n            expose: true\n            readonly: true\n          MYSQL_ROOT_PASSWORD:\n            default: ${PASSWORD}\n          MYSQL_USERNAME:\n            default: root\n            expose: true\n            readonly: true\n        configs:\n          - path: /etc/my.cnf\n            template: |\n              [mysqld]\n              default-authentication-plugin=mysql_native_password\n              skip-host-cache\n              skip-name-resolve\n              datadir=/var/lib/mysql\n              socket=/var/run/mysqld/mysqld.sock\n              secure-file-priv=/var/lib/mysql-files\n              user=mysql\n              max_allowed_packet=10M\n              \n              pid-file=/var/run/mysqld/mysqld.pid\n              [client]\n              socket=/var/run/mysqld/mysqld.sock\n              \n              !includedir /etc/mysql/conf.d/\n    - name: ChatNio\n      icon: https://coai.dev/logo.png\n      template: PREBUILT\n      spec:\n        source:\n          image: programzmh/chatnio\n        ports:\n          - id: service\n            port: 8094\n            type: HTTP\n        volumes:\n          - id: logs\n            dir: /logs\n          - id: config\n            dir: /config\n          - id: storage\n            dir: /storage\n      domainKey: PUBLIC_DOMAIN\nlocalization:\n  zh-CN:\n    description: 🥳 下一代 AI 一站式解决方案\n    variables:\n      - key: PUBLIC_DOMAIN\n        type: DOMAIN\n        name: 域名\n        description: 你想将 ChatNio 绑定在哪个域名上？\n    readme: |-\n      # [🥳 CoAI.Dev](https://coai.dev)\n\n      #### 🚀 下一代 AIGC 一站式商业解决方案\n      #### *“ CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api) ”*\n\n\n      [English](./README.md) · 简体中文 · [官网](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [部署文档](https://coai.dev/docs/deploy)\n\n      ## 📝 功能\n      1. 🤖️ **丰富模型支持**: 多模型服务商支持 (OpenAI / Anthropic / Gemini / Midjourney 等十余种格式兼容 & 私有化 LLM 支持)\n      2. 🤯 **美观 UI 设计**: UI 兼容 PC / Pad / 移动三端，遵循 [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) 设计规范，丰富美观的界面设计和后台仪表盘\n      3. 🎃 **完整 Markdown 支持**: 支持 **LaTeX 公式** / **Mermaid 思维导图** / 表格渲染 / 代码高亮 / 图表绘制 / 进度条等进阶 Markdown 语法支持\n      4. 👀 **多主题支持**: 支持多种主题切换，包含亮色主题的**明亮模式**和暗色主题的**深色模式**。 👉 [自定义配色](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)\n      5. 📚 **国际化支持**: 支持国际化，支持多语言切换 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 欢迎贡献翻译 [Pull Request](https://github.com/coaidev/coai/pulls) \n      6. 🎨 **文生图支持**: 支持多种文生图模型: **OpenAI DALL-E**✅ & **Midjourney** (支持 **U/V/R** 操作)✅ & Stable Diffusion✅ 等\n      7. 📡 **强大对话同步**: **用户 0 成本对话跨端同步支持**，支持**对话分享** (支持链接分享 & 保存为图片 & 分享管理), **无需 WebDav / WebRTC 等依赖和复杂学习成本**\n      8. 🎈 **模型市场 & 预设系统**: 支持后台可自定义的模型市场, 可提供模型介绍、标签等参数, 站长可根据情况自定义模型简介。同时支持预设系统，包含 **自定义预设** 和 **云端同步** 功能。\n      9. 📖 **丰富文件解析**: **开箱即用**, 支持**所有模型**的文件解析 (PDF / Docx / Pptx / Excel / 图片等格式解析), **支持更多云端图片存储方案** (S3 / R2 / MinIO 等), **支持 OCR 图片识别** 👉 详情参见项目 [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) (支持 Vercel / Docker 一键部署)\n      10. 🌏 **全模型联网搜索**: 基于 [SearXNG](https://github.com/searxng/searxng) 开源引擎, 支持 Google / Bing / DuckDuckGo / Yahoo / WikiPedia / Arxiv / Qwant 等丰富搜索引擎搜索, 支持安全搜索模式, 内容截断, 图片代理, 测试搜索可用性等功能。\n      11. 💕 **渐进式 Web 应用 (PWA)**: 支持 PWA 应用 & 支持桌面端 (桌面端基于 [Tauri](https://github.com/tauri-apps/tauri))\n      12. 🤩 **齐全后台管理**: 支持美观丰富的仪表盘, 公告 & 通知管理, 用户管理, 订阅管理, 礼品码 & 兑换码管理, 价格设定, 订阅设定, 自定义模型市场, 自定义站点名称 & Logo, SMTP 发件设置等功能\n      13. 🤑 **多种计费方式**: 支持 💴 **订阅制** 和 💴 **弹性计费** 两种计费方式, 弹性计费支持 次数计费 / Token 计费 / 不计费 / 可匿名调用 和 **最小请求点数** 检测等强大功能\n      14. 🎉 **创新模型缓存**: 支持开启模型缓存：即同一个请求入参 Hash 下, 如果之前已请求过, 将直接返回缓存结果 (击中缓存将不计费), 减少请求次数。可自行自定义是否缓存的模型、缓存时间、多种缓存结果数等高级缓存设置\n      15. 🥪 **附加功能** (停止支持): 🍎 **AI 项目生成器功能** / 📂 **批量文章生成功能** / 🥪 **AI 卡片功能** (已废弃)\n      16. 😎 **优秀渠道管理**: 自写优秀渠道算法, 支持⚡ **多渠道管理**, 支持🥳**优先级**设置渠道的调用顺序, 支持🥳**权重**设置同一优先级下的渠道均衡负载分配概率, 支持🥳**用户分组**, 🥳**失败自动重试**, 🥳**模型重定向**, 🥳**内置上游隐藏**, 🥳**渠道状态管理**等强大**企业级功能**\n      17. ⭐ **OpenAI API 分发 & 中转系统**: 支持以 **OpenAI API** 标准格式调用各种大模型, 集成强大的渠道管理功能, 仅需部署一个站点即可实现同时发展 B/C 端业务💖\n      18. 👌 **快速同步上游**: 渠道设置、模型市场、价格设定等设置都可快速同步上游站点，以此基础修改自己的站点配置，快速搭建自己的站点，省时省力，一键同步，快速上线\n      19. 👋 **SEO 优化**: 支持 SEO 优化，支持自定义站点名称、站点 Logo 等 SEO 优化设设置使搜索引擎更快的爬取，你的站点与众不同👋\n      20. 🎫 **多种兑换码体系**: 支持多种兑换码体系，支持礼品码和兑换码，支持批量生成，礼品码适合宣传分发，兑换码适合发卡销售，礼品码一个类型的多个码一个用户仅能兑换一个码，在宣传中一定程度上减少一个用户兑换多次的情况😀\n      21. 🥰 **商用友好协议**: 采用 **Apache-2.0** 开源协议, 商用二开 & 分发友好 (也请遵守 Apache-2.0 协议的规定, 请勿用于违法用途)\n\n      ## 🔨 支持模型\n      1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*\n      2. Anthropic Claude *(✅ Vision ✅ Function Calling)*\n      3. Google Gemini & PaLM2 *(✅ Vision)*\n      4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*\n      5. 讯飞星火 SparkDesk *(✅ Vision ✅ Function Calling)*\n      6. 智谱清言 ChatGLM *(✅ Vision)*\n      7. 通义千问 Tongyi Qwen\n      8. 腾讯混元 Tencent Hunyuan\n      9. 百川大模型 Baichuan AI\n      10. 月之暗面 Moonshot AI (👉 OpenAI)\n      11. 深度求索 DeepSeek AI (👉 OpenAI)\n      12. 字节云雀 ByteDance Skylark *(✅ Function Calling)*\n      13. Groq Cloud AI\n      14. OpenRouter (👉 OpenAI)\n      15. 360 GPT\n      16. LocalAI / Ollama (👉 OpenAI)\n\n      ## 👻 中转 OpenAI 兼容 API\n         - [x] Chat Completions _(/v1/chat/completions)_\n         - [x] Image Generation _(/v1/images)_\n         - [x] Model List _(/v1/models)_\n         - [x] Dashboard Billing _(/v1/billing)_\n\n      ## 🧪 部署\n      1. 填入你想要绑定的域名，并点击部署。\n      2. 部署成功后，默认管理用户名为 `root`，管理密码为 `chatnio123456`，请根据提示尽快更换初始密码。\n\n      更多更新功能和部署手册请查看 👉 [CoAI.Dev (Github)](https://github.com/coaidev/coai)\n"
  }
]