[
  {
    "path": ".dockerignore",
    "content": "/.github\n/Dockerfile\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [ztjhz]\nko_fi: betterchatgpt\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: 'pages'\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Use Node.js 18\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Install dependencies\n        run: yarn\n\n      - name: Build website\n        run: yarn build\n        env:\n          VITE_GOOGLE_CLIENT_ID: ${{ secrets.GCLIENT }}\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v1\n        with:\n          path: './dist'\n\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v1\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Build and publish desktop app\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: 'publish'\n  cancel-in-progress: true\n\njobs:\n  build:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Use Node.js 18\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Install dependencies\n        run: yarn\n\n      - name: Build\n        run: yarn make\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          VITE_GOOGLE_CLIENT_ID: ${{ secrets.GCLIENT }}\n"
  },
  {
    "path": ".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\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\nrelease/\n\n.env"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"jsxBracketSameLine\": false,\n  \"arrowParens\": \"always\",\n  \"quoteProps\": \"consistent\"\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine\n\nRUN addgroup -S appgroup && \\\n  adduser -S appuser -G appgroup && \\\n  mkdir -p /home/appuser/app && \\\n  chown appuser:appgroup /home/appuser/app\nUSER appuser\n\nRUN yarn config set prefix ~/.yarn && \\\n  yarn global add serve\n\nWORKDIR /home/appuser/app\nCOPY --chown=appuser:appgroup package.json yarn.lock ./\nRUN yarn install --frozen-lockfile\nCOPY --chown=appuser:appgroup . .\nRUN yarn build\n\nEXPOSE 3000\nCMD [\"/home/appuser/.yarn/bin/serve\", \"-s\", \"dist\", \"-l\", \"3000\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work."
  },
  {
    "path": "README-zh_CN.md",
    "content": "<h1 align=\"center\"><b>Better ChatGPT</b></h1>\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\" target=\"_blank\"><img src=\"public/apple-touch-icon.png\" alt=\"Better ChatGPT\" width=\"100\" /></a>\n</p>\n\n<h4 align=\"center\"><b>免费、无限、强大、智能、迷人</b></h4>\n\n<p align=\"center\">\n<a href=\"https://github.com/ztjhz/BetterChatGPT/blob/main/LICENSE\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/license/ztjhz/BetterChatGPT?style=flat-square\" alt=\"licence\" />\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/fork\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/forks/ztjhz/BetterChatGPT?style=flat-square\" alt=\"forks\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/stargazers\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/stars/ztjhz/BetterChatGPT?style=flat-square\" alt=\"stars\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/issues\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/issues/ztjhz/BetterChatGPT?style=flat-square\" alt=\"issues\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/pulls\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/issues-pr/ztjhz/BetterChatGPT?style=flat-square\" alt=\"pull-requests\"/>\n</a>\n<a href=\"https://twitter.com/intent/tweet?text=👋看看这个惊人的存储库%20https://github.com/ztjhz/BetterChatGPT，由%20@nikushii_%20创建。\"><img src=\"https://img.shields.io/twitter/url?label=%E5%88%86%E4%BA%AB%E5%88%B0%E6%8E%A8%E7%89%B9&style=social&url=https%3A%2F%2Fgithub.com%2Fztjhz%2FBetterChatGPT\"></a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\">进入网站</a>\n    ·\n    <a href=\"https://github.com/ztjhz/BetterChatGPT/issues/new/choose\">反馈问题</a>\n    ·\n    <a href=\"https://github.com/ztjhz/BetterChatGPT/issues/new/choose\">请求功能</a>\n</p>\n<p align=\"center\"><i>您喜欢使用 Better ChatGPT 吗？请给它一个星星以示支持！🌟</i></p>\n\n## 👋🏻 介绍 Better ChatGPT\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\" target=\"_blank\">\n        <img src=\"assets/preview-zh_CN.png\" alt=\"landing\" width=500 />\n    </a>\n</p>\n\n您准备好使用 Better ChatGPT 充分发掘 ChatGPT 的潜力了吗？\n\nBetter ChatGPT 是任何想要体验对话型人工智能无限潜力的人的终极目的地。我们的应用程序利用 OpenAI 的 ChatGPT API 的全部潜力，提供了一个无与伦比的聊天机器人体验，而且完全免费，并且没有任何限制。\n\n无论您是想与虚拟助手聊天、提高语言技能，还是想享受有趣而引人入胜的对话，我们的应用都能满足您的需求。那么，为什么还要等呢？立即加入我们，探索 Better ChatGPT 的精彩世界！\n\n# 🔥 功能\n\nBetter ChatGPT 已经包含了大量的功能。您可以使用以下功能：\n\n- 支持使用内置代理解決 ChatGPT 地区限制\n- 支持自定义提示词资料库\n- 支持使用文件夹（且带颜色）整理聊天\n- 支持筛选聊天和文件夹\n- 支持实时计算 token 数量和价格\n- 支持使用 ShareGPT 分享聊天\n- 支持自定义 API 参数（例如存在惩罚）\n- 支持自定义用户/助理/系统身份\n- 支持任意编辑/插入/调整消息顺序\n- 支持自动生成聊天标题\n- 支持自动保存聊天记录\n- 支持导入/导出聊天记录\n- 支持将聊天保存为 Markdown/图片/JSON\n- 支持与 Google Drive 同步\n- 支持 Azure OpenAI 终端\n- 支持多语言 (i18n)\n\n# 🛠️ 使用方法\n\n要开始使用，只需访问我们的网站：<https://bettergpt.chat/>。您有 3 种方法可以开始使用 Better ChatGPT。\n\n1. 在 API 菜单中输入您从 [OpenAI API Keys](https://platform.openai.com/account/api-keys) 获得的 OpenAI API 密钥。\n2. 使用提供的 API 端点代理：[ayaka14732/ChatGPTAPIFree](https://github.com/ayaka14732/ChatGPTAPIFree)。（如果您所在的区域无法访问 ChatGPT）\n3. 按照这里提供的说明托管自己的 API 端点：<https://github.com/ayaka14732/ChatGPTAPIFree>。随后，在 API 菜单中输入 API 端点。\n\n## 桌面应用\n\n在此下载桌面应用程序：<https://github.com/ztjhz/BetterChatGPT/releases/>\n\n| 操作系统 | 下载      |\n| -------- | --------- |\n| Windows  | .exe      |\n| MacOS    | .dmg      |\n| Linux    | .AppImage |\n\n### 功能\n\n- 无限本地存储\n- 本地运行（即使无法访问 Better ChatGPT 网站也可以使用）\n\n# 🛫 托管自己的实例\n\n如果您想运行自己的 Better ChatGPT 实例，可以按照以下步骤轻松完成：\n\n## Vercel\n\n使用 Vercel 一键部署\n\n[![Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fztjhz%2FBetterChatGPT)\n\n## GitHub 页面\n\n### 步骤\n\n1. 创建一个 GitHub 账户（如果您还没有账户）。\n1. 给此[存储库](https://github.com/ztjhz/BetterChatGPT) 一个星星 ⭐️\n1. Fork 此[存储库](https://github.com/ztjhz/BetterChatGPT)\n1. 在 fork 之后的存储库中点击 `Settings` 选项卡\n   ![image](https://user-images.githubusercontent.com/59118459/223753577-9b6f8266-26e8-471b-8f45-a1a02fbab232.png)\n1. 在左侧边栏中，单击 `Pages` ，在右侧区域中，为 `Source` 选择 `GitHub Actions`。\n   ![image](https://user-images.githubusercontent.com/59118459/227568881-d8fb7baa-f890-4dee-8fc2-b6b429ba2098.png)\n1. 现在点击 `Actions`\n   ![image](https://user-images.githubusercontent.com/59118459/223751928-cf2b91b9-4663-4a36-97de-5eb751b32c7e.png)\n1. 在左侧边栏中，点击 `Deploy to GitHub Pages`\n   ![image](https://user-images.githubusercontent.com/59118459/223752459-183ec23f-72f5-436e-a088-e3386492b8cb.png)\n1. 在运行的工作流列表上方，选择 `Run workflow` 。\n   ![image](https://user-images.githubusercontent.com/59118459/223753340-1270e038-d213-4d6f-938c-66a30dad7c88.png)\n1. 返回到 `Settings` 选项卡\n   ![image](https://user-images.githubusercontent.com/59118459/223753577-9b6f8266-26e8-471b-8f45-a1a02fbab232.png)\n1. 在左侧边栏中，单击 `Pages` 。然后在顶部部分，您可以看到 \"Your site is live at `XXX`\"。\n   ![image](https://user-images.githubusercontent.com/59118459/227568881-d8fb7baa-f890-4dee-8fc2-b6b429ba2098.png)\n\n### 在本地运行\n\n1. 确保您已安装以下内容：\n\n   - [node.js](https://nodejs.org/en/)\n   - [yarn](https://yarnpkg.com/) 或者 [npm](https://www.npmjs.com/)\n\n2. 通过运行 `git clone https://github.com/ztjhz/BetterChatGPT.git` 克隆此[存储库](https://github.com/ztjhz/BetterChatGPT)。\n3. 进入目录通过 `cd BetterChatGPT`\n4. 运行 `yarn` 或 `npm install`，具体取决于您是否安装了 yarn 或 npm。\n5. 运行 `yarn dev` 或 `npm run dev` 来启动应用程序。\n\n# ⭐️ 星星历史\n\n[![Star History Chart](https://api.star-history.com/svg?repos=ztjhz/BetterChatGPT&type=Date)](https://github.com/ztjhz/BetterChatGPT/stargazers)\n\n<h3 align=\"center\">\n    给 <b>Better ChatGPT</b> 一个星星 ⭐️ 可以让它更加锦上添花，让更多人受益匪浅。\n</h3>\n\n# ❤️ 贡献者\n\n感谢所有贡献者！\n\n<a href=\"https://github.com/ztjhz/BetterChatGPT/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=ztjhz/BetterChatGPT\" />\n</a>\n\n# 🙏 支持\n\n在 Better ChatGPT，我们致力于为您提供实用和惊人的功能。就像任何项目一样，您的支持和激励将对我们在保持前进方面起到至关重要的作用！\n\n如果您喜欢使用我们的应用程序，我们恳请您给这个项目一颗 ⭐️。您的认可对我们意义重大，鼓励我们更加努力，以提供最佳的体验。\n\n如果您想支持我们的团队，请考虑通过以下方法之一赞助我们。每一份贡献，无论多小，都有助于我们维护和改善我们的服务。\n\n| 付款方式       | 链接                                                                                                                                                 |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 支付宝 (Ayaka) | <img src=\"https://ayaka14732.github.io/sponsor/alipay.jpg\" width=150 />                                                                              |\n| 微信 (Ayaka)   | <img src=\"https://ayaka14732.github.io/sponsor/wechat.png\" width=150 />                                                                              |\n| GitHub         | [![GitHub Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/ztjhz) |\n| KoFi           | [![support](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/betterchatgpt)                                                             |\n\n感谢您成为我们社区的一员，我们期待着在未来为您提供更好的服务。\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\"><b>Better ChatGPT</b></h1>\n\n<p align=\"center\">\n   English Version |\n   <a href=\"README-zh_CN.md\">\n      简体中文版\n   </a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\" target=\"_blank\"><img src=\"public/apple-touch-icon.png\" alt=\"Better ChatGPT\" width=\"100\" /></a>\n</p>\n\n<h4 align=\"center\"><b>Free, Powerful, Limitless, Intelligent, Engaging</b></h4>\n\n<p align=\"center\">\n<a href=\"https://github.com/ztjhz/BetterChatGPT/blob/main/LICENSE\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/license/ztjhz/BetterChatGPT?style=flat-square\" alt=\"licence\" />\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/fork\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/forks/ztjhz/BetterChatGPT?style=flat-square\" alt=\"forks\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/stargazers\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/stars/ztjhz/BetterChatGPT?style=flat-square\" alt=\"stars\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/issues\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/issues/ztjhz/BetterChatGPT?style=flat-square\" alt=\"issues\"/>\n</a>\n<a href=\"https://github.com/ztjhz/BetterChatGPT/pulls\" target=\"_blank\">\n<img src=\"https://img.shields.io/github/issues-pr/ztjhz/BetterChatGPT?style=flat-square\" alt=\"pull-requests\"/>\n</a>\n<a href=\"https://twitter.com/intent/tweet?text=👋%20Check%20this%20amazing%20repo%20https://github.com/ztjhz/BetterChatGPT,%20created%20by%20@nikushii_\"><img src=\"https://img.shields.io/twitter/url?label=Share%20on%20Twitter&style=social&url=https%3A%2F%2Fgithub.com%2Fztjhz%2FBetterChatGPT\"></a>\n</p>\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\">Enter Website</a>\n    ·\n    <a href=\"https://github.com/ztjhz/BetterChatGPT/issues/new/choose\">Report Bug</a>\n    ·\n    <a href=\"https://github.com/ztjhz/BetterChatGPT/issues/new/choose\">Request Feature</a>\n</p>\n<p align=\"center\"><i>Did you enjoy using Better ChatGPT? Give it some love with a star! 🌟</i></p>\n\n## 👋🏻 Introducing Better ChatGPT\n\n<p align=\"center\">\n    <a href=\"https://bettergpt.chat\" target=\"_blank\">\n        <img src=\"assets/preview.png\" alt=\"landing\" width=500 />\n    </a>\n</p>\n\nAre you ready to unlock the full potential of ChatGPT with Better ChatGPT?\n\nBetter ChatGPT is the ultimate destination for anyone who wants to experience the limitless power of conversational AI. With no limits and completely free to use for all, our app harnesses the full potential of OpenAI's ChatGPT API to offer you an unparalleled chatbot experience.\n\nWhether you're looking to chat with a virtual assistant, improve your language skills, or simply enjoy a fun and engaging conversation, our app has got you covered. So why wait? Join us today and explore the exciting world of Better ChatGPT!\n\n# 🔥 Features\n\nBetter ChatGPT comes with a bundle of amazing features! Here are some of them:\n\n- Proxy to bypass ChatGPT regional restrictions\n- Prompt library\n- Organize chats into folders (with colours)\n- Filter chats and folders\n- Token count and pricing\n- ShareGPT integration\n- Custom model parameters (e.g. presence_penalty)\n- Chat as user / assistant / system\n- Edit, reorder and insert any messages, anywhere\n- Chat title generator\n- Save chat automatically to local storage\n- Import / Export chat\n- Download chat (markdown / image / json)\n- Sync to Google Drive\n- Azure OpenAI endpoint support\n- Multiple language support (i18n)\n\n# 🛠️ Usage\n\nTo get started, simply visit our website at <https://bettergpt.chat/>. There are 3 ways for you to start using Better ChatGPT.\n\n1. Enter into the API menu your OpenAI API Key obtained from [OpenAI API Keys](https://platform.openai.com/account/api-keys).\n2. Utilise the api endpoint proxy provided by [ayaka14732/ChatGPTAPIFree](https://github.com/ayaka14732/ChatGPTAPIFree) (if you are in a region with no access to ChatGPT)\n3. Host your own API endpoint by following the instructions provided here: <https://github.com/ayaka14732/ChatGPTAPIFree>. Subsequently, enter the API endpoint into the API menu.\n\n## Desktop App\n\nDownload the desktop app [here](https://github.com/ztjhz/BetterChatGPT/releases)\n\n| OS      | Download  |\n| ------- | --------- |\n| Windows | .exe      |\n| MacOS   | .dmg      |\n| Linux   | .AppImage |\n\n### Features:\n\n- Unlimited local storage\n- Runs locally (access Better ChatGPT even if the website is not accessible)\n\n# 🛫 Host your own Instance\n\nIf you'd like to run your own instance of Better ChatGPT, you can easily do so by following these steps:\n\n## Vercel\n\nOne click deploy with Vercel\n\n[![Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fztjhz%2FBetterChatGPT)\n\n## GitHub Pages\n\n### Steps\n\n1. Create a GitHub account (if you don't have one already)\n1. Star this [repository](https://github.com/ztjhz/BetterChatGPT) ⭐️\n1. Fork this [repository](https://github.com/ztjhz/BetterChatGPT)\n1. In your forked repository, navigate to the `Settings` tab\n   ![image](https://user-images.githubusercontent.com/59118459/223753577-9b6f8266-26e8-471b-8f45-a1a02fbab232.png)\n1. In the left sidebar, click on `Pages` and in the right section, select `GitHub Actions` for `source`.\n   ![image](https://user-images.githubusercontent.com/59118459/227568881-d8fb7baa-f890-4dee-8fc2-b6b429ba2098.png)\n1. Now, click on `Actions`\n   ![image](https://user-images.githubusercontent.com/59118459/223751928-cf2b91b9-4663-4a36-97de-5eb751b32c7e.png)\n1. In the left sidebar, click on `Deploy to GitHub Pages`\n   ![image](https://user-images.githubusercontent.com/59118459/223752459-183ec23f-72f5-436e-a088-e3386492b8cb.png)\n1. Above the list of workflow runs, select `Run workflow`.\n   ![image](https://user-images.githubusercontent.com/59118459/223753340-1270e038-d213-4d6f-938c-66a30dad7c88.png)\n1. Navigate back to the `Settings` tab\n   ![image](https://user-images.githubusercontent.com/59118459/223753577-9b6f8266-26e8-471b-8f45-a1a02fbab232.png)\n1. In the left sidebar, click on `Pages` and in the right section. Then at the top section, you can see that \"Your site is live at `XXX`\".\n   ![image](https://user-images.githubusercontent.com/59118459/227568881-d8fb7baa-f890-4dee-8fc2-b6b429ba2098.png)\n\n### Running it locally\n\n1. Ensure that you have the following installed:\n\n   - [node.js](https://nodejs.org/en/) (v14.18.0 or above)\n   - [yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/) (6.14.15 or above)\n\n2. Clone this [repository](https://github.com/ztjhz/BetterChatGPT) by running `git clone https://github.com/ztjhz/BetterChatGPT.git`\n3. Navigate into the directory by running `cd BetterChatGPT`\n4. Run `yarn` or `npm install`, depending on whether you have yarn or npm installed.\n5. Launch the app by running `yarn dev` or `npm run dev`\n\n### Running it locally using docker compose\n1. Ensure that you have the following installed:\n\n   - [docker](https://www.docker.com/) (v24.0.7 or above)\n      ```bash\n      curl https://get.docker.com | sh \\\n      && sudo usermod -aG docker $USER\n      ```\n\n2. Build the docker image\n   ```\n   docker compose build\n   ```\n\n3. Build and start the container using docker compose\n   ```\n   docker compose build\n   docker compose up -d\n   ```\n\n4. Stop the container\n   ```\n   docker compose down\n   ```\n\n# ⭐️ Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=ztjhz/BetterChatGPT&type=Date)](https://github.com/ztjhz/BetterChatGPT/stargazers)\n\n<h3 align=\"center\">\nA ⭐️ to <b>Better ChatGPT</b> is to make it shine brighter and benefit more people.\n</h3>\n\n# ❤️ Contributors\n\nThanks to all the contributors!\n\n<a href=\"https://github.com/ztjhz/BetterChatGPT/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=ztjhz/BetterChatGPT\" />\n</a>\n\n# 🙏 Support\n\nAt Better ChatGPT, we strive to provide you with useful and amazing features around the clock. And just like any project, your support and motivation will be instrumental in helping us keep moving forward!\n\nIf you have enjoyed using our app, we kindly ask you to give this project a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.\n\nIf you would like to support the team, consider sponsoring us through one of the methods below. Every contribution, no matter how small, helps us to maintain and improve our service.\n\n| Payment Method | Link                                                                                                                                                 |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| GitHub         | [![GitHub Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/ztjhz) |\n| KoFi           | [![support](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/betterchatgpt)                                                             |\n| Alipay (Ayaka) | <img src=\"https://ayaka14732.github.io/sponsor/alipay.jpg\" width=150 />                                                                              |\n| Wechat (Ayaka) | <img src=\"https://ayaka14732.github.io/sponsor/wechat.png\" width=150 />                                                                              |\n\nThank you for being a part of our community, and we look forward to serving you better in the future.\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  ui_dev:\n    restart: always\n    image: better-chat-gpt\n    build:\n      context: ./\n    ports:\n      - 5173:3000\n"
  },
  {
    "path": "electron/index.cjs",
    "content": "const path = require('path');\n\nconst {\n  app,\n  shell,\n  clipboard,\n  dialog,\n  download,\n  BrowserWindow,\n  Tray,\n  Menu,\n  MenuItem,\n} = require('electron');\nconst isDev = require('electron-is-dev');\nconst { autoUpdater } = require('electron-updater');\nlet win = null;\nconst instanceLock = app.requestSingleInstanceLock();\nconst isMacOS = process.platform === 'darwin';\n\nif (require('electron-squirrel-startup')) app.quit();\n\nconst PORT = isDev ? '5173' : '51735';\nconst ICON = 'icon-rounded.png';\nconst ICON_TEMPLATE = 'iconTemplate.png';\n\nconst setupLinksLeftClick = (win) => {\n  win.webContents.setWindowOpenHandler(({ url }) => {\n    shell.openExternal(url);\n    return { action: 'deny' };\n  });\n};\n\nconst setupContextMenu = (win) => {\n  win.webContents.on('context-menu', (_, params) => {\n    const { x, y, linkURL, selectionText } = params;\n\n    const template = [\n      { role: 'undo' },\n      { role: 'redo' },\n      { type: 'separator' },\n      { role: 'cut' },\n      { role: 'copy' },\n      { role: 'paste' },\n      { role: 'pasteAndMatchStyle' },\n      { role: 'delete' },\n      { type: 'separator' },\n      { role: 'selectAll' },\n      { type: 'separator' },\n      { role: 'toggleDevTools' },\n    ];\n\n    const spellingMenu = [];\n\n    if (selectionText && !linkURL) {\n      // Add each spelling suggestion\n      for (const suggestion of params.dictionarySuggestions) {\n        spellingMenu.push(\n          new MenuItem({\n            label: suggestion,\n            click: () => win.webContents.replaceMisspelling(suggestion),\n          })\n        );\n      }\n\n      // Allow users to add the misspelled word to the dictionary\n      if (params.misspelledWord) {\n        spellingMenu.push(\n          new MenuItem({\n            label: 'Add to dictionary',\n            click: () =>\n              win.webContents.session.addWordToSpellCheckerDictionary(\n                params.misspelledWord\n              ),\n          })\n        );\n      }\n\n      if (spellingMenu.length > 0) {\n        spellingMenu.push({ type: 'separator' });\n      }\n\n      template.push(\n        { type: 'separator' },\n        {\n          label: `Search Google for \"${selectionText}\"`,\n          click: () => {\n            shell.openExternal(\n              `https://www.google.com/search?q=${encodeURIComponent(\n                selectionText\n              )}`\n            );\n          },\n        },\n        {\n          label: `Search DuckDuckGo for \"${selectionText}\"`,\n          click: () => {\n            shell.openExternal(\n              `https://duckduckgo.com/?q=${encodeURIComponent(selectionText)}`\n            );\n          },\n        }\n      );\n    }\n\n    if (linkURL) {\n      template.push(\n        { type: 'separator' },\n        {\n          label: 'Open Link in Browser',\n          click: () => {\n            shell.openExternal(linkURL);\n          },\n        },\n        {\n          label: 'Copy Link Address',\n          click: () => {\n            clipboard.writeText(linkURL);\n          },\n        },\n        {\n          label: 'Save Link As...',\n          click: () => {\n            dialog.showSaveDialog(\n              win,\n              { defaultPath: path.basename(linkURL) },\n              (filePath) => {\n                if (filePath) {\n                  download(win, linkURL, { filename: filePath });\n                }\n              }\n            );\n          },\n        }\n      );\n    }\n\n    Menu.buildFromTemplate([...spellingMenu, ...template]).popup({\n      window: win,\n      x,\n      y,\n    });\n  });\n};\n\nfunction createWindow() {\n  autoUpdater.checkForUpdatesAndNotify();\n\n  win = new BrowserWindow({\n    autoHideMenuBar: true,\n    show: false,\n    icon: assetPath(ICON),\n  });\n\n  createTray(win);\n\n  win.maximize();\n  win.show();\n\n  isDev || createServer();\n\n  win.loadURL(`http://localhost:${PORT}`);\n\n  if (isDev) {\n    win.webContents.openDevTools({ mode: 'detach' });\n  }\n\n  setupLinksLeftClick(win);\n  setupContextMenu(win);\n\n  return win;\n}\n\nconst assetPath = (asset) => {\n  return path.join(\n    __dirname,\n    isDev ? `../public/${asset}` : `../dist/${asset}`\n  );\n};\n\nconst createTray = (win) => {\n  const tray = new Tray(assetPath(!isMacOS ? ICON : ICON_TEMPLATE));\n  const contextMenu = Menu.buildFromTemplate([\n    {\n      label: 'Show',\n      click: () => {\n        win.maximize();\n        win.show();\n      },\n    },\n    {\n      label: 'Exit',\n      click: () => {\n        app.isQuiting = true;\n        app.quit();\n      },\n    },\n  ]);\n\n  tray.on('click', () => {\n    win.maximize();\n    win.show();\n  });\n  tray.setToolTip('Better ChatGPT');\n  tray.setContextMenu(contextMenu);\n\n  return tray;\n};\n\napp.on('window-all-closed', () => {\n  if (!isMacOS) {\n    app.quit();\n  }\n});\n\nprocess.on('uncaughtException', (error) => {\n  // Perform any necessary cleanup tasks here\n  dialog.showErrorBox('An error occurred', error.stack);\n\n  // Exit the app\n  process.exit(1);\n});\n\nif (!instanceLock) {\n  app.quit();\n} else {\n  app.on('second-instance', (event, commandLine, workingDirectory) => {\n    if (win) {\n      if (win.isMinimized()) win.restore();\n      win.focus();\n    }\n  });\n\n  app.whenReady().then(() => {\n    win = createWindow();\n  });\n}\n\nconst createServer = () => {\n  // Dependencies\n  const http = require('http');\n  const fs = require('fs');\n  const path = require('path');\n\n  // MIME types for different file extensions\n  const mimeTypes = {\n    '.html': 'text/html',\n    '.css': 'text/css',\n    '.js': 'text/javascript',\n    '.wasm': 'application/wasm',\n    '.jpg': 'image/jpeg',\n    '.jpeg': 'image/jpeg',\n    '.png': 'image/png',\n    '.gif': 'image/gif',\n    '.svg': 'image/svg+xml',\n    '.json': 'application/json',\n  };\n\n  // Create a http server\n  const server = http.createServer((request, response) => {\n    // Get the file path from the URL\n    let filePath =\n      request.url === '/'\n        ? `${path.join(__dirname, '../dist/index.html')}`\n        : `${path.join(__dirname, `../dist/${request.url}`)}`;\n\n    // Get the file extension from the filePath\n    let extname = path.extname(filePath);\n\n    // Set the default MIME type to text/plain\n    let contentType = 'text/plain';\n\n    // Check if the file extension is in the MIME types object\n    if (extname in mimeTypes) {\n      contentType = mimeTypes[extname];\n    }\n\n    // Read the file from the disk\n    fs.readFile(filePath, (error, content) => {\n      if (error) {\n        // If file read error occurs\n        if (error.code === 'ENOENT') {\n          // File not found error\n          response.writeHead(404);\n          response.end('File Not Found');\n        } else {\n          // Server error\n          response.writeHead(500);\n          response.end(`Server Error: ${error.code}`);\n        }\n      } else {\n        // File read successful\n        response.writeHead(200, { 'Content-Type': contentType });\n        response.end(content, 'utf-8');\n      }\n    });\n  });\n\n  // Listen for request on port ${PORT}\n  server.listen(PORT, () => {\n    console.log(`Server listening on http://localhost:${PORT}/`);\n  });\n};\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"apple-touch-icon.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"favicon-32x32.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"favicon-16x16.png\" />\n    <meta property=\"og:image\" content=\"https://bettergpt.chat/social.png\" />\n    <meta name=\"twitter:image\" content=\"https://bettergpt.chat/social.png\" />\n    <meta\n      name=\"description\"\n      content=\"Play and chat smarter with BetterChatGPT - an amazing open-source web app with a better UI for exploring OpenAI's ChatGPT API! \"\n    />\n    <meta\n      name=\"twitter:description\"\n      content=\"Play and chat smarter with BetterChatGPT - an amazing open-source web app with a better UI for exploring OpenAI's ChatGPT API! \"\n    />\n    <meta name=\"twitter:title\" content=\"Better ChatGPT\" />\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <title>Better ChatGPT</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <div id=\"modal-root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"better-chatgpt\",\n  \"private\": true,\n  \"version\": \"1.0.5\",\n  \"type\": \"module\",\n  \"homepage\": \"./\",\n  \"main\": \"electron/index.cjs\",\n  \"author\": \"Jing Hua <betterchatgpt@mail.tjh.sg>\",\n  \"description\": \"Play and chat smarter with BetterChatGPT - an amazing open-source web app with a better UI for exploring OpenAI's ChatGPT API!\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\",\n    \"electron\": \"concurrently -k \\\"BROWSER=none yarn dev\\\" \\\"wait-on tcp:5173 && electron .\\\"\",\n    \"pack\": \"yarn build && electron-builder --dir\",\n    \"make\": \"yarn build && electron-builder\"\n  },\n  \"build\": {\n    \"appId\": \"better-chatgpt\",\n    \"productName\": \"Better ChatGPT\",\n    \"artifactName\": \"${os}-${name}-${version}-${arch}.${ext}\",\n    \"directories\": {\n      \"output\": \"release\"\n    },\n    \"dmg\": {\n      \"title\": \"${productName} ${version}\",\n      \"icon\": \"dist/icon-rounded-macos.png\"\n    },\n    \"mac\": {\n      \"icon\": \"dist/icon-rounded-macos.png\"\n    },\n    \"linux\": {\n      \"target\": [\n        \"tar.gz\",\n        \"AppImage\"\n      ],\n      \"category\": \"Chat\",\n      \"icon\": \"dist/icon-rounded.png\"\n    },\n    \"win\": {\n      \"target\": \"NSIS\",\n      \"icon\": \"dist/icon-rounded.png\"\n    }\n  },\n  \"dependencies\": {\n    \"@dqbd/tiktoken\": \"^1.0.2\",\n    \"@react-oauth/google\": \"^0.9.0\",\n    \"electron-is-dev\": \"^2.0.0\",\n    \"electron-squirrel-startup\": \"^1.0.0\",\n    \"electron-updater\": \"^5.3.0\",\n    \"html2canvas\": \"^1.4.1\",\n    \"i18next\": \"^22.4.11\",\n    \"i18next-browser-languagedetector\": \"^7.0.1\",\n    \"i18next-http-backend\": \"^2.1.1\",\n    \"jspdf\": \"^2.5.1\",\n    \"katex\": \"^0.16.4\",\n    \"lodash\": \"^4.17.21\",\n    \"match-sorter\": \"^6.3.1\",\n    \"papaparse\": \"^5.4.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-i18next\": \"^12.2.0\",\n    \"react-markdown\": \"^8.0.5\",\n    \"react-scroll-to-bottom\": \"^4.2.0\",\n    \"rehype-highlight\": \"^6.0.0\",\n    \"rehype-katex\": \"^6.0.2\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-math\": \"^5.1.1\",\n    \"uuid\": \"^9.0.0\",\n    \"zustand\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.9\",\n    \"@types/lodash\": \"^4.14.192\",\n    \"@types/papaparse\": \"^5.3.7\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@types/react-scroll-to-bottom\": \"^4.2.0\",\n    \"@types/uuid\": \"^9.0.1\",\n    \"@vitejs/plugin-react-swc\": \"^3.0.0\",\n    \"autoprefixer\": \"^10.4.13\",\n    \"concurrently\": \"^8.0.1\",\n    \"electron\": \"^23.2.0\",\n    \"electron-builder\": \"^23.6.0\",\n    \"postcss\": \"^8.4.21\",\n    \"tailwindcss\": \"^3.2.7\",\n    \"typescript\": \"^4.9.3\",\n    \"vite\": \"^4.1.0\",\n    \"vite-plugin-top-level-await\": \"^1.3.0\",\n    \"vite-plugin-wasm\": \"^3.2.2\",\n    \"wait-on\": \"^7.0.1\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "public/CNAME",
    "content": "bettergpt.chat"
  },
  {
    "path": "public/locales/da/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT er en fantastisk open-source webapp, der giver dig mulighed for at lege med OpenAI's ChatGPT API gratis!\",\n  \"sourceCode\": \"Tjek <0>kildekoden</0> ud på GitHub og giv den en ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Tjek <0><i>Open ChatGPT-initiativet</i></0> ud!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Support\",\n    \"paragraph1\": \"Hos Better ChatGPT stræber vi efter at give dig nyttige og fantastiske funktioner døgnet rundt. Og ligesom ethvert projekt vil din støtte og motivation være afgørende for at hjælpe os med at fortsætte fremad!\",\n    \"paragraph2\": \"Hvis du har nydt at bruge vores app, vil vi venligt bede dig om at give dette <0>projekt</0> en ⭐️. Din anerkendelse betyder meget for os og opmuntrer os til at arbejde hårdere mod at levere den bedst mulige oplevelse.\",\n    \"paragraph3\": \"Hvis du gerne vil støtte teamet, kan du overveje at sponsorere os gennem en af metoderne nedenfor. Hver bidrag, uanset hvor lille, hjælper os med at vedligeholde og forbedre vores service.\",\n    \"paragraph4\": \"Tak fordi du er en del af vores fællesskab, og vi ser frem til at betjene dig bedre i fremtiden.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"Vi inviterer dig til at deltage i vores Discord fællesskab! Vores Discord server er et fantastisk sted at udveksle ChatGPT idéer og tips samt indsende funktionsforespørgsler til Better ChatGPT. Du får mulighed for at interagere med udviklerne bag Better ChatGPT samt andre AI-entusiaster, der deler din passion.\",\n    \"paragraph2\": \"For at deltage i vores server skal du blot klikke på følgende link: <0>https://discord.gg/g3Qnwy4V6A</0>. Vi glæder os til at se dig der!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Privatlivserklæring\",\n    \"paragraph1\": \"Vi værdsætter dit privatliv meget og er engageret i at beskytte vores brugeres privatliv. Vi indsamler eller gemmer ikke nogen tekst, du indtaster eller modtager fra OpenAI-serveren, i nogen form. Vores kildekode er tilgængelig for din inspektion for at verificere denne udtalelse.\",\n    \"paragraph2\": \"Vi prioriterer sikkerheden af din API-nøgle og håndterer den med største omhu. Hvis du bruger din egen API-nøgle, opbevares din nøgleeksklusivt på din browser og deles aldrig med nogen tredjeparts enhed. Den bruges udelukkende til det tilsigtede formål at få adgang til OpenAI API og ikke til anden uautoriseret brug.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/da/api.json",
    "content": "{\n  \"securityMessage\": \"Vi prioriterer sikkerheden af din API-nøgle og håndterer den med største omhu. Din nøgle opbevares udelukkende på din browser og deles aldrig med tredjeparts enheder. Den bruges udelukkende til det tilsigtede formål at få adgang til OpenAI API og ikke til nogen anden uautoriseret brug.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API-endepunkt\",\n    \"description\": \"Når du vælger et uofficielt API-endepunkt, fungerer det som en proxy. En proxy fungerer ved at fungere som et mellemled mellem din enhed og destinationsserveren, i dette tilfælde OpenAI API. Ved at gøre dette, gør det det muligt for dig at få adgang til OpenAI API i regioner, hvor det ellers kan være begrænset.\",\n    \"warn\": \"Derudover, hvis du angiver et brugerdefineret API-endepunkt, der giver gratis adgang til OpenAI API, kan du bruge ChatGPT uden at skulle angive en API-nøgle ved blot at lade API-nøglefeltet være tomt. Det er dog afgørende at være forsigtig, når du bruger tredjeparts API-endepunkter, da utroværdige kan logge dine personlige oplysninger i samtalerne. Verificér altid pålideligheden af et API-endepunkt, før du bruger det for at beskytte dit privatliv og din sikkerhed.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Få din personlige API-nøgle <0>her</0>.\",\n    \"inputLabel\": \"API-nøgle\"\n  },\n  \"customEndpoint\": \"Brug brugerdefineret API-endepunkt\",\n  \"advancedConfig\": \"Se avanceret API-konfiguration <0>her</0>\",\n  \"noApiKeyWarning\": \"Ingen API-nøgle angivet! Tjek venligst dine API-indstillinger.\"\n}\n"
  },
  {
    "path": "public/locales/da/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/da/main.json",
    "content": "{\n  \"save\": \"Gem\",\n  \"generate\": \"Generere\",\n  \"cancel\": \"Annuller\",\n  \"confirm\": \"Bekræft\",\n  \"warning\": \"Advarsel\",\n  \"clearMessageWarning\": \"Vær opmærksom på, at ved at indsende denne besked vil alle efterfølgende beskeder blive slettet!\",\n  \"clearConversationWarning\": \"Vær opmærksom på, at ved at bekræfte denne handling vil alle beskeder blive slettet!\",\n  \"clearConversation\": \"Ryd samtale\",\n  \"import\": \"Importer\",\n  \"export\": \"Eksporter\",\n  \"author\": \"Lavet af Jing Hua\",\n  \"about\": \"Om & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personlig\",\n  \"free\": \"Gratis\",\n  \"downloadChat\": \"Download chat\",\n  \"user\": \"Bruger\",\n  \"assistant\": \"Assistent\",\n  \"system\": \"System\",\n  \"newChat\": \"Ny chat\",\n  \"lightMode\": \"Lys tilstand\",\n  \"darkMode\": \"Mørk tilstand\",\n  \"setting\": \"Indstillinger\",\n  \"image\": \"Billede\",\n  \"autoTitle\": \"Auto generer titel\",\n  \"advancedMode\": \"Avanceret tilstand\",\n  \"inlineLatex\": \"Indlejret LaTeX\",\n  \"prompt\": \"Opgave\",\n  \"promptLibrary\": \"Opgavebibliotek\",\n  \"name\": \"Navn\",\n  \"search\": \"Søg\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Nulstil Omkostninger\",\n  \"countTotalTokens\": \"Tæl totale tokens\",\n  \"morePrompts\": \"Du kan finde flere opgaver her: \",\n  \"clearPrompts\": \"Ryd prompter\",\n  \"postOnShareGPT\": {\n    \"title\": \"Indlæg på ShareGPT\",\n    \"warning\": \"Vær opmærksom på, at ved at poste din samtale på ShareGPT, vil den blive offentligt tilgængelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes og kan blive arkiveret eller delt af andre. Vi råder dig til at overveje nøje og undgå at dele følsomme eller private oplysninger på denne platform.\"\n  },\n  \"newFolder\": \"Ny mappe\",\n  \"cloneChat\": \"Klon Chat\",\n  \"cloned\": \"Klonet\",\n  \"enterToSubmit\": \"Tryk Enter for at sende\",\n  \"submitPlaceholder\": \"Skriv en besked eller klik på [/] for opgave...\"\n}\n"
  },
  {
    "path": "public/locales/da/model.json",
    "content": "{\n  \"configuration\": \"Konfiguration\",\n  \"model\": \"Model\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"Det maksimale antal tokens der skal genereres i chat-fuldførelsen. Den samlede længde af input tokens og genererede tokens er begrænset af modellens kontekstlængde.\"\n  },\n  \"default\": \"Standard\",\n  \"temperature\": {\n    \"label\": \"Temperatur\",\n    \"description\": \"Hvilken samplingstemperatur der skal bruges, mellem 0 og 2. Højere værdier som 0,8 vil gøre outputtet mere tilfældigt, mens lavere værdier som 0,2 vil gøre det mere fokuseret og deterministisk. Vi anbefaler generelt at ændre dette eller top p, men ikke begge. (Standard: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Tilstedeværelsesstraf\",\n    \"description\": \"Tal mellem -2,0 og 2,0. Positive værdier straffer nye tokens baseret på, om de vises i teksten hidtil, hvilket øger modellens sandsynlighed for at tale om nye emner. (Standard: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Tal mellem 0 og 1. Et alternativ til prøvetagning med temperatur, kaldet nucleus sampling, hvor modellen overvejer resultaterne af tokens med top p sandsynlighedsmasse. Så 0,1 betyder, at kun tokens, der udgør de øverste 10% sandsynlighedsmasse, overvejes. Vi anbefaler generelt at ændre dette eller temperaturen, men ikke begge. (Standard: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frekvensstraf\",\n    \"description\": \"Tal mellem -2,0 og 2,0. Positive værdier straffer nye tokens baseret på deres eksisterende frekvens i teksten hidtil, hvilket nedsætter modellens sandsynlighed for at gentage den samme linje ordret. (Standard: 0)\"\n  },\n  \"defaultChatConfig\": \"Standard Chat-konfiguration\",\n  \"defaultSystemMessage\": \"Standard Systembesked\",\n  \"resetToDefault\": \"Nulstil til standard\"\n}\n"
  },
  {
    "path": "public/locales/de/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT ist eine großartige open-source web app, welche es ermöglicht mit der ChatGPT API von OpenAI zu interagieren!\",\n  \"sourceCode\": \"Sieh dir den <0>source code</0> auf GitHub an und ⭐️ das Projekt!\",\n  \"initiative\": {\n    \"description\": \"Schau dir die <0><i>Open ChatGPT Initiative</i></0> an!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Support\",\n    \"paragraph1\": \"Unser Team arbeitet rund um die Uhr an neuen Features und Funktionen. Und wie bei jedem Projekt wird deine Unterstützung und Motivation entscheidend dazu beitragen, uns vorwärts zu bringen!\",\n    \"paragraph2\": \"Wenn dir Nutzen und Gefallen an Better ChatGPT findest, dann bitte gib unserem <0>Projekt</0> einen ⭐️. Deine Unterstützung bedeutet uns viel und motiviert uns dazu, noch härter zu arbeiten, um das Projekt voran zu bringen.\",\n    \"paragraph3\": \"Wenn du das Team unterstützen möchtest, wäre es super, uns durch eine der unten aufgeführten Methoden zu sponsern. Jeder Beitrag, egal wie klein, hilft uns dabei, unseren Service zu erhalten und zu verbessern.\",\n    \"paragraph4\": \"Vielen Dank, dass du ein Teil unserer Community bist. Wir freuen uns darauf, bald mehr Features zu präsentieren und das Projekt zu erweitern.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"Tritt unserer Discord-Community bei! Unser Server ist ein großartiger Ort, um Ideen und Tipps für ChatGPT auszutauschen und Feature-Anfragen für Better ChatGPT einzureichen. Du hast die Möglichkeit, mit den Entwicklern hinter Better ChatGPT sowie anderen KI-Enthusiasten zu interagieren, die deine und unsere Leidenschaft teilen.\",\n    \"paragraph2\": \"Um dem Server beizutreten, klick auf den folgenden Link: <0>https://discord.gg/g3Qnwy4V6A</0>. Wir freuen uns auf dich!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Datenschutzerklärung\",\n    \"paragraph1\": \"Wir legen großen Wert auf deine Privatsphäre und verpflichten uns, die Privatsphäre all unserer Benutzer zu schützen. Weder sammeln noch speichern wir die Texte die du eingibst oder von den OpenAI-Servern empfängst. Unsere Quellcode ist öffentlich einsehbar, damit du das auch überprüfen kannst.\",\n    \"paragraph2\": \"Die Sicherheit deines API-Schlüssels ist unsere höchste Priorität. Dieser wird ausschließlich lokal in deinem Browser gespeichert und niemals mit irgendeiner Drittpartei geteilt. Er wird ausschließlich für den vorgesehenen Zweck des Zugriffs auf die OpenAI-API verwendet und nicht für andere Zwecke.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/de/api.json",
    "content": "{\n  \"securityMessage\": \"Die Sicherheit deines API-Schlüssels ist unsere höchste Priorität. Dieser wird ausschließlich lokal in deinem Browser gespeichert und niemals mit irgendeiner Drittpartei geteilt. Er wird ausschließlich für den vorgesehenen Zweck des Zugriffs auf die OpenAI-API verwendet und nicht für andere Zwecke.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API Endpunkt\",\n    \"description\": \"Wenn du einen inoffiziellen API-Endpunkt auswählst, funktioniert er als Proxy. Ein Proxy fungiert als Vermittler zwischen deinem Gerät und dem Zielserver, in diesem Fall der OpenAI-API. Auf diese Weise ist es möglich, auf die OpenAI-API zuzugreifen, auch in Regionen, in denen der Zugriff normalerweise eingeschränkt ist.\",\n    \"warn\": \"Wenn du einen benutzerdefinierten API-Endpunkt verwendest, der kostenlosen Zugriff auf die OpenAI-API gewährt, kannst du ChatGPT ohne die Angabe eines API-Schlüssels nutzen, indem du das API-Schlüssel-Feld einfach leer lässt. Es ist jedoch wichtig, bei der Verwendung von API-Endpunkten von Drittanbietern vorsichtig zu sein, da unzuverlässige Endpunkte deine persönlichen Informationen in den Gesprächen protokollieren können. Überprüfe immer die Zuverlässigkeit eines API-Endpunkts, bevor du ihn verwendest, um deine Privatsphäre und Sicherheit zu schützen.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Besorge dir <0>hier</0> deinen eigenen API-Schlüssel.\",\n    \"inputLabel\": \"API-Schlüssel\"\n  },\n  \"customEndpoint\": \"Verwende einen benutzerdefinierten API-Endpunkt\",\n  \"advancedConfig\": \"Sieh dir die <0>erweiterten API Konfigurationen</0> an\",\n  \"noApiKeyWarning\": \"Kein API-Schlüssel angegeben! Überprüfe bitte deine API-Einstellungen.\"\n}\n"
  },
  {
    "path": "public/locales/de/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Synchronisiere deine Chat-Verläufe und Einstellungen einfach mit Google Drive.\",\n  \"button\": {\n    \"sync\": \"Jetzt synchronisieren\",\n    \"stop\": \"Synchronisation stoppen\",\n    \"create\": \"Neue Datei anlegen\",\n    \"confirm\": \"Auswahl bestätigen\"\n  },\n  \"notice\": \"Hinweis: Du musst dich bei jedem Besuch oder jede Stunde erneut anmelden. Verwende Better ChatGPT nicht gleichzeitig auf mehr als einem Gerät, um zu vermeiden, dass deine Cloud-Daten überschrieben werden.\",\n  \"privacy\": \"Deine Privatsphäre ist uns wichtig und um sie zu schützen, hat Better ChatGPT nur limitierten Zugriff. Das bedeutet, dass Better ChatGPT nur die eigenen Dateien und Ordner erstellen, anzeigen und verwalten kann.\",\n  \"toast\": {\n    \"sync\": \"Synchronisation erfolgreich!\",\n    \"stop\": \"Synchronisation angehalten\"\n  }\n}\n"
  },
  {
    "path": "public/locales/de/main.json",
    "content": "{\n  \"save\": \"Speichern\",\n  \"generate\": \"Generieren\",\n  \"cancel\": \"Abbrechen\",\n  \"confirm\": \"Bestätigen\",\n  \"warning\": \"Achtung\",\n  \"clearMessageWarning\": \"Bitte beachte, dass durch das Einreichen dieser neuen Nachricht alle nachfolgenden Nachrichten gelöscht werden!\",\n  \"clearConversationWarning\": \"Bitte beachte, dass bei Bestätigung dieser Aktion alle Nachrichten gelöscht werden!\",\n  \"clearConversation\": \"Chat-Verlauf löschen.\",\n  \"import\": \"Importieren\",\n  \"export\": \"Exportieren\",\n  \"author\": \"Made by Jing Hua\",\n  \"about\": \"Über uns & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Persönlich\",\n  \"free\": \"Kostenlos\",\n  \"downloadChat\": \"Chat downloaden\",\n  \"user\": \"Nutzer\",\n  \"assistant\": \"Assistent\",\n  \"system\": \"System\",\n  \"newChat\": \"Neuer Chat\",\n  \"lightMode\": \"Heller Modus\",\n  \"darkMode\": \"Dunkler Modus\",\n  \"setting\": \"Einstellungen\",\n  \"image\": \"Bild\",\n  \"autoTitle\": \"Chat-Titel automatisch generieren\",\n  \"advancedMode\": \"Erweiterter Modus\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Prompt Bibliothek\",\n  \"name\": \"Name\",\n  \"search\": \"Suche\",\n  \"total\": \"Gesamt\",\n  \"resetCost\": \"Kosten zurücksetzen\",\n  \"countTotalTokens\": \"Gesamte Tokens zusammenrechnen\",\n  \"morePrompts\": \"Du findest mehr Prompts hier: \",\n  \"clearPrompts\": \"Prompts löschen\",\n  \"postOnShareGPT\": {\n    \"title\": \"Auf ShareGPT teilen\",\n    \"warning\": \"Bitte beachte, dass durch das Posten deiner Konversation auf ShareGPT diese öffentlich zugänglich und für jeden einsehbar wird. Einmal veröffentlicht, kann die Konversation nicht mehr versteckt oder gelöscht werden und kann möglicherweise von anderen archiviert oder geteilt werden. Wir raten dir daher, sorgfältig zu drüber nachzudenken ob du die Konversation teilst. Vermeide grundsätzlich sensible oder private Informationen auf dieser Plattform zu teilen.\"\n  },\n  \"newFolder\": \"Neuer Ordner\",\n  \"cloneChat\": \"Chat klonen\",\n  \"cloned\": \"Klonen erfolgreich\",\n  \"enterToSubmit\": \"Drücke Enter zum absenden\",\n  \"submitPlaceholder\": \"Verfasse eine Nachricht oder klicke auf [/] für gespeicherte Prompts...\"\n}\n"
  },
  {
    "path": "public/locales/de/model.json",
    "content": "{\n  \"configuration\": \"Konfiguration\",\n  \"model\": \"Model\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"Die maximale Anzahl von Tokens, die bei der Chat-Vervollständigung generiert werden. Die Gesamtlänge der Eingabetokens und generierten Tokens wird durch die Kontextlänge des Modells begrenzt.\"\n  },\n  \"default\": \"Standardwert\",\n  \"temperature\": {\n    \"label\": \"Temperature\",\n    \"description\": \"Welche Abtastrate (sampling temmperature) verwendet werden soll. Möglichkeiten liegen zwischen 0 und 2. Höhere Werte wie 0,8 machen die Ausgabe zufälliger, während niedrigere Werte wie 0,2 sie fokussierter und deterministischer machen. Wir empfehlen im Allgemeinen, entweder diesen Wert oder den Top-p-Wert zu ändern, aber nicht beide gleichzeitig. (Standardwert: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Presence Penalty\",\n    \"description\": \"Eine Zahl zwischen -2,0 und 2,0. Positive Werte bestrafen neue Tokens basierend darauf, ob sie bisher im Text erscheinen sind, und erhöhen die Wahrscheinlichkeit des Modells, über neue Themen zu sprechen. (Standardwert: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Eine Zahl zwischen 0 und 1. Eine Alternative zum Abtasten mit Temperature (siehe oben), genannt Nukleus-sampling. Dabei berücksichtigt das Modell die Ergebnisse der Tokens mit Top-p-Wahrscheinlichkeitsmasse. Bei einem Wert von 0,1 werden nur die Tokens berücksichtigt, die die oberen 10% der Wahrscheinlichkeitsmasse ausmachen. Wir empfehlen im Allgemeinen, entweder diesen Wert oder die Temperatur zu ändern, aber nicht beide gleichzeitig. (Standardwert: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frequency Penalty\",\n    \"description\": \"Eine Zahl zwischen -2,0 und 2,0. Positive Werte bestrafen neue Tokens basierend auf ihrer bestehenden Häufigkeit im Text und verringern die Wahrscheinlichkeit des Modells, dieselbe Zeile wörtlich zu wiederholen. (Standardwert: 0)\"\n  },\n  \"defaultChatConfig\": \"Standard Chat Konfiguration\",\n  \"defaultSystemMessage\": \"Standard System Nachricht\",\n  \"resetToDefault\": \"Auf Standardkonfiguration zurücksetzen\"\n}\n"
  },
  {
    "path": "public/locales/en/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT is an amazing open-source web app that allows you to play with OpenAI's ChatGPT API for free!\",\n  \"sourceCode\": \"Checkout the <0>source code</0> on GitHub and give it a ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Checkout the <0><i>Open ChatGPT Initiative</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Support\",\n    \"paragraph1\": \"At Better ChatGPT, we strive to provide you with useful and amazing features around the clock. And just like any project, your support and motivation will be instrumental in helping us keep moving forward!\",\n    \"paragraph2\": \"If you have enjoyed using our app, we kindly ask you to give this <0>project</0> a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.\",\n    \"paragraph3\": \"If you would like to support the team, consider sponsoring us through one of the methods below. Every contribution, no matter how small, helps us to maintain and improve our service.\",\n    \"paragraph4\": \"Thank you for being a part of our community, and we look forward to serving you better in the future.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"We invite you to join our Discord community! Our Discord server is a great place to exchange ChatGPT ideas and tips, and submit feature requests for Better ChatGPT. You'll have the opportunity to interact with the developers behind Better ChatGPT as well as other AI enthusiasts who share your passion.\",\n    \"paragraph2\": \"To join our server, simply click on the following link: <0>https://discord.gg/g3Qnwy4V6A</0>. We can't wait to see you there!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Privacy Statement\",\n    \"paragraph1\": \"We highly value your privacy and are committed to safeguarding the privacy of our users. We do not collect or store any text you enter or receive from the OpenAI server in any form. Our source code is available for your inspection to verify this statement.\",\n    \"paragraph2\": \"We prioritise the security of your API key and handle it with utmost care. If you use your own API key, your key is exclusively stored on your browser and never shared with any third-party entity. It is solely used for the intended purpose of accessing the OpenAI API and not for any other unauthorised use.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/en/api.json",
    "content": "{\n  \"securityMessage\": \"We prioritise the security of your API key and handle it with utmost care. Your key is exclusively stored on your browser and never shared with any third-party entity. It is solely used for the intended purpose of accessing the OpenAI API and not for any other unauthorised use.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API Endpoint\",\n    \"description\": \"When you choose an unofficial API endpoint, it functions as a proxy. A proxy works by acting as an intermediary between your device and the destination server, in this case, the OpenAI API. By doing so, it enables you to access the OpenAI API in regions where it might otherwise be restricted.\",\n    \"warn\": \"Additionally, if you provide a custom API endpoint that grants free access to the OpenAI API, you can use ChatGPT without the need to supply an API key by simply leaving the API key field blank. However, it's crucial to be cautious when using third-party API endpoints, as untrustworthy ones may log your personal information in the conversations. Always verify the reliability of an API endpoint before using it to protect your privacy and security.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Get your personal API key <0>here</0>.\",\n    \"inputLabel\": \"API Key\"\n  },\n  \"customEndpoint\": \"Use custom API endpoint\",\n  \"advancedConfig\": \"View advanced API configuration <0>here</0>\",\n  \"noApiKeyWarning\": \"No API key supplied! Please check your API settings.\"\n}\n"
  },
  {
    "path": "public/locales/en/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/en/main.json",
    "content": "{\n  \"save\": \"Save\",\n  \"generate\": \"Generate\",\n  \"cancel\": \"Cancel\",\n  \"confirm\": \"Confirm\",\n  \"warning\": \"Warning\",\n  \"clearMessageWarning\": \"Please be advised that by submitting this message, all subsequent messages will be deleted!\",\n  \"clearConversationWarning\": \"Please be advised that by confirming this action, all messages will be deleted!\",\n  \"clearConversation\": \"Clear Conversation History\",\n  \"import\": \"Import\",\n  \"export\": \"Export\",\n  \"author\": \"Made by Jing Hua\",\n  \"about\": \"About & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personal\",\n  \"free\": \"Free\",\n  \"downloadChat\": \"Download Chat\",\n  \"user\": \"User\",\n  \"assistant\": \"Assistant\",\n  \"system\": \"System\",\n  \"newChat\": \"New Chat\",\n  \"lightMode\": \"Light Mode\",\n  \"darkMode\": \"Dark Mode\",\n  \"setting\": \"Settings\",\n  \"image\": \"Image\",\n  \"autoTitle\": \"Auto generate title\",\n  \"advancedMode\": \"Advanced mode\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Prompt Library\",\n  \"name\": \"Name\",\n  \"search\": \"Search\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Reset Costs\",\n  \"countTotalTokens\": \"Count total tokens\",\n  \"morePrompts\": \"You can find more prompts here: \",\n  \"clearPrompts\": \"Clear prompts\",\n  \"postOnShareGPT\": {\n    \"title\": \"Post on ShareGPT\",\n    \"warning\": \"Please be aware that by posting your conversation on ShareGPT, it will become publicly accessible and viewable to anyone. Once posted, the conversation cannot be hidden or deleted, and may be archived or shared by others. We advise you to consider carefully and avoid sharing sensitive or private information on this platform.\"\n  },\n  \"newFolder\": \"New Folder\",\n  \"cloneChat\": \"Clone Chat\",\n  \"cloned\": \"Cloned\",\n  \"enterToSubmit\": \"Enter to submit\",\n  \"submitPlaceholder\": \"Type a message or click [/] for prompts...\"\n}\n"
  },
  {
    "path": "public/locales/en/model.json",
    "content": "{\n  \"configuration\": \"Configuration\",\n  \"model\": \"Model\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length.\"\n  },\n  \"default\": \"Default\",\n  \"temperature\": {\n    \"label\": \"Temperature\",\n    \"description\": \"What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or top p but not both. (Default: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Presence Penalty\",\n    \"description\": \"Number between -2.0 and 2.0. Positive values penalise new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. (Default: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Number between 0 and 1. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. (Default: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frequency Penalty\",\n    \"description\": \"Number between -2.0 and 2.0. Positive values penalise new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. (Default: 0)\"\n  },\n  \"defaultChatConfig\": \"Default Chat Config\",\n  \"defaultSystemMessage\": \"Default System Message\",\n  \"resetToDefault\": \"Reset To Default\"\n}\n"
  },
  {
    "path": "public/locales/en-US/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT is an amazing open-source web app that allows you to play with OpenAI's ChatGPT API for free!\",\n  \"sourceCode\": \"Checkout the <0>source code</0> on GitHub and give it a ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Checkout the <0><i>Open ChatGPT Initiative</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Support\",\n    \"paragraph1\": \"At Better ChatGPT, we strive to provide you with useful and amazing features around the clock. And just like any project, your support and motivation will be instrumental in helping us keep moving forward!\",\n    \"paragraph2\": \"If you have enjoyed using our app, we kindly ask you to give this <0>project</0> a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.\",\n    \"paragraph3\": \"If you would like to support the team, consider sponsoring us through one of the methods below. Every contribution, no matter how small, helps us to maintain and improve our service.\",\n    \"paragraph4\": \"Thank you for being a part of our community, and we look forward to serving you better in the future.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"We invite you to join our Discord community! Our Discord server is a great place to exchange ChatGPT ideas and tips, and submit feature requests for Better ChatGPT. You'll have the opportunity to interact with the developers behind Better ChatGPT as well as other AI enthusiasts who share your passion.\",\n    \"paragraph2\": \"To join our server, simply click on the following link: <0>https://discord.gg/g3Qnwy4V6A</0>. We can't wait to see you there!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Privacy Statement\",\n    \"paragraph1\": \"We highly value your privacy and are committed to safeguarding the privacy of our users. We do not collect or store any text you enter or receive from the OpenAI server in any form. Our source code is available for your inspection to verify this statement.\",\n    \"paragraph2\": \"We prioritize the security of your API key and handle it with utmost care. If you use your own API key, your key is exclusively stored on your browser and never shared with any third-party entity. It is solely used for the intended purpose of accessing the OpenAI API and not for any other unauthorized use.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/en-US/api.json",
    "content": "{\n  \"securityMessage\": \"We prioritize the security of your API key and handle it with utmost care. Your key is exclusively stored on your browser and never shared with any third-party entity. It is solely used for the intended purpose of accessing the OpenAI API and not for any other unauthorized use.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API Endpoint\",\n    \"description\": \"When you choose an unofficial API endpoint, it functions as a proxy. A proxy works by acting as an intermediary between your device and the destination server, in this case, the OpenAI API. By doing so, it enables you to access the OpenAI API in regions where it might otherwise be restricted.\",\n    \"warn\": \"Additionally, if you provide a custom API endpoint that grants free access to the OpenAI API, you can use ChatGPT without the need to supply an API key by simply leaving the API key field blank. However, it's crucial to be cautious when using third-party API endpoints, as untrustworthy ones may log your personal information in the conversations. Always verify the reliability of an API endpoint before using it to protect your privacy and security.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Get your personal API key <0>here</0>.\",\n    \"inputLabel\": \"API Key\"\n  },\n  \"customEndpoint\": \"Use custom API endpoint\",\n  \"advancedConfig\": \"View advanced API configuration <0>here</0>\",\n  \"noApiKeyWarning\": \"No API key supplied! Please check your API settings.\"\n}\n"
  },
  {
    "path": "public/locales/en-US/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/en-US/main.json",
    "content": "{\n  \"save\": \"Save\",\n  \"generate\": \"Generate\",\n  \"cancel\": \"Cancel\",\n  \"confirm\": \"Confirm\",\n  \"warning\": \"Warning\",\n  \"clearMessageWarning\": \"Please be advised that by submitting this message, all subsequent messages will be deleted!\",\n  \"clearConversationWarning\": \"Please be advised that by confirming this action, all messages will be deleted!\",\n  \"clearConversation\": \"Clear Conversation History\",\n  \"import\": \"Import\",\n  \"export\": \"Export\",\n  \"author\": \"Made by Jing Hua\",\n  \"about\": \"About & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personal\",\n  \"free\": \"Free\",\n  \"downloadChat\": \"Download Chat\",\n  \"user\": \"User\",\n  \"assistant\": \"Assistant\",\n  \"system\": \"System\",\n  \"newChat\": \"New Chat\",\n  \"lightMode\": \"Light Mode\",\n  \"darkMode\": \"Dark Mode\",\n  \"setting\": \"Settings\",\n  \"image\": \"Image\",\n  \"autoTitle\": \"Auto generate title\",\n  \"advancedMode\": \"Advanced mode\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Prompt Library\",\n  \"name\": \"Name\",\n  \"search\": \"Search\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Reset Costs\",\n  \"countTotalTokens\": \"Count total tokens\",\n  \"morePrompts\": \"You can find more prompts here: \",\n  \"clearPrompts\": \"Clear prompts\",\n  \"postOnShareGPT\": {\n    \"title\": \"Post on ShareGPT\",\n    \"warning\": \"Please be aware that by posting your conversation on ShareGPT, it will become publicly accessible and viewable to anyone. Once posted, the conversation cannot be hidden or deleted, and may be archived or shared by others. We advise you to consider carefully and avoid sharing sensitive or private information on this platform.\"\n  },\n  \"newFolder\": \"New Folder\",\n  \"cloneChat\": \"Clone Chat\",\n  \"cloned\": \"Cloned\",\n  \"enterToSubmit\": \"Enter to submit\",\n  \"submitPlaceholder\": \"Type a message or click [/] for prompts...\"\n}\n"
  },
  {
    "path": "public/locales/en-US/model.json",
    "content": "{\n  \"configuration\": \"Configuration\",\n  \"model\": \"Model\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length.\"\n  },\n  \"default\": \"Default\",\n  \"temperature\": {\n    \"label\": \"Temperature\",\n    \"description\": \"What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or top p but not both. (Default: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Presence Penalty\",\n    \"description\": \"Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. (Default: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Number between 0 and 1. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. (Default: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frequency Penalty\",\n    \"description\": \"Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. (Default: 0)\"\n  },\n  \"defaultChatConfig\": \"Default Chat Config\",\n  \"defaultSystemMessage\": \"Default System Message\",\n  \"resetToDefault\": \"Reset To Default\"\n}\n"
  },
  {
    "path": "public/locales/es/about.json",
    "content": "{\n  \"description\": \"¡Better ChatGPT es una aplicación web de código abierto que te permite usar la API de OpenAI totalmente gratis!\",\n  \"sourceCode\": \"Comprueba el <0>código fuente</0> en GitHub y dale una ⭐️\",\n  \"initiative\": {\n    \"description\": \"¡Tómate un minuto para leer <0><i>La iniciativa de Open ChatGPT</i></0> (inglés)!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Apoya el proyecto\",\n    \"paragraph1\": \"Aquí, en Better ChatGPT, nos esforzamos mucho para brindarte mejoras y nuevas funcionalidades lo antes posible. Y, al igual que en todos los proyectos, ¡tu apoyo y motivación mantiene a Better ChatGPT vivo!\",\n    \"paragraph2\": \"Si disfrutas usando nuestra aplicación, te pedimos por favor que des una ⭐️ a este <0>proyecto</0>. Tu apoyo significa mucho para nosotros y nos ayuda a trabajar lo máximo posible para ofrecerte las mejores experencias posibles.\",\n    \"paragraph3\": \"Si deseas apoyar al equipo, considera patrocinarnos mediante alguno de los métodos a continuación. Toda contribución, aunque sea pequeña, nos ayuda a mantener y mejorar el servicio.\",\n    \"paragraph4\": \"Gracias por ser parte de nuestra comunidad. Estamos deseando servirte de la mejor manera posible en el futuro.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Servidor de Discord\",\n    \"paragraph1\": \"¡Te invitamos a unirte a nuestra comunidad de Discord! Nuestro servidor de Discord es genial para intercambiar temas relacionados con ChatGPT, como ideas y consejos, y solicitar nuevas funcionalidades para Better ChatGPT. También tendrás la oportunidad de hablar con los desarrolladores así como otros entusiastas de la IA.\",\n    \"paragraph2\": \"Para unirte al servidor, pulsa sobre el siguiente enlace: <0>https://discord.gg/g3Qnwy4V6A</0>. ¡Te esperamos!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Declaración de privacidad\",\n    \"paragraph1\": \"Valoramos mucho su privacidad y nos comprometemos a proteger la privacidad de nuestros usuarios. No recogemos ni almacenamos ningún dato que usted introduzca o reciba del servidor OpenAI de ninguna forma. Nuestro código fuente está disponible para su inspección para verificar esta declaración.\",\n    \"paragraph2\": \"Prioritamos la seguridad de su clave API y la tratamos con sumo cuidado. Si utiliza su propia clave de API, ésta se almacena exclusivamente en su navegador y nunca se comparte con ninguna entidad de terceros. Se utiliza únicamente para el fin exclusivo de acceder a la API de OpenAI y no para ningún otro uso no autorizado.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/es/api.json",
    "content": "{\n  \"securityMessage\": \"Prioritamos la seguridad de su clave API y la tratamos con sumo cuidado. Si utiliza su propia clave de API, ésta se almacena exclusivamente en su navegador y nunca se comparte con ninguna entidad de terceros. Se utiliza únicamente para el fin exclusivo de acceder a la API de OpenAI y no para ningún otro uso no autorizado.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"Punto final de acceso de la API\",\n    \"description\": \"Cuando eliges un punto final de API no oficial, básicamente funciona como un proxy. Un proxy funciona actuando como intermediario entre tu dispositivo y el servidor de destino, en este caso, la API de OpenAI. Al hacerlo, te permite acceder a la API de OpenAI en regiones donde de otro modo podría estar restringida.\",\n    \"warn\": \"Además, si proporcionas un punto final de API personalizado que otorga acceso gratuito a la API de OpenAI, puedes usar ChatGPT sin la necesidad de proporcionar una clave de API simplemente dejando en blanco el campo de la clave de API. Sin embargo, es fundamental tener precaución al usar puntos finales de API de terceros, ya que los que no sean confiables pueden registrar tu información personal en las conversaciones. Siempre verifica la fiabilidad de un punto final de API antes de usarlo para proteger tu privacidad y seguridad.\"\n\n  },\n  \"apiKey\": {\n    \"howTo\": \"Obtén tu clave personal <0>aquí</0>.\",\n    \"inputLabel\": \"Clave API\"\n  },\n  \"customEndpoint\": \"Usar un punto final de acceso personalizado\",\n  \"advancedConfig\": \"Ver configuración avanzada de API <0>aquí</0>\",\n  \"noApiKeyWarning\": \"¡No se proporcionó clave de API! Por favor, revisa tus ajustes de API.\"\n}\n"
  },
  {
    "path": "public/locales/es/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/es/main.json",
    "content": "{\n  \"save\": \"Guardar\",\n  \"generate\": \"Generar\",\n  \"cancel\": \"Cancelar\",\n  \"confirm\": \"Aceptar\",\n  \"warning\": \"Aviso\",\n  \"clearMessageWarning\": \"Al enviar este mensaje, todos los mensajes siguientes serán eliminados. ¿Está seguro que quiere continuar?\",\n  \"clearConversationWarning\": \"Al hacer clic en 'Aceptar', todos los mensajes de esta conversación serán eliminados.\",\n  \"clearConversation\": \"Limpiar conversación\",\n  \"import\": \"Importar\",\n  \"export\": \"Exportar\",\n  \"author\": \"Hecho por Jing Hua\",\n  \"about\": \"Acerca de y Patrocinadores\",\n  \"api\": \"API\",\n  \"personal\": \"Personal\",\n  \"free\": \"Gratuito\",\n  \"downloadChat\": \"Descargar conversación\",\n  \"user\": \"Usuario\",\n  \"assistant\": \"Asistente\",\n  \"system\": \"Sistema\",\n  \"newChat\": \"Nuevo Chat\",\n  \"lightMode\": \"Modo claro\",\n  \"darkMode\": \"Modo oscuro\",\n  \"setting\": \"Ajustes\",\n  \"image\": \"Imagen\",\n  \"autoTitle\": \"Generar automáticamente el título de la conversación.\",\n  \"advancedMode\": \"Modo avanzado\",\n  \"inlineLatex\": \"Latex en línea\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Librería de Prompts\",\n  \"name\": \"Nombre\",\n  \"search\": \"Buscar\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Reiniciar costos\",\n  \"countTotalTokens\": \"Contar tokens totales\",\n  \"morePrompts\": \"Puedes encontrar más prompts aquí: \",\n  \"clearPrompts\": \"Prompts claras\",\n  \"postOnShareGPT\": {\n    \"title\": \"Publicar en ShareGPT\",\n    \"warning\": \"Por favor, tenga en cuenta que al publicar su conversación en ShareGPT, esta será accesible y visible para cualquiera. Una vez publicada, la conversación no se podrá ocultar ni eliminar, y puede ser archivada o compartida por otros. Le aconsejamos que lo considere detenidamente y evite compartir información sensible o privada en esta plataforma.\"\n  },\n  \"newFolder\": \"Nueva Carpeta\",\n  \"cloneChat\": \"Clone Chat\",\n  \"cloned\": \"Cloned\",\n  \"enterToSubmit\": \"Enter to submit\",\n  \"submitPlaceholder\": \"Escribe un mensaje o haz clic en [/] para prompt...\"\n}\n"
  },
  {
    "path": "public/locales/es/model.json",
    "content": "{\n  \"configuration\": \"Configuración\",\n  \"model\": \"Modelo\",\n  \"token\": {\n    \"label\": \"Máximo número de tokens\",\n    \"description\": \"El máximo número de tokens que se utilizan para generar la respuesta. La longitud total de los tokens de entrada y los tokens generados está limitada por la longitud de contexto del modelo.\"\n  },\n  \"default\": \"Valores por defecto\",\n  \"temperature\": {\n    \"label\": \"Temperatura\",\n    \"description\": \"Qué temperatura a usar (valores entre 0 y 2). Si se elige un valor alto, por ejemplo 0.8, la salida será más aleatoria, mientras que un valor bajo, como 0.2, hará que la salida sea más enfocada y predecible. Como recomendación general, se sugiere ajustar este parámetro o el top p, pero no ambos al mismo tiempo. Por defecto, el valor es 1.\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Sanción por presencia\",\n    \"description\": \"Este parámetro permite controlar el nivel de repetición del modelo. La diferencia entre este parámetro y las sanciones por frecuencia, es que este se centra en evitar repeticiones de temas mientras que las sanciones por frecuencia en evitar repeticiones de palabras. Los valores positivos incrementan la probabilidad de que el modelo sea menos repetitivo y que hable sobre nuevos temas. Su valor de por defecto es 0.\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p (P máxima)\",\n    \"description\": \"Este argumento es una forma alternativa de controlar la aleatoridad y creatividad del texto. Por ejemplo, si establecemos su valor a 0.1, significa que solo se consideran los tokens que están comprendidos entre el 10% de la masa de probabilidad.\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Sanción por frecuencia\",\n    \"description\": \"Este argumento controla el nivel de repetición que permite usar al modelo en sus respuestas. Los valores positivos decrementan la probabilidad de que el modelo se repita. Su valor de por defecto es 0.\"\n  },\n  \"defaultChatConfig\": \"Configuración de chat de por defecto\",\n  \"defaultSystemMessage\": \"Mensaje de por defecto del sistema\",\n  \"resetToDefault\": \"Restablecer a los valores predeterminados\"\n}\n"
  },
  {
    "path": "public/locales/fr/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT est une application web open-source incroyable qui vous permet de jouer avec l'API ChatGPT d'OpenAI gratuitement !\",\n  \"sourceCode\": \"Consultez le <0>code source</0> sur GitHub et donnez-lui une ⭐️ !\",\n  \"initiative\": {\n    \"description\": \"Découvrez l'<0><i>Initiative Open ChatGPT</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Support\",\n    \"paragraph1\": \"Chez Better ChatGPT, nous nous efforçons de vous fournir des fonctionnalités utiles et incroyables 24 heures sur 24. Et comme tout projet, votre soutien et votre motivation seront essentiels pour nous aider à avancer !\",\n    \"paragraph2\": \"Si vous avez apprécié notre application, nous vous demandons gentiment de donner une ⭐️ à ce <0>projet</0>. Votre approbation nous tient à cœur et nous incite à travailler plus dur pour offrir la meilleure expérience possible.\",\n    \"paragraph3\": \"Si vous souhaitez soutenir l'équipe, envisagez de nous sponsoriser grâce à l'un des modes de paiement ci-dessous. Chaque contribution, aussi petite soit-elle, nous aide à maintenir et à améliorer notre service.\",\n    \"paragraph4\": \"Merci d'être membre de notre communauté, et nous espérons vous servir mieux à l'avenir.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Serveur Discord\",\n    \"paragraph1\": \"Nous vous invitons à rejoindre notre communauté Discord ! Notre serveur Discord est un excellent endroit pour échanger des idées et des astuces ChatGPT, et pour soumettre des demandes de fonctionnalités pour Better ChatGPT. Vous aurez l'occasion d'interagir avec les développeurs de Better ChatGPT ainsi qu'avec d'autres passionnés d'IA qui partagent votre passion.\",\n    \"paragraph2\": \"Pour rejoindre notre serveur, il vous suffit de cliquer sur le lien suivant : <0>https://discord.gg/g3Qnwy4V6A</0>. Nous avons hâte de vous y voir !\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Déclaration de confidentialité\",\n    \"paragraph1\": \"Nous attachons une grande importance à votre vie privée et nous nous engageons à protéger la vie privée de nos utilisateurs. Nous ne collectons ni ne stockons aucun texte que vous entrez ou que vous recevez du serveur OpenAI sous quelque forme que ce soit. Notre code source est disponible pour votre inspection afin de vérifier cette déclaration.\",\n    \"paragraph2\": \"Nous accordons la priorité à la sécurité de votre clé API et la traitons avec le plus grand soin. Si vous utilisez votre propre clé API, votre clé est exclusivement stockée sur votre navigateur et jamais partagée avec une entité tiers. Elle est utilisée uniquement dans le but prévu d'accéder à l'API OpenAI et non pour toute autre utilisation non autorisée.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/fr/api.json",
    "content": "{\n  \"securityMessage\": \"Nous accordons la priorité à la sécurité de votre clé API et la traitons avec le plus grand soin. Votre clé est exclusivement stockée sur votre navigateur et jamais partagée avec une entité tierce. Elle est utilisée uniquement dans le but prévu d'accéder à l'API OpenAI et non pour toute autre utilisation non autorisée.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"Point d'accès de l'API\",\n    \"description\": \"Lorsque vous choisissez un point d'accès de l'API non officiel, il fonctionne essentiellement comme un proxy. Un proxy agit en tant qu'intermédiaire entre votre appareil et le serveur de destination, dans ce cas-ci, l'API OpenAI. Ce faisant, il vous permet d'accéder à l'API OpenAI dans des régions où cela pourrait être autrement restreint.\",\n    \"warn\": \"De plus, si vous fournissez un point d'accès personnalisé à l'API qui offre un accès gratuit à l'API OpenAI, vous pouvez utiliser ChatGPT sans avoir besoin de fournir une clé API en laissant simplement le champ de clé API vide. Cependant, il est crucial de faire preuve de prudence lors de l'utilisation de points d'accès de l'API tiers, car ceux qui ne sont pas fiables peuvent enregistrer vos informations personnelles dans les conversations. Vérifiez toujours la fiabilité d'un point d'accès de l'API avant de l'utiliser pour protéger votre vie privée et votre sécurité.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Obtenez votre clé API personnelle <0>ici</0>.\",\n    \"inputLabel\": \"Clé API\"\n  },\n  \"customEndpoint\": \"Utiliser un point d'accès personnalisé à l'API\",\n  \"advancedConfig\": \"Voir la configuration avancée de l'API <0>ici</0>\",\n  \"noApiKeyWarning\": \"Aucune clé API fournie ! Veuillez vérifier vos paramètres d'API.\"\n}\n"
  },
  {
    "path": "public/locales/fr/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Synchronisez vos discussions et paramètres sans effort avec Google Drive.\",\n  \"button\": {\n    \"sync\": \"Synchroniser vos discussions\",\n    \"stop\": \"Arrêter la synchronisation\",\n    \"create\": \"Créer un nouveau fichier\",\n    \"confirm\": \"Confirmer la sélection\"\n  },\n  \"notice\": \"Note: Vous devrez vous reconnecter à chaque visite ou toutes les heures. Pour éviter que vos données cloud soient écrasées, n'utilisez pas Better ChatGPT sur plus d'un appareil en même temps.\",\n  \"privacy\": \"Votre vie privée est importante pour nous et pour la garantir, Better ChatGPT n'a que des accès non sensibles, c'est-à-dire qu'il peut seulement créer, visualiser et gérer ses propres fichiers et dossiers.\",\n  \"toast\": {\n    \"sync\": \"Synchronisation réussie!\",\n    \"stop\": \"Synchronisation arrêtée\"\n  }\n}\n"
  },
  {
    "path": "public/locales/fr/main.json",
    "content": "{\n  \"save\": \"Enregistrer\",\n  \"generate\": \"Générer\",\n  \"cancel\": \"Annuler\",\n  \"confirm\": \"Confirmer\",\n  \"warning\": \"Attention\",\n  \"clearMessageWarning\": \"Veuillez prendre note qu'en envoyant ce message, tous les messages suivants seront supprimés !\",\n  \"clearConversationWarning\": \"Veuillez prendre note que si vous confirmez cette action, tous les messages seront supprimés !\",\n  \"clearConversation\": \"Effacer la conversation\",\n  \"import\": \"Importer\",\n  \"export\": \"Exporter\",\n  \"author\": \"Créé par Jing Hua\",\n  \"about\": \"À propos et Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personnel\",\n  \"free\": \"Gratuit\",\n  \"downloadChat\": \"Télécharger la conversation\",\n  \"user\": \"Utilisateur\",\n  \"assistant\": \"Assistant\",\n  \"system\": \"Système\",\n  \"newChat\": \"Nouvelle Conversation\",\n  \"lightMode\": \"Mode clair\",\n  \"darkMode\": \"Mode sombre\",\n  \"setting\": \"Paramètres\",\n  \"image\": \"Image\",\n  \"autoTitle\": \"Générer le titre automatiquement\",\n  \"advancedMode\": \"Mode avancé\",\n  \"inlineLatex\": \"Latex en ligne\",\n  \"prompt\": \"Incitation\",\n  \"promptLibrary\": \"Bibliothèque de prompt\",\n  \"name\": \"Nom\",\n  \"search\": \"Recherche\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Réinitialiser les coûts\",\n  \"countTotalTokens\": \"Compter le nombre total de jetons\",\n  \"morePrompts\": \"Vous pouvez trouver plus de prompts ici : \",\n  \"clearPrompts\": \"Effacer les prompts\",\n  \"postOnShareGPT\": {\n    \"title\": \"Publier sur ShareGPT\",\n    \"warning\": \"Veuillez noter que si vous publiez votre conversation sur ShareGPT, elle deviendra accessible au public et visible par tous. Une fois publiée, la conversation ne peut pas être cachée ou supprimée, et peut être archivée ou partagée par d'autres. Nous vous conseillons de considérer attentivement et d'éviter de partager des informations sensibles ou privées sur cette plateforme.\"\n  },\n  \"newFolder\": \"Nouveau Dossier\",\n  \"cloneChat\": \"Cloner la Conversation\",\n  \"cloned\": \"Clonée\",\n  \"enterToSubmit\": \"Entrée pour soumettre\",\n  \"submitPlaceholder\": \"Saisissez un message ou cliquez sur [/] pour des prompts...\"\n}\n"
  },
  {
    "path": "public/locales/fr/model.json",
    "content": "{\n  \"configuration\": \"Configuration\",\n  \"model\": \"Modèle\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"Le nombre maximum de jetons à générer dans la complétion de la conversation. La longueur totale des jetons d'entrée et des jetons générés est limitée par la longueur de contexte du modèle.\"\n  },\n  \"default\": \"Défaut\",\n  \"temperature\": {\n    \"label\": \"Température\",\n    \"description\": \"La température d'échantillonnage, entre 0 et 2. Des valeurs plus élevées comme 0,8 rendent la sortie plus aléatoire, tandis que des valeurs plus basses comme 0,2 la rendent plus ciblée et déterminée. Nous recommandons généralement de modifier ceci ou top-p mais pas les deux. (Par défaut : 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Pénalité de présence\",\n    \"description\": \"Nombre entre -2.0 et 2.0. Les valeurs positives pénalisent les nouveaux jetons en fonction de leur apparition dans le texte jusqu'à présent, augmentant la probabilité du modèle de parler de nouveaux sujets. (Par défaut : 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Nombre entre 0 et 1. Une alternative à l'échantillonnage avec la température, appelée échantillonnage de noyau, où le modèle considère les résultats des jetons avec une probabilité de p-masse supérieure. Ainsi, 0,1 signifie que seuls les jetons constituant les 10 % supérieurs de la masse de probabilité sont considérés. Nous recommandons généralement de modifier ceci ou la température mais pas les deux. (Par défaut : 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Pénalité de fréquence\",\n    \"description\": \"Nombre entre -2.0 et 2.0. Les valeurs positives pénalisent les nouveaux jetons en fonction de leur fréquence existante dans le texte jusqu'à présent, diminuant la probabilité du modèle de répéter la même ligne mot pour mot. (Par défaut : 0)\"\n  },\n  \"defaultChatConfig\": \"Configuration de Chat Par Défaut\",\n  \"defaultSystemMessage\": \"Message Système Par Défaut\",\n  \"resetToDefault\": \"Réinitialiser aux paramètres par défaut\"\n}\n"
  },
  {
    "path": "public/locales/it/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT è un'incredibile applicazione web open-source che permette di giocare con l'API ChatGPT di OpenAI gratuitamente!\",\n  \"sourceCode\": \"Scopri il <0>codice sorgente</0> su GitHub e dai una ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Scopri l'iniziativa <0><i>Open ChatGPT</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Supporto\",\n    \"paragraph1\": \"Noi di Better ChatGPT ci sforziamo di fornirvi funzioni utili e sorprendenti 24 ore su 24. E proprio come ogni progetto, il tuo supporto e la tua motivazione ci aiuteranno a continuare a progredire. E proprio come in ogni progetto, il tuo supporto e la tua motivazione saranno fondamentali per aiutarci ad andare avanti!\",\n    \"paragraph2\": \"Se ti sei trovato bene con la nostra app, ti chiediamo gentilmente di dare una ⭐️ a questo <0>progetto</0>. La tua approvazione significa molto per noi e ci incoraggia a lavorare sempre di più per offrire la migliore esperienza possibile.\",\n    \"paragraph3\": \"Se desideri sostenere il team, prendi in considerazione la possibilità di sponsorizzarci attraverso uno dei metodi indicati di seguito. Ogni contributo, per quanto piccolo, ci aiuta a mantenere e migliorare il nostro servizio.\",\n    \"paragraph4\": \"Ti ringraziamo di far parte della nostra community e ci auguriamo di poterti servire meglio in futuro.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"Ti invitiamo ad unirti alla nostra community Discord! Il nostro server Discord è un luogo ideale per scambiare idee e suggerimenti su ChatGPT e per inviare richieste di funzionalità per Better ChatGPT. Avrai l'opportunità di interagire con gli sviluppatori di Better ChatGPT e con altri appassionati di IA che condividono la tua stessa passione.\",\n    \"paragraph2\": \"Per unirsi al nostro server, basta cliccare sul seguente link: <0>https://discord.gg/g3Qnwy4V6A</0>. Non vediamo l'ora di vederti lì!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Informativa sulla privacy\",\n    \"paragraph1\": \"Ci teniamo molto alla tua privacy e ci impegniamo a salvaguardare quella dei nostri utenti. Non raccogliamo né memorizziamo alcun testo immesso o ricevuto dal server OpenAI in alcuna forma. Il nostro codice sorgente è disponibile per la verifica di questa dichiarazione.\",\n    \"paragraph2\": \"Diamo priorità alla sicurezza della tua chiave API e la gestiamo con la massima cura. Se si utilizza la propria chiave API, questa viene memorizzata esclusivamente sul browser dell'utente e non viene mai condivisa con alcuna entità terza. Viene utilizzata esclusivamente per lo scopo previsto di accedere all'API OpenAI e non per altri usi non autorizzati.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/it/api.json",
    "content": "{\n  \"securityMessage\": \"Diamo priorità alla sicurezza della tua chiave API e la gestiamo con la massima cura. La chiave viene memorizzata esclusivamente nel browser dell'utente e non viene mai condivisa con terze parti. Viene utilizzata esclusivamente per lo scopo previsto di accedere all'API OpenAI e non per altri usi non autorizzati.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API Endpoint\",\n    \"description\": \"Quando si sceglie un endpoint API non ufficiale, questo funziona come un proxy. Un proxy funziona come un intermediario tra il dispositivo e il server di destinazione, in questo caso l'API OpenAI. In questo modo, consente di accedere all'API OpenAI in regioni in cui potrebbe essere limitata.\",\n    \"warn\": \"Inoltre, se si fornisce un endpoint API personalizzato che garantisce l'accesso gratuito all'API OpenAI, è possibile utilizzare ChatGPT senza la necessità di fornire una chiave API, semplicemente lasciando vuoto il campo della chiave API. Tuttavia, è fondamentale essere cauti quando si utilizzano endpoint API di terze parti, poiché quelli non affidabili potrebbero registrare le informazioni personali nelle conversazioni. Verifica sempre l'affidabilità di un endpoint API prima di utilizzarlo per proteggere la tua privacy e la sicurezza.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Ottieni la tua chiave API personale <0>qui</0>.\",\n    \"inputLabel\": \"Chiave API\"\n  },\n  \"customEndpoint\": \"Usa un endpoint API personalizzato\",\n  \"advancedConfig\": \"Visualizza la configurazione avanzata dell'API <0>qui</0>\",\n  \"noApiKeyWarning\": \"Non è stata fornita alcuna chiave API! Controlla le impostazioni API.\"\n}\n"
  },
  {
    "path": "public/locales/it/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/it/main.json",
    "content": "{\n  \"save\": \"Salva\",\n  \"generate\": \"Generare\",\n  \"cancel\": \"Annulla\",\n  \"confirm\": \"Conferma\",\n  \"warning\": \"Attenzione\",\n  \"clearMessageWarning\": \"Ti informiamo che inviando questo messaggio, tutti i messaggi successivi saranno cancellati!\",\n  \"clearConversationWarning\": \"Ti informiamo che confermando questa azione, tutti i messaggi saranno cancellati!\",\n  \"clearConversation\": \"Elimina Conversazione\",\n  \"import\": \"Importa\",\n  \"export\": \"Esporta\",\n  \"author\": \"Realizzato da Jing Hua\",\n  \"about\": \"Info & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personale\",\n  \"free\": \"Gratuito\",\n  \"downloadChat\": \"Scarica Conversazione\",\n  \"user\": \"Utente\",\n  \"assistant\": \"Assistente\",\n  \"system\": \"Sistema\",\n  \"newChat\": \"Nuova Conversazione\",\n  \"lightMode\": \"Modalità Chiara\",\n  \"darkMode\": \"Modalità Scura\",\n  \"setting\": \"Impostazioni\",\n  \"image\": \"Immagine\",\n  \"autoTitle\": \"Genera automaticamente il titolo\",\n  \"advancedMode\": \"Modalità avanzata\",\n  \"inlineLatex\": \"Latex in linea\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Libreria Prompt\",\n  \"name\": \"Nome\",\n  \"search\": \"Cerca\",\n  \"total\": \"Totale\",\n  \"resetCost\": \"Ripristina costi\",\n  \"countTotalTokens\": \"Conteggio totale dei token\",\n  \"morePrompts\": \"Puoi trovare altri prompt qui:\",\n  \"clearPrompts\": \"Cancella prompts\",\n  \"postOnShareGPT\": {\n    \"title\": \"Pubblica su ShareGPT\",\n    \"warning\": \"Ti ricordiamo che pubblicando la tua conversazione su ShareGPT, questa diventerà pubblicamente accessibile e visualizzabile da chiunque. Una volta pubblicata, la conversazione non può essere nascosta o cancellata e può essere archiviata o condivisa da altri. Ti consigliamo di valutare attentamente e di evitare di condividere informazioni sensibili o private su questa piattaforma.\"\n  },\n  \"newFolder\": \"Nuova Cartella\",\n  \"cloneChat\": \"Duplica Conversazione\",\n  \"cloned\": \"Duplicata\",\n  \"enterToSubmit\": \"Invio per inviare\",\n  \"submitPlaceholder\": \"Digita un messaggio o fai clic su [/] per prompt...\"\n}\n"
  },
  {
    "path": "public/locales/it/model.json",
    "content": "{\n  \"configuration\": \"Configurazione\",\n  \"model\": \"Modello\",\n  \"token\": {\n    \"label\": \"Token Massimo\",\n    \"description\": \"Il numero massimo di token da generare nel completamento della chat. La lunghezza totale dei token in ingresso e di quelli generati è limitata dalla lunghezza del contesto del modello.\"\n  },\n  \"default\": \"Default\",\n  \"temperature\": {\n    \"label\": \"Temperatura\",\n    \"description\": \"Quale temperatura di campionamento utilizzare, tra 0 e 2. Valori più alti, come 0,8, renderanno l'output più casuale, mentre valori più bassi, come 0,2, lo renderanno più mirato e deterministico. In genere si consiglia di modificare questo valore o quello superiore, ma non entrambi. (Valore predefinito: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Presenza Penalità\",\n    \"description\": \"Numero compreso tra -2,0 e 2,0. I valori positivi penalizzano i nuovi token in base alla loro presenza nel testo, aumentando la probabilità che il modello parli di nuovi argomenti. (Valore predefinito: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Numero compreso tra 0 e 1. Un'alternativa al campionamento con temperatura, chiamato campionamento del nucleo, in cui il modello considera i risultati dei token con la massa di probabilità p più alta. Quindi 0,1 significa che vengono considerati solo i token che comprendono il 10% della massa di probabilità. In genere si consiglia di modificare questo parametro o la temperatura, ma non entrambi. (Predefinito: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Penalità di frequenza\",\n    \"description\": \"Numero compreso tra -2,0 e 2,0. I valori positivi penalizzano i nuovi token in base alla loro frequenza nel testo fino a quel momento, diminuendo la probabilità del modello di ripetere testualmente la stessa riga. (Valore predefinito: 0)\"\n  },\n  \"defaultChatConfig\": \"Configurazione predefinita della conversazione\",\n  \"defaultSystemMessage\": \"Messaggio di sistema predefinito\",\n  \"resetToDefault\": \"Ripristina alle impostazioni predefinite\"\n}\n"
  },
  {
    "path": "public/locales/ja/about.json",
    "content": "{\n  \"description\": \"Better ChatGPTは、OpenAIのChatGPT APIを無料でお試しいただける素晴らしいオープンソースのWebアプリです！\",\n  \"sourceCode\": \"GitHubで<0>ソースコード</0>をチェックして、⭐️を付けてください！\",\n  \"initiative\": {\n    \"description\": \"<0><i>Open ChatGPTイニシアチブ</i></0>をチェックしてください！\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"サポート\",\n    \"paragraph1\": \"Better ChatGPTでは、お客様に24時間365日、便利で素晴らしい機能を提供することを目指しています。そして、どんなプロジェクトでも、皆様からのサポートと励ましが、私たちが前進し続ける原動力となります！\",\n    \"paragraph2\": \"アプリをお楽しみいただけた場合は、この<0>プロジェクト</0>に⭐️を付けていただけると嬉しいです。皆様からの応援が私たちの励みとなり、より良い体験を提供するために努力する原動力となります。\",\n    \"paragraph3\": \"チームをサポートしたい場合は、以下の方法でスポンサーになっていただくことを検討してください。どんなに小さな寄付でも、サービスの維持・向上に役立ちます。\",\n    \"paragraph4\": \"私たちのコミュニティの一員であることに感謝し、今後もより良いサービスを提供していくことを楽しみにしています。\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discordサーバー\",\n    \"paragraph1\": \"Discordコミュニティへのご参加をお待ちしております！Discordサーバーでは、ChatGPTに関するアイデアやヒントの交換、Better ChatGPTの機能リクエストの提出などができます。Better ChatGPTの開発者や、同じAIに情熱を持つ他のエンスージアストと交流する機会もあります。\",\n    \"paragraph2\": \"サーバーに参加するには、次のリンクをクリックしてください：<0>https://discord.gg/g3Qnwy4V6A</0>。お待ちしております！\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"プライバシーに関する声明\",\n    \"paragraph1\": \"私たちは皆様のプライバシーを非常に重要視し、ユーザーのプライバシーを保護することに力を入れています。OpenAIサーバーから入力または受信したテキストは一切収集・保存していません。この声明の確認のために、ソースコードを公開しています。\",\n    \"paragraph2\": \"皆様のAPIキーのセキュリティを最優先にし、最大限の注意を払って取り扱っています。独自のAPIキーを使用する場合、キーはブラウザ上でのみ保存され、第三者とは一切共有されません。キーはOpenAI APIにアクセスする目的でのみ使用され、他の無許可の使用には利用されません。\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ja/api.json",
    "content": "{\n  \"securityMessage\": \"APIキーのセキュリティを最優先し、細心の注意を払って取り扱っています。キーはお客様のブラウザにのみ保存され、第三者とは一切共有されません。OpenAI APIにアクセスする目的でのみ使用され、他の不正な目的で使用されることはありません。\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"APIエンドポイント\",\n    \"description\": \"非公式のAPIエンドポイントを選択すると、プロキシとして機能します。プロキシは、あなたのデバイスと目的のサーバ（この場合はOpenAI API）の間に中継役として働くことによって、制限されている可能性のある地域でもOpenAI APIにアクセスできるようになります。\",\n    \"warn\": \"さらに、無料でOpenAI APIにアクセスできるカスタムAPIエンドポイントを提供する場合、APIキー欄を空白にするだけでAPIキーを提供せずにChatGPTを利用できます。ただし、第三者のAPIエンドポイントを利用する際は注意が必要で、信頼性の低いものは会話の中で個人情報を記録することがあります。プライバシーとセキュリティを保護するために、APIエンドポイントを使用する前に信頼性を確認してください。\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"個人用APIキーは<0>こちら</0>で取得できます。\",\n    \"inputLabel\": \"APIキー\"\n  },\n  \"customEndpoint\": \"カスタムAPIエンドポイントを使用する\",\n  \"advancedConfig\": \"詳細なAPI設定は<0>こちら</0>で表示できます。\",\n  \"noApiKeyWarning\": \"APIキーが入力されていません！API設定を確認してください。\"\n}\n"
  },
  {
    "path": "public/locales/ja/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ja/main.json",
    "content": "{\n  \"save\": \"保存\",\n  \"generate\": \"生成\",\n  \"cancel\": \"キャンセル\",\n  \"confirm\": \"確認\",\n  \"warning\": \"警告\",\n  \"clearMessageWarning\": \"このメッセージを送信すると、以降のメッセージがすべて削除されることに注意してください！\",\n  \"clearConversationWarning\": \"この操作を確認すると、すべてのメッセージが削除されることに注意してください！\",\n  \"clearConversation\": \"会話をクリア\",\n  \"import\": \"インポート\",\n  \"export\": \"エクスポート\",\n  \"author\": \"Jing Hua作\",\n  \"about\": \"概要 & スポンサー\",\n  \"api\": \"API\",\n  \"personal\": \"個人\",\n  \"free\": \"無料\",\n  \"downloadChat\": \"チャットをダウンロード\",\n  \"user\": \"ユーザー\",\n  \"assistant\": \"アシスタント\",\n  \"system\": \"システム\",\n  \"newChat\": \"新しいチャット\",\n  \"lightMode\": \"ライトモード\",\n  \"darkMode\": \"ダークモード\",\n  \"setting\": \"設定\",\n  \"image\": \"画像\",\n  \"autoTitle\": \"タイトルを自動生成\",\n  \"advancedMode\": \"上級モード\",\n  \"inlineLatex\": \"インライン LaTeX\",\n  \"prompt\": \"プロンプト\",\n  \"promptLibrary\": \"プロンプトライブラリ\",\n  \"name\": \"名前\",\n  \"search\": \"検索\",\n  \"total\": \"合計\",\n  \"resetCost\": \"コストをリセットする\",\n  \"countTotalTokens\": \"トークンの合計数をカウント\",\n  \"morePrompts\": \"ここでさらにプロンプトを見つけることができます：\",\n  \"clearPrompts\": \"プロンプトをクリア\",\n  \"postOnShareGPT\": {\n    \"title\": \"ShareGPTに投稿\",\n    \"warning\": \"ShareGPTに会話を投稿すると、誰でもアクセスして閲覧できるようになることに注意してください。一度投稿すると、会話は非表示にできず、削除もできません。また、他の人がアーカイブや共有する可能性があります。このプラットフォームで機密性のある情報や個人情報を共有しないように注意してください。\"\n  },\n  \"newFolder\": \"新しいフォルダー\",\n  \"cloneChat\": \"チャットのコピーを作成\",\n  \"cloned\": \"完了しました\",\n  \"enterToSubmit\": \"Enterキーを押して送信\",\n  \"submitPlaceholder\": \"メッセージを入力するか、[/] をクリックしてプロンプトを表示します...\"\n}\n"
  },
  {
    "path": "public/locales/ja/model.json",
    "content": "{\n  \"configuration\": \"設定\",\n  \"model\": \"モデル\",\n  \"token\": {\n    \"label\": \"最大トークン数\",\n    \"description\": \"チャット完了時に生成するトークンの最大数。入力トークンと生成されたトークンの合計長は、モデルのコンテキスト長で制限されます。\"\n  },\n  \"default\": \"デフォルト\",\n  \"temperature\": {\n    \"label\": \"温度\",\n    \"description\": \"使用するサンプリング温度。0から2までの間で指定します。0.8などの高い値は、出力をよりランダムにします。一方、0.2などの低い値は、より焦点を絞って決定論的にします。通常、これまたはtop pを変更することをお勧めしますが、両方を変更しないでください。(デフォルト：1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"存在ペナルティ\",\n    \"description\": \"-2.0から2.0までの数値。正の値は、新しいトークンがテキストに現れるかどうかに基づいて新しいトピックについて話すモデルの可能性を高めるため、新しいトークンにペナルティを課します。(デフォルト：0)\"\n  },\n  \"topP\": {\n    \"label\": \"トップp\",\n    \"description\": \"0から1の数値。温度を使ったサンプリングの代わりに、モデルはトップp確率質量を持つトークンの結果を考慮する核サンプリングと呼ばれる手法を使用します。つまり、0.1は確率質量の上位10％を占めるトークンのみが考慮されます。通常、これまたは温度のどちらか一方を変更することをお勧めします。（デフォルト：1）\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"頻度ペナルティ\",\n    \"description\": \"-2.0から2.0の数値。正の値は、テキスト内での新しいトークンの既存の頻度に基づいて新しいトークンにペナルティを課し、同じ行をそのまま繰り返す可能性を減らします。（デフォルト：0）\"\n  },\n  \"defaultChatConfig\": \"デフォルトチャット設定\",\n  \"defaultSystemMessage\": \"デフォルトシステムメッセージ\",\n  \"resetToDefault\": \"デフォルトにリセット\"\n}\n"
  },
  {
    "path": "public/locales/ms/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT merupakan aplikasi web sumber terbuka yang menakjubkan yang membolehkan anda bermain dengan API ChatGPT OpenAI secara percuma!\",\n  \"sourceCode\": \"Lihat <0>kod sumber</0> di GitHub dan beri ia ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Lihat <0><i>Inisiatif ChatGPT Terbuka</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Sokongan\",\n    \"paragraph1\": \"Di Better ChatGPT, kami berusaha untuk menyediakan anda dengan ciri-ciri berguna dan menakjubkan sepanjang masa. Dan seperti mana-mana projek, sokongan dan motivasi anda akan membantu kami untuk terus maju!\",\n    \"paragraph2\": \"Jika anda menikmati penggunaan aplikasi kami, kami dengan rendah hati meminta anda memberi <0>projek</0> ini ⭐️. Dukungan anda sangat bermakna kepada kami dan mendorong kami untuk bekerja lebih keras dalam menyediakan pengalaman terbaik.\",\n    \"paragraph3\": \"Jika anda ingin menyokong pasukan kami, pertimbangkan untuk menaja kami melalui salah satu kaedah di bawah. Setiap sumbangan, tidak kira seberapa kecil, membantu kami untuk mengekalkan dan meningkatkan perkhidmatan kami.\",\n    \"paragraph4\": \"Terima kasih kerana menjadi sebahagian daripada komuniti kami, dan kami tidak sabar untuk melayani anda dengan lebih baik pada masa hadapan.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Server Discord\",\n    \"paragraph1\": \"Kami menjemput anda untuk menyertai komuniti Discord kami! Server Discord kami adalah tempat yang hebat untuk bertukar idea dan petua ChatGPT, serta mengemukakan permintaan ciri untuk Better ChatGPT. Anda akan berpeluang berinteraksi dengan para pembangun di sebalik Better ChatGPT serta peminat AI lain yang berkongsi minat anda.\",\n    \"paragraph2\": \"Untuk menyertai server kami, hanya klik pautan berikut: <0>https://discord.gg/g3Qnwy4V6A</0>. Kami tidak sabar untuk melihat anda di sana!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Kenyataan Privasi\",\n    \"paragraph1\": \"Kami sangat menghargai privasi anda dan komited untuk melindungi privasi pengguna kami. Kami tidak mengumpul atau menyimpan teks yang anda masukkan atau terima dari pelayan OpenAI dalam bentuk apa pun. Kod sumber kami boleh diakses untuk pemeriksaan anda bagi mengesahkan kenyataan ini.\",\n    \"paragraph2\": \"Kami mengutamakan keselamatan kunci API anda dan mengendalikannya dengan penuh berhati-hati. Jika anda menggunakan kunci API anda sendiri, kunci tersebut hanya disimpan di pelayar anda dan tidak dikongsi dengan mana-mana entiti pihak ketiga. Ia hanya digunakan untuk tujuan yang dimaksudkan, iaitu mengakses API OpenAI dan bukan untuk penggunaan yang tidak sah.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ms/api.json",
    "content": "{\n  \"securityMessage\": \"Kami mengutamakan keselamatan kunci API anda dan mengendalikannya dengan penuh berhati-hati. Kunci anda disimpan secara eksklusif di pelayar anda dan tidak pernah dikongsi dengan mana-mana entiti pihak ketiga. Ia hanya digunakan untuk tujuan yang dimaksudkan untuk mengakses API OpenAI dan bukan untuk penggunaan yang tidak sah.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"Hujung API\",\n    \"description\": \"Apabila anda memilih hujung nyawa API yang tidak rasmi, ia pada dasarnya berfungsi sebagai proksi. Proksi berfungsi dengan bertindak sebagai perantara di antara peranti anda dan pelayan destinasi, dalam kes ini, API OpenAI. Dengan berbuat demikian, ia membolehkan anda mengakses API OpenAI di kawasan di mana ia mungkin sebaliknya terhad.\",\n    \"warn\": \"Selain itu, jika anda menyediakan hujung nyawa API tersendiri yang memberikan akses percuma ke API OpenAI, anda boleh menggunakan ChatGPT tanpa perlu menyediakan kunci API dengan hanya meninggalkan medan kunci API kosong. Walau bagaimanapun, amat penting untuk berhati-hati semasa menggunakan hujung nyawa API pihak ketiga, kerana yang tidak boleh dipercayai mungkin merekodkan maklumat peribadi anda dalam perbualan. Sentiasa sahkan kebolehpercayaan hujung nyawa API sebelum menggunakannya untuk melindungi privasi dan keselamatan anda.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Dapatkan kunci API peribadi anda <0>di sini</0>.\",\n    \"inputLabel\": \"Kunci API\"\n  },\n  \"customEndpoint\": \"Gunakan hujung API tersuai\",\n  \"advancedConfig\": \"Lihat konfigurasi API lanjutan <0>di sini</0>\",\n  \"noApiKeyWarning\": \"Tiada kunci API yang dibekalkan! Sila periksa tetapan API anda.\"\n}\n"
  },
  {
    "path": "public/locales/ms/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ms/main.json",
    "content": "{\n  \"save\": \"Simpan\",\n  \"generate\": \"Hasilkan\",\n  \"cancel\": \"Batal\",\n  \"confirm\": \"Sahkan\",\n  \"warning\": \"Amaran\",\n  \"clearMessageWarning\": \"Sila ambil perhatian bahawa dengan menghantar mesej ini, semua mesej seterusnya akan dipadam!\",\n  \"clearConversationWarning\": \"Sila ambil perhatian bahawa dengan mengesahkan tindakan ini, semua mesej akan dipadam!\",\n  \"clearConversation\": \"Padam Perbualan\",\n  \"import\": \"Import\",\n  \"export\": \"Eksport\",\n  \"author\": \"Dibuat oleh Jing Hua\",\n  \"about\": \"Mengenai & Penaja\",\n  \"api\": \"API\",\n  \"personal\": \"Peribadi\",\n  \"free\": \"Percuma\",\n  \"downloadChat\": \"Muat Turun Perbualan\",\n  \"user\": \"Pengguna\",\n  \"assistant\": \"Pembantu\",\n  \"system\": \"Sistem\",\n  \"newChat\": \"Perbualan Baru\",\n  \"lightMode\": \"Mod Terang\",\n  \"darkMode\": \"Mod Gelap\",\n  \"setting\": \"Tetapan\",\n  \"image\": \"Imej\",\n  \"autoTitle\": \"Jana tajuk secara automatik\",\n  \"advancedMode\": \"Mod lanjutan\",\n  \"inlineLatex\": \"Latex Sebaris\",\n  \"prompt\": \"Arahan\",\n  \"promptLibrary\": \"Pustaka Arahan\",\n  \"name\": \"Nama\",\n  \"search\": \"Cari\",\n  \"total\": \"Jumlah\",\n  \"resetCost\": \"Reset Kos\",\n  \"countTotalTokens\": \"Kira jumlah token keseluruhan\",\n  \"morePrompts\": \"Anda boleh mencari lebih banyak arahan di sini: \",\n  \"clearPrompts\": \"Kosongkan arahan\",\n  \"postOnShareGPT\": {\n    \"title\": \"Siarkan di ShareGPT\",\n    \"warning\": \"Sila ambil perhatian bahawa dengan menyiarkan perbualan anda di ShareGPT, ia akan menjadi boleh diakses dan dilihat oleh sesiapa sahaja. Setelah disiarkan, perbualan tidak boleh disembunyikan atau dipadam, dan mungkin diarkibkan atau dikongsi oleh orang lain. Kami menasihatkan anda untuk mempertimbangkan dengan teliti dan mengelakkan berkongsi maklumat sensitif atau peribadi di platform ini.\"\n  },\n  \"newFolder\": \"Folder Baru\",\n  \"cloneChat\": \"Buat salinan perbualan ini\",\n  \"cloned\": \"Dicipta\",\n  \"enterToSubmit\": \"Tekan Enter untuk hantar\",\n  \"submitPlaceholder\": \"Taip mesej atau klik [/] untuk arahan...\"\n}\n"
  },
  {
    "path": "public/locales/ms/model.json",
    "content": "{\n  \"configuration\": \"Konfigurasi\",\n  \"model\": \"Model\",\n  \"token\": {\n    \"label\": \"Token Maksimum\",\n    \"description\": \"Jumlah token maksimum untuk dijana dalam lengkapan sembang. Panjang keseluruhan token input dan token yang dijana adalah terhad oleh panjang konteks model.\"\n  },\n  \"default\": \"Lalai\",\n  \"temperature\": {\n    \"label\": \"Suhu\",\n    \"description\": \"Suhu pensampelan yang digunakan, antara 0 dan 2. Nilai yang lebih tinggi seperti 0.8 akan menjadikan keluaran lebih rawak, manakala nilai yang lebih rendah seperti 0.2 akan menjadikannya lebih terarah dan deterministik. Kami secara umumnya mengesyorkan mengubah ini atau atas p tetapi bukan kedua-duanya. (Lalai: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Hukuman Kehadiran\",\n    \"description\": \"Nombor antara -2.0 dan 2.0. Nilai positif menghukum token baru berdasarkan sama ada mereka muncul dalam teks sejauh ini, meningkatkan kemungkinan model untuk bercakap mengenai topik baru. (Lalai: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Nombor antara 0 dan 1. Alternatif kepada pensampelan dengan suhu, dipanggil pensampelan nukleus, di mana model mempertimbangkan hasil token dengan jisim kebarangkalian top p. Jadi 0.1 bermaksud hanya token yang terdiri daripada 10% jisim kebarangkalian teratas dipertimbangkan. Secara umumnya, kami mengesyorkan untuk mengubah suhu atau nilai paling atas tetapi bukan kedua-duanya. (Lalai: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Penalti Frekuensi\",\n    \"description\": \"Nombor antara -2.0 dan 2.0. Nilai positif memberi hukuman kepada token baru berdasarkan frekuensi sedia ada mereka dalam teks sejauh ini, mengurangkan kebarangkalian model untuk mengulangi baris yang sama secara harfiah. (Lalai: 0)\"\n  },\n  \"defaultChatConfig\": \"Konfigurasi Cakap Lalai\",\n  \"defaultSystemMessage\": \"Mesej Sistem Lalai\",\n  \"resetToDefault\": \"Set Semula ke Lalai\"\n}\n"
  },
  {
    "path": "public/locales/nb/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT er en fantastisk åpen kildekode web-app som lar deg leke med OpenAI's ChatGPT API gratis!\",\n  \"sourceCode\": \"Sjekk ut <0>kildekoden</0> på GitHub og gi den en ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Sjekk ut <0><i>Open ChatGPT Initiative</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Støtte\",\n    \"paragraph1\": \"På Better ChatGPT streber vi etter å tilby deg nyttige og fantastiske funksjoner døgnet rundt. Akkurat som alle prosjekter, vil din støtte og motivasjon være avgjørende for å hjelpe oss å fortsette fremover!\",\n    \"paragraph2\": \"Hvis du har likt å bruke appen vår, ber vi deg vennligst om å gi dette <0>prosjektet</0> en ⭐️. Din støtte betyr mye for oss og oppmuntrer oss til å jobbe hardere mot å levere den beste mulige opplevelsen.\",\n    \"paragraph3\": \"Hvis du ønsker å støtte teamet, kan du vurdere å sponse oss gjennom en av metodene nedenfor. Hver bidrag, uansett hvor lite, hjelper oss med å opprettholde og forbedre tjenesten vår.\",\n    \"paragraph4\": \"Takk for at du er en del av samfunnet vårt, og vi ser frem til å betjene deg bedre i fremtiden.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord Server\",\n    \"paragraph1\": \"Vi inviterer deg til å bli med i Discord-samfunnet vårt! Discord-serveren vår er et flott sted å utveksle ChatGPT-ideer og tips, og sende inn funksjonsforespørsler for Better ChatGPT. Du får muligheten til å samhandle med utviklerne bak Better ChatGPT, samt andre AI-entusiaster som deler lidenskapen din.\",\n    \"paragraph2\": \"For å bli med på serveren vår, klikk bare på følgende lenke: <0>https://discord.gg/g3Qnwy4V6A</0>. Vi gleder oss til å se deg der!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Personvernerklæring\",\n    \"paragraph1\": \"Vi verdsetter personvernet ditt høyt og er forpliktet til å beskytte personvernet til brukerne våre. Vi samler ikke inn eller lagrer noen tekst du skriver inn eller mottar fra OpenAI-serveren i noen form. Kildekoden vår er tilgjengelig for din inspeksjon for å bekrefte denne uttalelsen.\",\n    \"paragraph2\": \"Vi prioriterer sikkerheten til API-nøkkelen din og behandler den med største forsiktighet. Hvis du bruker din egen API-nøkkel, lagres nøkkelen utelukkende pånettleseren din og deles aldri med noen tredjeparts enhet. Den brukes kun for det tiltenkte formålet med å få tilgang til OpenAI API og ikke for noen annen uautorisert bruk.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/nb/api.json",
    "content": "{\n  \"securityMessage\": \"Vi prioriterer sikkerheten til API-nøkkelen din og behandler den med største forsiktighet. Nøkkelen din er kun lagret i nettleseren din og deles aldri med noen tredjeparts enhet. Den brukes utelukkende for det tiltenkte formålet med å få tilgang til OpenAI API og ikke for annen uautorisert bruk.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API-endepunkt\",\n    \"description\": \"Når du velger et uoffisielt API-endepunkt, fungerer det som en proxy. En proxy fungerer ved å opptre som et mellomledd mellom enheten din og destinasjonsserveren, i dette tilfellet OpenAI API-et. Ved å gjøre dette, gjør det deg i stand til å få tilgang til OpenAI API-et i regioner hvor det ellers kunne være begrenset.\",\n    \"warn\": \"I tillegg, hvis du oppgir et egendefinert API-endepunkt som gir gratis tilgang til OpenAI API-et, kan du bruke ChatGPT uten behov for å oppgi en API-nøkkel ved å ganske enkelt la API-nøkkelfeltet stå tomt. Det er imidlertid viktig å være forsiktig når du bruker tredjeparts API-endepunkter, ettersom upålitelige kan logge personlig informasjon i samtaler. Bekreft alltid påliteligheten til et API-endepunkt før du bruker det for å beskytte personvernet og sikkerheten din.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Få din personlige API-nøkkel <0>her</0>.\",\n    \"inputLabel\": \"API-nøkkel\"\n  },\n  \"customEndpoint\": \"Bruk egendefinert API-endepunkt\",\n  \"advancedConfig\": \"Vis avansert API-konfigurasjon <0>her</0>\",\n  \"noApiKeyWarning\": \"Ingen API-nøkkel angitt! Vennligst sjekk API-innstillingene dine.\"\n}\n"
  },
  {
    "path": "public/locales/nb/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/nb/main.json",
    "content": "{\n  \"save\": \"Lagre\",\n  \"generate\": \"Generere\",\n  \"cancel\": \"Avbryt\",\n  \"confirm\": \"Bekreft\",\n  \"warning\": \"Advarsel\",\n  \"clearMessageWarning\": \"Vær oppmerksom på at ved å sende inn denne meldingen, vil alle påfølgende meldinger bli slettet!\",\n  \"clearConversationWarning\": \"Vær oppmerksom på at ved å bekrefte denne handlingen, vil alle meldinger bli slettet!\",\n  \"clearConversation\": \"Tøm samtale\",\n  \"import\": \"Importer\",\n  \"export\": \"Eksporter\",\n  \"author\": \"Laget av Jing Hua\",\n  \"about\": \"Om & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personlig\",\n  \"free\": \"Gratis\",\n  \"downloadChat\": \"Last ned Chat\",\n  \"user\": \"Bruker\",\n  \"assistant\": \"Assistent\",\n  \"system\": \"System\",\n  \"newChat\": \"Ny Chat\",\n  \"lightMode\": \"Lys Modus\",\n  \"darkMode\": \"Mørk Modus\",\n  \"setting\": \"Innstillinger\",\n  \"image\": \"Bilde\",\n  \"autoTitle\": \"Auto generer tittel\",\n  \"advancedMode\": \"Avansert modus\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Oppgave\",\n  \"promptLibrary\": \"Oppgavebibliotek\",\n  \"name\": \"Navn\",\n  \"search\": \"Søk\",\n  \"total\": \"Totalt\",\n  \"resetCost\": \"Tilbakestill kostnader\",\n  \"countTotalTokens\": \"Tell totale token\",\n  \"morePrompts\": \"Du kan finne flere oppgaver her: \",\n  \"clearPrompts\": \"Tøm oppgave\",\n  \"postOnShareGPT\": {\n    \"title\": \"Innlegg på ShareGPT\",\n    \"warning\": \"Vær oppmerksom på at ved å poste samtalen din på ShareGPT, vil den bli offentlig tilgjengelig og synlig for alle. Når den er postet, kan samtalen ikke skjules eller slettes, og den kan bli arkivert eller delt av andre. Vi anbefaler deg å tenke nøye gjennom og unngå å dele sensitiv eller privat informasjon på denne plattformen.\"\n  },\n  \"newFolder\": \"Ny mappe\",\n  \"cloneChat\": \"Klone chat\",\n  \"cloned\": \"Klonet\",\n  \"enterToSubmit\": \"Trykk enter for å sende\",\n  \"submitPlaceholder\": \"Skriv en melding eller klikk på [/] for oppgave...\"\n}\n"
  },
  {
    "path": "public/locales/nb/model.json",
    "content": "{\n  \"configuration\": \"Konfigurasjon\",\n  \"model\": \"Modell\",\n  \"token\": {\n    \"label\": \"Maks Token\",\n    \"description\": \"Maksimalt antall tokens som skal genereres i chat fullføringen. Den totale lengden av inndata-tokens og genererte tokens er begrenset av modellens kontekstlengde.\"\n  },\n  \"default\": \"Standard\",\n  \"temperature\": {\n    \"label\": \"Temperatur\",\n    \"description\": \"Hvilken prøvetakingstemperatur du skal bruke, mellom 0 og 2. Høyere verdier som 0,8 vil gjøre utdataene mer tilfeldige, mens lavere verdier som 0,2 vil gjøre dem mer fokuserte og deterministiske. Vi anbefaler generelt å endre dette eller topp-p, men ikke begge. (Standard: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Tilstedeværelsesstraff\",\n    \"description\": \"Tall mellom -2,0 og 2,0. Positive verdier straffer nye tokens basert på om de vises i teksten så langt, noe som øker modellens sannsynlighet for å snakke om nye emner. (Standard: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Topp-p\",\n    \"description\": \"Tall mellom 0 og 1. Et alternativ til prøvetaking med temperatur, kalt kjernesampling, der modellen vurderer resultatene av tokens med topp-p sannsynlighetsmasse. Så 0,1 betyr at bare tokens som utgjør de øverste 10% sannsynlighetsmassene blir vurdert. Vi anbefaler generelt å endre dette eller temperaturen, men ikke begge. (Standard: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frekvensstraff\",\n    \"description\": \"Tall mellom -2,0 og 2,0. Positive verdier straffer nye tokens basert på deres eksisterende frekvens i teksten så langt, noe som reduserer modellens sannsynlighet for å gjenta samme linje ordrett. (Standard: 0)\"\n  },\n  \"defaultChatConfig\": \"Standard Chat-konfigurasjon\",\n  \"defaultSystemMessage\": \"Standard Systemmelding\",\n  \"resetToDefault\": \"Tilbakestill til standard\"\n}\n"
  },
  {
    "path": "public/locales/ro/about.json",
    "content": "{\n   \"description\": \"Better ChatGPT este o aplicație web uimitoare cu sursă deschisă care vă permite să vă jucați gratuit cu API-ul ChatGPT al OpenAI!\",\n   \"sourceCode\": \"Verifică <0>codul sursă</0> pe GitHub și dă-i un ⭐️!\",\n   \"initiative\": {\n     \"description\": \"Verificați <0><i>Inițiativa Deschide ChatGPT</i></0>!\",\n     \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n   },\n   \"support\": {\n     \"title\": \"Suport\",\n     \"paragraph1\": \"La Better ChatGPT, ne străduim să vă oferim funcții utile și uimitoare non-stop. Și, la fel ca orice proiect, sprijinul și motivația dvs. vor fi esențiale pentru a ne ajuta să continuăm înainte!\",\n     \"paragraph2\": \"Dacă v-a plăcut să utilizați aplicația noastră, vă rugăm să acordați un ⭐️ acestui <0>proiect</0>. Susținerea dvs. înseamnă foarte mult pentru noi și ne încurajează să muncim mai mult pentru a oferi cea mai bună experiență posibilă. .\",\n     \"paragraph3\": \"Dacă doriți să susțineți echipa, luați în considerare să ne sponsorizați prin una dintre metodele de mai jos. Fiecare contribuție, oricât de mică, ne ajută să ne menținem și să îmbunătățim serviciul.\",\n     \"paragraph4\": \"Vă mulțumim că faceți parte din comunitatea noastră și așteptăm cu nerăbdare să vă servim mai bine în viitor.\",\n     \"alipay\": \"Alipay\",\n     \"wechatPay\": \"WeChat Pay\"\n   },\n   \"discordServer\": {\n     \"title\": \"Server Discord\",\n     \"paragraph1\": \"Vă invităm să vă alăturați comunității noastre Discord! Serverul nostru Discord este un loc minunat pentru a face schimb de idei și sfaturi ChatGPT și pentru a trimite solicitări de funcții pentru Better ChatGPT. Veți avea ocazia să interacționați cu dezvoltatorii din spatele Better ChatGPT. precum și alți entuziaști AI care vă împărtășesc pasiunea.\",\n     \"paragraph2\": \"Pentru a vă alătura serverului nostru, faceți clic pe următorul link: <0>https://discord.gg/g3Qnwy4V6A</0>. Abia așteptăm să ne vedem acolo!\"\n   },\n   \"privacyStatement\": {\n     \"title\": \"Declarație de confidențialitate\",\n     \"paragraph1\": \"Apreciem foarte mult confidențialitatea dumneavoastră și ne angajăm să protejăm confidențialitatea utilizatorilor noștri. Nu colectăm și nu stocăm niciun text pe care îl introduceți sau primiți de la serverul OpenAI sub nicio formă. Codul nostru sursă este disponibil pentru inspecția dvs. pentru verificați această afirmație.\",\n     \"paragraph2\": \"Prioritatezăm securitatea cheii dvs. API și o gestionăm cu cea mai mare atenție. Dacă utilizați propria cheie API, cheia dvs. este stocată exclusiv în browser și nu este niciodată partajată cu nicio entitate terță parte. Este folosită exclusiv pentru scopul propus de a accesa API-ul OpenAI și nu pentru orice altă utilizare neautorizată.\"\n   }\n}\n"
  },
  {
    "path": "public/locales/ro/api.json",
    "content": "{\n   \"securityMessage\": \"Prioritizează securitatea cheii tale API și o gestionăm cu cea mai mare atenție. Cheia este stocată exclusiv în browser-ul tău și nu este niciodată partajată cu vreo entitate terță. Este folosită exclusiv în scopul propus de a accesa OpenAI API și nu pentru orice altă utilizare neautorizată.\",\n   \"apiEndpoint\": {\n     \"inputLabel\": \"Punctul final API\",\n     \"description\": \"Când alegeți un punct final API neoficial, acesta funcționează ca un proxy. Un proxy funcționează ca intermediar între dispozitivul dvs. și serverul de destinație, în acest caz, API-ul OpenAI. Procedând astfel, vă permite pentru a accesa API-ul OpenAI în regiuni în care altfel ar putea fi restricționat.\",\n     \"warn\": \"În plus, dacă furnizați un punct final API personalizat care oferă acces gratuit la API-ul OpenAI, puteți utiliza ChatGPT fără a fi nevoie să furnizați o cheie API, pur și simplu lăsând câmpul cheie API necompletat. Cu toate acestea, este esențial să fiți prudenți atunci când utilizați puncte finale API terțe, deoarece cele nedemne de încredere vă pot înregistra informațiile personale în conversații. Verificați întotdeauna fiabilitatea unui punct final API înainte de a-l folosi pentru a vă proteja confidențialitatea și securitatea.\"\n   },\n   \"apiKey\": {\n     \"howTo\": \"Obțineți cheia personală API <0>aici</0>.\",\n     \"inputLabel\": \"Cheie API\"\n   },\n   \"customEndpoint\": \"Utilizați un punct final API personalizat\",\n   \"advancedConfig\": \"Vedeți configurația avansată API <0>aici</0>\",\n   \"noApiKeyWarning\": \"Nu a fost furnizată nicio cheie API! Vă rugăm să verificați setările API.\"\n}\n"
  },
  {
    "path": "public/locales/ro/drive.json",
    "content": "{\n   \"name\": \"Google Sync\",\n   \"tagline\": \"Sincronizați fără efort chaturile și setările cu Google Drive.\",\n   \"buton\": {\n     \"sync\": \"Sincronizează-ți chaturile\",\n     \"stop\": \"Opriți sincronizarea\",\n     \"create\": \"Creați un fișier nou\",\n     \"confirm\": \"Confirmați selecția\"\n   },\n   \"notice\": \"Notă: va trebui să vă reconectați la fiecare vizită sau la fiecare oră. Pentru a evita suprascrierea datelor din cloud, nu utilizați Better ChatGPT pe mai multe dispozitive în același timp.\",\n   \"privacy\": \"Confidențialitatea ta este importantă pentru noi și, pentru a o asigura, Better ChatGPT are doar acces non-sensibil, ceea ce înseamnă că poate crea, vizualiza și gestiona doar propriile fișiere și foldere.\",\n   \"toast\": {\n     \"sync\": \"Sincronizare reușită!\",\n     \"stop\": \"Sincronizare oprită\"\n   }\n}\n"
  },
  {
    "path": "public/locales/ro/main.json",
    "content": "{\n   \"save\": \"Salvează\",\n   \"generate\": \"Generează\",\n   \"cancel\": \"Anulează\",\n   \"confirm\": \"Confirmați\",\n   \"warning\": \"Avertisment\",\n   \"clearMessageWarning\": \"Vă rugăm să fiți informat că prin trimiterea acestui mesaj, toate mesajele ulterioare vor fi șterse!\",\n   \"clearConversationWarning\": \"Vă rugăm să fiți informat că prin confirmarea acestei acțiuni, toate mesajele vor fi șterse!\",\n   \"clearConversation\": \"Ștergeți istoricul conversațiilor\",\n   \"import\": \"Import\",\n   \"export\": \"Export\",\n   \"author\": \"Făcut de Jing Hua\",\n   \"about\": \"Despre și sponsorizează\",\n   \"api\": \"API\",\n   \"personal\": \"Personal\",\n   \"free\": \"Gratuit\",\n   \"downloadChat\": \"Descărcați chat\",\n   \"user\": \"Utilizator\",\n   \"assistant\": \"Asistent\",\n   \"system\": \"Sistem\",\n   \"newChat\": \"Chat nou\",\n   \"lightMode\": \"Mod de lumină\",\n   \"darkMode\": \"Mod întunecat\",\n   \"setting\": \"Setări\",\n   \"image\": \"Imagine\",\n   \"autoTitle\": \"Generează automat titlul\",\n   \"advancedMode\": \"Mod avansat\",\n   \"inlineLatex\": \"Latex în linie\",\n   \"prompt\": \"Prompt\",\n   \"promptLibrary\": \"Prompt Library\",\n   \"name\": \"Nume\",\n   \"search\": \"Căutare\",\n   \"total\": \"Total\",\n   \"resetCost\": \"Resetați costurile\",\n   \"countTotalTokens\": \"Numără numărul total de jetoane\",\n   \"morePrompts\": \"Puteți găsi mai multe solicitări aici: \",\n   \"clearPrompts\": \"Ștergeți solicitările\",\n   \"postOnShareGPT\": {\n     \"title\": \"Postați pe ShareGPT\",\n     \"warning\": \"Vă rugăm să rețineți că, prin postarea conversației dvs. pe ShareGPT, aceasta va deveni accesibilă public și va fi vizibilă pentru oricine. Odată postată, conversația nu poate fi ascunsă sau ștearsă și poate fi arhivată sau partajată de alții. Vă sfătuim să faceți luați în considerare cu atenție și evitați partajarea informațiilor sensibile sau private pe această platformă.\"\n   },\n   \"newFolder\": \"Folder nou\",\n   \"cloneChat\": \"Clone Chat\",\n   \"cloned\": \"Clonat\",\n   \"enterToSubmit\": \"Intrați pentru a trimite\",\n   \"submitPlaceholder\": \"Tastați un mesaj sau faceți clic pe [/] pentru solicitări...\"\n}\n"
  },
  {
    "path": "public/locales/ro/model.json",
    "content": "{\n   \"configuration\": \"Configurare\",\n   \"model\": \"Model\",\n   \"token\": {\n     \"label\": \"Token maxim\",\n     \"description\": \"Numărul maxim de jetoane de generat la finalizarea chat-ului. Lungimea totală a jetoanelor de intrare și a jetoanelor generate este limitată de lungimea contextului modelului.\"\n   },\n   \"default\": \"Implicit\",\n   \"temperatura\": {\n     \"label\": \"Temperatura\",\n     \"description\": \"Ce temperatură de eșantionare să folosiți, între 0 și 2. Valorile mai mari, cum ar fi 0,8, vor face ieșirea mai aleatorie, în timp ce valori mai mici, cum ar fi 0,2, o vor face mai concentrată și deterministă. În general, vă recomandăm să modificați acest lucru sau p superior, dar nu ambele. (Implicit: 1)\"\n   },\n   \"presencePenalty\": {\n     \"label\": \"Penalizare de prezență\",\n     \"description\": \"Număr între -2,0 și 2,0. Valorile pozitive penalizează noile jetoane în funcție de faptul dacă acestea apar în text până acum, crescând probabilitatea modelului de a vorbi despre noi subiecte. (Implicit: 0)\"\n   },\n   \"topP\": {\n     \"label\": \"Top-p\",\n     \"description\": \"Număr între 0 și 1. O alternativă la eșantionarea cu temperatură, numită eșantionare nucleu, în care modelul ia în considerare rezultatele jetoanelor cu masa de probabilitate maximă p. Deci 0,1 înseamnă doar jetoanele care cuprind masa de top 10% probabilitate sunt luate în considerare. În general, recomandăm modificarea acestei temperaturi sau a temperaturii, dar nu a ambelor. (Implicit: 1)\"\n   },\n   \"frequencyPenalty\": {\n     \"label\": \"Penalizare de frecvență\",\n     \"description\": \"Număr între -2,0 și 2,0. Valorile pozitive penalizează noile jetoane pe baza frecvenței lor existente în text până acum, scăzând probabilitatea modelului de a repeta literal același rând. (Implicit: 0)\"\n   },\n   \"defaultChatConfig\": \"Configurație implicită de chat\",\n   \"defaultSystemMessage\": \"Mesaj implicit de sistem\",\n   \"resetToDefault\": \"Resetați la valoarea implicită\"\n}\n"
  },
  {
    "path": "public/locales/ru/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT - это потрясающее открытое веб-приложение, позволяющее вам бесплатно использовать API ChatGPT от OpenAI!\",\n  \"sourceCode\": \"Ознакомьтесь с <0>исходным кодом</0> на GitHub и поставьте ему ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Ознакомьтесь с <0><i>Инициативой Open ChatGPT</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Поддержка\",\n    \"paragraph1\": \"В Better ChatGPT мы стремимся предоставлять вам удивительные возможности круглосуточно. И, как и в любом проекте, ваша поддержка и мотивация играют важную роль в нашем развитии!\",\n    \"paragraph2\": \"Если вам понравилось наше приложение, удостойте его <0>⭐️</0>. Ваше одобрение очень значимо для нас и мотивирует продолжать развивать лучший пользовательский опыт.\",\n    \"paragraph3\": \"Если вы хотите поддержать команду, рассмотрите возможность спонсирования через один из представленных ниже методов. Любая помощь, даже самая маленькая, способствует улучшению нашего сервиса.\",\n    \"paragraph4\": \"Спасибо, что являетесь частью нашего сообщества, и мы с нетерпением ждем возможности служить вам лучше в будущем.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord сервер\",\n    \"paragraph1\": \"Приглашаем вас присоединиться к нашему сообществу Discord! Наш сервер в Discord - отличное место для обмена идеями и советами по ChatGPT, а также для отправки пожеланий на развитие Better ChatGPT. Общайтесь с разработчиками Better ChatGPT и другими энтузиастами искусственного интеллекта, разделяющими вашу страсть.\",\n    \"paragraph2\": \"Чтобы присоединиться к нашему серверу, перейдите по следующей ссылке: <0>https://discord.gg/g3Qnwy4V6A</0>. Ждем вас с нетерпением!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Заявление о конфиденциальности\",\n    \"paragraph1\": \"Ваша конфиденциальность важна для нас, и мы стремимся гарантировать защиту данных наших пользователей. Мы никаким образом не собираем или храним текст, отправленный или полученный от сервера OpenAI. Наш исходный код открыт для проверки этого утверждения.\",\n    \"paragraph2\": \"Мы очень внимательно относимся к безопасности вашего API-ключа и обрабатываем его с максимальной ответственностью. При использовании собственного ключа API, ваш ключ хранится только в вашем браузере и недоступен третьим сторонам. Он используется исключительно для предоставления доступа к API OpenAI без применения для других несанкционированных действий.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ru/api.json",
    "content": "{\n  \"securityMessage\": \"Мы приоритизируем безопасность вашего API-ключа и обращаемся с ним с максимальной осторожностью. Ваш ключ хранится исключительно в вашем браузере и никогда не передается третьим лицам. Он используется только для предполагаемого доступа к API OpenAI и не применяется для других несанкционированных действий.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API Endpoint\",\n    \"description\": \"Когда вы выбираете неофициальный конечный пункт API, он функционирует в качестве прокси. Прокси работает, выступая в качестве посредника между вашим устройством и сервером назначения, в данном случае - API OpenAI. Таким образом, это позволяет вам получить доступ к API OpenAI в регионах, где он может быть ограничен.\",\n    \"warn\": \"Кроме того, если вы предоставите собственный конечный пункт API, который предоставляет бесплатный доступ к API OpenAI, вы можете использовать ChatGPT без необходимости предоставления API-ключа, оставив поле API-ключа пустым. Однако важно быть осторожными при использовании сторонних конечных точек API, так как ненадежные могут регистрировать вашу личную информацию в беседах. Всегда проверяйте надежность конечной точки API перед использованием, чтобы обеспечить вашу конфиденциальность и безопасность.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Получите ваш личный API-ключ <0>здесь</0>.\",\n    \"inputLabel\": \"API-ключ\"\n  },\n  \"customEndpoint\": \"Использовать пользовательский API Endpoint\",\n  \"advancedConfig\": \"Посмотрите расширенную конфигурацию API <0>здесь</0>\",\n  \"noApiKeyWarning\": \"API-ключ не указан! Пожалуйста, проверьте ваши настройки API.\"\n}\n"
  },
  {
    "path": "public/locales/ru/drive.json",
    "content": "{\n  \"name\": \"Google Синхронизация\",\n  \"tagline\": \"Без усилий синхронизируйте ваши чаты и настройки с Google Диском.\",\n  \"button\": {\n    \"sync\": \"Синхронизировать чаты\",\n    \"stop\": \"Остановить синхронизацию\",\n    \"create\": \"Создать новый файл\",\n    \"confirm\": \"Подтвердить выбор\"\n  },\n  \"notice\": \"Примечание: Вам потребуется повторный вход при каждом посещении или каждый час. Чтобы избежать перезаписи данных облачного хранения, не используйте Better ChatGPT одновременно на нескольких устройствах.\",\n  \"privacy\": \"Ваша конфиденциальность важна для нас, и чтобы обеспечить ее, Better ChatGPT имеет доступ только с низким уровнем чувствительности, что означает, что он может только создавать, просматривать и управлять своими собственными файлами и папками.\",\n  \"toast\": {\n    \"sync\": \"Синхронизация успешно выполнена!\",\n    \"stop\": \"Синхронизация остановлена\"\n  }\n}\n"
  },
  {
    "path": "public/locales/ru/main.json",
    "content": "{\n  \"save\": \"Сохранить\",\n  \"generate\": \"Генерировать\",\n  \"cancel\": \"Отмена\",\n  \"confirm\": \"Подтвердить\",\n  \"warning\": \"Предупреждение\",\n  \"clearMessageWarning\": \"Обратите внимание, что после отправки этого сообщения все последующие сообщения будут удалены!\",\n  \"clearConversationWarning\": \"Обратите внимание, что подтверждение этого действия приведет к удалению всех сообщений!\",\n  \"clearConversation\": \"Очистить историю разговора\",\n  \"import\": \"Импорт\",\n  \"export\": \"Экспорт\",\n  \"author\": \"Автор: Jing Hua\",\n  \"about\": \"О программе и спонсоре\",\n  \"api\": \"API\",\n  \"personal\": \"Личный\",\n  \"free\": \"Бесплатный\",\n  \"downloadChat\": \"Скачать чат\",\n  \"user\": \"Пользователь\",\n  \"assistant\": \"Ассистент\",\n  \"system\": \"Система\",\n  \"newChat\": \"Новый чат\",\n  \"lightMode\": \"Светлый режим\",\n  \"darkMode\": \"Темный режим\",\n  \"setting\": \"Настройки\",\n  \"image\": \"Изображение\",\n  \"autoTitle\": \"Автоматическое создание заголовка\",\n  \"advancedMode\": \"Расширенный режим\",\n  \"inlineLatex\": \"Встроенный Latex\",\n  \"prompt\": \"Подсказка\",\n  \"promptLibrary\": \"Библиотека подсказок\",\n  \"name\": \"Имя\",\n  \"search\": \"Поиск\",\n  \"total\": \"Всего\",\n  \"resetCost\": \"Сбросить стоимость\",\n  \"countTotalTokens\": \"Посчитать общее количество токенов\",\n  \"morePrompts\": \"Больше подсказок вы можете найти здесь: \",\n  \"clearPrompts\": \"Очистить подсказки\",\n  \"postOnShareGPT\": {\n    \"title\": \"Опубликовать на ShareGPT\",\n    \"warning\": \"Обратите внимание, что публикация вашей беседы на ShareGPT сделает ее общедоступной и видимой для всех. После публикации беседу нельзя скрыть или удалить, и другие могут архивировать или делиться ею. Мы советуем вам тщательно подумать и избегать обмена конфиденциальной или частной информацией на этой платформе.\"\n  },\n  \"newFolder\": \"Новая папка\",\n  \"cloneChat\": \"Клонировать чат\",\n  \"cloned\": \"Клонировано\",\n  \"enterToSubmit\": \"Нажмите Enter для отправки\",\n  \"submitPlaceholder\": \"Напишите сообщение или нажмите [/] для подсказок...\"\n}\n"
  },
  {
    "path": "public/locales/ru/model.json",
    "content": "{\n  \"configuration\": \"Конфигурация\",\n  \"model\": \"Модель\",\n  \"token\": {\n    \"label\": \"Макс. токенов\",\n    \"description\": \"Максимальное количество токенов для генерации в чате. Общая длина входных токенов и сгенерированных токенов ограничена контекстной длиной модели.\"\n  },\n  \"default\": \"По умолчанию\",\n  \"temperature\": {\n    \"label\": \"Температура\",\n    \"description\": \"Значение температуры выборки от 0 до 2. Более высокие значения, например 0.8, сделают выходной результат более случайным, в то время как более низкие значения, например 0.2, более фокусированными и детерминированными. Мы обычно рекомендуем изменять это или top-p, но не оба. (По умолчанию: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Штраф за присутствие\",\n    \"description\": \"Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе их появления в тексте до этого момента, увеличивая вероятность перехода модели к новым темам. (По умолчанию: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Число от 0 до 1. Альтернатива выборке с температурой, называемая выборка ядра, при которой модель учитывает результаты токенов с верхним p вероятностных масс. Так, значение 0.1 означает, что рассматриваются только токены, составляющие верхние 10% вероятностной массы. Мы обычно рекомендуем изменять это или температуру, но не оба. (По умолчанию: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Штраф за частоту\",\n    \"description\": \"Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе их имеющейся частоты в тексте на данный момент, уменьшая вероятность повторения той же строки дословно. (По умолчанию: 0)\"\n  },\n  \"defaultChatConfig\": \"Конфигурация чата по умолчанию\",\n  \"defaultSystemMessage\": \"Системное сообщение по умолчанию\",\n  \"resetToDefault\": \"Восстановить значения по умолчанию\"\n}\n"
  },
  {
    "path": "public/locales/sv/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT är en fantastisk öppen källkodswebbapp som låter dig använda OpenAI:s ChatGPT API gratis!\",\n  \"sourceCode\": \"Kolla in <0>källkoden</0> på GitHub och ge den en ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Kolla in <0><i>Open ChatGPT-initiativet</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Stöd\",\n    \"paragraph1\": \"På Better ChatGPT strävar vi efter att erbjuda dig användbara och fantastiska funktioner dygnet runt. Precis som för alla projekt kommer ditt stöd och motivation vara avgörande för att hjälpa oss att fortsätta framåt!\",\n    \"paragraph2\": \"Om du har uppskattat att använda vår app, ber vi dig vänligen att ge detta <0>projekt</0> en ⭐️. Ditt stöd betyder mycket för oss och uppmuntrar oss att arbeta hårdare för att erbjuda den bästa möjliga upplevelsen.\",\n    \"paragraph3\": \"Om du vill stödja teamet kan du överväga att sponsra oss genom en av metoderna nedan. Varje bidrag, oavsett hur litet, hjälper oss att underhålla och förbättra vår tjänst.\",\n    \"paragraph4\": \"Tack för att du är en del av vår gemenskap och vi ser fram emot att betjäna dig bättre i framtiden.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord-server\",\n    \"paragraph1\": \"Vi bjuder in dig att gå med i vår Discord-community! Vår Discord-server är en utmärkt plats att utbyta ChatGPT-idéer och tips samt skicka in funktionsförfrågningar för Better ChatGPT. Du får möjlighet att interagera med utvecklarna bakom Better ChatGPT samt andra AI-entusiaster som delar din passion.\",\n    \"paragraph2\": \"För att gå med i vår server, klicka helt enkelt på följande länk: <0>https://discord.gg/g3Qnwy4V6A</0>. Vi ser fram emot att träffa dig där!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Integritetspolicy\",\n    \"paragraph1\": \"Vi värderar din integritet högt och är engagerade i att skydda våra användares privatliv. Vi samlar inte in eller lagrar någon text du skriver in eller tar emot från OpenAI-servern i någon form. Vår källkod finns tillgänglig för din granskning för att verifiera detta påstående.\",\n    \"paragraph2\": \"Vi prioriterar säkerheten för din API-nyckel och hanterar den med största omsorg. Om du använder din egen API-nyckel lagras din nyckel uteslutande i din webbläsare och delas aldrig med någon tredjepartsaktör. Den används enbart för det avsedda syftet att få tillgång till OpenAI API och inte för någon annan obehörig användning.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/sv/api.json",
    "content": "{\n  \"securityMessage\": \"Vi prioriterar säkerheten för din API-nyckel och hanterar den med största omsorg. Din nyckel lagras uteslutande på din webbläsare och delas aldrig med någon tredje part. Den används enbart för det avsedda ändamålet att få tillgång till OpenAI API och inte för någon annan obehörig användning.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API-slutpunkt\",\n    \"description\": \"När du väljer en inofficiell API-slutpunkt fungerar den som en proxy. En proxy fungerar genom att agera som mellanhand mellan din enhet och destinationsservern, i det här fallet OpenAI API. Genom att göra detta kan du få tillgång till OpenAI API i regioner där det annars kan vara begränsat.\",\n    \"warn\": \"Dessutom, om du anger en anpassad API-slutpunkt som ger gratis åtkomst till OpenAI API, kan du använda ChatGPT utan att behöva ange en API-nyckel genom att helt enkelt lämna API-nyckelfältet tomt. Det är dock viktigt att vara försiktig när du använder tredjeparts API-slutpunkter, eftersom opålitliga sådana kan logga din personliga information i konversationerna. Verifiera alltid tillförlitligheten hos en API-slutpunkt innan du använder den för att skydda din integritet och säkerhet.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Få din personliga API-nyckel <0>här</0>.\",\n    \"inputLabel\": \"API-nyckel\"\n  },\n  \"customEndpoint\": \"Använd anpassad API-slutpunkt\",\n  \"advancedConfig\": \"Visa avancerad API-konfiguration <0>här</0>\",\n  \"noApiKeyWarning\": \"Ingen API-nyckel angiven! Vänligen kontrollera dina API-inställningar.\"\n}\n"
  },
  {
    "path": "public/locales/sv/drive.json",
    "content": "{\n  \"name\": \"Google Sync\",\n  \"tagline\": \"Effortlessly synchronize your chats and settings with Google Drive.\",\n  \"button\": {\n    \"sync\": \"Sync your chats\",\n    \"stop\": \"Stop syncing\",\n    \"create\": \"Create new file\",\n    \"confirm\": \"Confirm selection\"\n  },\n  \"notice\": \"Note: You will need to re-login on every visit or every hour. To avoid your cloud data being overwritten, do not use Better ChatGPT on more than one device at the same time.\",\n  \"privacy\": \"Your privacy is important to us, and to ensure it, Better ChatGPT only has non-sensitive access, meaning it can only create, view, and manage its own files and folders.\",\n  \"toast\": {\n    \"sync\": \"Sync successful!\",\n    \"stop\": \"Syncing stopped\"\n  }\n}\n"
  },
  {
    "path": "public/locales/sv/main.json",
    "content": "{\n  \"save\": \"Spara\",\n  \"generate\": \"Generera\",\n  \"cancel\": \"Avbryt\",\n  \"confirm\": \"Bekräfta\",\n  \"warning\": \"Varning\",\n  \"clearMessageWarning\": \"Observera att genom att skicka detta meddelande kommer alla efterföljande meddelanden att raderas!\",\n  \"clearConversationWarning\": \"Observera att genom att bekräfta denna åtgärd kommer alla meddelanden att raderas!\",\n  \"clearConversation\": \"Rensa konversation\",\n  \"import\": \"Importera\",\n  \"export\": \"Exportera\",\n  \"author\": \"Skapad av Jing Hua\",\n  \"about\": \"Om & Sponsor\",\n  \"api\": \"API\",\n  \"personal\": \"Personlig\",\n  \"free\": \"Gratis\",\n  \"downloadChat\": \"Ladda ner chatt\",\n  \"user\": \"Användare\",\n  \"assistant\": \"Assistent\",\n  \"system\": \"System\",\n  \"newChat\": \"Ny chatt\",\n  \"lightMode\": \"Ljusläge\",\n  \"darkMode\": \"Mörkläge\",\n  \"setting\": \"Inställningar\",\n  \"image\": \"Bild\",\n  \"autoTitle\": \"Auto generera titel\",\n  \"advancedMode\": \"Avancerat läge\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Uppmaning\",\n  \"promptLibrary\": \"Uppmaningsbibliotek\",\n  \"name\": \"Namn\",\n  \"search\": \"Sök\",\n  \"total\": \"Total\",\n  \"resetCost\": \"Återställ kostnader\",\n  \"countTotalTokens\": \"Räkna totala token\",\n  \"morePrompts\": \"Du kan hitta fler uppmaningar här: \",\n  \"clearPrompts\": \"Rensa uppmaningar\",\n  \"postOnShareGPT\": {\n    \"title\": \"Inlägg på ShareGPT\",\n    \"warning\": \"Var medveten om att genom att posta din konversation på ShareGPT kommer den att bli offentligt tillgänglig och synlig för alla. När den väl är postad kan konversationen varken döljas eller raderas och kan arkiveras eller delas av andra. Vi rekommenderar dig att tänka noggrant igenom och undvika att dela känslig eller privat information på denna plattform.\"\n  },\n  \"newFolder\": \"Ny mapp\",\n  \"cloneChat\": \"Klona chatt\",\n  \"cloned\": \"Klonad\",\n  \"enterToSubmit\": \"Tryck på Enter för att skicka\",\n  \"submitPlaceholder\": \"Skriv ett meddelande eller klicka på [/] för uppmaning...\"\n}\n"
  },
  {
    "path": "public/locales/sv/model.json",
    "content": "{\n  \"configuration\": \"Konfiguration\",\n  \"model\": \"Modell\",\n  \"token\": {\n    \"label\": \"Max Token\",\n    \"description\": \"Det maximala antalet token att generera i chatkomplettering. Den totala längden på inmatade token och genererade token är begränsad av modellens kontextlängd.\"\n  },\n  \"default\": \"Standard\",\n  \"temperature\": {\n    \"label\": \"Temperatur\",\n    \"description\": \"Vilken samplings-temperatur som ska användas, mellan 0 och 2. Högre värden som 0,8 gör utdata mer slumpmässiga, medan lägre värden som 0,2 gör dem mer fokuserade och deterministiska. Vi rekommenderar generellt att ändra detta eller topp-p, men inte båda. (Standard: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Närvarostraff\",\n    \"description\": \"Tal mellan -2,0 och 2,0. Positiva värden straffar nya token baserat på om de förekommer i texten hittills, vilket ökar modellens sannolikhet att prata om nya ämnen. (Standard: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Topp-p\",\n    \"description\": \"Tal mellan 0 och 1. Ett alternativ till samplings-temperatur, kallat kärnsampling, där modellen beaktar resultaten av token med topp-p sannolikhetsmassa. Så 0,1 innebär att endast de token som utgör de 10% högsta sannolikhetsmassan beaktas. Vi rekommenderar generellt att ändra detta eller temperatur, men inte båda. (Standard: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Frekvensstraff\",\n    \"description\": \"Tal mellan -2,0 och 2,0. Positiva värden straffar nya token baserat på deras befintliga frekvens i texten hittills, vilket minskar modellens sannolikhet att upprepa samma rad ordagrant. (Standard: 0)\"\n  },\n  \"defaultChatConfig\": \"Standard Chatkonfiguration\",\n  \"defaultSystemMessage\": \"Standard Systemmeddelande\",\n  \"resetToDefault\": \"Återställ till Standard\"\n}\n"
  },
  {
    "path": "public/locales/vi-VN/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT là một ứng dụng web nguồn mở tuyệt vời cho phép bạn sử dụng miễn phí với API ChatGPT của OpenAI!\",\n  \"sourceCode\": \"Hãy xem <0>mã nguồn</0> trên GitHub và cho nó ⭐️!\",\n  \"initiative\": {\n    \"description\": \"Hãy ghé qua <0><i>Open ChatGPT Initiative</i></0>!\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"Hỗ trợ\",\n    \"paragraph1\": \"Tại Better ChatGPT, chúng tôi cố gắng cung cấp cho bạn các tính năng hữu ích và tuyệt vời suốt ngày đêm. Và cũng giống như bất kỳ dự án nào, sự hỗ trợ và động lực của bạn sẽ là công cụ giúp chúng tôi tiếp tục tiến về phía trước!\",\n    \"paragraph2\": \"Nếu bạn thích sử dụng ứng dụng của chúng tôi, chúng tôi vui lòng yêu cầu bạn cho <0>dự án</0> này một ⭐️. Sự chứng thực của bạn có ý nghĩa rất lớn đối với chúng tôi và khuyến khích chúng tôi làm việc chăm chỉ hơn để mang lại trải nghiệm tốt nhất có thể.\",\n    \"paragraph3\": \"Nếu bạn muốn hỗ trợ nhóm, hãy cân nhắc tài trợ cho chúng tôi thông qua một trong các phương pháp dưới đây. Mọi đóng góp dù như thế nào cũng đều giúp chúng tôi duy trì và cải thiện dịch vụ của mình.\",\n    \"paragraph4\": \"Cảm ơn bạn đã trở thành một phần của cộng đồng chúng tôi và chúng tôi mong muốn được phục vụ bạn tốt hơn trong tương lai.\",\n    \"alipay\": \"Alipay\",\n    \"wechatPay\": \"WeChat\"\n  },\n  \"discordServer\": {\n    \"title\": \"Máy chủ Discord\",\n    \"paragraph1\": \"Chúng tôi mời bạn tham gia cộng đồng Discord của chúng tôi! Máy chủ Discord của chúng tôi là nơi tuyệt vời để trao đổi ý tưởng và mẹo ChatGPT cũng như gửi yêu cầu tính năng cho ChatGPT tốt hơn. Bạn sẽ có cơ hội tương tác với các nhà phát triển đằng sau Better ChatGPT cũng như những người đam mê AI khác có chung niềm đam mê với bạn.\",\n    \"paragraph2\": \"Để tham gia máy chủ của chúng tôi, chỉ cần nhấp vào liên kết sau: <0>https://discord.gg/g3Qnwy4V6A</0>. Chúng tôi rất mong được gặp bạn ở đó!\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"Cam kết quyền riêng tư\",\n    \"paragraph1\": \"Chúng tôi đánh giá cao quyền riêng tư của bạn và cam kết bảo vệ quyền riêng tư của người dùng. Chúng tôi không thu thập hoặc lưu trữ bất kỳ văn bản nào bạn nhập hoặc nhận từ máy chủ OpenAI dưới mọi hình thức. Mã nguồn của chúng tôi có sẵn để bạn kiểm tra nhằm xác minh tuyên bố này.\",\n    \"paragraph2\": \"Chúng tôi ưu tiên bảo mật khóa API của bạn và xử lý nó một cách cẩn thận nhất. Nếu bạn sử dụng khóa API của riêng mình, khóa của bạn sẽ được lưu trữ riêng trên trình duyệt của bạn và không bao giờ được chia sẻ với bất kỳ thực thể bên thứ ba nào. Nó chỉ được sử dụng cho mục đích truy cập API OpenAI chứ không phải cho bất kỳ mục đích sử dụng trái phép nào khác.\"\n  }\n}\n"
  },
  {
    "path": "public/locales/vi-VN/api.json",
    "content": "{\n  \"securityMessage\": \"Chúng tôi ưu tiên bảo mật khóa API của bạn và xử lý nó một cách cẩn thận nhất. Khóa của bạn được lưu trữ duy nhất trên trình duyệt của bạn và không bao giờ được chia sẻ với bất kỳ tổ chức bên thứ ba nào. Nó chỉ được sử dụng cho mục đích truy cập API OpenAI chứ không phải cho bất kỳ mục đích sử dụng trái phép nào khác.\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"Điểm cuối API\",\n    \"description\": \"Khi bạn chọn điểm cuối API không chính thức, nó sẽ hoạt động như một proxy. Proxy hoạt động bằng cách đóng vai trò trung gian giữa thiết bị của bạn và máy chủ đích, trong trường hợp này là API OpenAI. Bằng cách đó, nó cho phép bạn truy cập API OpenAI ở những khu vực có thể bị hạn chế.\",\n    \"warn\": \"Ngoài ra, nếu bạn cung cấp điểm cuối API tùy chỉnh cấp quyền truy cập miễn phí vào API OpenAI, bạn có thể sử dụng ChatGPT mà không cần cung cấp khóa API bằng cách chỉ cần để trống trường khóa API. Tuy nhiên, điều quan trọng là phải thận trọng khi sử dụng điểm cuối API của bên thứ ba, vì những điểm cuối API không đáng tin cậy có thể ghi lại thông tin cá nhân của bạn trong các cuộc trò chuyện. Luôn xác minh độ tin cậy của điểm cuối API trước khi sử dụng để bảo vệ quyền riêng tư và bảo mật của bạn.\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"Nhận khóa API cá nhân của bạn <0>tại đây</0>.\",\n    \"inputLabel\": \"Mã API\"\n  },\n  \"customEndpoint\": \"Sử dụng điểm cuối API tùy chỉnh\",\n  \"advancedConfig\": \"Xem cấu hình API nâng cao <0>tại đây</0>\",\n  \"noApiKeyWarning\": \"Không có khóa API nào được cung cấp! Vui lòng kiểm tra cài đặt API của bạn.\"\n}\n"
  },
  {
    "path": "public/locales/vi-VN/drive.json",
    "content": "{\n  \"name\": \"Đồng bộ với Google\",\n  \"tagline\": \"Dễ dàng đồng bộ hóa các cuộc trò chuyện và cài đặt của bạn với Google Drive.\",\n  \"button\": {\n    \"sync\": \"Đồng bộ hóa cuộc trò chuyện của bạn\",\n    \"stop\": \"Dừng đồng bộ hóa\",\n    \"create\": \"Tạo tập tin mới\",\n    \"confirm\": \"Xác nhận lựa chọn của bạn\"\n  },\n  \"notice\": \"Lưu ý: Bạn sẽ cần phải đăng nhập lại sau mỗi lần truy cập hoặc mỗi giờ. Để tránh dữ liệu đám mây của bạn bị ghi đè, không sử dụng Better ChatGPT trên nhiều thiết bị cùng một lúc.\",\n  \"privacy\": \"Quyền riêng tư của bạn rất quan trọng đối với chúng tôi và để đảm bảo điều đó, Better ChatGPT chỉ có quyền truy cập không nhạy cảm, nghĩa là nó chỉ có thể tạo, xem và quản lý các tệp và thư mục của riêng mình.\",\n  \"toast\": {\n    \"sync\": \"Đồng bộ hóa thành công!\",\n    \"stop\": \"Đồng bộ hóa đã dừng\"\n  }\n}\n"
  },
  {
    "path": "public/locales/vi-VN/main.json",
    "content": "{\n  \"save\": \"Lưu\",\n  \"generate\": \"Tạo ra\",\n  \"cancel\": \"Hủy bỏ\",\n  \"confirm\": \"Xác nhận\",\n  \"warning\": \"Cảnh báo\",\n  \"clearMessageWarning\": \"Xin lưu ý rằng bằng cách gửi tin nhắn này, tất cả các tin nhắn tiếp theo sẽ bị xóa!\",\n  \"clearConversationWarning\": \"Xin lưu ý rằng bằng cách xác nhận hành động này, tất cả tin nhắn sẽ bị xóa!\",\n  \"clearConversation\": \"Xóa lịch sử trò chuyện\",\n  \"import\": \"Nhập\",\n  \"export\": \"Xuất\",\n  \"author\": \"Được tạo ra bởi Jing Hua\",\n  \"about\": \"Về chúng tôi và tài trợ\",\n  \"api\": \"API\",\n  \"personal\": \"Cá nhân\",\n  \"free\": \"Miễn Phí\",\n  \"downloadChat\": \"Tải cuộc trò chuyện\",\n  \"user\": \"Người dùng\",\n  \"assistant\": \"Trợ lý\",\n  \"system\": \"Hệ thống\",\n  \"newChat\": \"Trò chuyện mới\",\n  \"lightMode\": \"Chế độ sáng\",\n  \"darkMode\": \"Chế độ tối\",\n  \"setting\": \"Cài đặt\",\n  \"image\": \"Ảnh\",\n  \"autoTitle\": \"Tự động tạo tiêu đề\",\n  \"advancedMode\": \"Chế độ nâng cao\",\n  \"inlineLatex\": \"Inline Latex\",\n  \"prompt\": \"Lời nhắc\",\n  \"promptLibrary\": \"Thư viện lời nhắc\",\n  \"name\": \"Tên\",\n  \"search\": \"Tìm\",\n  \"total\": \"Tổng\",\n  \"resetCost\": \"Đặt lại giá tiền\",\n  \"countTotalTokens\": \"Tính tổng mã thông báo\",\n  \"morePrompts\": \"Bạn có thể tìm thêm nhiều lời nhắc nữa tại đây: \",\n  \"clearPrompts\": \"Xóa lời nhắc\",\n  \"postOnShareGPT\": {\n    \"title\": \"Đăng lên ShareGPT\",\n    \"warning\": \"Xin lưu ý rằng bằng cách đăng cuộc trò chuyện của bạn lên ShareGPT, mọi người sẽ có thể truy cập và xem cuộc trò chuyện đó một cách công khai. Sau khi đăng, cuộc trò chuyện không thể bị ẩn hoặc xóa và có thể được người khác lưu trữ hoặc chia sẻ. Chúng tôi khuyên bạn nên cân nhắc cẩn thận và tránh chia sẻ thông tin nhạy cảm hoặc riêng tư trên nền tảng này.\"\n  },\n  \"newFolder\": \"Thư mục mới\",\n  \"cloneChat\": \"Nhân đôi cuộc trò chuyện\",\n  \"cloned\": \"Đã nhân bản\",\n  \"enterToSubmit\": \"Enter để nộp\",\n  \"submitPlaceholder\": \"Nhập tin nhắn hoặc ấn [/] để tìm các lời nhắc...\"\n}\n"
  },
  {
    "path": "public/locales/vi-VN/model.json",
    "content": "{\n  \"configuration\": \"Cấu hình\",\n  \"model\": \"Mô hình\",\n  \"token\": {\n    \"label\": \"Mã thông báo tối đa\",\n    \"description\": \"Số lượng mã thông báo tối đa cần tạo khi hoàn thành trò chuyện. Tổng chiều dài của mã thông báo đầu vào và mã thông báo được tạo bị giới hạn bởi độ dài ngữ cảnh của mô hình.\"\n  },\n  \"default\": \"Mặc định\",\n  \"temperature\": {\n    \"label\": \"Nhiệt độ\",\n    \"description\": \"Nên sử dụng nhiệt độ lấy mẫu nào, trong khoảng từ 0 đến 2. Các giá trị cao hơn như 0,8 sẽ làm cho đầu ra ngẫu nhiên hơn, trong khi các giá trị thấp hơn như 0,2 sẽ làm cho đầu ra tập trung và mang tính quyết định hơn. Chúng tôi thường khuyên bạn nên thay đổi phần này hoặc Top-p nhưng không nên thay đổi cả hai. (Mặc định: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"Hình phạt hiện diện\",\n    \"description\": \"Số từ -2,0 đến 2,0. Các giá trị dương sẽ phạt các mã thông báo mới dựa trên việc chúng có xuất hiện trong văn bản cho đến nay hay không, làm tăng khả năng mô hình nói về các chủ đề mới. (Mặc định: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"Số từ 0 đến 1. Một cách thay thế cho việc lấy mẫu bằng nhiệt độ, được gọi là lấy mẫu hạt nhân, trong đó mô hình xem xét kết quả của các mã thông báo có khối lượng xác suất p cao nhất. Vì vậy, 0,1 có nghĩa là chỉ các Mã thông báo có khối lượng xác suất 10% cao nhất mới được xem xét. Chúng tôi thường khuyên bạn nên thay đổi điều này hoặc nhiệt độ nhưng không nên thay đổi cả hai. (Mặc định: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"Hình phạt tần suất\",\n    \"description\": \"Số từ -2,0 đến 2,0. Các giá trị dương sẽ xử phạt các mã thông báo mới dựa trên tần suất hiện có của chúng trong văn bản cho đến nay, làm giảm khả năng mô hình lặp lại nguyên văn cùng một dòng. (Mặc định: 0)\"\n  },\n  \"defaultChatConfig\": \"Cấu hình trò chuyện mặc định\",\n  \"defaultSystemMessage\": \"Thông báo hệ thống mặc định\",\n  \"resetToDefault\": \"Đặt lại về mặc định\"\n}\n"
  },
  {
    "path": "public/locales/zh-CN/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT 是一个神奇的开源 Web 应用，允许您免费使用 OpenAI 的 ChatGPT API 进行对话！\",\n  \"sourceCode\": \"在 GitHub 上查看<0>源代码</0>并给它一个⭐️！\",\n  \"initiative\": {\n    \"description\": \"看看《<0>开放 ChatGPT 倡议</0>》吧！\",\n    \"link\": \"https://medium.com/@ayaka_90553/%E5%BC%80%E6%94%BE-chatgpt-%E5%80%A1%E8%AE%AE-eaac01243dae\"\n  },\n  \"support\": {\n    \"title\": \"支持\",\n    \"paragraph1\": \"在 Better ChatGPT，我们致力于为您提供实用和惊人的功能。就像任何项目一样，您的支持和激励将对我们在保持前进方面起到至关重要的作用！\",\n    \"paragraph2\": \"如果您喜欢使用我们的应用程序，我们恳请您给这个<0>项目</0>一个⭐️。您的认可对我们意义重大，鼓励我们更加努力，以提供最佳的体验。\",\n    \"paragraph3\": \"如果您想支持我们的团队，请考虑通过以下方法之一赞助我们。每一份贡献，无论多小，都有助于我们维护和改善我们的服务。\",\n    \"paragraph4\": \"感谢您成为我们社区的一员，我们期待着在未来为您提供更好的服务。\",\n    \"alipay\": \"支付宝\",\n    \"wechatPay\": \"微信\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord 服务器\",\n    \"paragraph1\": \"我们邀请您加入我们的 Discord 社区！我们的 Discord 服务器是一个风水宝地，可以交流 ChatGPT 的想法和技巧，并提交 Better ChatGPT 的功能请求。您将有机会与 Better ChatGPT 的开发人员以及其他分享您热情的人工智能爱好者互动。\",\n    \"paragraph2\": \"要加入我们的服务器，只需单击以下链接：<0>https://discord.gg/g3Qnwy4V6A</0>。我们迫不及待地想见到您！\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"隐私声明\",\n    \"paragraph1\": \"我们非常重视您的隐私，并致力于保护用户的隐私。我们不会以任何形式收集或存储您输入或从 OpenAI 服务器接收的任何文本。我们的源代码可以供您检查，以验证此声明。\",\n    \"paragraph2\": \"我们高度优先考虑您的 API 密钥的安全，并非常小心地处理它。如果您使用自己的 API 密钥，您的密钥将专门存储在您的浏览器中，并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途，而不会用于任何其他未经授权的用途。\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-CN/api.json",
    "content": "{\n  \"securityMessage\": \"我们高度优先考虑您的 API 密钥的安全，并非常小心地处理它。您的密钥将专门存储在您的浏览器中，并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途，而不是用于任何其他未经授权的用途。\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API 端点\",\n    \"description\": \"选用非官方 API 端点时，它会作为代理运作。代理作用是在您的设备和目标服务器（在本例中为 OpenAI API）之间充当中介。通过这样做，您能够在被限制的地区访问 OpenAI API。\",\n    \"warn\": \"此外，如果您提供自定义 API 端点并授予免费访问 OpenAI API 的权限，您可以通过留空 API 密钥字段来使用 ChatGPT，而无需提供API密钥。但是，使用第三方 API 端点时务必谨慎，因为不可信的端点可能会在对话中记录您的个人信息。使用之前请始终验证 API 端点的可靠性以保护您的隐私和安全。\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"在<0>此处</0>获取您的个人 API 密钥。\",\n    \"inputLabel\": \"API 密钥\"\n  },\n  \"customEndpoint\": \"使用自定义 API 端点\",\n  \"advancedConfig\": \"在<0>此处</0>查看进阶 API 设置\",\n  \"noApiKeyWarning\": \"缺少 API key，请检查 API 设置。\"\n}\n"
  },
  {
    "path": "public/locales/zh-CN/drive.json",
    "content": "{\n  \"name\": \"Google 同步\",\n  \"tagline\": \"轻松地将您的聊天和设置与 Google Drive 同步。\",\n  \"button\": {\n    \"sync\": \"同步您的聊天\",\n    \"stop\": \"停止同步\",\n    \"create\": \"创建新文件\",\n    \"confirm\": \"确认选择\"\n  },\n  \"notice\": \"注意：每次访问或每小时您都需要重新登录。为了避免您的云数据被覆盖，不要在多于一个设备上同时使用 Better ChatGPT。\",\n  \"privacy\": \"您的隐私对我们很重要。为了确保安全，Better ChatGPT 只具有非敏感访问权，这意味着它只能创建、查看和管理其自己的文件和文件夹。\",\n  \"toast\": {\n    \"sync\": \"同步成功！\",\n    \"stop\": \"已停止同步\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-CN/main.json",
    "content": "{\n  \"save\": \"保存\",\n  \"generate\": \"生成\",\n  \"cancel\": \"取消\",\n  \"confirm\": \"确认\",\n  \"warning\": \"警告\",\n  \"clearMessageWarning\": \"请注意，通过提交此消息，所有后续消息都将被删除！\",\n  \"clearConversationWarning\": \"请注意，确认此操作将删除所有消息！\",\n  \"clearConversation\": \"清除会话\",\n  \"import\": \"导入\",\n  \"export\": \"导出\",\n  \"author\": \"由 Jing Hua 制作\",\n  \"about\": \"关于和赞助\",\n  \"api\": \"API\",\n  \"personal\": \"个人\",\n  \"free\": \"免费\",\n  \"downloadChat\": \"下载聊天记录\",\n  \"user\": \"用户\",\n  \"assistant\": \"助手\",\n  \"system\": \"系统\",\n  \"newChat\": \"新聊天\",\n  \"lightMode\": \"亮色模式\",\n  \"darkMode\": \"黑暗模式\",\n  \"setting\": \"设置\",\n  \"image\": \"图片\",\n  \"autoTitle\": \"自动生成标题\",\n  \"advancedMode\": \"进阶模式\",\n  \"inlineLatex\": \"行内 Latex\",\n  \"prompt\": \"提示词\",\n  \"promptLibrary\": \"提示词资料库\",\n  \"name\": \"名称\",\n  \"search\": \"搜索\",\n  \"total\": \"合计\",\n  \"resetCost\": \"重置费用\",\n  \"countTotalTokens\": \"计算总 Token 数\",\n  \"morePrompts\": \"更多提示词请点击：\",\n  \"clearPrompts\": \"清除提示词\",\n  \"postOnShareGPT\": {\n    \"title\": \"发布至 ShareGPT\",\n    \"warning\": \"请注意，把您的对话发布到 ShareGPT 后，任何人都可以公开访问和查看。发布后，对话不能被隐藏或删除，且可能被其他人存档或分享。建议您慎重考虑，在这个平台上避免分享敏感或私密信息。\"\n  },\n  \"newFolder\": \"新文件夹\",\n  \"cloneChat\": \"创建聊天副本\",\n  \"cloned\": \"已创建副本\",\n  \"enterToSubmit\": \"按回车键提交\",\n  \"submitPlaceholder\": \"输入消息或点击 [/] 以使用提示词…\"\n}\n"
  },
  {
    "path": "public/locales/zh-CN/model.json",
    "content": "{\n  \"configuration\": \"配置\",\n  \"model\": \"模型\",\n  \"token\": {\n    \"label\": \"最大 Token\",\n    \"description\": \"助手生成一条信息可以包含的最大 token 数。最大 token 数也受到模型的总长度限制，上文的 token 数和生成的 token 数之和不能超过模型的 token 总数（例如 gpt-3.5-turbo 的 token 总数是 4096）。\"\n  },\n  \"default\": \"默认\",\n  \"temperature\": {\n    \"label\": \"采样温度\",\n    \"description\": \"使用何种采样温度，值在 0 到 2 之间。较高的数值如 0.8 会使输出更加随机，而较低的数值如 0.2 会使输出更加集中和确定。我们通常建议修改此参数或 Top-p，但不要同时修改两者。(默认: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"存在惩罚\",\n    \"description\": \"数值在 -2.0 到 2.0 之间。正值会根据新 token 是否已经出现在文本中来惩罚它们，增加模型谈论新话题的可能性。 (默认: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"数值在 0 到 1 之间。采用核采样（nucleus sampling）的一种采样温度的替代方法，模型仅考虑前 Top-p 概率质量的 token。因此，0.1 表示仅考虑前 10% 概率质量的 token。我们通常建议修改此参数或采样温度，但不要同时修改两者。(默认: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"频率惩罚\",\n    \"description\": \"数值在 -2.0 到 2.0 之间。正值会根据新 token 在文本中的现有频率来惩罚它们，降低模型直接重复相同语句的可能性。(默认: 0)\"\n  },\n  \"defaultChatConfig\": \"默认聊天配置\",\n  \"defaultSystemMessage\": \"默认系统消息\",\n  \"resetToDefault\": \"重置为默认值\"\n}\n"
  },
  {
    "path": "public/locales/zh-HK/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT 係一款好犀利嘅開源 Web App，佢使用 OpenAI 嘅 ChatGPT API，令到你可以免費同 ChatGPT 傾偈！\",\n  \"sourceCode\": \"喺 GitHub 上檢視<0>原始碼</0>同埋畀個 ⭐️ 我哋！\",\n  \"initiative\": {\n    \"description\": \"睇下《<0>開放 ChatGPT 倡議</0>》啦！\",\n    \"link\": \"https://medium.com/@ayaka_45434/the-open-chatgpt-initiative-e76b0b62a3ae\"\n  },\n  \"support\": {\n    \"title\": \"支持\",\n    \"paragraph1\": \"Better ChatGPT 致力於提供實用同驚人嘅特性，你嘅支持同激勵將鼓勵我哋繼續前行！\",\n    \"paragraph2\": \"如果你中意呢款 App，我哋請你喺 <0>GitHub</0> 上面畀個 ⭐️。你嘅認可對我哋非同小可，鼓勵我哋更加努力，不斷提供最佳嘅使用體驗。\",\n    \"paragraph3\": \"如果你想支持我哋嘅團隊，你可以透過以下方式贊助我哋。每一分貢獻，無論幾細，都幫助我哋維護同埋改善服務。\",\n    \"paragraph4\": \"多謝你成為我哋社群嘅一員，我哋期待喺未來提供更好嘅服務畀你。\",\n    \"alipay\": \"支付寶\",\n    \"wechatPay\": \"微信\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord 伺服器\",\n    \"paragraph1\": \"歡迎加入我哋嘅 Discord 社羣！呢個 Discord 伺服器係一個風水寶地，可以交流 ChatGPT 嘅靈感同埋技巧，並提交 Better ChatGPT 嘅功能建議。你可以同 Better ChatGPT 嘅開發者同埋其他分享你熱情嘅人工智能愛好者傾偈。\",\n    \"paragraph2\": \"要加入我哋嘅伺服器，只需要撳呢條 link：<0>https://discord.gg/g3Qnwy4V6A</0>，我哋好想見到你！\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"私隱聲明\",\n    \"paragraph1\": \"我哋非常重視你嘅私隱，並致力於保護用家嘅私隱。我哋唔會以任何形式收集或儲存你鍵入或由 OpenAI 伺服器接收嘅任何文字。我哋嘅原始碼可以供你檢查，以驗證呢項聲明。\",\n    \"paragraph2\": \"我哋非常重視你嘅 API key 安全，非常小心噉處理佢。如果你用自己嘅 API key，你嘅 key 將專門儲存喺你嘅瀏覽器入面，並且永遠唔會共享畀任何第三方實體。佢僅用於訪問 OpenAI API 呢項預期用途，唔會用於任何其他未經授權嘅用途。\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-HK/api.json",
    "content": "{\n  \"securityMessage\": \"我哋將你嘅 API key 嘅安全擺喺首位，非常小心噉處理佢。你嘅 key 專門儲存喺你嘅瀏覽器入面，並且唔會共享畀任何第三方實體。佢僅用於存取 OpenAI API 呢項指定用途，唔會用於任何其他未經授權嘅用途。\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API 端點\",\n    \"description\": \"如果你選擇非官方嘅 API 端點，佢充當代理。代理係你嘅設備同目標伺服器（呢度即係 OpenAI API）之間嘅中介，噉你就可以喺用唔到 OpenAI API 嘅地區使用。\",\n    \"warn\": \"另外，如果你提供咗一個允許免費存取 OpenAI API 嘅自訂嘅 API 端點，噉你唔使填寫 API key 就可以用到 ChatGPT。不過，用第三方 API 端點嗰陣必須謹慎，因為唔可信嘅 API 端點可能會記低你喺傾偈入面嘅個人資料。用第三方 API 端點之前要驗證佢是否可信，噉樣可以保護你嘅私隱同安全。\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"喺<0>呢度</0>取得你嘅個人 API key。\",\n    \"inputLabel\": \"API key\"\n  },\n  \"customEndpoint\": \"使用自訂 API 端點\",\n  \"advancedConfig\": \"喺<0>呢度</0>檢視進階 API 設定\",\n  \"noApiKeyWarning\": \"冇填寫 API key，請 check 返個 API 設定。\"\n}\n"
  },
  {
    "path": "public/locales/zh-HK/drive.json",
    "content": "{\n  \"name\": \"Google 同步\",\n  \"tagline\": \"輕鬆將你嘅傾偈同設定同步至 Google Drive。\",\n  \"button\": {\n    \"sync\": \"同步你嘅傾偈\",\n    \"stop\": \"停止同步\",\n    \"create\": \"建立新檔案\",\n    \"confirm\": \"確認選擇\"\n  },\n  \"notice\": \"注意：你需要喺每次存取時或每個鐘重新登入。為避免雲端資料被覆蓋，請勿同一時間喺多個裝置上使用 Better ChatGPT。\",\n  \"privacy\": \"你嘅私隱對我哋非常重要。為確保你嘅私隱，Better ChatGPT 淨係具有非敏感存取權限，即係佢淨係可以建立、檢視同管理佢自己嘅檔案同資料夾。\",\n  \"toast\": {\n    \"sync\": \"同步成功！\",\n    \"stop\": \"同步已停止\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-HK/main.json",
    "content": "{\n  \"save\": \"儲存\",\n  \"generate\": \"生成\",\n  \"cancel\": \"取消\",\n  \"confirm\": \"確認\",\n  \"warning\": \"警告\",\n  \"clearMessageWarning\": \"請注意，送出呢條訊息之後，所有後續訊息都將被刪除！\",\n  \"clearConversationWarning\": \"請注意，呢個操作會刪晒所有訊息！\",\n  \"clearConversation\": \"清空傾偈\",\n  \"import\": \"匯入\",\n  \"export\": \"匯出\",\n  \"author\": \"由 Jing Hua 製作\",\n  \"about\": \"關於同贊助\",\n  \"api\": \"API\",\n  \"personal\": \"個人\",\n  \"free\": \"免費\",\n  \"downloadChat\": \"儲存傾偈記錄\",\n  \"user\": \"用户\",\n  \"assistant\": \"助理\",\n  \"system\": \"系統\",\n  \"newChat\": \"新傾偈\",\n  \"lightMode\": \"亮色模式\",\n  \"darkMode\": \"黑暗模式\",\n  \"setting\": \"設定\",\n  \"image\": \"圖片\",\n  \"autoTitle\": \"自動產生標題\",\n  \"advancedMode\": \"進階模式\",\n  \"inlineLatex\": \"行內 Latex\",\n  \"prompt\": \"Prompt\",\n  \"promptLibrary\": \"Prompt 資料庫\",\n  \"name\": \"名\",\n  \"search\": \"檢索\",\n  \"total\": \"合計\",\n  \"resetCost\": \"重置費用\",\n  \"countTotalTokens\": \"計算總 Token 數\",\n  \"morePrompts\": \"如果你想揾更多 prompt，撳呢度：\",\n  \"clearPrompts\": \"清空 prompts\",\n  \"postOnShareGPT\": {\n    \"title\": \"Po 上 ShareGPT\",\n    \"warning\": \"請注意，你將呢個傾偈 po 上 ShareGPT 之後，佢會係公開嘅，所有人都可以見到你寫嘅嘢。Po 咗之後，呢個傾偈將冇得被隱藏或刪除，亦都可能畀人存檔同分享。我哋建議你仔細諗下，唔好喺嗰度分享敏感或私人資料。\"\n  },\n  \"newFolder\": \"新資料夾\",\n  \"cloneChat\": \"建立傾偈副本\",\n  \"cloned\": \"建立成功\",\n  \"enterToSubmit\": \"撳 Enter 鍵送出\",\n  \"submitPlaceholder\": \"輸入訊息或撳 [/] 以使用提示詞…\"\n}\n"
  },
  {
    "path": "public/locales/zh-HK/model.json",
    "content": "{\n  \"configuration\": \"設定\",\n  \"model\": \"模型\",\n  \"token\": {\n    \"label\": \"最大 Token\",\n    \"description\": \"控制助理嘅一條 msg 最多可以 gen 幾多 token。最大 token 數仲受到模型總長度嘅限制，上文嘅 token 數同生成嘅 token 數加埋一齊唔可以超過模型嘅 token 總數（譬如 gpt-3.5-turbo 嘅 token 總數係 4096）。\"\n  },\n  \"default\": \"預設\",\n  \"temperature\": {\n    \"label\": \"取樣温度\",\n    \"description\": \"係一個 0 到 2 之間嘅數值。較高嘅數值如 0.8 會令到輸出更加隨機，而較低嘅數值如 0.2 會令到輸出更加集中同確定。通常建議修改呢個參數或者 Top-p，但係唔好同時修改兩者。(預設: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"存在懲罰\",\n    \"description\": \"係一個 -2.0 到 2.0 之間嘅數值。正嘅數值表示，如果某個 token 已經出現喺文字當中，輸出嗰陣就會懲罰佢，令到佢被揀中嘅機率降低，即係可以增加模型講新話題嘅機會。 (預設: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"係一個 0 到 1 之間嘅數值。喺核心取樣（根據温度取樣嘅一種替代方法）入面，取樣嗰陣會由機率最高嗰啲 token 當中揀，0.1 表示僅考慮機率求和達到 10% 嘅 token。通常建議修改呢個參數或取樣温度，但唔好同時修改兩者。(預設: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"頻率懲罰\",\n    \"description\": \"係一個 -2.0 到 2.0 之間嘅數值。正嘅數值表示，如果 token 喺之前嘅文字中出現頻率越高，輸出嗰陣就會越大力噉懲罰佢，令到佢被揀中嘅機率降低，即係可以降低模型重複同一句説話嘅機會。(預設: 0)\"\n  },\n  \"defaultChatConfig\": \"預設傾偈設定\",\n  \"defaultSystemMessage\": \"預設系統消息\",\n  \"resetToDefault\": \"重置為預設值\"\n}\n"
  },
  {
    "path": "public/locales/zh-TW/about.json",
    "content": "{\n  \"description\": \"Better ChatGPT 是一個神奇的開源 Web 應用程式，允許您免費使用 OpenAI 的 ChatGPT API 進行對話！\",\n  \"sourceCode\": \"在 GitHub 上檢視<0>原始碼</0>並給它一個⭐️！\",\n  \"initiative\": {\n    \"description\": \"看看《<0>開放 ChatGPT 倡議</0>》吧！\",\n    \"link\": \"https://medium.com/@ayaka_90553/%E5%BC%80%E6%94%BE-chatgpt-%E5%80%A1%E8%AE%AE-eaac01243dae\"\n  },\n  \"support\": {\n    \"title\": \"支持\",\n    \"paragraph1\": \"在 Better ChatGPT，我們致力於為您提供實用和驚人的功能。就像任何專案一樣，您的支持和激勵將對我們在保持前進方面起到至關重要的作用！\",\n    \"paragraph2\": \"如果您喜歡使用我們的應用程式，我們懇請您給這個<0>專案</0>一個⭐️。您的認可對我們意義重大，鼓勵我們更加努力，以提供最佳的體驗。\",\n    \"paragraph3\": \"如果您想支持我們的團隊，請考慮透過以下方法之一讚助我們。每一份貢獻，無論多小，都有助於我們維護和改善我們的服務。\",\n    \"paragraph4\": \"感謝您成為我們社群的一員，我們期待在未來為您提供更好的服務。\",\n    \"alipay\": \"支付寶\",\n    \"wechatPay\": \"微信\"\n  },\n  \"discordServer\": {\n    \"title\": \"Discord 伺服器\",\n    \"paragraph1\": \"我們邀請您加入我們的 Discord 社群！我們的 Discord 伺服器是一個風水寶地，可以交流 ChatGPT 的想法和技巧，並提交 Better ChatGPT 的功能請求。您將有機會與 Better ChatGPT 的開發人員以及其他分享您熱情的人工智慧愛好者互動。\",\n    \"paragraph2\": \"要加入我們的伺服器，只需點選以下連結：<0>https://discord.gg/g3Qnwy4V6A</0>。我們迫不及待地想見到您！\"\n  },\n  \"privacyStatement\": {\n    \"title\": \"隱私宣告\",\n    \"paragraph1\": \"我們非常重視您的隱私，並致力於保護使用者的隱私。我們不會以任何形式收集或儲存您輸入或從 OpenAI 伺服器接收的任何文字。我們的原始碼可以供您檢查，以驗證此宣告。\",\n    \"paragraph2\": \"我們非常重視您的 API 金鑰安全，並非常小心地處理它。如果您使用自己的 API 金鑰，您的金鑰將專門儲存在您的瀏覽器中，並且永遠不會與任何第三方實體共享。它僅用於存取 OpenAI API 的預期用途，而不會用於任何其他未經授權的用途。\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-TW/api.json",
    "content": "{\n  \"securityMessage\": \"我們高度重視您的 API 金鑰安全，並非常小心地處理它。您的金鑰專門儲存在您的瀏覽器中，並且不會與任何第三方實體共享。它僅用於存取 OpenAI API 的指定用途，不會用於任何其他未經授權的用途。\",\n  \"apiEndpoint\": {\n    \"inputLabel\": \"API 端點\",\n    \"description\": \"當您選擇非官方 API 端點時，它將作為代理。代理的作用是在您的裝置與目的伺服器（本例中為 OpenAI API）之間作為中介。透過這個方式，它讓您能夠在其他可能受限的地區存取 OpenAI API。\",\n    \"warn\": \"此外，如果您提供了一個允許免費存取 OpenAI API 的自訂 API 端點，您只需將 API 金鑰欄位留空即可使用 ChatGPT，無需提供 API 金鑰。不過，在使用第三方 API 端點時必須謹慎，因為不可靠的端點可能會在對話中記錄您的個人資訊。在使用 API 端點前，請始終確認其可靠性，以保護您的隱私和安全。\"\n  },\n  \"apiKey\": {\n    \"howTo\": \"在<0>此處</0>取得您的個人 API 金鑰。\",\n    \"inputLabel\": \"API 金鑰\"\n  },\n  \"customEndpoint\": \"使用自訂 API 端點\",\n  \"advancedConfig\": \"在<0>此處</0>檢視進階 API 設定\",\n  \"noApiKeyWarning\": \"未提供 API 金鑰！請檢查 API 設定。\"\n}\n"
  },
  {
    "path": "public/locales/zh-TW/drive.json",
    "content": "{\n  \"name\": \"Google 同步\",\n  \"tagline\": \"輕鬆地將您的聊天和設定與 Google Drive 同步。\",\n  \"button\": {\n    \"sync\": \"同步您的聊天\",\n    \"stop\": \"停止同步\",\n    \"create\": \"建立新檔案\",\n    \"confirm\": \"確認選擇\"\n  },\n  \"notice\": \"注意：您需要在每次存取時或每個小時重新登入。為避免雲端資料被覆蓋，請勿在同一時間在多個裝置上使用 Better ChatGPT。\",\n  \"privacy\": \"您的隱私對我們非常重要，為確保您的隱私，Better ChatGPT 只具有非敏感存取權限，即只能建立、檢視和管理其自己的檔案和資料夾。\",\n  \"toast\": {\n    \"sync\": \"同步成功！\",\n    \"stop\": \"同步已停止\"\n  }\n}\n"
  },
  {
    "path": "public/locales/zh-TW/main.json",
    "content": "{\n  \"save\": \"儲存\",\n  \"generate\": \"生成\",\n  \"cancel\": \"取消\",\n  \"confirm\": \"確認\",\n  \"warning\": \"警告\",\n  \"clearMessageWarning\": \"請注意，透過送出此訊息，所有後續訊息都將被刪除！\",\n  \"clearConversationWarning\": \"請注意，確認此操作將刪除所有訊息！\",\n  \"clearConversation\": \"清除對話\",\n  \"import\": \"匯入\",\n  \"export\": \"匯出\",\n  \"author\": \"由 Jing Hua 製作\",\n  \"about\": \"關於和贊助\",\n  \"api\": \"API\",\n  \"personal\": \"個人\",\n  \"free\": \"免費\",\n  \"downloadChat\": \"下載聊天記錄\",\n  \"user\": \"使用者\",\n  \"assistant\": \"助理\",\n  \"system\": \"系統\",\n  \"newChat\": \"新聊天\",\n  \"lightMode\": \"亮色模式\",\n  \"darkMode\": \"黑暗模式\",\n  \"setting\": \"設定\",\n  \"image\": \"圖片\",\n  \"autoTitle\": \"自動產生標題\",\n  \"advancedMode\": \"進階模式\",\n  \"inlineLatex\": \"行內 Latex\",\n  \"prompt\": \"提示詞\",\n  \"promptLibrary\": \"提示詞資料庫\",\n  \"name\": \"名稱\",\n  \"search\": \"搜尋\",\n  \"total\": \"合計\",\n  \"resetCost\": \"重置費用\",\n  \"countTotalTokens\": \"計算總 Token 數\",\n  \"morePrompts\": \"更多提示詞請點選：\",\n  \"clearPrompts\": \"清除提示詞\",\n  \"postOnShareGPT\": {\n    \"title\": \"發佈至 ShareGPT\",\n    \"warning\": \"請注意，將您的對話發佈至 ShareGPT 後，任何人都可以公開存取和檢視。一旦發佈，對話將無法隱藏或刪除，並且可能被他人存檔或分享。我們建議您慎重考慮，並避免在此平臺上分享敏感或私人資訊。\"\n  },\n  \"newFolder\": \"新資料夾\",\n  \"cloneChat\": \"建立聊天副本\",\n  \"cloned\": \"已建立副本\",\n  \"enterToSubmit\": \"按 Enter 鍵送出\",\n  \"submitPlaceholder\": \"輸入訊息或點選 [/] 以使用提示詞…\"\n}\n"
  },
  {
    "path": "public/locales/zh-TW/model.json",
    "content": "{\n  \"configuration\": \"設定\",\n  \"model\": \"模型\",\n  \"token\": {\n    \"label\": \"最大 Token\",\n    \"description\": \"助理生成一條資訊可以包含的最大 token 數。最大 token 數也受到模型的總長度限制，上文的 token 數和生成的 token 數之和不能超過模型的 token 總數（例如 gpt-3.5-turbo 的 token 總數是 4096）。\"\n  },\n  \"default\": \"預設\",\n  \"temperature\": {\n    \"label\": \"取樣溫度\",\n    \"description\": \"使用何種取樣溫度，值在 0 到 2 之間。較高的數值如 0.8 會使輸出更加隨機，而較低的數值如 0.2 會使輸出更加集中和確定。我們通常建議修改此參數或機率質量，但不要同時修改兩者。(預設: 1)\"\n  },\n  \"presencePenalty\": {\n    \"label\": \"存在懲罰\",\n    \"description\": \"數值在 -2.0 到 2.0 之間。正值會根據新 token 是否已經出現在文字中來懲罰它們，增加模型談論新話題的可能性。 (預設: 0)\"\n  },\n  \"topP\": {\n    \"label\": \"Top-p\",\n    \"description\": \"數值在 0 到 1 之間。採用核心取樣（nucleus sampling）的一種取樣溫度的替代方法，模型僅考慮前 Top-p 機率質量的 token。因此，0.1 表示僅考慮佔前 10% 機率質量的 token。我們通常建議修改此參數或取樣溫度，但不要同時修改兩者。(預設: 1)\"\n  },\n  \"frequencyPenalty\": {\n    \"label\": \"頻率懲罰\",\n    \"description\": \"數值在 -2.0 到 2.0 之間。正值會根據新 token 在文字中的現有頻率來懲罰它們，降低模型直接重複相同語句的可能性。(預設: 0)\"\n  },\n  \"defaultChatConfig\": \"預設聊天設定\",\n  \"defaultSystemMessage\": \"預設系統訊息\",\n  \"resetToDefault\": \"重設為預設值\"\n}\n"
  },
  {
    "path": "src/App.tsx",
    "content": "import React, { useEffect } from 'react';\nimport useStore from '@store/store';\nimport i18n from './i18n';\n\nimport Chat from '@components/Chat';\nimport Menu from '@components/Menu';\n\nimport useInitialiseNewChat from '@hooks/useInitialiseNewChat';\nimport { ChatInterface } from '@type/chat';\nimport { Theme } from '@type/theme';\nimport ApiPopup from '@components/ApiPopup';\nimport Toast from '@components/Toast';\n\nfunction App() {\n  const initialiseNewChat = useInitialiseNewChat();\n  const setChats = useStore((state) => state.setChats);\n  const setTheme = useStore((state) => state.setTheme);\n  const setApiKey = useStore((state) => state.setApiKey);\n  const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n\n  useEffect(() => {\n    document.documentElement.lang = i18n.language;\n    i18n.on('languageChanged', (lng) => {\n      document.documentElement.lang = lng;\n    });\n  }, []);\n\n  useEffect(() => {\n    // legacy local storage\n    const oldChats = localStorage.getItem('chats');\n    const apiKey = localStorage.getItem('apiKey');\n    const theme = localStorage.getItem('theme');\n\n    if (apiKey) {\n      // legacy local storage\n      setApiKey(apiKey);\n      localStorage.removeItem('apiKey');\n    }\n\n    if (theme) {\n      // legacy local storage\n      setTheme(theme as Theme);\n      localStorage.removeItem('theme');\n    }\n\n    if (oldChats) {\n      // legacy local storage\n      try {\n        const chats: ChatInterface[] = JSON.parse(oldChats);\n        if (chats.length > 0) {\n          setChats(chats);\n          setCurrentChatIndex(0);\n        } else {\n          initialiseNewChat();\n        }\n      } catch (e: unknown) {\n        console.log(e);\n        initialiseNewChat();\n      }\n      localStorage.removeItem('chats');\n    } else {\n      // existing local storage\n      const chats = useStore.getState().chats;\n      const currentChatIndex = useStore.getState().currentChatIndex;\n      if (!chats || chats.length === 0) {\n        initialiseNewChat();\n      }\n      if (\n        chats &&\n        !(currentChatIndex >= 0 && currentChatIndex < chats.length)\n      ) {\n        setCurrentChatIndex(0);\n      }\n    }\n  }, []);\n\n  return (\n    <div className='overflow-hidden w-full h-full relative'>\n      <Menu />\n      <Chat />\n      <ApiPopup />\n      <Toast />\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "src/Roboto.css",
    "content": "/* Roboto Thin */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Thin'),\n    url('./fonts/Roboto-Thin.woff2') format('woff2'),\n    url('./fonts/Roboto-Thin.ttf') format('truetype');\n  font-weight: 100;\n  font-style: normal;\n}\n\n/* Roboto Thin Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Thin Italic'),\n    url('./fonts/Roboto-ThinItalic.woff2') format('woff2'),\n    url('./fonts/Roboto-ThinItalic.ttf') format('truetype');\n  font-weight: 100;\n  font-style: italic;\n}\n\n/* Roboto Light */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Light'),\n    url('./fonts/Roboto-Light.woff2') format('woff2'),\n    url('./fonts/Roboto-Light.ttf') format('truetype');\n  font-weight: 300;\n  font-style: normal;\n}\n\n/* Roboto Light Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Light Italic'),\n    url('./fonts/Roboto-LightItalic.woff2') format('woff2'),\n    url('./fonts/Roboto-LightItalic.ttf') format('truetype');\n  font-weight: 300;\n  font-style: italic;\n}\n\n/* Roboto Regular */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto'),\n    url('./fonts/Roboto-Regular.woff2') format('woff2'),\n    url('./fonts/Roboto-Regular.ttf') format('truetype');\n  font-weight: 400;\n  font-style: normal;\n}\n\n/* Roboto Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Italic'),\n    url('./fonts/Roboto-Italic.woff2') format('woff2'),\n    url('./fonts/Roboto-Italic.ttf') format('truetype');\n  font-weight: 400;\n  font-style: italic;\n}\n\n/* Roboto Medium */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Medium'),\n    url('./fonts/Roboto-Medium.woff2') format('woff2'),\n    url('./fonts/Roboto-Medium.ttf') format('truetype');\n  font-weight: 500;\n  font-style: normal;\n}\n\n/* Roboto Medium Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Medium Italic'),\n    url('./fonts/Roboto-MediumItalic.woff2') format('woff2'),\n    url('./fonts/Roboto-MediumItalic.ttf') format('truetype');\n  font-weight: 500;\n  font-style: italic;\n}\n\n/* Roboto Bold */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Bold'),\n    url('./fonts/Roboto-Bold.woff2') format('woff2'),\n    url('./fonts/Roboto-Bold.ttf') format('truetype');\n  font-weight: 700;\n  font-style: normal;\n}\n\n/* Roboto Bold Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Bold Italic'),\n    url('./fonts/Roboto-BoldItalic.woff2') format('woff2'),\n    url('./fonts/Roboto-BoldItalic.ttf') format('truetype');\n  font-weight: 700;\n  font-style: italic;\n}\n\n/* Roboto Black */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Black'),\n    url('./fonts/Roboto-Black.woff2') format('woff2'),\n    url('./fonts/Roboto-Black.ttf') format('truetype');\n  font-weight: 900;\n  font-style: normal;\n}\n\n/* Roboto Black Italic */\n@font-face {\n  font-family: 'Roboto';\n  src: local('Roboto Black Italic'),\n    url('./fonts/Roboto-BlackItalic.woff2') format('woff2'),\n    url('./fonts/Roboto-BlackItalic.ttf') format('truetype');\n  font-weight: 900;\n  font-style: italic;\n}\n"
  },
  {
    "path": "src/api/api.ts",
    "content": "import { ShareGPTSubmitBodyInterface } from '@type/api';\nimport { ConfigInterface, MessageInterface, ModelOptions } from '@type/chat';\nimport { isAzureEndpoint } from '@utils/api';\n\nexport const getChatCompletion = async (\n  endpoint: string,\n  messages: MessageInterface[],\n  config: ConfigInterface,\n  apiKey?: string,\n  customHeaders?: Record<string, string>\n) => {\n  const headers: HeadersInit = {\n    'Content-Type': 'application/json',\n    ...customHeaders,\n  };\n  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;\n\n  if (isAzureEndpoint(endpoint) && apiKey) {\n    headers['api-key'] = apiKey;\n\n    const modelmapping: Partial<Record<ModelOptions, string>> = {\n      'gpt-3.5-turbo': 'gpt-35-turbo',\n      'gpt-3.5-turbo-16k': 'gpt-35-turbo-16k',\n      'gpt-3.5-turbo-1106': 'gpt-35-turbo-1106',\n      'gpt-3.5-turbo-0125': 'gpt-35-turbo-0125',\n    };\n\n    const model = modelmapping[config.model] || config.model;\n\n    // set api version to 2023-07-01-preview for gpt-4 and gpt-4-32k, otherwise use 2023-03-15-preview\n    const apiVersion =\n      model === 'gpt-4' || model === 'gpt-4-32k'\n        ? '2023-07-01-preview'\n        : '2023-03-15-preview';\n\n    const path = `openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;\n\n    if (!endpoint.endsWith(path)) {\n      if (!endpoint.endsWith('/')) {\n        endpoint += '/';\n      }\n      endpoint += path;\n    }\n  }\n\n  const response = await fetch(endpoint, {\n    method: 'POST',\n    headers,\n    body: JSON.stringify({\n      messages,\n      ...config,\n      max_tokens: undefined,\n    }),\n  });\n  if (!response.ok) throw new Error(await response.text());\n\n  const data = await response.json();\n  return data;\n};\n\nexport const getChatCompletionStream = async (\n  endpoint: string,\n  messages: MessageInterface[],\n  config: ConfigInterface,\n  apiKey?: string,\n  customHeaders?: Record<string, string>\n) => {\n  const headers: HeadersInit = {\n    'Content-Type': 'application/json',\n    ...customHeaders,\n  };\n  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;\n\n  if (isAzureEndpoint(endpoint) && apiKey) {\n    headers['api-key'] = apiKey;\n\n    const modelmapping: Partial<Record<ModelOptions, string>> = {\n      'gpt-3.5-turbo': 'gpt-35-turbo',\n      'gpt-3.5-turbo-16k': 'gpt-35-turbo-16k',\n    };\n\n    const model = modelmapping[config.model] || config.model;\n\n    // set api version to 2023-07-01-preview for gpt-4 and gpt-4-32k, otherwise use 2023-03-15-preview\n    const apiVersion =\n      model === 'gpt-4' || model === 'gpt-4-32k'\n        ? '2023-07-01-preview'\n        : '2023-03-15-preview';\n    const path = `openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;\n\n    if (!endpoint.endsWith(path)) {\n      if (!endpoint.endsWith('/')) {\n        endpoint += '/';\n      }\n      endpoint += path;\n    }\n  }\n\n  const response = await fetch(endpoint, {\n    method: 'POST',\n    headers,\n    body: JSON.stringify({\n      messages,\n      ...config,\n      max_tokens: undefined,\n      stream: true,\n    }),\n  });\n  if (response.status === 404 || response.status === 405) {\n    const text = await response.text();\n\n    if (text.includes('model_not_found')) {\n      throw new Error(\n        text +\n          '\\nMessage from Better ChatGPT:\\nPlease ensure that you have access to the GPT-4 API!'\n      );\n    } else {\n      throw new Error(\n        'Message from Better ChatGPT:\\nInvalid API endpoint! We recommend you to check your free API endpoint.'\n      );\n    }\n  }\n\n  if (response.status === 429 || !response.ok) {\n    const text = await response.text();\n    let error = text;\n    if (text.includes('insufficient_quota')) {\n      error +=\n        '\\nMessage from Better ChatGPT:\\nWe recommend changing your API endpoint or API key';\n    } else if (response.status === 429) {\n      error += '\\nRate limited!';\n    }\n    throw new Error(error);\n  }\n\n  const stream = response.body;\n  return stream;\n};\n\nexport const submitShareGPT = async (body: ShareGPTSubmitBodyInterface) => {\n  const request = await fetch('https://sharegpt.com/api/conversations', {\n    body: JSON.stringify(body),\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    method: 'POST',\n  });\n\n  const response = await request.json();\n  const { id } = response;\n  const url = `https://shareg.pt/${id}`;\n  window.open(url, '_blank');\n};\n"
  },
  {
    "path": "src/api/google-api.ts",
    "content": "import { debounce } from 'lodash';\nimport { StorageValue } from 'zustand/middleware';\nimport useStore from '@store/store';\nimport useCloudAuthStore from '@store/cloud-auth-store';\nimport {\n  GoogleTokenInfo,\n  GoogleFileResource,\n  GoogleFileList,\n} from '@type/google-api';\nimport PersistStorageState from '@type/persist';\n\nimport { createMultipartRelatedBody } from './helper';\n\nexport const createDriveFile = async (\n  file: File,\n  accessToken: string\n): Promise<GoogleFileResource> => {\n  const boundary = 'better_chatgpt';\n  const metadata = {\n    name: file.name,\n    mimeType: file.type,\n  };\n  const requestBody = createMultipartRelatedBody(metadata, file, boundary);\n\n  const response = await fetch(\n    'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',\n    {\n      method: 'POST',\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n        'Content-Type': `multipart/related; boundary=${boundary}`,\n        'Content-Length': requestBody.size.toString(),\n      },\n      body: requestBody,\n    }\n  );\n\n  if (response.ok) {\n    const result: GoogleFileResource = await response.json();\n    return result;\n  } else {\n    throw new Error(\n      `Error uploading file: ${response.status} ${response.statusText}`\n    );\n  }\n};\n\nexport const getDriveFile = async <S>(\n  fileId: string,\n  accessToken: string\n): Promise<StorageValue<S>> => {\n  const response = await fetch(\n    `https://content.googleapis.com/drive/v3/files/${fileId}?alt=media`,\n    {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${accessToken}`,\n      },\n    }\n  );\n  const result: StorageValue<S> = await response.json();\n  return result;\n};\n\nexport const getDriveFileTyped = async (\n  fileId: string,\n  accessToken: string\n): Promise<StorageValue<PersistStorageState>> => {\n  return await getDriveFile(fileId, accessToken);\n};\n\nexport const listDriveFiles = async (\n  accessToken: string\n): Promise<GoogleFileList> => {\n  const response = await fetch(\n    'https://www.googleapis.com/drive/v3/files?orderBy=createdTime desc',\n    {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${accessToken}`,\n      },\n    }\n  );\n\n  if (!response.ok) {\n    throw new Error(\n      `Error listing google drive files: ${response.status} ${response.statusText}`\n    );\n  }\n\n  const result: GoogleFileList = await response.json();\n  return result;\n};\n\nexport const updateDriveFile = async (\n  file: File,\n  fileId: string,\n  accessToken: string\n): Promise<GoogleFileResource> => {\n  const response = await fetch(\n    `https://www.googleapis.com/upload/drive/v3/files/${fileId}`,\n    {\n      method: 'PATCH',\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n      },\n      body: file,\n    }\n  );\n  if (response.ok) {\n    const result: GoogleFileResource = await response.json();\n    return result;\n  } else {\n    throw new Error(\n      `Error uploading file: ${response.status} ${response.statusText}`\n    );\n  }\n};\n\nexport const updateDriveFileName = async (\n  fileName: string,\n  fileId: string,\n  accessToken: string\n) => {\n  const response = await fetch(\n    `https://www.googleapis.com/drive/v3/files/${fileId}`,\n    {\n      method: 'PATCH',\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n      },\n      body: JSON.stringify({ name: fileName }),\n    }\n  );\n  if (response.ok) {\n    const result: GoogleFileResource = await response.json();\n    return result;\n  } else {\n    throw new Error(\n      `Error updating file name: ${response.status} ${response.statusText}`\n    );\n  }\n};\n\nexport const deleteDriveFile = async (fileId: string, accessToken: string) => {\n  const response = await fetch(\n    `https://www.googleapis.com/drive/v3/files/${fileId}`,\n    {\n      method: 'DELETE',\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${accessToken}`,\n      },\n    }\n  );\n\n  if (response.ok) {\n    return true;\n  } else {\n    throw new Error(\n      `Error deleting file name: ${response.status} ${response.statusText}`\n    );\n  }\n};\n\nexport const validateGoogleOath2AccessToken = async (accessToken: string) => {\n  const response = await fetch(\n    `https://oauth2.googleapis.com/tokeninfo?access_token=${accessToken}`\n  );\n  if (!response.ok) return false;\n  const result: GoogleTokenInfo = await response.json();\n  return result;\n};\n\nexport const updateDriveFileDebounced = debounce(\n  async (file: File, fileId: string, accessToken: string) => {\n    try {\n      const result = await updateDriveFile(file, fileId, accessToken);\n      useCloudAuthStore.getState().setSyncStatus('synced');\n      return result;\n    } catch (e: unknown) {\n      useStore.getState().setToastMessage((e as Error).message);\n      useStore.getState().setToastShow(true);\n      useStore.getState().setToastStatus('error');\n      useCloudAuthStore.getState().setSyncStatus('unauthenticated');\n    }\n  },\n  5000\n);\n"
  },
  {
    "path": "src/api/helper.ts",
    "content": "import { EventSourceData } from '@type/api';\n\nexport const parseEventSource = (\n  data: string\n): '[DONE]' | EventSourceData[] => {\n  const result = data\n    .split('\\n\\n')\n    .filter(Boolean)\n    .map((chunk) => {\n      const jsonString = chunk\n        .split('\\n')\n        .map((line) => line.replace(/^data: /, ''))\n        .join('');\n      if (jsonString === '[DONE]') return jsonString;\n      try {\n        const json = JSON.parse(jsonString);\n        return json;\n      } catch {\n        return jsonString;\n      }\n    });\n  return result;\n};\n\nexport const createMultipartRelatedBody = (\n  metadata: object,\n  file: File,\n  boundary: string\n): Blob => {\n  const encoder = new TextEncoder();\n\n  const metadataPart = encoder.encode(\n    `--${boundary}\\r\\nContent-Type: application/json; charset=UTF-8\\r\\n\\r\\n${JSON.stringify(\n      metadata\n    )}\\r\\n`\n  );\n  const filePart = encoder.encode(\n    `--${boundary}\\r\\nContent-Type: ${file.type}\\r\\n\\r\\n`\n  );\n  const endBoundary = encoder.encode(`\\r\\n--${boundary}--`);\n\n  return new Blob([metadataPart, filePart, file, endBoundary], {\n    type: 'multipart/related; boundary=' + boundary,\n  });\n};\n"
  },
  {
    "path": "src/assets/icons/AboutIcon.tsx",
    "content": "import React from 'react';\n\nconst AboutIcon = () => {\n  return (\n    <svg\n      xmlns='http://www.w3.org/2000/svg'\n      viewBox='0 0 512 512'\n      className='h-4 w-4'\n      fill='white'\n    >\n      <path d='M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z' />\n    </svg>\n  );\n};\n\nexport default AboutIcon;\n"
  },
  {
    "path": "src/assets/icons/ArrowBottom.tsx",
    "content": "import React from 'react';\n\nconst ArrowBottom = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      version='1.1'\n      xmlns='http://www.w3.org/2000/svg'\n      className='h-4 w-4'\n      width='1em'\n      height='1em'\n      {...props}\n    >\n      <path d='M140.16 332.16a40.96 40.96 0 0 0 0 58.24l343.04 338.56a40.96 40.96 0 0 0 58.24 0l342.4-338.56a40.96 40.96 0 1 0-58.24-58.24L512 640 197.76 332.16a40.96 40.96 0 0 0-57.6 0z'></path>\n    </svg>\n  );\n};\n\nexport default ArrowBottom;\n"
  },
  {
    "path": "src/assets/icons/CalculatorIcon.tsx",
    "content": "import React from 'react';\n\nconst CalculatorIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      fill='currentColor'\n      viewBox='0 0 16 16'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M2 2a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V2zm2 .5v2a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-2a.5.5 0 00-.5-.5h-7a.5.5 0 00-.5.5zm0 4v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zM4.5 9a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1zM4 12.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zM7.5 6a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1zM7 9.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zm.5 2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1zM10 6.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zm.5 2.5a.5.5 0 00-.5.5v4a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-4a.5.5 0 00-.5-.5h-1z' />\n    </svg>\n  );\n};\n\nexport default CalculatorIcon;\n"
  },
  {
    "path": "src/assets/icons/ChatIcon.tsx",
    "content": "import React from 'react';\n\nconst ChatIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'></path>\n    </svg>\n  );\n};\n\nexport default ChatIcon;\n"
  },
  {
    "path": "src/assets/icons/CloneIcon.tsx",
    "content": "import React from 'react';\n\nconst CloneIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 512 512'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M64 464h224c8.8 0 16-7.2 16-16v-64h48v64c0 35.3-28.7 64-64 64H64c-35.35 0-64-28.7-64-64V224c0-35.3 28.65-64 64-64h64v48H64c-8.84 0-16 7.2-16 16v224c0 8.8 7.16 16 16 16zm96-400c0-35.35 28.7-64 64-64h224c35.3 0 64 28.65 64 64v224c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64V64zm64 240h224c8.8 0 16-7.2 16-16V64c0-8.84-7.2-16-16-16H224c-8.8 0-16 7.16-16 16v224c0 8.8 7.2 16 16 16z' />\n    </svg>\n  );\n};\n\nexport default CloneIcon;\n"
  },
  {
    "path": "src/assets/icons/ColorPaletteIcon.tsx",
    "content": "import React from 'react';\n\nconst ColorPaletteIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 24 24'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M12 22A10 10 0 012 12 10 10 0 0112 2c5.5 0 10 4 10 9a6 6 0 01-6 6h-1.8c-.3 0-.5.2-.5.5 0 .1.1.2.1.3.4.5.6 1.1.6 1.7.1 1.4-1 2.5-2.4 2.5m0-18a8 8 0 00-8 8 8 8 0 008 8c.3 0 .5-.2.5-.5 0-.2-.1-.3-.1-.4-.4-.5-.6-1-.6-1.6 0-1.4 1.1-2.5 2.5-2.5H16a4 4 0 004-4c0-3.9-3.6-7-8-7m-5.5 6c.8 0 1.5.7 1.5 1.5S7.3 13 6.5 13 5 12.3 5 11.5 5.7 10 6.5 10m3-4c.8 0 1.5.7 1.5 1.5S10.3 9 9.5 9 8 8.3 8 7.5 8.7 6 9.5 6m5 0c.8 0 1.5.7 1.5 1.5S15.3 9 14.5 9 13 8.3 13 7.5 13.7 6 14.5 6m3 4c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5-1.5-.7-1.5-1.5.7-1.5 1.5-1.5z' />\n    </svg>\n  );\n};\n\nexport default ColorPaletteIcon;\n"
  },
  {
    "path": "src/assets/icons/CopyIcon.tsx",
    "content": "import React from 'react';\n\nconst CopyIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'></path>\n      <rect x='8' y='2' width='8' height='4' rx='1' ry='1'></rect>\n    </svg>\n  );\n};\n\nexport default CopyIcon;\n"
  },
  {
    "path": "src/assets/icons/CrossIcon.tsx",
    "content": "import React from 'react';\n\nconst CrossIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <line x1='18' y1='6' x2='6' y2='18'></line>\n      <line x1='6' y1='6' x2='18' y2='18'></line>\n    </svg>\n  );\n};\n\nexport default CrossIcon;\n"
  },
  {
    "path": "src/assets/icons/CrossIcon2.tsx",
    "content": "import React from 'react';\n\nconst CrossIcon2 = () => {\n  return (\n    <svg\n      aria-hidden='true'\n      className='w-5 h-5'\n      fill='currentColor'\n      viewBox='0 0 20 20'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        fillRule='evenodd'\n        d='M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z'\n        clipRule='evenodd'\n      ></path>\n    </svg>\n  );\n};\n\nexport default CrossIcon2;\n"
  },
  {
    "path": "src/assets/icons/DeleteIcon.tsx",
    "content": "import React from 'react';\n\nconst DeleteIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <polyline points='3 6 5 6 21 6'></polyline>\n      <path d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'></path>\n      <line x1='10' y1='11' x2='10' y2='17'></line>\n      <line x1='14' y1='11' x2='14' y2='17'></line>\n    </svg>\n  );\n};\n\nexport default DeleteIcon;\n"
  },
  {
    "path": "src/assets/icons/DownArrow.tsx",
    "content": "import React from 'react';\n\nconst DownArrow = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4 m-1'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n      {...props}\n    >\n      <line x1='12' y1='5' x2='12' y2='19'></line>\n      <polyline points='19 12 12 19 5 12'></polyline>\n    </svg>\n  );\n};\n\nexport default DownArrow;\n"
  },
  {
    "path": "src/assets/icons/DownChevronArrow.tsx",
    "content": "import React from 'react';\n\nconst DownChevronArrow = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      className={'w-4 h-4' + ' ' + className}\n      aria-hidden='true'\n      fill='none'\n      stroke='currentColor'\n      viewBox='0 0 24 24'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        strokeLinecap='round'\n        strokeLinejoin='round'\n        strokeWidth='2'\n        d='M19 9l-7 7-7-7'\n      ></path>\n    </svg>\n  );\n};\n\nexport default DownChevronArrow;\n"
  },
  {
    "path": "src/assets/icons/EditIcon.tsx",
    "content": "import React from 'react';\n\nconst EditIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M12 20h9'></path>\n      <path d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z'></path>\n    </svg>\n  );\n};\n\nexport default EditIcon;\n"
  },
  {
    "path": "src/assets/icons/EditIcon2.tsx",
    "content": "import React from 'react';\n\nconst EditIcon2 = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'></path>\n      <path d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z'></path>\n    </svg>\n  );\n};\n\nexport default EditIcon2;\n"
  },
  {
    "path": "src/assets/icons/ExportIcon.tsx",
    "content": "import React from 'react';\n\nconst ExportIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      fill='none'\n      stroke='currentColor'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      strokeWidth={2}\n      viewBox='0 0 24 24'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path stroke='none' d='M0 0h24v24H0z' />\n      <path d='M14 3v4a1 1 0 001 1h4' />\n      <path d='M11.5 21H7a2 2 0 01-2-2V5a2 2 0 012-2h7l5 5v5m-5 6h7m-3-3l3 3-3 3' />\n    </svg>\n  );\n};\n\nexport default ExportIcon;\n"
  },
  {
    "path": "src/assets/icons/FileTextIcon.tsx",
    "content": "import React from 'react';\n\nconst FileTextIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z' />\n    </svg>\n  );\n};\n\nexport default FileTextIcon;\n"
  },
  {
    "path": "src/assets/icons/FolderIcon.tsx",
    "content": "import React from 'react';\n\nconst FolderIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M880 298.4H521L403.7 186.2a8.15 8.15 0 00-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z' />\n    </svg>\n  );\n};\n\nexport default FolderIcon;\n"
  },
  {
    "path": "src/assets/icons/GoogleIcon.tsx",
    "content": "import React from 'react';\n\nconst GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      fill='currentColor'\n      viewBox='0 0 16 16'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M15.545 6.558a9.42 9.42 0 01.139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 118 0a7.689 7.689 0 015.352 2.082l-2.284 2.284A4.347 4.347 0 008 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.792 4.792 0 000 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.702 3.702 0 001.599-2.431H8v-3.08h7.545z' />\n    </svg>\n  );\n};\n\nexport default GoogleIcon;\n"
  },
  {
    "path": "src/assets/icons/HeartIcon.tsx",
    "content": "import React from 'react';\n\nconst HeartIcon = () => {\n  return (\n    <svg\n      xmlns='http://www.w3.org/2000/svg'\n      className='h-4 w-4 p-[1px]'\n      fill='white'\n      viewBox='0 0 512 512'\n    >\n      <path d='M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z' />\n    </svg>\n  );\n};\n\nexport default HeartIcon;\n"
  },
  {
    "path": "src/assets/icons/ImageIcon.tsx",
    "content": "import React from 'react';\n\nconst ImageIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M553.1 509.1l-77.8 99.2-41.1-52.4a8 8 0 00-12.6 0l-99.8 127.2a7.98 7.98 0 006.3 12.9H696c6.7 0 10.4-7.7 6.3-12.9l-136.5-174a8.1 8.1 0 00-12.7 0zM360 442a40 40 0 1080 0 40 40 0 10-80 0zm494.6-153.4L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z' />\n    </svg>\n  );\n};\n\nexport default ImageIcon;\n"
  },
  {
    "path": "src/assets/icons/JsonIcon.tsx",
    "content": "import React from 'react';\n\nconst JsonIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      fill='currentColor'\n      viewBox='0 0 16 16'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path\n        fillRule='evenodd'\n        d='M14 4.5V11h-1V4.5h-2A1.5 1.5 0 019.5 3V1H4a1 1 0 00-1 1v9H2V2a2 2 0 012-2h5.5L14 4.5zM4.151 15.29a1.176 1.176 0 01-.111-.449h.764a.578.578 0 00.255.384c.07.049.154.087.25.114.095.028.201.041.319.041.164 0 .301-.023.413-.07a.559.559 0 00.255-.193.507.507 0 00.084-.29.387.387 0 00-.152-.326c-.101-.08-.256-.144-.463-.193l-.618-.143a1.72 1.72 0 01-.539-.214 1.001 1.001 0 01-.352-.367 1.068 1.068 0 01-.123-.524c0-.244.064-.457.19-.639.128-.181.304-.322.528-.422.225-.1.484-.149.777-.149.304 0 .564.05.779.152.217.102.384.239.5.41.12.17.186.359.2.566h-.75a.56.56 0 00-.12-.258.624.624 0 00-.246-.181.923.923 0 00-.37-.068c-.216 0-.387.05-.512.152a.472.472 0 00-.185.384c0 .121.048.22.144.3a.97.97 0 00.404.175l.621.143c.217.05.406.12.566.211a1 1 0 01.375.358c.09.148.135.335.135.56 0 .247-.063.466-.188.656a1.216 1.216 0 01-.539.439c-.234.105-.52.158-.858.158-.254 0-.476-.03-.665-.09a1.404 1.404 0 01-.478-.252 1.13 1.13 0 01-.29-.375zm-3.104-.033a1.32 1.32 0 01-.082-.466h.764a.576.576 0 00.074.27.499.499 0 00.454.246c.19 0 .33-.055.422-.164.091-.11.137-.265.137-.466v-2.745h.791v2.725c0 .44-.119.774-.357 1.005-.237.23-.565.345-.985.345a1.59 1.59 0 01-.568-.094 1.145 1.145 0 01-.407-.266 1.14 1.14 0 01-.243-.39zm9.091-1.585v.522c0 .256-.039.47-.117.641a.862.862 0 01-.322.387.877.877 0 01-.47.126.883.883 0 01-.47-.126.87.87 0 01-.32-.387 1.55 1.55 0 01-.117-.641v-.522c0-.258.039-.471.117-.641a.87.87 0 01.32-.387.868.868 0 01.47-.129c.177 0 .333.043.47.129a.862.862 0 01.322.387c.078.17.117.383.117.641zm.803.519v-.513c0-.377-.069-.701-.205-.973a1.46 1.46 0 00-.59-.63c-.253-.146-.559-.22-.916-.22-.356 0-.662.074-.92.22a1.441 1.441 0 00-.589.628c-.137.271-.205.596-.205.975v.513c0 .375.068.699.205.973.137.271.333.48.589.626.258.145.564.217.92.217.357 0 .663-.072.917-.217.256-.146.452-.355.589-.626.136-.274.205-.598.205-.973zm1.29-.935v2.675h-.746v-3.999h.662l1.752 2.66h.032v-2.66h.75v4h-.656l-1.761-2.676h-.032z'\n      />\n    </svg>\n  );\n};\n\nexport default JsonIcon;\n"
  },
  {
    "path": "src/assets/icons/LinkIcon.tsx",
    "content": "import React from 'react';\n\nconst LinkIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'></path>\n      <polyline points='15 3 21 3 21 9'></polyline>\n      <line x1='10' y1='14' x2='21' y2='3'></line>\n    </svg>\n  );\n};\n\nexport default LinkIcon;\n"
  },
  {
    "path": "src/assets/icons/LogoutIcon.tsx",
    "content": "import React from 'react';\n\nconst LogoutIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'></path>\n      <polyline points='16 17 21 12 16 7'></polyline>\n      <line x1='21' y1='12' x2='9' y2='12'></line>\n    </svg>\n  );\n};\n\nexport default LogoutIcon;\n"
  },
  {
    "path": "src/assets/icons/MarkdownIcon.tsx",
    "content": "import React from 'react';\n\nconst MarkdownIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM429 481.2c-1.9-4.4-6.2-7.2-11-7.2h-35c-6.6 0-12 5.4-12 12v272c0 6.6 5.4 12 12 12h27.1c6.6 0 12-5.4 12-12V582.1l66.8 150.2a12 12 0 0011 7.1H524c4.7 0 9-2.8 11-7.1l66.8-150.6V758c0 6.6 5.4 12 12 12H641c6.6 0 12-5.4 12-12V486c0-6.6-5.4-12-12-12h-34.7c-4.8 0-9.1 2.8-11 7.2l-83.1 191-83.2-191z' />\n    </svg>\n  );\n};\n\nexport default MarkdownIcon;\n"
  },
  {
    "path": "src/assets/icons/MenuIcon.tsx",
    "content": "import React from 'react';\n\nconst MenuIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='1.5'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-6 w-6'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n      {...props}\n    >\n      <line x1='3' y1='12' x2='21' y2='12'></line>\n      <line x1='3' y1='6' x2='21' y2='6'></line>\n      <line x1='3' y1='18' x2='21' y2='18'></line>\n    </svg>\n  );\n};\n\nexport default MenuIcon;\n"
  },
  {
    "path": "src/assets/icons/MoneyIcon.tsx",
    "content": "import React from 'react';\n\nconst MoneyIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 24 24'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path fill='none' d='M0 0h24v24H0z' />\n      <path d='M3 3h18a1 1 0 011 1v16a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1zm5.5 11v2H11v2h2v-2h1a2.5 2.5 0 100-5h-4a.5.5 0 110-1h5.5V8H13V6h-2v2h-1a2.5 2.5 0 000 5h4a.5.5 0 110 1H8.5z' />\n    </svg>\n  );\n};\n\nexport default MoneyIcon;\n"
  },
  {
    "path": "src/assets/icons/MoonIcon.tsx",
    "content": "import React from 'react';\n\nconst MoonIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'></path>\n    </svg>\n  );\n};\n\nexport default MoonIcon;\n"
  },
  {
    "path": "src/assets/icons/NewFolderIcon.tsx",
    "content": "import React from 'react';\n\nconst NewFolderIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className={className ? className : 'h-4 w-4'}\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d=\"M12 19H5C3.89543 19 3 18.1046 3 17V7C3 5.89543 3.89543 5 5 5H9.58579C9.851 5 10.1054 5.10536 10.2929 5.29289L12 7H19C20.1046 7 21 7.89543 21 9V11\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n      <path d=\"M18 14V17M18 20V17M18 17H15M18 17H21\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n    </svg>\n  );\n};\n\nexport default NewFolderIcon;\n"
  },
  {
    "path": "src/assets/icons/PdfIcon.tsx",
    "content": "import React from 'react';\n\nconst PdfIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      viewBox='0 0 1024 1024'\n      fill='currentColor'\n      height='1em'\n      width='1em'\n      {...props}\n    >\n      <path d='M531.3 574.4l.3-1.4c5.8-23.9 13.1-53.7 7.4-80.7-3.8-21.3-19.5-29.6-32.9-30.2-15.8-.7-29.9 8.3-33.4 21.4-6.6 24-.7 56.8 10.1 98.6-13.6 32.4-35.3 79.5-51.2 107.5-29.6 15.3-69.3 38.9-75.2 68.7-1.2 5.5.2 12.5 3.5 18.8 3.7 7 9.6 12.4 16.5 15 3 1.1 6.6 2 10.8 2 17.6 0 46.1-14.2 84.1-79.4 5.8-1.9 11.8-3.9 17.6-5.9 27.2-9.2 55.4-18.8 80.9-23.1 28.2 15.1 60.3 24.8 82.1 24.8 21.6 0 30.1-12.8 33.3-20.5 5.6-13.5 2.9-30.5-6.2-39.6-13.2-13-45.3-16.4-95.3-10.2-24.6-15-40.7-35.4-52.4-65.8zM421.6 726.3c-13.9 20.2-24.4 30.3-30.1 34.7 6.7-12.3 19.8-25.3 30.1-34.7zm87.6-235.5c5.2 8.9 4.5 35.8.5 49.4-4.9-19.9-5.6-48.1-2.7-51.4.8.1 1.5.7 2.2 2zm-1.6 120.5c10.7 18.5 24.2 34.4 39.1 46.2-21.6 4.9-41.3 13-58.9 20.2-4.2 1.7-8.3 3.4-12.3 5 13.3-24.1 24.4-51.4 32.1-71.4zm155.6 65.5c.1.2.2.5-.4.9h-.2l-.2.3c-.8.5-9 5.3-44.3-8.6 40.6-1.9 45 7.3 45.1 7.4zm191.4-388.2L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z' />\n    </svg>\n  );\n};\n\nexport default PdfIcon;\n"
  },
  {
    "path": "src/assets/icons/PersonIcon.tsx",
    "content": "import React from 'react';\n\nconst PersonIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'></path>\n      <circle cx='12' cy='7' r='4'></circle>\n    </svg>\n  );\n};\n\nexport default PersonIcon;\n"
  },
  {
    "path": "src/assets/icons/PlusIcon.tsx",
    "content": "import React from 'react';\n\nconst PlusIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className={className ? className : 'h-4 w-4'}\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <line x1='12' y1='5' x2='12' y2='19'></line>\n      <line x1='5' y1='12' x2='19' y2='12'></line>\n    </svg>\n  );\n};\n\nexport default PlusIcon;\n"
  },
  {
    "path": "src/assets/icons/RefreshIcon.tsx",
    "content": "import React from 'react';\n\nconst RefreshIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='1.5'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-3 w-3'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n      {...props}\n    >\n      <polyline points='1 4 1 10 7 10'></polyline>\n      <polyline points='23 20 23 14 17 14'></polyline>\n      <path d='M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15'></path>\n    </svg>\n  );\n};\n\nexport default RefreshIcon;\n"
  },
  {
    "path": "src/assets/icons/SendIcon.tsx",
    "content": "import React from 'react';\n\nconst SendIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4 mr-1'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <line x1='22' y1='2' x2='11' y2='13'></line>\n      <polygon points='22 2 15 22 11 13 2 9 22 2'></polygon>\n    </svg>\n  );\n};\n\nexport default SendIcon;\n"
  },
  {
    "path": "src/assets/icons/SettingIcon.tsx",
    "content": "import React from 'react';\n\nconst SettingIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      xmlns='http://www.w3.org/2000/svg'\n      fill='white'\n      viewBox='0 0 512 512'\n      {...props}\n    >\n      <path d='M481.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-30.9 28.1c-7.7 7.1-11.4 17.5-10.9 27.9c.1 2.9 .2 5.8 .2 8.8s-.1 5.9-.2 8.8c-.5 10.5 3.1 20.9 10.9 27.9l30.9 28.1c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-39.7-12.6c-10-3.2-20.8-1.1-29.7 4.6c-4.9 3.1-9.9 6.1-15.1 8.7c-9.3 4.8-16.5 13.2-18.8 23.4l-8.9 40.7c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-8.9-40.7c-2.2-10.2-9.5-18.6-18.8-23.4c-5.2-2.7-10.2-5.6-15.1-8.7c-8.8-5.7-19.7-7.8-29.7-4.6L69.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l30.9-28.1c7.7-7.1 11.4-17.5 10.9-27.9c-.1-2.9-.2-5.8-.2-8.8s.1-5.9 .2-8.8c.5-10.5-3.1-20.9-10.9-27.9L8.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l39.7 12.6c10 3.2 20.8 1.1 29.7-4.6c4.9-3.1 9.9-6.1 15.1-8.7c9.3-4.8 16.5-13.2 18.8-23.4l8.9-40.7c2-9.1 9-16.3 18.2-17.8C213.3 1.2 227.5 0 242 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l8.9 40.7c2.2 10.2 9.4 18.6 18.8 23.4c5.2 2.7 10.2 5.6 15.1 8.7c8.8 5.7 19.7 7.7 29.7 4.6l39.7-12.6c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM242 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z' />\n    </svg>\n  );\n};\n\nexport default SettingIcon;\n"
  },
  {
    "path": "src/assets/icons/SpinnerIcon.tsx",
    "content": "import React from 'react';\n\nconst SpinnerIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      aria-hidden='true'\n      viewBox='0 0 100 101'\n      fill='none'\n      xmlns='http://www.w3.org/2000/svg'\n      {...props}\n    >\n      <path\n        d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'\n        fill='currentColor'\n      />\n      <path\n        d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'\n        fill='currentFill'\n      />\n    </svg>\n  );\n};\n\nexport default SpinnerIcon;\n"
  },
  {
    "path": "src/assets/icons/SunIcon.tsx",
    "content": "import React from 'react';\n\nconst SunIcon = () => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <circle cx='12' cy='12' r='5'></circle>\n      <line x1='12' y1='1' x2='12' y2='3'></line>\n      <line x1='12' y1='21' x2='12' y2='23'></line>\n      <line x1='4.22' y1='4.22' x2='5.64' y2='5.64'></line>\n      <line x1='18.36' y1='18.36' x2='19.78' y2='19.78'></line>\n      <line x1='1' y1='12' x2='3' y2='12'></line>\n      <line x1='21' y1='12' x2='23' y2='12'></line>\n      <line x1='4.22' y1='19.78' x2='5.64' y2='18.36'></line>\n      <line x1='18.36' y1='5.64' x2='19.78' y2='4.22'></line>\n    </svg>\n  );\n};\n\nexport default SunIcon;\n"
  },
  {
    "path": "src/assets/icons/TickIcon.tsx",
    "content": "import React from 'react';\n\nconst TickIcon = (props: React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      stroke='currentColor'\n      fill='none'\n      strokeWidth='2'\n      viewBox='0 0 24 24'\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      className='h-4 w-4'\n      height='1em'\n      width='1em'\n      xmlns='http://www.w3.org/2000/svg'\n      {...props}\n    >\n      <polyline points='20 6 9 17 4 12'></polyline>\n    </svg>\n  );\n};\n\nexport default TickIcon;\n"
  },
  {
    "path": "src/components/AboutMenu/AboutMenu.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation, Trans } from 'react-i18next';\nimport PopupModal from '@components/PopupModal';\nimport AboutIcon from '@icon/AboutIcon';\n\nconst AboutMenu = () => {\n  const { t } = useTranslation(['main', 'about']);\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  return (\n    <>\n      <a\n        className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n      >\n        <div>\n          <AboutIcon />\n        </div>\n        {t('about')}\n      </a>\n      {isModalOpen && (\n        <PopupModal\n          title={t('about') as string}\n          setIsModalOpen={setIsModalOpen}\n          cancelButton={false}\n        >\n          <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n            <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm flex flex-col gap-3 leading-relaxed'>\n              <p>{t('description', { ns: 'about' })}</p>\n              <p>\n                <Trans\n                  i18nKey='sourceCode'\n                  ns='about'\n                  components={[\n                    <a\n                      href='https://github.com/ztjhz/BetterChatGPT'\n                      target='_blank'\n                      className='link'\n                    />,\n                  ]}\n                />\n              </p>\n\n              <p>\n                <Trans\n                  i18nKey='initiative.description'\n                  ns='about'\n                  components={[\n                    <a\n                      href={t('initiative.link', { ns: 'about' }) as string}\n                      target='_blank'\n                      className='link'\n                    />,\n                  ]}\n                />\n              </p>\n\n              <h2 className='text-lg font-bold'>\n                {t('discordServer.title', { ns: 'about' })}\n              </h2>\n              <p>{t('discordServer.paragraph1', { ns: 'about' })}</p>\n\n              <p>\n                <Trans\n                  i18nKey='discordServer.paragraph2'\n                  ns='about'\n                  components={[\n                    <a\n                      className='link'\n                      href='https://discord.gg/g3Qnwy4V6A'\n                      target='_blank'\n                    />,\n                  ]}\n                />\n              </p>\n\n              <>\n                <h2 className='text-lg font-bold'>\n                  {t('support.title', { ns: 'about' })}\n                </h2>\n                <p>{t('support.paragraph1', { ns: 'about' })}</p>\n                <p>\n                  <Trans\n                    i18nKey='support.paragraph2'\n                    ns='about'\n                    components={[\n                      <a\n                        href='https://github.com/ztjhz/BetterChatGPT'\n                        target='_blank'\n                        className='link'\n                      />,\n                    ]}\n                  />\n                </p>\n                <p>{t('support.paragraph3', { ns: 'about' })}</p>\n\n                <div className='flex flex-col items-center gap-4 my-4'>\n                  <a href='https://github.com/sponsors/ztjhz' target='_blank'>\n                    <img\n                      src='https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86'\n                      width='120px'\n                      alt='Support us through GitHub Sponsors'\n                    />\n                  </a>\n                  <a href='https://ko-fi.com/betterchatgpt' target='_blank'>\n                    <img\n                      src='./kofi.svg'\n                      alt='Support us through the Ko-fi platform.'\n                    />\n                  </a>\n                  <div className='flex gap-x-10 gap-y-4 flex-wrap justify-center'>\n                    <div className='flex flex-col items-center justify-center gap-1'>\n                      <div>{t('support.alipay', { ns: 'about' })} (Ayaka)</div>\n                      <img\n                        className='rounded-md w-32 h-32'\n                        src='https://ayaka14732.github.io/sponsor/alipay.jpg'\n                        alt='Support us through Alipay'\n                      />\n                    </div>\n                    <div className='flex flex-col items-center justify-center gap-1'>\n                      <div>\n                        {t('support.wechatPay', { ns: 'about' })} (Ayaka)\n                      </div>\n                      <img\n                        className='rounded-md w-32 h-32'\n                        src='https://ayaka14732.github.io/sponsor/wechat.png'\n                        alt='Support us through WeChat Pay'\n                      />\n                    </div>\n                  </div>\n                </div>\n                <p>{t('support.paragraph4', { ns: 'about' })}</p>\n              </>\n\n              <h2 className='text-lg font-bold'>\n                {t('privacyStatement.title', { ns: 'about' })}\n              </h2>\n              <p>{t('privacyStatement.paragraph1', { ns: 'about' })}</p>\n\n              <p>{t('privacyStatement.paragraph2', { ns: 'about' })}</p>\n            </div>\n          </div>\n        </PopupModal>\n      )}\n    </>\n  );\n};\n\nexport default AboutMenu;\n"
  },
  {
    "path": "src/components/AboutMenu/index.ts",
    "content": "export { default } from './AboutMenu';\n"
  },
  {
    "path": "src/components/ApiMenu/ApiMenu.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation, Trans } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';\n\nimport PopupModal from '@components/PopupModal';\n\nimport { availableEndpoints, defaultAPIEndpoint } from '@constants/auth';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\n\nconst ApiMenu = ({\n  setIsModalOpen,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  const { t } = useTranslation(['main', 'api']);\n\n  const apiKey = useStore((state) => state.apiKey);\n  const setApiKey = useStore((state) => state.setApiKey);\n  const apiEndpoint = useStore((state) => state.apiEndpoint);\n  const setApiEndpoint = useStore((state) => state.setApiEndpoint);\n\n  const [_apiKey, _setApiKey] = useState<string>(apiKey || '');\n  const [_apiEndpoint, _setApiEndpoint] = useState<string>(apiEndpoint);\n  const [_customEndpoint, _setCustomEndpoint] = useState<boolean>(\n    !availableEndpoints.includes(apiEndpoint)\n  );\n\n  const handleSave = () => {\n    setApiKey(_apiKey);\n    setApiEndpoint(_apiEndpoint);\n    setIsModalOpen(false);\n  };\n\n  const handleToggleCustomEndpoint = () => {\n    if (_customEndpoint) _setApiEndpoint(defaultAPIEndpoint);\n    else _setApiEndpoint('');\n    _setCustomEndpoint((prev) => !prev);\n  };\n\n  return (\n    <PopupModal\n      title={t('api') as string}\n      setIsModalOpen={setIsModalOpen}\n      handleConfirm={handleSave}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n        <label className='flex gap-2 text-gray-900 dark:text-gray-300 text-sm items-center mb-4'>\n          <input\n            type='checkbox'\n            checked={_customEndpoint}\n            className='w-4 h-4'\n            onChange={handleToggleCustomEndpoint}\n          />\n          {t('customEndpoint', { ns: 'api' })}\n        </label>\n\n        <div className='flex gap-2 items-center mb-6'>\n          <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>\n            {t('apiEndpoint.inputLabel', { ns: 'api' })}\n          </div>\n          {_customEndpoint ? (\n            <input\n              type='text'\n              className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'\n              value={_apiEndpoint}\n              onChange={(e) => {\n                _setApiEndpoint(e.target.value);\n              }}\n            />\n          ) : (\n            <ApiEndpointSelector\n              _apiEndpoint={_apiEndpoint}\n              _setApiEndpoint={_setApiEndpoint}\n            />\n          )}\n        </div>\n\n        <div className='flex gap-2 items-center justify-center mt-2'>\n          <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>\n            {t('apiKey.inputLabel', { ns: 'api' })}\n          </div>\n          <input\n            type='text'\n            className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'\n            value={_apiKey}\n            onChange={(e) => {\n              _setApiKey(e.target.value);\n            }}\n          />\n        </div>\n\n        <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm flex flex-col gap-3 leading-relaxed'>\n          <p className='mt-4'>\n            <Trans\n              i18nKey='apiKey.howTo'\n              ns='api'\n              components={[\n                <a\n                  href='https://platform.openai.com/account/api-keys'\n                  className='link'\n                  target='_blank'\n                />,\n              ]}\n            />\n          </p>\n\n          <p>{t('securityMessage', { ns: 'api' })}</p>\n\n          <p>{t('apiEndpoint.description', { ns: 'api' })}</p>\n\n          <p>{t('apiEndpoint.warn', { ns: 'api' })}</p>\n        </div>\n      </div>\n    </PopupModal>\n  );\n};\n\nconst ApiEndpointSelector = ({\n  _apiEndpoint,\n  _setApiEndpoint,\n}: {\n  _apiEndpoint: string;\n  _setApiEndpoint: React.Dispatch<React.SetStateAction<string>>;\n}) => {\n  const [dropDown, setDropDown, dropDownRef] = useHideOnOutsideClick();\n\n  return (\n    <div className='w-[40vw] relative flex-1'>\n      <button\n        className='btn btn-neutral btn-small flex justify-between w-full'\n        type='button'\n        aria-label='expand api menu'\n        onClick={() => setDropDown((prev) => !prev)}\n      >\n        <span className='truncate'>{_apiEndpoint}</span>\n        <DownChevronArrow />\n      </button>\n      <div\n        id='dropdown'\n        ref={dropDownRef}\n        className={`${\n          dropDown ? '' : 'hidden'\n        } absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90 w-32 w-full`}\n      >\n        <ul\n          className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'\n          aria-labelledby='dropdownDefaultButton'\n        >\n          {availableEndpoints.map((endpoint) => (\n            <li\n              className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer truncate'\n              onClick={() => {\n                _setApiEndpoint(endpoint);\n                setDropDown(false);\n              }}\n              key={endpoint}\n            >\n              {endpoint}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n};\n\nexport default ApiMenu;\n"
  },
  {
    "path": "src/components/ApiMenu/index.ts",
    "content": "export { default } from './ApiMenu';\n"
  },
  {
    "path": "src/components/ApiPopup/ApiPopup.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport useStore from '@store/store';\nimport { useTranslation, Trans } from 'react-i18next';\n\nimport PopupModal from '@components/PopupModal';\nimport CrossIcon from '@icon/CrossIcon';\n\nconst ApiPopup = () => {\n  const { t } = useTranslation(['main', 'api']);\n\n  const apiKey = useStore((state) => state.apiKey);\n  const setApiKey = useStore((state) => state.setApiKey);\n  const firstVisit = useStore((state) => state.firstVisit);\n  const setFirstVisit = useStore((state) => state.setFirstVisit);\n\n  const [_apiKey, _setApiKey] = useState<string>(apiKey || '');\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(\n    !apiKey && firstVisit\n  );\n  const [error, setError] = useState<string>('');\n\n  const handleConfirm = () => {\n    if (_apiKey.length === 0) {\n      setError(t('noApiKeyWarning', { ns: 'api' }) as string);\n    } else {\n      setError('');\n      setApiKey(_apiKey);\n      setIsModalOpen(false);\n    }\n  };\n\n  useEffect(() => {\n    setFirstVisit(false);\n  }, []);\n\n  return isModalOpen ? (\n    <PopupModal\n      title='Setup your API key'\n      handleConfirm={handleConfirm}\n      setIsModalOpen={setIsModalOpen}\n      cancelButton={false}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n        <div className='flex gap-2 items-center justify-center mt-2'>\n          <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>\n            {t('apiKey.inputLabel', { ns: 'api' })}\n          </div>\n          <input\n            type='text'\n            className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'\n            value={_apiKey}\n            onChange={(e) => {\n              _setApiKey(e.target.value);\n            }}\n          />\n        </div>\n\n        <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>\n          <Trans\n            i18nKey='apiKey.howTo'\n            ns='api'\n            components={[\n              <a\n                href='https://platform.openai.com/account/api-keys'\n                className='link'\n                target='_blank'\n              />,\n            ]}\n          />\n        </div>\n        <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>\n          <Trans\n            i18nKey='advancedConfig'\n            ns='api'\n            components={[\n              <a\n                className='link cursor-pointer'\n                onClick={() => {\n                  setIsModalOpen(false);\n                  document.getElementById('api-menu')?.click();\n                }}\n              />,\n            ]}\n          />\n        </div>\n\n        <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>\n          {t('securityMessage', { ns: 'api' })}\n        </div>\n\n        {error.length > 0 && (\n          <div className='relative py-2 px-3 w-full mt-3 border rounded-md border-red-500 bg-red-500/10'>\n            <div className='text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap'>\n              {error}\n            </div>\n            <div\n              className='text-white absolute top-1 right-1 cursor-pointer'\n              onClick={() => {\n                setError('');\n              }}\n            >\n              <CrossIcon />\n            </div>\n          </div>\n        )}\n      </div>\n    </PopupModal>\n  ) : (\n    <></>\n  );\n};\n\nexport default ApiPopup;\n"
  },
  {
    "path": "src/components/ApiPopup/index.ts",
    "content": "export { default } from './ApiPopup';\n"
  },
  {
    "path": "src/components/Chat/Chat.tsx",
    "content": "import React from 'react';\nimport useStore from '@store/store';\n\nimport ChatContent from './ChatContent';\nimport MobileBar from '../MobileBar';\nimport StopGeneratingButton from '@components/StopGeneratingButton/StopGeneratingButton';\n\nconst Chat = () => {\n  const hideSideMenu = useStore((state) => state.hideSideMenu);\n\n  return (\n    <div\n      className={`flex h-full flex-1 flex-col ${\n        hideSideMenu ? 'md:pl-0' : 'md:pl-[260px]'\n      }`}\n    >\n      <MobileBar />\n      <main className='relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1'>\n        <ChatContent />\n        <StopGeneratingButton />\n      </main>\n    </div>\n  );\n};\n\nexport default Chat;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/ChatContent.tsx",
    "content": "import React, { useEffect, useRef } from 'react';\nimport ScrollToBottom from 'react-scroll-to-bottom';\nimport useStore from '@store/store';\n\nimport ScrollToBottomButton from './ScrollToBottomButton';\nimport ChatTitle from './ChatTitle';\nimport Message from './Message';\nimport NewMessageButton from './Message/NewMessageButton';\nimport CrossIcon from '@icon/CrossIcon';\n\nimport useSubmit from '@hooks/useSubmit';\nimport DownloadChat from './DownloadChat';\nimport CloneChat from './CloneChat';\nimport ShareGPT from '@components/ShareGPT';\n\nconst ChatContent = () => {\n  const inputRole = useStore((state) => state.inputRole);\n  const setError = useStore((state) => state.setError);\n  const messages = useStore((state) =>\n    state.chats &&\n    state.chats.length > 0 &&\n    state.currentChatIndex >= 0 &&\n    state.currentChatIndex < state.chats.length\n      ? state.chats[state.currentChatIndex].messages\n      : []\n  );\n  const stickyIndex = useStore((state) =>\n    state.chats &&\n    state.chats.length > 0 &&\n    state.currentChatIndex >= 0 &&\n    state.currentChatIndex < state.chats.length\n      ? state.chats[state.currentChatIndex].messages.length\n      : 0\n  );\n  const advancedMode = useStore((state) => state.advancedMode);\n  const generating = useStore.getState().generating;\n  const hideSideMenu = useStore((state) => state.hideSideMenu);\n\n  const saveRef = useRef<HTMLDivElement>(null);\n\n  // clear error at the start of generating new messages\n  useEffect(() => {\n    if (generating) {\n      setError('');\n    }\n  }, [generating]);\n\n  const { error } = useSubmit();\n\n  return (\n    <div className='flex-1 overflow-hidden'>\n      <ScrollToBottom\n        className='h-full dark:bg-gray-800'\n        followButtonClassName='hidden'\n      >\n        <ScrollToBottomButton />\n        <div className='flex flex-col items-center text-sm dark:bg-gray-800'>\n          <div\n            className='flex flex-col items-center text-sm dark:bg-gray-800 w-full'\n            ref={saveRef}\n          >\n            {advancedMode && <ChatTitle />}\n            {!generating && advancedMode && messages?.length === 0 && (\n              <NewMessageButton messageIndex={-1} />\n            )}\n            {messages?.map((message, index) => (\n              (advancedMode || index !== 0 || message.role !== 'system') && (\n                <React.Fragment key={index}>\n                  <Message\n                    role={message.role}\n                    content={message.content}\n                    messageIndex={index}\n                  />\n                  {!generating && advancedMode && <NewMessageButton messageIndex={index} />}\n                </React.Fragment>\n              )\n            ))}\n          </div>\n\n          <Message\n            role={inputRole}\n            content=''\n            messageIndex={stickyIndex}\n            sticky\n          />\n          {error !== '' && (\n            <div className='relative py-2 px-3 w-3/5 mt-3 max-md:w-11/12 border rounded-md border-red-500 bg-red-500/10'>\n              <div className='text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap'>\n                {error}\n              </div>\n              <div\n                className='text-white absolute top-1 right-1 cursor-pointer'\n                onClick={() => {\n                  setError('');\n                }}\n              >\n                <CrossIcon />\n              </div>\n            </div>\n          )}\n          <div\n            className={`mt-4 w-full m-auto  ${\n              hideSideMenu\n                ? 'md:max-w-5xl lg:max-w-5xl xl:max-w-6xl'\n                : 'md:max-w-3xl lg:max-w-3xl xl:max-w-4xl'\n            }`}\n          >\n            {useStore.getState().generating || (\n              <div className='md:w-[calc(100%-50px)] flex gap-4 flex-wrap justify-center'>\n                <DownloadChat saveRef={saveRef} />\n                <ShareGPT />\n                <CloneChat />\n              </div>\n            )}\n          </div>\n          <div className='w-full h-36'></div>\n        </div>\n      </ScrollToBottom>\n    </div>\n  );\n};\n\nexport default ChatContent;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/ChatTitle.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { shallow } from 'zustand/shallow';\nimport useStore from '@store/store';\nimport ConfigMenu from '@components/ConfigMenu';\nimport { ChatInterface, ConfigInterface } from '@type/chat';\nimport { _defaultChatConfig } from '@constants/chat';\n\nconst ChatTitle = React.memo(() => {\n  const { t } = useTranslation('model');\n  const config = useStore(\n    (state) =>\n      state.chats &&\n      state.chats.length > 0 &&\n      state.currentChatIndex >= 0 &&\n      state.currentChatIndex < state.chats.length\n        ? state.chats[state.currentChatIndex].config\n        : undefined,\n    shallow\n  );\n  const setChats = useStore((state) => state.setChats);\n  const currentChatIndex = useStore((state) => state.currentChatIndex);\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  const setConfig = (config: ConfigInterface) => {\n    const updatedChats: ChatInterface[] = JSON.parse(\n      JSON.stringify(useStore.getState().chats)\n    );\n    updatedChats[currentChatIndex].config = config;\n    setChats(updatedChats);\n  };\n\n  // for migrating from old ChatInterface to new ChatInterface (with config)\n  useEffect(() => {\n    const chats = useStore.getState().chats;\n    if (chats && chats.length > 0 && currentChatIndex !== -1 && !config) {\n      const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));\n      updatedChats[currentChatIndex].config = { ..._defaultChatConfig };\n      setChats(updatedChats);\n    }\n  }, [currentChatIndex]);\n\n  return config ? (\n    <>\n      <div\n        className='flex gap-x-4 gap-y-1 flex-wrap w-full items-center justify-center border-b border-black/10 bg-gray-50 p-3 dark:border-gray-900/50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 cursor-pointer'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n      >\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('model')}: {config.model}\n        </div>\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('token.label')}: {config.max_tokens}\n        </div>\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('temperature.label')}: {config.temperature}\n        </div>\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('topP.label')}: {config.top_p}\n        </div>\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('presencePenalty.label')}: {config.presence_penalty}\n        </div>\n        <div className='text-center p-1 rounded-md bg-gray-300/20 dark:bg-gray-900/10 hover:bg-gray-300/50 dark:hover:bg-gray-900/50'>\n          {t('frequencyPenalty.label')}: {config.frequency_penalty}\n        </div>\n      </div>\n      {isModalOpen && (\n        <ConfigMenu\n          setIsModalOpen={setIsModalOpen}\n          config={config}\n          setConfig={setConfig}\n        />\n      )}\n    </>\n  ) : (\n    <></>\n  );\n});\n\nexport default ChatTitle;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/CloneChat.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport { ChatInterface } from '@type/chat';\n\nimport TickIcon from '@icon/TickIcon';\n\nconst CloneChat = React.memo(() => {\n  const { t } = useTranslation();\n\n  const setChats = useStore((state) => state.setChats);\n  const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n\n  const [cloned, setCloned] = useState<boolean>(false);\n\n  const cloneChat = () => {\n    const chats = useStore.getState().chats;\n\n    if (chats) {\n      const index = useStore.getState().currentChatIndex;\n      let title = `Copy of ${chats[index].title}`;\n      let i = 0;\n\n      while (chats.some((chat) => chat.title === title)) {\n        i += 1;\n        title = `Copy ${i} of ${chats[index].title}`;\n      }\n\n      const clonedChat = JSON.parse(JSON.stringify(chats[index]));\n      clonedChat.title = title;\n\n      const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));\n      updatedChats.unshift(clonedChat);\n\n      setChats(updatedChats);\n      setCurrentChatIndex(useStore.getState().currentChatIndex + 1);\n      setCloned(true);\n\n      window.setTimeout(() => {\n        setCloned(false);\n      }, 3000);\n    }\n  };\n\n  return (\n    <button\n      className='btn btn-neutral flex gap-1'\n      aria-label={t('cloneChat') as string}\n      onClick={cloneChat}\n    >\n      {cloned ? (\n        <>\n          <TickIcon /> {t('cloned')}\n        </>\n      ) : (\n        <>{t('cloneChat')}</>\n      )}\n    </button>\n  );\n});\n\nexport default CloneChat;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/DownloadChat.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport PopupModal from '@components/PopupModal';\nimport {\n  chatToMarkdown,\n  downloadImg,\n  downloadMarkdown,\n  // downloadPDF,\n  htmlToImg,\n} from '@utils/chat';\nimport ImageIcon from '@icon/ImageIcon';\nimport PdfIcon from '@icon/PdfIcon';\nimport MarkdownIcon from '@icon/MarkdownIcon';\nimport JsonIcon from '@icon/JsonIcon';\n\nimport downloadFile from '@utils/downloadFile';\n\nconst DownloadChat = React.memo(\n  ({ saveRef }: { saveRef: React.RefObject<HTMLDivElement> }) => {\n    const { t } = useTranslation();\n    const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n    return (\n      <>\n        <button\n          className='btn btn-neutral'\n          aria-label={t('downloadChat') as string}\n          onClick={() => {\n            setIsModalOpen(true);\n          }}\n        >\n          {t('downloadChat')}\n        </button>\n        {isModalOpen && (\n          <PopupModal\n            setIsModalOpen={setIsModalOpen}\n            title={t('downloadChat') as string}\n            cancelButton={false}\n          >\n            <div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'>\n              <button\n                className='btn btn-neutral gap-2'\n                aria-label='image'\n                onClick={async () => {\n                  if (saveRef && saveRef.current) {\n                    const imgData = await htmlToImg(saveRef.current);\n                    downloadImg(\n                      imgData,\n                      `${\n                        useStore\n                          .getState()\n                          .chats?.[\n                            useStore.getState().currentChatIndex\n                          ].title.trim() ?? 'download'\n                      }.png`\n                    );\n                  }\n                }}\n              >\n                <ImageIcon />\n                Image\n              </button>\n              {/* <button\n                className='btn btn-neutral gap-2'\n                onClick={async () => {\n                  if (saveRef && saveRef.current) {\n                    const imgData = await htmlToImg(saveRef.current);\n                    downloadPDF(\n                      imgData,\n                      useStore.getState().theme,\n                      `${\n                        useStore\n                          .getState()\n                          .chats?.[\n                            useStore.getState().currentChatIndex\n                          ].title.trim() ?? 'download'\n                      }.pdf`\n                    );\n                  }\n                }}\n              >\n                <PdfIcon />\n                PDF\n              </button> */}\n              <button\n                className='btn btn-neutral gap-2'\n                aria-label='markdown'\n                onClick={async () => {\n                  if (saveRef && saveRef.current) {\n                    const chats = useStore.getState().chats;\n                    if (chats) {\n                      const markdown = chatToMarkdown(\n                        chats[useStore.getState().currentChatIndex]\n                      );\n                      downloadMarkdown(\n                        markdown,\n                        `${\n                          chats[\n                            useStore.getState().currentChatIndex\n                          ].title.trim() ?? 'download'\n                        }.md`\n                      );\n                    }\n                  }\n                }}\n              >\n                <MarkdownIcon />\n                Markdown\n              </button>\n              <button\n                className='btn btn-neutral gap-2'\n                aria-label='json'\n                onClick={async () => {\n                  const chats = useStore.getState().chats;\n                  if (chats) {\n                    const chat = chats[useStore.getState().currentChatIndex];\n                    downloadFile([chat], chat.title);\n                  }\n                }}\n              >\n                <JsonIcon />\n                JSON\n              </button>\n            </div>\n          </PopupModal>\n        )}\n      </>\n    );\n  }\n);\n\nexport default DownloadChat;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/Avatar.tsx",
    "content": "import React from 'react';\nimport { Role } from '@type/chat';\nimport SettingIcon from '@icon/SettingIcon';\nimport PersonIcon from '@icon/PersonIcon';\n\nconst Avatar = React.memo(({ role }: { role: Role }) => {\n  return (\n    <div className='w-[30px] flex flex-col relative items-end'>\n      {role === 'user' && <UserAvatar />}\n      {role === 'assistant' && <AssistantAvatar />}\n      {role === 'system' && <SystemAvatar />}\n    </div>\n  );\n});\n\nconst UserAvatar = () => {\n  return (\n    <div\n      className='relative h-[30px] w-[30px] p-1 rounded-sm text-white flex items-center justify-center'\n      style={{ backgroundColor: 'rgb(200, 70, 70)' }}\n    >\n      <PersonIcon />\n    </div>\n  );\n};\n\nconst AssistantAvatar = () => {\n  return (\n    <div\n      className='relative h-[30px] w-[30px] p-1 rounded-sm text-white flex items-center justify-center'\n      style={{ backgroundColor: 'rgb(16, 163, 127)' }}\n    >\n      <svg\n        width='41'\n        height='41'\n        viewBox='0 0 41 41'\n        fill='none'\n        xmlns='http://www.w3.org/2000/svg'\n        strokeWidth='1.5'\n        className='h-6 w-6'\n      >\n        <path\n          d='M37.5324 16.8707C37.9808 15.5241 38.1363 14.0974 37.9886 12.6859C37.8409 11.2744 37.3934 9.91076 36.676 8.68622C35.6126 6.83404 33.9882 5.3676 32.0373 4.4985C30.0864 3.62941 27.9098 3.40259 25.8215 3.85078C24.8796 2.7893 23.7219 1.94125 22.4257 1.36341C21.1295 0.785575 19.7249 0.491269 18.3058 0.500197C16.1708 0.495044 14.0893 1.16803 12.3614 2.42214C10.6335 3.67624 9.34853 5.44666 8.6917 7.47815C7.30085 7.76286 5.98686 8.3414 4.8377 9.17505C3.68854 10.0087 2.73073 11.0782 2.02839 12.312C0.956464 14.1591 0.498905 16.2988 0.721698 18.4228C0.944492 20.5467 1.83612 22.5449 3.268 24.1293C2.81966 25.4759 2.66413 26.9026 2.81182 28.3141C2.95951 29.7256 3.40701 31.0892 4.12437 32.3138C5.18791 34.1659 6.8123 35.6322 8.76321 36.5013C10.7141 37.3704 12.8907 37.5973 14.9789 37.1492C15.9208 38.2107 17.0786 39.0587 18.3747 39.6366C19.6709 40.2144 21.0755 40.5087 22.4946 40.4998C24.6307 40.5054 26.7133 39.8321 28.4418 38.5772C30.1704 37.3223 31.4556 35.5506 32.1119 33.5179C33.5027 33.2332 34.8167 32.6547 35.9659 31.821C37.115 30.9874 38.0728 29.9178 38.7752 28.684C39.8458 26.8371 40.3023 24.6979 40.0789 22.5748C39.8556 20.4517 38.9639 18.4544 37.5324 16.8707ZM22.4978 37.8849C20.7443 37.8874 19.0459 37.2733 17.6994 36.1501C17.7601 36.117 17.8666 36.0586 17.936 36.0161L25.9004 31.4156C26.1003 31.3019 26.2663 31.137 26.3813 30.9378C26.4964 30.7386 26.5563 30.5124 26.5549 30.2825V19.0542L29.9213 20.998C29.9389 21.0068 29.9541 21.0198 29.9656 21.0359C29.977 21.052 29.9842 21.0707 29.9867 21.0902V30.3889C29.9842 32.375 29.1946 34.2791 27.7909 35.6841C26.3872 37.0892 24.4838 37.8806 22.4978 37.8849ZM6.39227 31.0064C5.51397 29.4888 5.19742 27.7107 5.49804 25.9832C5.55718 26.0187 5.66048 26.0818 5.73461 26.1244L13.699 30.7248C13.8975 30.8408 14.1233 30.902 14.3532 30.902C14.583 30.902 14.8088 30.8408 15.0073 30.7248L24.731 25.1103V28.9979C24.7321 29.0177 24.7283 29.0376 24.7199 29.0556C24.7115 29.0736 24.6988 29.0893 24.6829 29.1012L16.6317 33.7497C14.9096 34.7416 12.8643 35.0097 10.9447 34.4954C9.02506 33.9811 7.38785 32.7263 6.39227 31.0064ZM4.29707 13.6194C5.17156 12.0998 6.55279 10.9364 8.19885 10.3327C8.19885 10.4013 8.19491 10.5228 8.19491 10.6071V19.808C8.19351 20.0378 8.25334 20.2638 8.36823 20.4629C8.48312 20.6619 8.64893 20.8267 8.84863 20.9404L18.5723 26.5542L15.206 28.4979C15.1894 28.5089 15.1703 28.5155 15.1505 28.5173C15.1307 28.5191 15.1107 28.516 15.0924 28.5082L7.04046 23.8557C5.32135 22.8601 4.06716 21.2235 3.55289 19.3046C3.03862 17.3858 3.30624 15.3413 4.29707 13.6194ZM31.955 20.0556L22.2312 14.4411L25.5976 12.4981C25.6142 12.4872 25.6333 12.4805 25.6531 12.4787C25.6729 12.4769 25.6928 12.4801 25.7111 12.4879L33.7631 17.1364C34.9967 17.849 36.0017 18.8982 36.6606 20.1613C37.3194 21.4244 37.6047 22.849 37.4832 24.2684C37.3617 25.6878 36.8382 27.0432 35.9743 28.1759C35.1103 29.3086 33.9415 30.1717 32.6047 30.6641C32.6047 30.5947 32.6047 30.4733 32.6047 30.3889V21.188C32.6066 20.9586 32.5474 20.7328 32.4332 20.5338C32.319 20.3348 32.154 20.1698 31.955 20.0556ZM35.3055 15.0128C35.2464 14.9765 35.1431 14.9142 35.069 14.8717L27.1045 10.2712C26.906 10.1554 26.6803 10.0943 26.4504 10.0943C26.2206 10.0943 25.9948 10.1554 25.7963 10.2712L16.0726 15.8858V11.9982C16.0715 11.9783 16.0753 11.9585 16.0837 11.9405C16.0921 11.9225 16.1048 11.9068 16.1207 11.8949L24.1719 7.25025C25.4053 6.53903 26.8158 6.19376 28.2383 6.25482C29.6608 6.31589 31.0364 6.78077 32.2044 7.59508C33.3723 8.40939 34.2842 9.53945 34.8334 10.8531C35.3826 12.1667 35.5464 13.6095 35.3055 15.0128ZM14.2424 21.9419L10.8752 19.9981C10.8576 19.9893 10.8423 19.9763 10.8309 19.9602C10.8195 19.9441 10.8122 19.9254 10.8098 19.9058V10.6071C10.8107 9.18295 11.2173 7.78848 11.9819 6.58696C12.7466 5.38544 13.8377 4.42659 15.1275 3.82264C16.4173 3.21869 17.8524 2.99464 19.2649 3.1767C20.6775 3.35876 22.0089 3.93941 23.1034 4.85067C23.0427 4.88379 22.937 4.94215 22.8668 4.98473L14.9024 9.58517C14.7025 9.69878 14.5366 9.86356 14.4215 10.0626C14.3065 10.2616 14.2466 10.4877 14.2479 10.7175L14.2424 21.9419ZM16.071 17.9991L20.4018 15.4978L24.7325 17.9975V22.9985L20.4018 25.4983L16.071 22.9985V17.9991Z'\n          fill='currentColor'\n        ></path>\n      </svg>\n    </div>\n  );\n};\n\nconst SystemAvatar = () => {\n  return (\n    <div\n      className='relative h-[30px] w-[30px] p-1 rounded-sm text-white flex items-center justify-center'\n      style={{ backgroundColor: 'rgb(126, 163, 227)' }}\n    >\n      <SettingIcon />\n    </div>\n  );\n};\n\nexport default Avatar;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/CodeBlock.tsx",
    "content": "import React, { useRef, useState } from 'react';\n\nimport CopyIcon from '@icon/CopyIcon';\nimport TickIcon from '@icon/TickIcon';\n\nconst CodeBlock = ({\n  lang,\n  codeChildren,\n}: {\n  lang: string;\n  codeChildren: React.ReactNode & React.ReactNode[];\n}) => {\n  const codeRef = useRef<HTMLElement>(null);\n\n  return (\n    <div className='bg-black rounded-md'>\n      <CodeBar lang={lang} codeRef={codeRef} />\n      <div className='p-4 overflow-y-auto'>\n        <code ref={codeRef} className={`!whitespace-pre hljs language-${lang}`}>\n          {codeChildren}\n        </code>\n      </div>\n    </div>\n  );\n};\n\nconst CodeBar = React.memo(\n  ({\n    lang,\n    codeRef,\n  }: {\n    lang: string;\n    codeRef: React.RefObject<HTMLElement>;\n  }) => {\n    const [isCopied, setIsCopied] = useState<boolean>(false);\n    return (\n      <div className='flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans'>\n        <span className=''>{lang}</span>\n        <button\n          className='flex ml-auto gap-2'\n          aria-label='copy codeblock'\n          onClick={async () => {\n            const codeString = codeRef.current?.textContent;\n            if (codeString)\n              navigator.clipboard.writeText(codeString).then(() => {\n                setIsCopied(true);\n                setTimeout(() => setIsCopied(false), 3000);\n              });\n          }}\n        >\n          {isCopied ? (\n            <>\n              <TickIcon />\n              Copied!\n            </>\n          ) : (\n            <>\n              <CopyIcon />\n              Copy code\n            </>\n          )}\n        </button>\n      </div>\n    );\n  }\n);\nexport default CodeBlock;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/CommandPrompt/CommandPrompt.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport useStore from '@store/store';\n\nimport { useTranslation } from 'react-i18next';\nimport { matchSorter } from 'match-sorter';\nimport { Prompt } from '@type/prompt';\n\nimport useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';\n\nconst CommandPrompt = ({\n  _setContent,\n}: {\n  _setContent: React.Dispatch<React.SetStateAction<string>>;\n}) => {\n  const { t } = useTranslation();\n  const prompts = useStore((state) => state.prompts);\n  const [_prompts, _setPrompts] = useState<Prompt[]>(prompts);\n  const [input, setInput] = useState<string>('');\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const [dropDown, setDropDown, dropDownRef] = useHideOnOutsideClick();\n\n  useEffect(() => {\n    if (dropDown && inputRef.current) {\n      // When dropdown is visible, focus the input\n      inputRef.current.focus();\n    }\n  }, [dropDown]);\n\n  useEffect(() => {\n    const filteredPrompts = matchSorter(useStore.getState().prompts, input, {\n      keys: ['name'],\n    });\n    _setPrompts(filteredPrompts);\n  }, [input]);\n\n  useEffect(() => {\n    _setPrompts(prompts);\n    setInput('');\n  }, [prompts]);\n\n  return (\n    <div className='relative max-wd-sm' ref={dropDownRef}>\n      <button\n        className='btn btn-neutral btn-small'\n        aria-label='prompt library'\n        onClick={() => setDropDown(!dropDown)}\n      >\n        /\n      </button>\n      <div\n        className={`${\n          dropDown ? '' : 'hidden'\n        } absolute top-100 bottom-100 right-0 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}\n      >\n        <div className='text-sm px-4 py-2 w-max'>{t('promptLibrary')}</div>\n        <input\n          ref={inputRef}\n          type='text'\n          className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 m-0 w-full mr-0 h-8 focus:outline-none'\n          value={input}\n          placeholder={t('search') as string}\n          onChange={(e) => {\n            setInput(e.target.value);\n          }}\n        />\n        <ul className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0 w-max max-w-sm max-md:max-w-[90vw] max-h-32 overflow-auto'>\n          {_prompts.map((cp) => (\n            <li\n              className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer text-start w-full'\n              onClick={() => {\n                _setContent((prev) => prev + cp.prompt);\n                setDropDown(false);\n              }}\n              key={cp.id}\n            >\n              {cp.name}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n};\n\nexport default CommandPrompt;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/CommandPrompt/index.ts",
    "content": "export { default } from './CommandPrompt';\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/Message.tsx",
    "content": "import React from 'react';\nimport useStore from '@store/store';\n\nimport Avatar from './Avatar';\nimport MessageContent from './MessageContent';\n\nimport { Role } from '@type/chat';\nimport RoleSelector from './RoleSelector';\n\n// const backgroundStyle: { [role in Role]: string } = {\n//   user: 'dark:bg-gray-800',\n//   assistant: 'bg-gray-50 dark:bg-gray-650',\n//   system: 'bg-gray-50 dark:bg-gray-650',\n// };\nconst backgroundStyle = ['dark:bg-gray-800', 'bg-gray-50 dark:bg-gray-650'];\n\nconst Message = React.memo(\n  ({\n    role,\n    content,\n    messageIndex,\n    sticky = false,\n  }: {\n    role: Role;\n    content: string;\n    messageIndex: number;\n    sticky?: boolean;\n  }) => {\n    const hideSideMenu = useStore((state) => state.hideSideMenu);\n    const advancedMode = useStore((state) => state.advancedMode);\n\n    return (\n      <div\n        className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${\n          backgroundStyle[messageIndex % 2]\n        }`}\n      >\n        <div\n          className={`text-base gap-4 md:gap-6 m-auto p-4 md:py-6 flex transition-all ease-in-out ${\n            hideSideMenu\n              ? 'md:max-w-5xl lg:max-w-5xl xl:max-w-6xl'\n              : 'md:max-w-3xl lg:max-w-3xl xl:max-w-4xl'\n          }`}\n        >\n          <Avatar role={role} />\n          <div className='w-[calc(100%-50px)] '>\n            {advancedMode &&\n              <RoleSelector\n                role={role}\n                messageIndex={messageIndex}\n                sticky={sticky}\n              />}\n            <MessageContent\n              role={role}\n              content={content}\n              messageIndex={messageIndex}\n              sticky={sticky}\n            />\n          </div>\n        </div>\n      </div>\n    );\n  }\n);\n\nexport default Message;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/MessageContent.tsx",
    "content": "import React, { useState } from 'react';\nimport useStore from '@store/store';\n\nimport ContentView from './View/ContentView';\nimport EditView from './View/EditView';\n\nconst MessageContent = ({\n  role,\n  content,\n  messageIndex,\n  sticky = false,\n}: {\n  role: string;\n  content: string;\n  messageIndex: number;\n  sticky?: boolean;\n}) => {\n  const [isEdit, setIsEdit] = useState<boolean>(sticky);\n  const advancedMode = useStore((state) => state.advancedMode);\n\n  return (\n    <div className='relative flex flex-col gap-2 md:gap-3 lg:w-[calc(100%-115px)]'>\n      {advancedMode && <div className='flex flex-grow flex-col gap-3'></div>}\n      {isEdit ? (\n        <EditView\n          content={content}\n          setIsEdit={setIsEdit}\n          messageIndex={messageIndex}\n          sticky={sticky}\n        />\n      ) : (\n        <ContentView\n          role={role}\n          content={content}\n          setIsEdit={setIsEdit}\n          messageIndex={messageIndex}\n        />\n      )}\n    </div>\n  );\n};\n\nexport default MessageContent;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/NewMessageButton.tsx",
    "content": "import React from 'react';\nimport useStore from '@store/store';\n\nimport PlusIcon from '@icon/PlusIcon';\n\nimport { ChatInterface } from '@type/chat';\nimport { generateDefaultChat } from '@constants/chat';\n\nconst NewMessageButton = React.memo(\n  ({ messageIndex }: { messageIndex: number }) => {\n    const setChats = useStore((state) => state.setChats);\n    const currentChatIndex = useStore((state) => state.currentChatIndex);\n    const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n\n    const addChat = () => {\n      const chats = useStore.getState().chats;\n      if (chats) {\n        const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));\n        let titleIndex = 1;\n        let title = `New Chat ${titleIndex}`;\n\n        while (chats.some((chat) => chat.title === title)) {\n          titleIndex += 1;\n          title = `New Chat ${titleIndex}`;\n        }\n\n        updatedChats.unshift(generateDefaultChat(title));\n        setChats(updatedChats);\n        setCurrentChatIndex(0);\n      }\n    };\n\n    const addMessage = () => {\n      if (currentChatIndex === -1) {\n        addChat();\n      } else {\n        const updatedChats: ChatInterface[] = JSON.parse(\n          JSON.stringify(useStore.getState().chats)\n        );\n        updatedChats[currentChatIndex].messages.splice(messageIndex + 1, 0, {\n          content: '',\n          role: 'user',\n        });\n        setChats(updatedChats);\n      }\n    };\n\n    return (\n      <div\n        className='h-0 w-0 relative'\n        key={messageIndex}\n        aria-label='insert message'\n      >\n        <div\n          className='absolute top-0 right-0 translate-x-1/2 translate-y-[-50%] text-gray-600 dark:text-white cursor-pointer bg-gray-200 dark:bg-gray-600/80 rounded-full p-1 text-sm hover:bg-gray-300 dark:hover:bg-gray-800/80 transition-bg duration-200'\n          onClick={addMessage}\n        >\n          <PlusIcon />\n        </div>\n      </div>\n    );\n  }\n);\n\nexport default NewMessageButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/RoleSelector.tsx",
    "content": "import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\nimport { ChatInterface, Role, roles } from '@type/chat';\n\nimport useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';\n\nconst RoleSelector = React.memo(\n  ({\n    role,\n    messageIndex,\n    sticky,\n  }: {\n    role: Role;\n    messageIndex: number;\n    sticky?: boolean;\n  }) => {\n    const { t } = useTranslation();\n    const setInputRole = useStore((state) => state.setInputRole);\n    const setChats = useStore((state) => state.setChats);\n    const currentChatIndex = useStore((state) => state.currentChatIndex);\n\n    const [dropDown, setDropDown, dropDownRef] = useHideOnOutsideClick();\n\n    return (\n      <div className='prose dark:prose-invert relative'>\n        <button\n          className='btn btn-neutral btn-small flex gap-1'\n          aria-label={t(role) as string}\n          type='button'\n          onClick={() => setDropDown((prev) => !prev)}\n        >\n          {t(role)}\n          <DownChevronArrow />\n        </button>\n        <div\n          ref={dropDownRef}\n          id='dropdown'\n          className={`${\n            dropDown ? '' : 'hidden'\n          } absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}\n        >\n          <ul\n            className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'\n            aria-labelledby='dropdownDefaultButton'\n          >\n            {roles.map((r) => (\n              <li\n                className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'\n                onClick={() => {\n                  if (!sticky) {\n                    const updatedChats: ChatInterface[] = JSON.parse(\n                      JSON.stringify(useStore.getState().chats)\n                    );\n                    updatedChats[currentChatIndex].messages[messageIndex].role =\n                      r;\n                    setChats(updatedChats);\n                  } else {\n                    setInputRole(r);\n                  }\n                  setDropDown(false);\n                }}\n                key={r}\n              >\n                {t(r)}\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    );\n  }\n);\nexport default RoleSelector;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/BaseButton.tsx",
    "content": "import React from 'react';\n\nconst BaseButton = ({\n  onClick,\n  icon,\n  buttonProps,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n  icon: React.ReactElement;\n  buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;\n}) => {\n  return (\n    <div className='text-gray-400 flex self-end lg:self-center justify-center gap-3 md:gap-4  visible'>\n      <button\n        className='p-1 rounded-md hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible'\n        onClick={onClick}\n        {...buttonProps}\n      >\n        {icon}\n      </button>\n    </div>\n  );\n};\n\nexport default BaseButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/CopyButton.tsx",
    "content": "import React, { useState } from 'react';\n\nimport TickIcon from '@icon/TickIcon';\nimport CopyIcon from '@icon/CopyIcon';\n\nimport BaseButton from './BaseButton';\n\nconst CopyButton = ({\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n}) => {\n  const [isCopied, setIsCopied] = useState<boolean>(false);\n\n  return (\n    <BaseButton\n      icon={isCopied ? <TickIcon /> : <CopyIcon />}\n      buttonProps={{ 'aria-label': 'copy message' }}\n      onClick={(e) => {\n        onClick(e);\n        setIsCopied(true);\n        window.setTimeout(() => {\n          setIsCopied(false);\n        }, 3000);\n      }}\n    />\n  );\n};\n\nexport default CopyButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/DeleteButton.tsx",
    "content": "import React, { memo } from 'react';\n\nimport DeleteIcon from '@icon/DeleteIcon';\n\nimport BaseButton from './BaseButton';\n\nconst DeleteButton = memo(\n  ({\n    setIsDelete,\n  }: {\n    setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;\n  }) => {\n    return (\n      <BaseButton\n        icon={<DeleteIcon />}\n        buttonProps={{ 'aria-label': 'delete message' }}\n        onClick={() => setIsDelete(true)}\n      />\n    );\n  }\n);\n\nexport default DeleteButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/DownButton.tsx",
    "content": "import React from 'react';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\n\nimport BaseButton from './BaseButton';\n\nconst DownButton = ({\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n}) => {\n  return (\n    <BaseButton\n      icon={<DownChevronArrow />}\n      buttonProps={{ 'aria-label': 'shift message down' }}\n      onClick={onClick}\n    />\n  );\n};\n\nexport default DownButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/EditButton.tsx",
    "content": "import React, { memo } from 'react';\n\nimport EditIcon2 from '@icon/EditIcon2';\n\nimport BaseButton from './BaseButton';\n\nconst EditButton = memo(\n  ({\n    setIsEdit,\n  }: {\n    setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;\n  }) => {\n    return (\n      <BaseButton\n        icon={<EditIcon2 />}\n        buttonProps={{ 'aria-label': 'edit message' }}\n        onClick={() => setIsEdit(true)}\n      />\n    );\n  }\n);\n\nexport default EditButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/MarkdownModeButton.tsx",
    "content": "import React, { useState } from 'react';\n\nimport useStore from '@store/store';\n\nimport BaseButton from './BaseButton';\n\nimport MarkdownIcon from '@icon/MarkdownIcon';\nimport FileTextIcon from '@icon/FileTextIcon';\n\nconst MarkdownModeButton = () => {\n  const markdownMode = useStore((state) => state.markdownMode);\n  const setMarkdownMode = useStore((state) => state.setMarkdownMode);\n\n  return (\n    <BaseButton\n      icon={markdownMode ? <MarkdownIcon /> : <FileTextIcon />}\n      buttonProps={{ 'aria-label': 'toggle markdown mode' }}\n      onClick={() => {\n        setMarkdownMode(!markdownMode);\n      }}\n    />\n  );\n};\n\nexport default MarkdownModeButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/RefreshButton.tsx",
    "content": "import React from 'react';\n\nimport RefreshIcon from '@icon/RefreshIcon';\n\nimport BaseButton from './BaseButton';\n\nconst RefreshButton = ({\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n}) => {\n  return (\n    <BaseButton\n      icon={<RefreshIcon />}\n      buttonProps={{ 'aria-label': 'regenerate message' }}\n      onClick={onClick}\n    />\n  );\n};\n\nexport default RefreshButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/Button/UpButton.tsx",
    "content": "import React from 'react';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\n\nimport BaseButton from './BaseButton';\n\nconst UpButton = ({\n  onClick,\n}: {\n  onClick: React.MouseEventHandler<HTMLButtonElement>;\n}) => {\n  return (\n    <BaseButton\n      icon={<DownChevronArrow className='rotate-180' />}\n      buttonProps={{ 'aria-label': 'shift message up' }}\n      onClick={onClick}\n    />\n  );\n};\n\nexport default UpButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/ContentView.tsx",
    "content": "import React, {\n  DetailedHTMLProps,\n  HTMLAttributes,\n  memo,\n  useState,\n} from 'react';\n\nimport ReactMarkdown from 'react-markdown';\nimport { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react';\n\nimport rehypeKatex from 'rehype-katex';\nimport rehypeHighlight from 'rehype-highlight';\nimport remarkMath from 'remark-math';\nimport remarkGfm from 'remark-gfm';\nimport useStore from '@store/store';\n\nimport TickIcon from '@icon/TickIcon';\nimport CrossIcon from '@icon/CrossIcon';\n\nimport useSubmit from '@hooks/useSubmit';\n\nimport { ChatInterface } from '@type/chat';\n\nimport { codeLanguageSubset } from '@constants/chat';\n\nimport RefreshButton from './Button/RefreshButton';\nimport UpButton from './Button/UpButton';\nimport DownButton from './Button/DownButton';\nimport CopyButton from './Button/CopyButton';\nimport EditButton from './Button/EditButton';\nimport DeleteButton from './Button/DeleteButton';\nimport MarkdownModeButton from './Button/MarkdownModeButton';\n\nimport CodeBlock from '../CodeBlock';\n\nconst ContentView = memo(\n  ({\n    role,\n    content,\n    setIsEdit,\n    messageIndex,\n  }: {\n    role: string;\n    content: string;\n    setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;\n    messageIndex: number;\n  }) => {\n    const { handleSubmit } = useSubmit();\n\n    const [isDelete, setIsDelete] = useState<boolean>(false);\n\n    const currentChatIndex = useStore((state) => state.currentChatIndex);\n    const setChats = useStore((state) => state.setChats);\n    const lastMessageIndex = useStore((state) =>\n      state.chats ? state.chats[state.currentChatIndex].messages.length - 1 : 0\n    );\n    const inlineLatex = useStore((state) => state.inlineLatex);\n    const markdownMode = useStore((state) => state.markdownMode);\n\n    const handleDelete = () => {\n      const updatedChats: ChatInterface[] = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      updatedChats[currentChatIndex].messages.splice(messageIndex, 1);\n      setChats(updatedChats);\n    };\n\n    const handleMove = (direction: 'up' | 'down') => {\n      const updatedChats: ChatInterface[] = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      const updatedMessages = updatedChats[currentChatIndex].messages;\n      const temp = updatedMessages[messageIndex];\n      if (direction === 'up') {\n        updatedMessages[messageIndex] = updatedMessages[messageIndex - 1];\n        updatedMessages[messageIndex - 1] = temp;\n      } else {\n        updatedMessages[messageIndex] = updatedMessages[messageIndex + 1];\n        updatedMessages[messageIndex + 1] = temp;\n      }\n      setChats(updatedChats);\n    };\n\n    const handleMoveUp = () => {\n      handleMove('up');\n    };\n\n    const handleMoveDown = () => {\n      handleMove('down');\n    };\n\n    const handleRefresh = () => {\n      const updatedChats: ChatInterface[] = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      const updatedMessages = updatedChats[currentChatIndex].messages;\n      updatedMessages.splice(updatedMessages.length - 1, 1);\n      setChats(updatedChats);\n      handleSubmit();\n    };\n\n    const handleCopy = () => {\n      navigator.clipboard.writeText(content);\n    };\n\n    return (\n      <>\n        <div className='markdown prose w-full md:max-w-full break-words dark:prose-invert dark share-gpt-message'>\n          {markdownMode ? (\n            <ReactMarkdown\n              remarkPlugins={[\n                remarkGfm,\n                [remarkMath, { singleDollarTextMath: inlineLatex }],\n              ]}\n              rehypePlugins={[\n                rehypeKatex,\n                [\n                  rehypeHighlight,\n                  {\n                    detect: true,\n                    ignoreMissing: true,\n                    subset: codeLanguageSubset,\n                  },\n                ],\n              ]}\n              linkTarget='_new'\n              components={{\n                code,\n                p,\n              }}\n            >\n              {content}\n            </ReactMarkdown>\n          ) : (\n            <span className='whitespace-pre-wrap'>{content}</span>\n          )}\n        </div>\n        <div className='flex justify-end gap-2 w-full mt-2'>\n          {isDelete || (\n            <>\n              {!useStore.getState().generating &&\n                role === 'assistant' &&\n                messageIndex === lastMessageIndex && (\n                  <RefreshButton onClick={handleRefresh} />\n                )}\n              {messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}\n              {messageIndex !== lastMessageIndex && (\n                <DownButton onClick={handleMoveDown} />\n              )}\n\n              <MarkdownModeButton />\n              <CopyButton onClick={handleCopy} />\n              <EditButton setIsEdit={setIsEdit} />\n              <DeleteButton setIsDelete={setIsDelete} />\n            </>\n          )}\n          {isDelete && (\n            <>\n              <button\n                className='p-1 hover:text-white'\n                aria-label='cancel'\n                onClick={() => setIsDelete(false)}\n              >\n                <CrossIcon />\n              </button>\n              <button\n                className='p-1 hover:text-white'\n                aria-label='confirm'\n                onClick={handleDelete}\n              >\n                <TickIcon />\n              </button>\n            </>\n          )}\n        </div>\n      </>\n    );\n  }\n);\n\nconst code = memo((props: CodeProps) => {\n  const { inline, className, children } = props;\n  const match = /language-(\\w+)/.exec(className || '');\n  const lang = match && match[1];\n\n  if (inline) {\n    return <code className={className}>{children}</code>;\n  } else {\n    return <CodeBlock lang={lang || 'text'} codeChildren={children} />;\n  }\n});\n\nconst p = memo(\n  (\n    props?: Omit<\n      DetailedHTMLProps<\n        HTMLAttributes<HTMLParagraphElement>,\n        HTMLParagraphElement\n      >,\n      'ref'\n    > &\n      ReactMarkdownProps\n  ) => {\n    return <p className='whitespace-pre-wrap'>{props?.children}</p>;\n  }\n);\n\nexport default ContentView;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/View/EditView.tsx",
    "content": "import React, { memo, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport useSubmit from '@hooks/useSubmit';\n\nimport { ChatInterface } from '@type/chat';\n\nimport PopupModal from '@components/PopupModal';\nimport TokenCount from '@components/TokenCount';\nimport CommandPrompt from '../CommandPrompt';\n\nconst EditView = ({\n  content,\n  setIsEdit,\n  messageIndex,\n  sticky,\n}: {\n  content: string;\n  setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;\n  messageIndex: number;\n  sticky?: boolean;\n}) => {\n  const inputRole = useStore((state) => state.inputRole);\n  const setChats = useStore((state) => state.setChats);\n  const currentChatIndex = useStore((state) => state.currentChatIndex);\n\n  const [_content, _setContent] = useState<string>(content);\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n  const textareaRef = React.createRef<HTMLTextAreaElement>();\n\n  const { t } = useTranslation();\n\n  const resetTextAreaHeight = () => {\n    if (textareaRef.current) textareaRef.current.style.height = 'auto';\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    const isMobile =\n      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|playbook|silk/i.test(\n        navigator.userAgent\n      );\n\n    if (e.key === 'Enter' && !isMobile && !e.nativeEvent.isComposing) {\n      const enterToSubmit = useStore.getState().enterToSubmit;\n\n      if (e.ctrlKey && e.shiftKey) {\n        e.preventDefault();\n        handleGenerate();\n        resetTextAreaHeight();\n      } else if (\n        (enterToSubmit && !e.shiftKey) ||\n        (!enterToSubmit && (e.ctrlKey || e.shiftKey))\n      ) {\n        if (sticky) {\n          e.preventDefault();\n          handleGenerate();\n          resetTextAreaHeight();\n        } else {\n          handleSave();\n        }\n      }\n    }\n  };\n\n  const handleSave = () => {\n    if (sticky && (_content === '' || useStore.getState().generating)) return;\n    const updatedChats: ChatInterface[] = JSON.parse(\n      JSON.stringify(useStore.getState().chats)\n    );\n    const updatedMessages = updatedChats[currentChatIndex].messages;\n    if (sticky) {\n      updatedMessages.push({ role: inputRole, content: _content });\n      _setContent('');\n      resetTextAreaHeight();\n    } else {\n      updatedMessages[messageIndex].content = _content;\n      setIsEdit(false);\n    }\n    setChats(updatedChats);\n  };\n\n  const { handleSubmit } = useSubmit();\n  const handleGenerate = () => {\n    if (useStore.getState().generating) return;\n    const updatedChats: ChatInterface[] = JSON.parse(\n      JSON.stringify(useStore.getState().chats)\n    );\n    const updatedMessages = updatedChats[currentChatIndex].messages;\n    if (sticky) {\n      if (_content !== '') {\n        updatedMessages.push({ role: inputRole, content: _content });\n      }\n      _setContent('');\n      resetTextAreaHeight();\n    } else {\n      updatedMessages[messageIndex].content = _content;\n      updatedChats[currentChatIndex].messages = updatedMessages.slice(\n        0,\n        messageIndex + 1\n      );\n      setIsEdit(false);\n    }\n    setChats(updatedChats);\n    handleSubmit();\n  };\n\n  useEffect(() => {\n    if (textareaRef.current) {\n      textareaRef.current.style.height = 'auto';\n      textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;\n    }\n  }, [_content]);\n\n  useEffect(() => {\n    if (textareaRef.current) {\n      textareaRef.current.style.height = 'auto';\n      textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;\n    }\n  }, []);\n\n  return (\n    <>\n      <div\n        className={`w-full ${\n          sticky\n            ? 'py-2 md:py-3 px-2 md:px-4 border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]'\n            : ''\n        }`}\n      >\n        <textarea\n          ref={textareaRef}\n          className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden focus:ring-0 focus-visible:ring-0 leading-7 w-full placeholder:text-gray-500/40'\n          onChange={(e) => {\n            _setContent(e.target.value);\n          }}\n          value={_content}\n          placeholder={t('submitPlaceholder') as string}\n          onKeyDown={handleKeyDown}\n          rows={1}\n        ></textarea>\n      </div>\n      <EditViewButtons\n        sticky={sticky}\n        handleGenerate={handleGenerate}\n        handleSave={handleSave}\n        setIsModalOpen={setIsModalOpen}\n        setIsEdit={setIsEdit}\n        _setContent={_setContent}\n      />\n      {isModalOpen && (\n        <PopupModal\n          setIsModalOpen={setIsModalOpen}\n          title={t('warning') as string}\n          message={t('clearMessageWarning') as string}\n          handleConfirm={handleGenerate}\n        />\n      )}\n    </>\n  );\n};\n\nconst EditViewButtons = memo(\n  ({\n    sticky = false,\n    handleGenerate,\n    handleSave,\n    setIsModalOpen,\n    setIsEdit,\n    _setContent,\n  }: {\n    sticky?: boolean;\n    handleGenerate: () => void;\n    handleSave: () => void;\n    setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n    setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;\n    _setContent: React.Dispatch<React.SetStateAction<string>>;\n  }) => {\n    const { t } = useTranslation();\n    const generating = useStore.getState().generating;\n    const advancedMode = useStore((state) => state.advancedMode);\n\n    return (\n      <div className='flex'>\n        <div className='flex-1 text-center mt-2 flex justify-center'>\n          {sticky && (\n            <button\n              className={`btn relative mr-2 btn-primary ${\n                generating ? 'cursor-not-allowed opacity-40' : ''\n              }`}\n              onClick={handleGenerate}\n              aria-label={t('generate') as string}\n            >\n              <div className='flex items-center justify-center gap-2'>\n                {t('generate')}\n              </div>\n            </button>\n          )}\n\n          {sticky || (\n            <button\n              className='btn relative mr-2 btn-primary'\n              onClick={() => {\n                !generating && setIsModalOpen(true);\n              }}\n            >\n              <div className='flex items-center justify-center gap-2'>\n                {t('generate')}\n              </div>\n            </button>\n          )}\n\n          <button\n            className={`btn relative mr-2 ${\n              sticky\n                ? `btn-neutral ${\n                    generating ? 'cursor-not-allowed opacity-40' : ''\n                  }`\n                : 'btn-neutral'\n            }`}\n            onClick={handleSave}\n            aria-label={t('save') as string}\n          >\n            <div className='flex items-center justify-center gap-2'>\n              {t('save')}\n            </div>\n          </button>\n\n          {sticky || (\n            <button\n              className='btn relative btn-neutral'\n              onClick={() => setIsEdit(false)}\n              aria-label={t('cancel') as string}\n            >\n              <div className='flex items-center justify-center gap-2'>\n                {t('cancel')}\n              </div>\n            </button>\n          )}\n        </div>\n        {sticky && advancedMode && <TokenCount />}\n        <CommandPrompt _setContent={_setContent} />\n      </div>\n    );\n  }\n);\n\nexport default EditView;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/Message/index.ts",
    "content": "export { default } from './Message';\n"
  },
  {
    "path": "src/components/Chat/ChatContent/ScrollToBottomButton.tsx",
    "content": "import React from 'react';\nimport { useAtBottom, useScrollToBottom } from 'react-scroll-to-bottom';\n\nimport DownArrow from '@icon/DownArrow';\n\nconst ScrollToBottomButton = React.memo(() => {\n  const scrollToBottom = useScrollToBottom();\n  const [atBottom] = useAtBottom();\n\n  return (\n    <button\n      className={`cursor-pointer absolute right-6 bottom-[60px] md:bottom-[60px] z-10 rounded-full border border-gray-200 bg-gray-50 text-gray-600 dark:border-white/10 dark:bg-white/10 dark:text-gray-200 ${\n        atBottom ? 'hidden' : ''\n      }`}\n      aria-label='scroll to bottom'\n      onClick={scrollToBottom}\n    >\n      <DownArrow />\n    </button>\n  );\n});\n\nexport default ScrollToBottomButton;\n"
  },
  {
    "path": "src/components/Chat/ChatContent/index.ts",
    "content": "export { default } from './ChatContent';\n"
  },
  {
    "path": "src/components/Chat/ChatInput.tsx",
    "content": "import React from 'react';\nimport RefreshIcon from '@icon/RefreshIcon';\nimport SendIcon from '@icon/SendIcon';\n\nconst ChatInput = () => {\n  return (\n    <div className='w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent md:bg-vert-light-gradient bg-white dark:bg-gray-800 md:!bg-transparent dark:md:bg-vert-dark-gradient'>\n      <form className='stretch mx-2 flex flex-row gap-3 pt-2 last:mb-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6'>\n        <div className='relative flex h-full flex-1 md:flex-col'>\n          <TextField />\n        </div>\n      </form>\n    </div>\n  );\n};\n\nconst TextField = () => {\n  return (\n    <div className='flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]'>\n      <textarea\n        tabIndex={0}\n        data-id='2557e994-6f98-4656-a955-7808084f8b8c'\n        rows={1}\n        className='m-0 w-full resize-none border-0 bg-transparent p-0 pl-2 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent md:pl-0'\n        style={{ maxHeight: '200px', height: '24px', overflowY: 'hidden' }}\n      ></textarea>\n      <button\n        className='absolute p-1 rounded-md text-gray-500 bottom-1.5 right-1 md:bottom-2.5 md:right-2 hover:bg-gray-100 dark:hover:text-gray-400 dark:hover:bg-gray-900 disabled:hover:bg-transparent dark:disabled:hover:bg-transparent'\n        aria-label='submit'\n      >\n        <SendIcon />\n      </button>\n    </div>\n  );\n};\n\nexport default ChatInput;\n"
  },
  {
    "path": "src/components/Chat/index.ts",
    "content": "export { default } from './Chat';\n"
  },
  {
    "path": "src/components/ChatConfigMenu/ChatConfigMenu.tsx",
    "content": "import React, { useState } from 'react';\nimport useStore from '@store/store';\nimport { useTranslation } from 'react-i18next';\n\nimport PopupModal from '@components/PopupModal';\nimport {\n  FrequencyPenaltySlider,\n  MaxTokenSlider,\n  ModelSelector,\n  PresencePenaltySlider,\n  TemperatureSlider,\n  TopPSlider,\n} from '@components/ConfigMenu/ConfigMenu';\n\nimport { ModelOptions } from '@type/chat';\nimport { _defaultChatConfig, _defaultSystemMessage } from '@constants/chat';\n\nconst ChatConfigMenu = () => {\n  const { t } = useTranslation('model');\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n  return (\n    <div>\n      <button\n        className='btn btn-neutral'\n        onClick={() => setIsModalOpen(true)}\n        aria-label={t('defaultChatConfig') as string}\n      >\n        {t('defaultChatConfig')}\n      </button>\n      {isModalOpen && <ChatConfigPopup setIsModalOpen={setIsModalOpen} />}\n    </div>\n  );\n};\n\nconst ChatConfigPopup = ({\n  setIsModalOpen,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  const config = useStore.getState().defaultChatConfig;\n  const setDefaultChatConfig = useStore((state) => state.setDefaultChatConfig);\n  const setDefaultSystemMessage = useStore(\n    (state) => state.setDefaultSystemMessage\n  );\n\n  const [_systemMessage, _setSystemMessage] = useState<string>(\n    useStore.getState().defaultSystemMessage\n  );\n  const [_model, _setModel] = useState<ModelOptions>(config.model);\n  const [_maxToken, _setMaxToken] = useState<number>(config.max_tokens);\n  const [_temperature, _setTemperature] = useState<number>(config.temperature);\n  const [_topP, _setTopP] = useState<number>(config.top_p);\n  const [_presencePenalty, _setPresencePenalty] = useState<number>(\n    config.presence_penalty\n  );\n  const [_frequencyPenalty, _setFrequencyPenalty] = useState<number>(\n    config.frequency_penalty\n  );\n\n  const { t } = useTranslation('model');\n\n  const handleSave = () => {\n    setDefaultChatConfig({\n      model: _model,\n      max_tokens: _maxToken,\n      temperature: _temperature,\n      top_p: _topP,\n      presence_penalty: _presencePenalty,\n      frequency_penalty: _frequencyPenalty,\n    });\n    setDefaultSystemMessage(_systemMessage);\n    setIsModalOpen(false);\n  };\n\n  const handleReset = () => {\n    _setModel(_defaultChatConfig.model);\n    _setMaxToken(_defaultChatConfig.max_tokens);\n    _setTemperature(_defaultChatConfig.temperature);\n    _setTopP(_defaultChatConfig.top_p);\n    _setPresencePenalty(_defaultChatConfig.presence_penalty);\n    _setFrequencyPenalty(_defaultChatConfig.frequency_penalty);\n    _setSystemMessage(_defaultSystemMessage);\n  };\n\n  return (\n    <PopupModal\n      title={t('defaultChatConfig') as string}\n      setIsModalOpen={setIsModalOpen}\n      handleConfirm={handleSave}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'>\n        <DefaultSystemChat\n          _systemMessage={_systemMessage}\n          _setSystemMessage={_setSystemMessage}\n        />\n        <ModelSelector _model={_model} _setModel={_setModel} />\n        <MaxTokenSlider\n          _maxToken={_maxToken}\n          _setMaxToken={_setMaxToken}\n          _model={_model}\n        />\n        <TemperatureSlider\n          _temperature={_temperature}\n          _setTemperature={_setTemperature}\n        />\n        <TopPSlider _topP={_topP} _setTopP={_setTopP} />\n        <PresencePenaltySlider\n          _presencePenalty={_presencePenalty}\n          _setPresencePenalty={_setPresencePenalty}\n        />\n        <FrequencyPenaltySlider\n          _frequencyPenalty={_frequencyPenalty}\n          _setFrequencyPenalty={_setFrequencyPenalty}\n        />\n        <div\n          className='btn btn-neutral cursor-pointer mt-5'\n          onClick={handleReset}\n        >\n          {t('resetToDefault')}\n        </div>\n      </div>\n    </PopupModal>\n  );\n};\n\nconst DefaultSystemChat = ({\n  _systemMessage,\n  _setSystemMessage,\n}: {\n  _systemMessage: string;\n  _setSystemMessage: React.Dispatch<React.SetStateAction<string>>;\n}) => {\n  const { t } = useTranslation('model');\n\n  const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    e.target.style.height = 'auto';\n    e.target.style.height = `${e.target.scrollHeight}px`;\n    e.target.style.maxHeight = `${e.target.scrollHeight}px`;\n  };\n\n  const handleOnFocus = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {\n    e.target.style.height = 'auto';\n    e.target.style.height = `${e.target.scrollHeight}px`;\n    e.target.style.maxHeight = `${e.target.scrollHeight}px`;\n  };\n\n  const handleOnBlur = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {\n    e.target.style.height = 'auto';\n    e.target.style.maxHeight = '2.5rem';\n  };\n\n  return (\n    <div>\n      <div className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('defaultSystemMessage')}\n      </div>\n      <textarea\n        className='my-2 mx-0 px-2 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 border border-gray-400/50 focus:ring-1 focus:ring-blue w-full max-h-10 transition-all'\n        onFocus={handleOnFocus}\n        onBlur={handleOnBlur}\n        onChange={(e) => {\n          _setSystemMessage(e.target.value);\n        }}\n        onInput={handleInput}\n        value={_systemMessage}\n        rows={1}\n      ></textarea>\n    </div>\n  );\n};\n\nexport default ChatConfigMenu;\n"
  },
  {
    "path": "src/components/ChatConfigMenu/index.ts",
    "content": "export { default } from './ChatConfigMenu';\n"
  },
  {
    "path": "src/components/ConfigMenu/ConfigMenu.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport useStore from '@store/store';\nimport { useTranslation } from 'react-i18next';\nimport PopupModal from '@components/PopupModal';\nimport { ConfigInterface, ModelOptions } from '@type/chat';\nimport DownChevronArrow from '@icon/DownChevronArrow';\nimport { modelMaxToken, modelOptions } from '@constants/chat';\n\nconst ConfigMenu = ({\n  setIsModalOpen,\n  config,\n  setConfig,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  config: ConfigInterface;\n  setConfig: (config: ConfigInterface) => void;\n}) => {\n  const [_maxToken, _setMaxToken] = useState<number>(config.max_tokens);\n  const [_model, _setModel] = useState<ModelOptions>(config.model);\n  const [_temperature, _setTemperature] = useState<number>(config.temperature);\n  const [_presencePenalty, _setPresencePenalty] = useState<number>(\n    config.presence_penalty\n  );\n  const [_topP, _setTopP] = useState<number>(config.top_p);\n  const [_frequencyPenalty, _setFrequencyPenalty] = useState<number>(\n    config.frequency_penalty\n  );\n  const { t } = useTranslation('model');\n\n  const handleConfirm = () => {\n    setConfig({\n      max_tokens: _maxToken,\n      model: _model,\n      temperature: _temperature,\n      presence_penalty: _presencePenalty,\n      top_p: _topP,\n      frequency_penalty: _frequencyPenalty,\n    });\n    setIsModalOpen(false);\n  };\n\n  return (\n    <PopupModal\n      title={t('configuration') as string}\n      setIsModalOpen={setIsModalOpen}\n      handleConfirm={handleConfirm}\n      handleClickBackdrop={handleConfirm}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n        <ModelSelector _model={_model} _setModel={_setModel} />\n        <MaxTokenSlider\n          _maxToken={_maxToken}\n          _setMaxToken={_setMaxToken}\n          _model={_model}\n        />\n        <TemperatureSlider\n          _temperature={_temperature}\n          _setTemperature={_setTemperature}\n        />\n        <TopPSlider _topP={_topP} _setTopP={_setTopP} />\n        <PresencePenaltySlider\n          _presencePenalty={_presencePenalty}\n          _setPresencePenalty={_setPresencePenalty}\n        />\n        <FrequencyPenaltySlider\n          _frequencyPenalty={_frequencyPenalty}\n          _setFrequencyPenalty={_setFrequencyPenalty}\n        />\n      </div>\n    </PopupModal>\n  );\n};\n\nexport const ModelSelector = ({\n  _model,\n  _setModel,\n}: {\n  _model: ModelOptions;\n  _setModel: React.Dispatch<React.SetStateAction<ModelOptions>>;\n}) => {\n  const [dropDown, setDropDown] = useState<boolean>(false);\n\n  return (\n    <div className='mb-4'>\n      <button\n        className='btn btn-neutral btn-small flex gap-1'\n        type='button'\n        onClick={() => setDropDown((prev) => !prev)}\n        aria-label='model'\n      >\n        {_model}\n        <DownChevronArrow />\n      </button>\n      <div\n        id='dropdown'\n        className={`${\n          dropDown ? '' : 'hidden'\n        } absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}\n      >\n        <ul\n          className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'\n          aria-labelledby='dropdownDefaultButton'\n        >\n          {modelOptions.map((m) => (\n            <li\n              className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'\n              onClick={() => {\n                _setModel(m);\n                setDropDown(false);\n              }}\n              key={m}\n            >\n              {m}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n};\n\nexport const MaxTokenSlider = ({\n  _maxToken,\n  _setMaxToken,\n  _model,\n}: {\n  _maxToken: number;\n  _setMaxToken: React.Dispatch<React.SetStateAction<number>>;\n  _model: ModelOptions;\n}) => {\n  const { t } = useTranslation('model');\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    inputRef &&\n      inputRef.current &&\n      _setMaxToken(Number(inputRef.current.value));\n  }, [_model]);\n\n  return (\n    <div>\n      <label className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('token.label')}: {_maxToken}\n      </label>\n      <input\n        type='range'\n        ref={inputRef}\n        value={_maxToken}\n        onChange={(e) => {\n          _setMaxToken(Number(e.target.value));\n        }}\n        min={0}\n        max={modelMaxToken[_model]}\n        step={1}\n        className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'\n      />\n      <div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>\n        {t('token.description')}\n      </div>\n    </div>\n  );\n};\n\nexport const TemperatureSlider = ({\n  _temperature,\n  _setTemperature,\n}: {\n  _temperature: number;\n  _setTemperature: React.Dispatch<React.SetStateAction<number>>;\n}) => {\n  const { t } = useTranslation('model');\n\n  return (\n    <div className='mt-5 pt-5 border-t border-gray-500'>\n      <label className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('temperature.label')}: {_temperature}\n      </label>\n      <input\n        id='default-range'\n        type='range'\n        value={_temperature}\n        onChange={(e) => {\n          _setTemperature(Number(e.target.value));\n        }}\n        min={0}\n        max={2}\n        step={0.1}\n        className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'\n      />\n      <div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>\n        {t('temperature.description')}\n      </div>\n    </div>\n  );\n};\n\nexport const TopPSlider = ({\n  _topP,\n  _setTopP,\n}: {\n  _topP: number;\n  _setTopP: React.Dispatch<React.SetStateAction<number>>;\n}) => {\n  const { t } = useTranslation('model');\n\n  return (\n    <div className='mt-5 pt-5 border-t border-gray-500'>\n      <label className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('topP.label')}: {_topP}\n      </label>\n      <input\n        id='default-range'\n        type='range'\n        value={_topP}\n        onChange={(e) => {\n          _setTopP(Number(e.target.value));\n        }}\n        min={0}\n        max={1}\n        step={0.05}\n        className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'\n      />\n      <div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>\n        {t('topP.description')}\n      </div>\n    </div>\n  );\n};\n\nexport const PresencePenaltySlider = ({\n  _presencePenalty,\n  _setPresencePenalty,\n}: {\n  _presencePenalty: number;\n  _setPresencePenalty: React.Dispatch<React.SetStateAction<number>>;\n}) => {\n  const { t } = useTranslation('model');\n\n  return (\n    <div className='mt-5 pt-5 border-t border-gray-500'>\n      <label className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('presencePenalty.label')}: {_presencePenalty}\n      </label>\n      <input\n        id='default-range'\n        type='range'\n        value={_presencePenalty}\n        onChange={(e) => {\n          _setPresencePenalty(Number(e.target.value));\n        }}\n        min={-2}\n        max={2}\n        step={0.1}\n        className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'\n      />\n      <div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>\n        {t('presencePenalty.description')}\n      </div>\n    </div>\n  );\n};\n\nexport const FrequencyPenaltySlider = ({\n  _frequencyPenalty,\n  _setFrequencyPenalty,\n}: {\n  _frequencyPenalty: number;\n  _setFrequencyPenalty: React.Dispatch<React.SetStateAction<number>>;\n}) => {\n  const { t } = useTranslation('model');\n\n  return (\n    <div className='mt-5 pt-5 border-t border-gray-500'>\n      <label className='block text-sm font-medium text-gray-900 dark:text-white'>\n        {t('frequencyPenalty.label')}: {_frequencyPenalty}\n      </label>\n      <input\n        id='default-range'\n        type='range'\n        value={_frequencyPenalty}\n        onChange={(e) => {\n          _setFrequencyPenalty(Number(e.target.value));\n        }}\n        min={-2}\n        max={2}\n        step={0.1}\n        className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'\n      />\n      <div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>\n        {t('frequencyPenalty.description')}\n      </div>\n    </div>\n  );\n};\n\nexport default ConfigMenu;\n"
  },
  {
    "path": "src/components/ConfigMenu/index.ts",
    "content": "export { default } from './ConfigMenu';\n"
  },
  {
    "path": "src/components/GoogleSync/GoogleSync.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { GoogleOAuthProvider } from '@react-oauth/google';\nimport { useTranslation } from 'react-i18next';\n\nimport useStore from '@store/store';\nimport useGStore from '@store/cloud-auth-store';\n\nimport {\n  createDriveFile,\n  deleteDriveFile,\n  updateDriveFileName,\n  validateGoogleOath2AccessToken,\n} from '@api/google-api';\nimport { getFiles, stateToFile } from '@utils/google-api';\nimport createGoogleCloudStorage from '@store/storage/GoogleCloudStorage';\n\nimport GoogleSyncButton from './GoogleSyncButton';\nimport PopupModal from '@components/PopupModal';\n\nimport GoogleIcon from '@icon/GoogleIcon';\nimport TickIcon from '@icon/TickIcon';\nimport RefreshIcon from '@icon/RefreshIcon';\n\nimport { GoogleFileResource, SyncStatus } from '@type/google-api';\nimport EditIcon from '@icon/EditIcon';\nimport CrossIcon from '@icon/CrossIcon';\nimport DeleteIcon from '@icon/DeleteIcon';\n\nconst GoogleSync = ({ clientId }: { clientId: string }) => {\n  const { t } = useTranslation(['drive']);\n\n  const fileId = useGStore((state) => state.fileId);\n  const setFileId = useGStore((state) => state.setFileId);\n  const googleAccessToken = useGStore((state) => state.googleAccessToken);\n  const syncStatus = useGStore((state) => state.syncStatus);\n  const cloudSync = useGStore((state) => state.cloudSync);\n  const setSyncStatus = useGStore((state) => state.setSyncStatus);\n\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(cloudSync);\n  const [files, setFiles] = useState<GoogleFileResource[]>([]);\n\n  const initialiseState = async (_googleAccessToken: string) => {\n    const validated = await validateGoogleOath2AccessToken(_googleAccessToken);\n    if (validated) {\n      try {\n        const _files = await getFiles(_googleAccessToken);\n        if (_files) {\n          setFiles(_files);\n          if (_files.length === 0) {\n            // _files is empty, create new file in google drive and set the file id\n            const googleFile = await createDriveFile(\n              stateToFile(),\n              _googleAccessToken\n            );\n            setFileId(googleFile.id);\n          } else {\n            if (_files.findIndex((f) => f.id === fileId) !== -1) {\n              // local storage file id matches one of the file ids returned\n              setFileId(fileId);\n            } else {\n              // default set file id to the latest one\n              setFileId(_files[0].id);\n            }\n          }\n          useStore.persist.setOptions({\n            storage: createGoogleCloudStorage(),\n          });\n          useStore.persist.rehydrate();\n        }\n      } catch (e: unknown) {\n        console.log(e);\n      }\n    } else {\n      setSyncStatus('unauthenticated');\n    }\n  };\n\n  useEffect(() => {\n    if (googleAccessToken) {\n      setSyncStatus('syncing');\n      initialiseState(googleAccessToken);\n    }\n  }, [googleAccessToken]);\n\n  return (\n    <GoogleOAuthProvider clientId={clientId}>\n      <div\n        className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n      >\n        <GoogleIcon /> {t('name')}\n        {cloudSync && <SyncIcon status={syncStatus} />}\n      </div>\n      {isModalOpen && (\n        <GooglePopup\n          setIsModalOpen={setIsModalOpen}\n          files={files}\n          setFiles={setFiles}\n        />\n      )}\n    </GoogleOAuthProvider>\n  );\n};\n\nconst GooglePopup = ({\n  setIsModalOpen,\n  files,\n  setFiles,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  files: GoogleFileResource[];\n  setFiles: React.Dispatch<React.SetStateAction<GoogleFileResource[]>>;\n}) => {\n  const { t } = useTranslation(['drive']);\n\n  const syncStatus = useGStore((state) => state.syncStatus);\n  const setSyncStatus = useGStore((state) => state.setSyncStatus);\n  const cloudSync = useGStore((state) => state.cloudSync);\n  const googleAccessToken = useGStore((state) => state.googleAccessToken);\n  const setFileId = useGStore((state) => state.setFileId);\n\n  const setToastStatus = useStore((state) => state.setToastStatus);\n  const setToastMessage = useStore((state) => state.setToastMessage);\n  const setToastShow = useStore((state) => state.setToastShow);\n\n  const [_fileId, _setFileId] = useState<string>(\n    useGStore.getState().fileId || ''\n  );\n\n  const createSyncFile = async () => {\n    if (!googleAccessToken) return;\n    try {\n      setSyncStatus('syncing');\n      await createDriveFile(stateToFile(), googleAccessToken);\n      const _files = await getFiles(googleAccessToken);\n      if (_files) setFiles(_files);\n      setSyncStatus('synced');\n    } catch (e: unknown) {\n      setSyncStatus('unauthenticated');\n      setToastMessage((e as Error).message);\n      setToastShow(true);\n      setToastStatus('error');\n    }\n  };\n\n  return (\n    <PopupModal\n      title={t('name') as string}\n      setIsModalOpen={setIsModalOpen}\n      cancelButton={false}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600 text-gray-900 dark:text-gray-300 text-sm flex flex-col items-center gap-4 text-center'>\n        <p>{t('tagline')}</p>\n        <GoogleSyncButton\n          loginHandler={() => {\n            setIsModalOpen(false);\n            window.setTimeout(() => {\n              setIsModalOpen(true);\n            }, 3540000); // timeout - 3540000ms = 59 min (access token last 60 min)\n          }}\n        />\n        <p className='border border-gray-400 px-3 py-2 rounded-md'>\n          {t('notice')}\n        </p>\n        {cloudSync && syncStatus !== 'unauthenticated' && (\n          <div className='flex flex-col gap-2 items-center'>\n            {files.map((file) => (\n              <FileSelector\n                id={file.id}\n                name={file.name}\n                _fileId={_fileId}\n                _setFileId={_setFileId}\n                setFiles={setFiles}\n                key={file.id}\n              />\n            ))}\n            {syncStatus !== 'syncing' && (\n              <div className='flex gap-4 flex-wrap justify-center'>\n                <div\n                  className='btn btn-primary cursor-pointer'\n                  onClick={async () => {\n                    setFileId(_fileId);\n                    await useStore.persist.rehydrate();\n                    setToastStatus('success');\n                    setToastMessage(t('toast.sync'));\n                    setToastShow(true);\n                    setIsModalOpen(false);\n                  }}\n                >\n                  {t('button.confirm')}\n                </div>\n                <div\n                  className='btn btn-neutral cursor-pointer'\n                  onClick={createSyncFile}\n                >\n                  {t('button.create')}\n                </div>\n              </div>\n            )}\n            <div className='h-4 w-4'>\n              {syncStatus === 'syncing' && <SyncIcon status='syncing' />}\n            </div>\n          </div>\n        )}\n        <p>{t('privacy')}</p>\n      </div>\n    </PopupModal>\n  );\n};\n\nconst FileSelector = ({\n  name,\n  id,\n  _fileId,\n  _setFileId,\n  setFiles,\n}: {\n  name: string;\n  id: string;\n  _fileId: string;\n  _setFileId: React.Dispatch<React.SetStateAction<string>>;\n  setFiles: React.Dispatch<React.SetStateAction<GoogleFileResource[]>>;\n}) => {\n  const syncStatus = useGStore((state) => state.syncStatus);\n  const setSyncStatus = useGStore((state) => state.setSyncStatus);\n\n  const setToastStatus = useStore((state) => state.setToastStatus);\n  const setToastMessage = useStore((state) => state.setToastMessage);\n  const setToastShow = useStore((state) => state.setToastShow);\n\n  const [isEditing, setIsEditing] = useState<boolean>(false);\n  const [isDeleting, setIsDeleting] = useState<boolean>(false);\n  const [_name, _setName] = useState<string>(name);\n\n  const syncing = syncStatus === 'syncing';\n\n  const updateFileName = async () => {\n    if (syncing) return;\n    setIsEditing(false);\n    const accessToken = useGStore.getState().googleAccessToken;\n    if (!accessToken) return;\n\n    try {\n      setSyncStatus('syncing');\n      const newFileName = _name.endsWith('.json') ? _name : _name + '.json';\n      await updateDriveFileName(newFileName, id, accessToken);\n      const _files = await getFiles(accessToken);\n      if (_files) setFiles(_files);\n      setSyncStatus('synced');\n    } catch (e: unknown) {\n      setSyncStatus('unauthenticated');\n      setToastMessage((e as Error).message);\n      setToastShow(true);\n      setToastStatus('error');\n    }\n  };\n\n  const deleteFile = async () => {\n    if (syncing) return;\n    setIsDeleting(false);\n    const accessToken = useGStore.getState().googleAccessToken;\n    if (!accessToken) return;\n\n    try {\n      setSyncStatus('syncing');\n      await deleteDriveFile(id, accessToken);\n      const _files = await getFiles(accessToken);\n      if (_files) setFiles(_files);\n      setSyncStatus('synced');\n    } catch (e: unknown) {\n      setSyncStatus('unauthenticated');\n      setToastMessage((e as Error).message);\n      setToastShow(true);\n      setToastStatus('error');\n    }\n  };\n\n  return (\n    <label\n      className={`w-full flex items-center justify-between mb-2 gap-2 text-sm font-medium text-gray-900 dark:text-gray-300 ${\n        syncing ? 'cursor-not-allowed opacity-40' : ''\n      }`}\n    >\n      <input\n        type='radio'\n        checked={_fileId === id}\n        className='w-4 h-4'\n        onChange={() => {\n          if (!syncing) _setFileId(id);\n        }}\n        disabled={syncing}\n      />\n      <div className='flex-1 text-left'>\n        {isEditing ? (\n          <input\n            type='text'\n            className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'\n            value={_name}\n            onChange={(e) => {\n              _setName(e.target.value);\n            }}\n          />\n        ) : (\n          <>\n            {name} <div className='text-[10px] md:text-xs'>{`<${id}>`}</div>\n          </>\n        )}\n      </div>\n      {isEditing || isDeleting ? (\n        <div className='flex gap-1'>\n          <div\n            className={`${syncing ? 'cursor-not-allowed' : 'cursor-pointer'}`}\n            onClick={() => {\n              if (isEditing) updateFileName();\n              if (isDeleting) deleteFile();\n            }}\n          >\n            <TickIcon />\n          </div>\n          <div\n            className={`${syncing ? 'cursor-not-allowed' : 'cursor-pointer'}`}\n            onClick={() => {\n              if (!syncing) {\n                setIsEditing(false);\n                setIsDeleting(false);\n              }\n            }}\n          >\n            <CrossIcon />\n          </div>\n        </div>\n      ) : (\n        <div className='flex gap-1'>\n          <div\n            className={`${syncing ? 'cursor-not-allowed' : 'cursor-pointer'}`}\n            onClick={() => {\n              if (!syncing) setIsEditing(true);\n            }}\n          >\n            <EditIcon />\n          </div>\n          <div\n            className={`${syncing ? 'cursor-not-allowed' : 'cursor-pointer'}`}\n            onClick={() => {\n              if (!syncing) setIsDeleting(true);\n            }}\n          >\n            <DeleteIcon />\n          </div>\n        </div>\n      )}\n    </label>\n  );\n};\n\nconst SyncIcon = ({ status }: { status: SyncStatus }) => {\n  const statusToIcon = {\n    unauthenticated: (\n      <div className='bg-red-600/80 rounded-full w-4 h-4 text-xs flex justify-center items-center'>\n        !\n      </div>\n    ),\n    syncing: (\n      <div className='bg-gray-600/80 rounded-full p-1 animate-spin'>\n        <RefreshIcon className='h-2 w-2' />\n      </div>\n    ),\n    synced: (\n      <div className='bg-gray-600/80 rounded-full p-1'>\n        <TickIcon className='h-2 w-2' />\n      </div>\n    ),\n  };\n  return statusToIcon[status] || null;\n};\n\nexport default GoogleSync;\n"
  },
  {
    "path": "src/components/GoogleSync/GoogleSyncButton.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useGoogleLogin, googleLogout } from '@react-oauth/google';\nimport useGStore from '@store/cloud-auth-store';\nimport useStore from '@store/store';\nimport { createJSONStorage } from 'zustand/middleware';\n\nconst GoogleSyncButton = ({ loginHandler }: { loginHandler?: () => void }) => {\n  const { t } = useTranslation(['drive']);\n\n  const setGoogleAccessToken = useGStore((state) => state.setGoogleAccessToken);\n  const setSyncStatus = useGStore((state) => state.setSyncStatus);\n  const setCloudSync = useGStore((state) => state.setCloudSync);\n  const cloudSync = useGStore((state) => state.cloudSync);\n\n  const setToastStatus = useStore((state) => state.setToastStatus);\n  const setToastMessage = useStore((state) => state.setToastMessage);\n  const setToastShow = useStore((state) => state.setToastShow);\n\n  const login = useGoogleLogin({\n    onSuccess: (codeResponse) => {\n      setGoogleAccessToken(codeResponse.access_token);\n      setCloudSync(true);\n      loginHandler && loginHandler();\n      setToastStatus('success');\n      setToastMessage(t('toast.sync'));\n      setToastShow(true);\n    },\n    onError: (error) => {\n      console.log('Login Failed');\n      setToastStatus('error');\n      setToastMessage(error?.error_description || 'Error in authenticating!');\n      setToastShow(true);\n    },\n    scope: 'https://www.googleapis.com/auth/drive.file',\n  });\n\n  const logout = () => {\n    setGoogleAccessToken(undefined);\n    setSyncStatus('unauthenticated');\n    setCloudSync(false);\n    googleLogout();\n    useStore.persist.setOptions({\n      storage: createJSONStorage(() => localStorage),\n    });\n    useStore.persist.rehydrate();\n    setToastStatus('success');\n    setToastMessage(t('toast.stop'));\n    setToastShow(true);\n  };\n\n  return (\n    <div className='flex gap-4 flex-wrap justify-center'>\n      <button\n        className='btn btn-primary'\n        onClick={() => login()}\n        aria-label={t('button.sync') as string}\n      >\n        {t('button.sync')}\n      </button>\n      {cloudSync && (\n        <button\n          className='btn btn-neutral'\n          onClick={logout}\n          aria-label={t('button.stop') as string}\n        >\n          {t('button.stop')}\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport default GoogleSyncButton;\n"
  },
  {
    "path": "src/components/GoogleSync/index.ts",
    "content": "export { default } from './GoogleSync';\n"
  },
  {
    "path": "src/components/ImportExportChat/ExportChat.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport useStore from '@store/store';\n\nimport downloadFile from '@utils/downloadFile';\nimport { getToday } from '@utils/date';\n\nimport Export from '@type/export';\n\nconst ExportChat = () => {\n  const { t } = useTranslation();\n\n  return (\n    <div className='mt-6'>\n      <div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {t('export')} (JSON)\n      </div>\n      <button\n        className='btn btn-small btn-primary'\n        onClick={() => {\n          const fileData: Export = {\n            chats: useStore.getState().chats,\n            folders: useStore.getState().folders,\n            version: 1,\n          };\n          downloadFile(fileData, getToday());\n        }}\n        aria-label={t('export') as string}\n      >\n        {t('export')}\n      </button>\n    </div>\n  );\n};\nexport default ExportChat;\n"
  },
  {
    "path": "src/components/ImportExportChat/ImportChat.tsx",
    "content": "import React, { useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport useStore from '@store/store';\n\nimport {\n  isLegacyImport,\n  validateAndFixChats,\n  validateExportV1,\n} from '@utils/import';\n\nimport { ChatInterface, Folder, FolderCollection } from '@type/chat';\nimport { ExportBase } from '@type/export';\n\nconst ImportChat = () => {\n  const { t } = useTranslation();\n  const setChats = useStore.getState().setChats;\n  const setFolders = useStore.getState().setFolders;\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [alert, setAlert] = useState<{\n    message: string;\n    success: boolean;\n  } | null>(null);\n\n  const handleFileUpload = () => {\n    if (!inputRef || !inputRef.current) return;\n    const file = inputRef.current.files?.[0];\n\n    if (file) {\n      const reader = new FileReader();\n\n      reader.onload = (event) => {\n        const data = event.target?.result as string;\n\n        try {\n          const parsedData = JSON.parse(data);\n          if (isLegacyImport(parsedData)) {\n            if (validateAndFixChats(parsedData)) {\n              // import new folders\n              const folderNameToIdMap: Record<string, string> = {};\n              const parsedFolders: string[] = [];\n\n              parsedData.forEach((data) => {\n                const folder = data.folder;\n                if (folder) {\n                  if (!parsedFolders.includes(folder)) {\n                    parsedFolders.push(folder);\n                    folderNameToIdMap[folder] = uuidv4();\n                  }\n                  data.folder = folderNameToIdMap[folder];\n                }\n              });\n\n              const newFolders: FolderCollection = parsedFolders.reduce(\n                (acc, curr, index) => {\n                  const id = folderNameToIdMap[curr];\n                  const _newFolder: Folder = {\n                    id,\n                    name: curr,\n                    expanded: false,\n                    order: index,\n                  };\n                  return { [id]: _newFolder, ...acc };\n                },\n                {}\n              );\n\n              // increment the order of existing folders\n              const offset = parsedFolders.length;\n\n              const updatedFolders = useStore.getState().folders;\n              Object.values(updatedFolders).forEach((f) => (f.order += offset));\n\n              setFolders({ ...newFolders, ...updatedFolders });\n\n              // import chats\n              const prevChats = useStore.getState().chats;\n              if (prevChats) {\n                const updatedChats: ChatInterface[] = JSON.parse(\n                  JSON.stringify(prevChats)\n                );\n                setChats(parsedData.concat(updatedChats));\n              } else {\n                setChats(parsedData);\n              }\n              setAlert({ message: 'Succesfully imported!', success: true });\n            } else {\n              setAlert({\n                message: 'Invalid chats data format',\n                success: false,\n              });\n            }\n          } else {\n            switch ((parsedData as ExportBase).version) {\n              case 1:\n                if (validateExportV1(parsedData)) {\n                  // import folders\n                  parsedData.folders;\n                  // increment the order of existing folders\n                  const offset = Object.keys(parsedData.folders).length;\n\n                  const updatedFolders = useStore.getState().folders;\n                  Object.values(updatedFolders).forEach(\n                    (f) => (f.order += offset)\n                  );\n\n                  setFolders({ ...parsedData.folders, ...updatedFolders });\n\n                  // import chats\n                  const prevChats = useStore.getState().chats;\n                  if (parsedData.chats) {\n                    if (prevChats) {\n                      const updatedChats: ChatInterface[] = JSON.parse(\n                        JSON.stringify(prevChats)\n                      );\n                      setChats(parsedData.chats.concat(updatedChats));\n                    } else {\n                      setChats(parsedData.chats);\n                    }\n                  }\n\n                  setAlert({ message: 'Succesfully imported!', success: true });\n                } else {\n                  setAlert({\n                    message: 'Invalid format',\n                    success: false,\n                  });\n                }\n                break;\n            }\n          }\n        } catch (error: unknown) {\n          setAlert({ message: (error as Error).message, success: false });\n        }\n      };\n\n      reader.readAsText(file);\n    }\n  };\n\n  return (\n    <>\n      <label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {t('import')} (JSON)\n      </label>\n      <input\n        className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'\n        type='file'\n        ref={inputRef}\n      />\n      <button\n        className='btn btn-small btn-primary mt-3'\n        onClick={handleFileUpload}\n        aria-label={t('import') as string}\n      >\n        {t('import')}\n      </button>\n      {alert && (\n        <div\n          className={`relative py-2 px-3 w-full mt-3 border rounded-md text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap ${\n            alert.success\n              ? 'border-green-500 bg-green-500/10'\n              : 'border-red-500 bg-red-500/10'\n          }`}\n        >\n          {alert.message}\n        </div>\n      )}\n    </>\n  );\n};\n\nexport default ImportChat;\n"
  },
  {
    "path": "src/components/ImportExportChat/ImportChatOpenAI.tsx",
    "content": "import React, { useRef } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport useStore from '@store/store';\n\nimport { importOpenAIChatExport } from '@utils/import';\n\nimport { ChatInterface } from '@type/chat';\n\nconst ImportChatOpenAI = ({\n  setIsModalOpen,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  const { t } = useTranslation();\n\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const setToastStatus = useStore((state) => state.setToastStatus);\n  const setToastMessage = useStore((state) => state.setToastMessage);\n  const setToastShow = useStore((state) => state.setToastShow);\n  const setChats = useStore.getState().setChats;\n\n  const handleFileUpload = () => {\n    if (!inputRef || !inputRef.current) return;\n    const file = inputRef.current.files?.[0];\n    if (!file) return;\n\n    const reader = new FileReader();\n\n    reader.onload = (event) => {\n      const data = event.target?.result as string;\n\n      try {\n        const parsedData = JSON.parse(data);\n        const chats = importOpenAIChatExport(parsedData);\n        const prevChats: ChatInterface[] = JSON.parse(\n          JSON.stringify(useStore.getState().chats)\n        );\n        setChats(chats.concat(prevChats));\n\n        setToastStatus('success');\n        setToastMessage('Imported successfully!');\n        setIsModalOpen(false);\n      } catch (error: unknown) {\n        setToastStatus('error');\n        setToastMessage(`Invalid format! ${(error as Error).message}`);\n      }\n      setToastShow(true);\n    };\n\n    reader.readAsText(file);\n  };\n\n  return (\n    <>\n      <div className='text-lg font-bold text-gray-900 dark:text-gray-300 text-center mb-3'>\n        {t('import')} OpenAI ChatGPT {t('export')}\n      </div>\n      <label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {t('import')} (JSON)\n      </label>\n      <input\n        className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'\n        type='file'\n        ref={inputRef}\n      />\n      <button\n        className='btn btn-small btn-primary mt-3'\n        onClick={handleFileUpload}\n        aria-label={t('import') as string}\n      >\n        {t('import')}\n      </button>\n    </>\n  );\n};\n\nexport default ImportChatOpenAI;\n"
  },
  {
    "path": "src/components/ImportExportChat/ImportExportChat.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ExportIcon from '@icon/ExportIcon';\nimport PopupModal from '@components/PopupModal';\n\nimport ImportChat from './ImportChat';\nimport ExportChat from './ExportChat';\nimport ImportChatOpenAI from './ImportChatOpenAI';\n\nconst ImportExportChat = () => {\n  const { t } = useTranslation();\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  return (\n    <>\n      <a\n        className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n      >\n        <ExportIcon className='w-4 h-4' />\n        {t('import')} / {t('export')}\n      </a>\n      {isModalOpen && (\n        <PopupModal\n          title={`${t('import')} / ${t('export')}`}\n          setIsModalOpen={setIsModalOpen}\n          cancelButton={false}\n        >\n          <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n            <ImportChat />\n            <ExportChat />\n            <div className='border-t my-3 border-gray-200 dark:border-gray-600' />\n            <ImportChatOpenAI setIsModalOpen={setIsModalOpen} />\n          </div>\n        </PopupModal>\n      )}\n    </>\n  );\n};\n\nexport default ImportExportChat;\n"
  },
  {
    "path": "src/components/ImportExportChat/index.ts",
    "content": "export { default } from './ImportExportChat';\n"
  },
  {
    "path": "src/components/LanguageSelector/LanguageSelector.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\nimport { languageCodeToName, selectableLanguages } from '@constants/language';\n\nconst LanguageSelector = () => {\n  const { i18n } = useTranslation();\n\n  const [dropDown, setDropDown] = useState<boolean>(false);\n  return (\n    <div className='prose dark:prose-invert relative'>\n      <button\n        className='btn btn-neutral btn-small w-36 flex justify-between'\n        type='button'\n        onClick={() => setDropDown((prev) => !prev)}\n        aria-label='language selector'\n      >\n        {languageCodeToName[i18n.language as keyof typeof languageCodeToName] ??\n          i18n.language}\n        <DownChevronArrow />\n      </button>\n      <div\n        id='dropdown'\n        className={`${\n          dropDown ? '' : 'hidden'\n        } absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90 w-36`}\n      >\n        <ul\n          className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0 max-h-72 overflow-auto'\n          aria-labelledby='dropdownDefaultButton'\n        >\n          {selectableLanguages.map((lang) => (\n            <li\n              className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'\n              onClick={() => {\n                i18n.changeLanguage(lang);\n                setDropDown(false);\n              }}\n              key={lang}\n              lang={lang}\n            >\n              {languageCodeToName[lang]}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n};\n\nexport default LanguageSelector;\n"
  },
  {
    "path": "src/components/LanguageSelector/index.ts",
    "content": "export { default } from './LanguageSelector';\n"
  },
  {
    "path": "src/components/Menu/ChatFolder.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport useStore from '@store/store';\n\nimport DownChevronArrow from '@icon/DownChevronArrow';\nimport FolderIcon from '@icon/FolderIcon';\nimport {\n  ChatHistoryInterface,\n  ChatInterface,\n  FolderCollection,\n} from '@type/chat';\n\nimport ChatHistory from './ChatHistory';\nimport NewChat from './NewChat';\nimport EditIcon from '@icon/EditIcon';\nimport DeleteIcon from '@icon/DeleteIcon';\nimport CrossIcon from '@icon/CrossIcon';\nimport TickIcon from '@icon/TickIcon';\nimport ColorPaletteIcon from '@icon/ColorPaletteIcon';\nimport RefreshIcon from '@icon/RefreshIcon';\n\nimport { folderColorOptions } from '@constants/color';\n\nimport useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';\n\nconst ChatFolder = ({\n  folderChats,\n  folderId,\n}: {\n  folderChats: ChatHistoryInterface[];\n  folderId: string;\n}) => {\n  const folderName = useStore((state) => state.folders[folderId]?.name);\n  const isExpanded = useStore((state) => state.folders[folderId]?.expanded);\n  const color = useStore((state) => state.folders[folderId]?.color);\n\n  const setChats = useStore((state) => state.setChats);\n  const setFolders = useStore((state) => state.setFolders);\n\n  const inputRef = useRef<HTMLInputElement>(null);\n  const folderRef = useRef<HTMLDivElement>(null);\n  const gradientRef = useRef<HTMLDivElement>(null);\n\n  const [_folderName, _setFolderName] = useState<string>(folderName);\n  const [isEdit, setIsEdit] = useState<boolean>(false);\n  const [isDelete, setIsDelete] = useState<boolean>(false);\n  const [isHover, setIsHover] = useState<boolean>(false);\n\n  const [showPalette, setShowPalette, paletteRef] = useHideOnOutsideClick();\n\n  const editTitle = () => {\n    const updatedFolders: FolderCollection = JSON.parse(\n      JSON.stringify(useStore.getState().folders)\n    );\n    updatedFolders[folderId].name = _folderName;\n    setFolders(updatedFolders);\n    setIsEdit(false);\n  };\n\n  const deleteFolder = () => {\n    const updatedChats: ChatInterface[] = JSON.parse(\n      JSON.stringify(useStore.getState().chats)\n    );\n    updatedChats.forEach((chat) => {\n      if (chat.folder === folderId) delete chat.folder;\n    });\n    setChats(updatedChats);\n\n    const updatedFolders: FolderCollection = JSON.parse(\n      JSON.stringify(useStore.getState().folders)\n    );\n    delete updatedFolders[folderId];\n    setFolders(updatedFolders);\n\n    setIsDelete(false);\n  };\n\n  const updateColor = (_color?: string) => {\n    const updatedFolders: FolderCollection = JSON.parse(\n      JSON.stringify(useStore.getState().folders)\n    );\n    if (_color) updatedFolders[folderId].color = _color;\n    else delete updatedFolders[folderId].color;\n    setFolders(updatedFolders);\n    setShowPalette(false);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      editTitle();\n    }\n  };\n\n  const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {\n    e.stopPropagation();\n\n    if (isEdit) editTitle();\n    else if (isDelete) deleteFolder();\n  };\n\n  const handleCross = () => {\n    setIsDelete(false);\n    setIsEdit(false);\n  };\n\n  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n    if (e.dataTransfer) {\n      e.stopPropagation();\n      setIsHover(false);\n\n      // expand folder on drop\n      const updatedFolders: FolderCollection = JSON.parse(\n        JSON.stringify(useStore.getState().folders)\n      );\n      updatedFolders[folderId].expanded = true;\n      setFolders(updatedFolders);\n\n      // update chat folderId to new folderId\n      const chatIndex = Number(e.dataTransfer.getData('chatIndex'));\n      const updatedChats: ChatInterface[] = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      updatedChats[chatIndex].folder = folderId;\n      setChats(updatedChats);\n    }\n  };\n\n  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsHover(true);\n  };\n\n  const handleDragLeave = () => {\n    setIsHover(false);\n  };\n\n  const toggleExpanded = () => {\n    const updatedFolders: FolderCollection = JSON.parse(\n      JSON.stringify(useStore.getState().folders)\n    );\n    updatedFolders[folderId].expanded = !updatedFolders[folderId].expanded;\n    setFolders(updatedFolders);\n  };\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) inputRef.current.focus();\n  }, [isEdit]);\n\n  return (\n    <div\n      className={`w-full transition-colors group/folder ${\n        isHover ? 'bg-gray-800/40' : ''\n      }`}\n      onDrop={handleDrop}\n      onDragOver={handleDragOver}\n      onDragLeave={handleDragLeave}\n    >\n      <div\n        style={{ background: color || '' }}\n        className={`${\n          color ? '' : 'hover:bg-gray-850'\n        } transition-colors flex py-2 pl-2 pr-1 items-center gap-3 relative rounded-md break-all cursor-pointer parent-sibling`}\n        onClick={toggleExpanded}\n        ref={folderRef}\n        onMouseEnter={() => {\n          if (color && folderRef.current)\n            folderRef.current.style.background = `${color}dd`;\n          if (gradientRef.current) gradientRef.current.style.width = '0px';\n        }}\n        onMouseLeave={() => {\n          if (color && folderRef.current)\n            folderRef.current.style.background = color;\n          if (gradientRef.current) gradientRef.current.style.width = '1rem';\n        }}\n      >\n        <FolderIcon className='h-4 w-4' />\n        <div className='flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative'>\n          {isEdit ? (\n            <input\n              type='text'\n              className='focus:outline-blue-600 text-sm border-none bg-transparent p-0 m-0 w-full'\n              value={_folderName}\n              onChange={(e) => {\n                _setFolderName(e.target.value);\n              }}\n              onClick={(e) => e.stopPropagation()}\n              onKeyDown={handleKeyDown}\n              ref={inputRef}\n            />\n          ) : (\n            _folderName\n          )}\n          {isEdit || (\n            <div\n              ref={gradientRef}\n              className='absolute inset-y-0 right-0 w-4 z-10 transition-all'\n              style={{\n                background:\n                  color &&\n                  `linear-gradient(to left, ${\n                    color || 'var(--color-900)'\n                  }, rgb(32 33 35 / 0))`,\n              }}\n            />\n          )}\n        </div>\n        <div\n          className='flex text-gray-300'\n          onClick={(e) => e.stopPropagation()}\n        >\n          {isDelete || isEdit ? (\n            <>\n              <button\n                className='p-1 hover:text-white'\n                onClick={handleTick}\n                aria-label='confirm'\n              >\n                <TickIcon />\n              </button>\n              <button\n                className='p-1 hover:text-white'\n                onClick={handleCross}\n                aria-label='cancel'\n              >\n                <CrossIcon />\n              </button>\n            </>\n          ) : (\n            <>\n              <div\n                className='relative md:hidden group-hover/folder:md:inline'\n                ref={paletteRef}\n              >\n                <button\n                  className='p-1 hover:text-white'\n                  onClick={() => {\n                    setShowPalette((prev) => !prev);\n                  }}\n                  aria-label='folder color'\n                >\n                  <ColorPaletteIcon />\n                </button>\n                {showPalette && (\n                  <div className='absolute left-0 bottom-0 translate-y-full p-2 z-20 bg-gray-900 rounded border border-gray-600 flex flex-col gap-2 items-center'>\n                    <>\n                      {folderColorOptions.map((c) => (\n                        <button\n                          key={c}\n                          style={{ background: c }}\n                          className={`hover:scale-90 transition-transform h-4 w-4 rounded-full`}\n                          onClick={() => {\n                            updateColor(c);\n                          }}\n                          aria-label={c}\n                        />\n                      ))}\n                      <button\n                        onClick={() => {\n                          updateColor();\n                        }}\n                        aria-label='default color'\n                      >\n                        <RefreshIcon />\n                      </button>\n                    </>\n                  </div>\n                )}\n              </div>\n\n              <button\n                className='p-1 hover:text-white md:hidden group-hover/folder:md:inline'\n                onClick={() => setIsEdit(true)}\n                aria-label='edit folder title'\n              >\n                <EditIcon />\n              </button>\n              <button\n                className='p-1 hover:text-white md:hidden group-hover/folder:md:inline'\n                onClick={() => setIsDelete(true)}\n                aria-label='delete folder'\n              >\n                <DeleteIcon />\n              </button>\n              <button\n                className='p-1 hover:text-white'\n                onClick={toggleExpanded}\n                aria-label='expand folder'\n              >\n                <DownChevronArrow\n                  className={`${\n                    isExpanded ? 'rotate-180' : ''\n                  } transition-transform`}\n                />\n              </button>\n            </>\n          )}\n        </div>\n      </div>\n      <div className='ml-3 pl-1 border-l-2 border-gray-700 flex flex-col gap-1 parent'>\n        {isExpanded && <NewChat folder={folderId} />}\n        {isExpanded &&\n          folderChats.map((chat) => (\n            <ChatHistory\n              title={chat.title}\n              chatIndex={chat.index}\n              key={`${chat.title}-${chat.index}`}\n            />\n          ))}\n      </div>\n    </div>\n  );\n};\n\nexport default ChatFolder;\n"
  },
  {
    "path": "src/components/Menu/ChatHistory.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\n\nimport useInitialiseNewChat from '@hooks/useInitialiseNewChat';\n\nimport ChatIcon from '@icon/ChatIcon';\nimport CrossIcon from '@icon/CrossIcon';\nimport DeleteIcon from '@icon/DeleteIcon';\nimport EditIcon from '@icon/EditIcon';\nimport TickIcon from '@icon/TickIcon';\nimport useStore from '@store/store';\n\nconst ChatHistoryClass = {\n  normal:\n    'flex py-2 px-2 items-center gap-3 relative rounded-md bg-gray-900 hover:bg-gray-850 break-all hover:pr-4 group transition-opacity',\n  active:\n    'flex py-2 px-2 items-center gap-3 relative rounded-md break-all pr-14 bg-gray-800 hover:bg-gray-800 group transition-opacity',\n  normalGradient:\n    'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-900 group-hover:from-gray-850',\n  activeGradient:\n    'absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-800',\n};\n\nconst ChatHistory = React.memo(\n  ({ title, chatIndex }: { title: string; chatIndex: number }) => {\n    const initialiseNewChat = useInitialiseNewChat();\n    const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n    const setChats = useStore((state) => state.setChats);\n    const active = useStore((state) => state.currentChatIndex === chatIndex);\n    const generating = useStore((state) => state.generating);\n\n    const [isDelete, setIsDelete] = useState<boolean>(false);\n    const [isEdit, setIsEdit] = useState<boolean>(false);\n    const [_title, _setTitle] = useState<string>(title);\n    const inputRef = useRef<HTMLInputElement>(null);\n\n    const editTitle = () => {\n      const updatedChats = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      updatedChats[chatIndex].title = _title;\n      setChats(updatedChats);\n      setIsEdit(false);\n    };\n\n    const deleteChat = () => {\n      const updatedChats = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      updatedChats.splice(chatIndex, 1);\n      if (updatedChats.length > 0) {\n        setCurrentChatIndex(0);\n        setChats(updatedChats);\n      } else {\n        initialiseNewChat();\n      }\n      setIsDelete(false);\n    };\n\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === 'Enter') {\n        e.preventDefault();\n        editTitle();\n      }\n    };\n\n    const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {\n      e.stopPropagation();\n\n      if (isEdit) editTitle();\n      else if (isDelete) deleteChat();\n    };\n\n    const handleCross = () => {\n      setIsDelete(false);\n      setIsEdit(false);\n    };\n\n    const handleDragStart = (e: React.DragEvent<HTMLAnchorElement>) => {\n      if (e.dataTransfer) {\n        e.dataTransfer.setData('chatIndex', String(chatIndex));\n      }\n    };\n\n    useEffect(() => {\n      if (inputRef && inputRef.current) inputRef.current.focus();\n    }, [isEdit]);\n\n    return (\n      <a\n        className={`${\n          active ? ChatHistoryClass.active : ChatHistoryClass.normal\n        } ${\n          generating\n            ? 'cursor-not-allowed opacity-40'\n            : 'cursor-pointer opacity-100'\n        }`}\n        onClick={() => {\n          if (!generating) setCurrentChatIndex(chatIndex);\n        }}\n        draggable\n        onDragStart={handleDragStart}\n      >\n        <ChatIcon />\n        <div className='flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative' title={title}>\n          {isEdit ? (\n            <input\n              type='text'\n              className='focus:outline-blue-600 text-sm border-none bg-transparent p-0 m-0 w-full'\n              value={_title}\n              onChange={(e) => {\n                _setTitle(e.target.value);\n              }}\n              onKeyDown={handleKeyDown}\n              ref={inputRef}\n            />\n          ) : (\n            _title\n          )}\n\n          {isEdit || (\n            <div\n              className={\n                active\n                  ? ChatHistoryClass.activeGradient\n                  : ChatHistoryClass.normalGradient\n              }\n            />\n          )}\n        </div>\n        {active && (\n          <div className='absolute flex right-1 z-10 text-gray-300 visible'>\n            {isDelete || isEdit ? (\n              <>\n                <button\n                  className='p-1 hover:text-white'\n                  onClick={handleTick}\n                  aria-label='confirm'\n                >\n                  <TickIcon />\n                </button>\n                <button\n                  className='p-1 hover:text-white'\n                  onClick={handleCross}\n                  aria-label='cancel'\n                >\n                  <CrossIcon />\n                </button>\n              </>\n            ) : (\n              <>\n                <button\n                  className='p-1 hover:text-white'\n                  onClick={() => setIsEdit(true)}\n                  aria-label='edit chat title'\n                >\n                  <EditIcon />\n                </button>\n                <button\n                  className='p-1 hover:text-white'\n                  onClick={() => setIsDelete(true)}\n                  aria-label='delete chat'\n                >\n                  <DeleteIcon />\n                </button>\n              </>\n            )}\n          </div>\n        )}\n      </a>\n    );\n  }\n);\n\nexport default ChatHistory;\n"
  },
  {
    "path": "src/components/Menu/ChatHistoryList.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport useStore from '@store/store';\nimport { shallow } from 'zustand/shallow';\n\nimport ChatFolder from './ChatFolder';\nimport ChatHistory from './ChatHistory';\nimport ChatSearch from './ChatSearch';\n\nimport {\n  ChatHistoryInterface,\n  ChatHistoryFolderInterface,\n  ChatInterface,\n  FolderCollection,\n} from '@type/chat';\n\nconst ChatHistoryList = () => {\n  const currentChatIndex = useStore((state) => state.currentChatIndex);\n  const setChats = useStore((state) => state.setChats);\n  const setFolders = useStore((state) => state.setFolders);\n  const chatTitles = useStore(\n    (state) => state.chats?.map((chat) => chat.title),\n    shallow\n  );\n\n  const [isHover, setIsHover] = useState<boolean>(false);\n  const [chatFolders, setChatFolders] = useState<ChatHistoryFolderInterface>(\n    {}\n  );\n  const [noChatFolders, setNoChatFolders] = useState<ChatHistoryInterface[]>(\n    []\n  );\n  const [filter, setFilter] = useState<string>('');\n\n  const chatsRef = useRef<ChatInterface[]>(useStore.getState().chats || []);\n  const foldersRef = useRef<FolderCollection>(useStore.getState().folders);\n  const filterRef = useRef<string>(filter);\n\n  const updateFolders = useRef(() => {\n    const _folders: ChatHistoryFolderInterface = {};\n    const _noFolders: ChatHistoryInterface[] = [];\n    const chats = useStore.getState().chats;\n    const folders = useStore.getState().folders;\n\n    Object.values(folders)\n      .sort((a, b) => a.order - b.order)\n      .forEach((f) => (_folders[f.id] = []));\n\n    if (chats) {\n      chats.forEach((chat, index) => {\n        const _filterLowerCase = filterRef.current.toLowerCase();\n        const _chatTitle = chat.title.toLowerCase();\n        const _chatFolderName = chat.folder\n          ? folders[chat.folder].name.toLowerCase()\n          : '';\n\n        if (\n          !_chatTitle.includes(_filterLowerCase) &&\n          !_chatFolderName.includes(_filterLowerCase) &&\n          index !== useStore.getState().currentChatIndex\n        )\n          return;\n\n        if (!chat.folder) {\n          _noFolders.push({ title: chat.title, index: index, id: chat.id });\n        } else {\n          if (!_folders[chat.folder]) _folders[_chatFolderName] = [];\n          _folders[chat.folder].push({\n            title: chat.title,\n            index: index,\n            id: chat.id,\n          });\n        }\n      });\n    }\n\n    setChatFolders(_folders);\n    setNoChatFolders(_noFolders);\n  }).current;\n\n  useEffect(() => {\n    updateFolders();\n\n    useStore.subscribe((state) => {\n      if (\n        !state.generating &&\n        state.chats &&\n        state.chats !== chatsRef.current\n      ) {\n        updateFolders();\n        chatsRef.current = state.chats;\n      } else if (state.folders !== foldersRef.current) {\n        updateFolders();\n        foldersRef.current = state.folders;\n      }\n    });\n  }, []);\n\n  useEffect(() => {\n    if (\n      chatTitles &&\n      currentChatIndex >= 0 &&\n      currentChatIndex < chatTitles.length\n    ) {\n      // set title\n      document.title = chatTitles[currentChatIndex];\n\n      // expand folder of current chat\n      const chats = useStore.getState().chats;\n      if (chats) {\n        const folderId = chats[currentChatIndex].folder;\n\n        if (folderId) {\n          const updatedFolders: FolderCollection = JSON.parse(\n            JSON.stringify(useStore.getState().folders)\n          );\n\n          updatedFolders[folderId].expanded = true;\n          setFolders(updatedFolders);\n        }\n      }\n    }\n  }, [currentChatIndex, chatTitles]);\n\n  useEffect(() => {\n    filterRef.current = filter;\n    updateFolders();\n  }, [filter]);\n\n  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n    if (e.dataTransfer) {\n      e.stopPropagation();\n      setIsHover(false);\n\n      const chatIndex = Number(e.dataTransfer.getData('chatIndex'));\n      const updatedChats: ChatInterface[] = JSON.parse(\n        JSON.stringify(useStore.getState().chats)\n      );\n      delete updatedChats[chatIndex].folder;\n      setChats(updatedChats);\n    }\n  };\n\n  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    setIsHover(true);\n  };\n\n  const handleDragLeave = () => {\n    setIsHover(false);\n  };\n\n  const handleDragEnd = () => {\n    setIsHover(false);\n  };\n\n  return (\n    <div\n      className={`flex-col flex-1 overflow-y-auto hide-scroll-bar border-b border-white/20 ${\n        isHover ? 'bg-gray-800/40' : ''\n      }`}\n      onDrop={handleDrop}\n      onDragOver={handleDragOver}\n      onDragLeave={handleDragLeave}\n      onDragEnd={handleDragEnd}\n    >\n      <ChatSearch filter={filter} setFilter={setFilter} />\n      <div className='flex flex-col gap-2 text-gray-100 text-sm'>\n        {Object.keys(chatFolders).map((folderId) => (\n          <ChatFolder\n            folderChats={chatFolders[folderId]}\n            folderId={folderId}\n            key={folderId}\n          />\n        ))}\n        {noChatFolders.map(({ title, index, id }) => (\n          <ChatHistory title={title} key={`${title}-${id}`} chatIndex={index} />\n        ))}\n      </div>\n      <div className='w-full h-10' />\n    </div>\n  );\n};\n\nconst ShowMoreButton = () => {\n  return (\n    <button className='btn relative btn-dark btn-small m-auto mb-2'>\n      <div className='flex items-center justify-center gap-2'>Show more</div>\n    </button>\n  );\n};\n\nexport default ChatHistoryList;\n"
  },
  {
    "path": "src/components/Menu/ChatSearch.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { debounce } from 'lodash';\nimport useStore from '@store/store';\n\nimport SearchBar from '@components/SearchBar';\n\nconst ChatSearch = ({\n  filter,\n  setFilter,\n}: {\n  filter: string;\n  setFilter: React.Dispatch<React.SetStateAction<string>>;\n}) => {\n  const [_filter, _setFilter] = useState<string>(filter);\n  const generating = useStore((state) => state.generating);\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    _setFilter(e.target.value);\n  };\n\n  const debouncedUpdateFilter = useRef(\n    debounce((f) => {\n      setFilter(f);\n    }, 500)\n  ).current;\n\n  useEffect(() => {\n    debouncedUpdateFilter(_filter);\n  }, [_filter]);\n\n  return (\n    <SearchBar\n      value={_filter}\n      handleChange={handleChange}\n      className='h-8 mb-2'\n      disabled={generating}\n    />\n  );\n};\n\nexport default ChatSearch;\n"
  },
  {
    "path": "src/components/Menu/Menu.tsx",
    "content": "import React, { useEffect, useRef } from 'react';\n\nimport useStore from '@store/store';\n\nimport NewChat from './NewChat';\nimport NewFolder from './NewFolder';\nimport ChatHistoryList from './ChatHistoryList';\nimport MenuOptions from './MenuOptions';\n\nimport CrossIcon2 from '@icon/CrossIcon2';\nimport DownArrow from '@icon/DownArrow';\nimport MenuIcon from '@icon/MenuIcon';\n\nconst Menu = () => {\n  const hideSideMenu = useStore((state) => state.hideSideMenu);\n  const setHideSideMenu = useStore((state) => state.setHideSideMenu);\n\n  const windowWidthRef = useRef<number>(window.innerWidth);\n\n  useEffect(() => {\n    if (window.innerWidth < 768) setHideSideMenu(true);\n    window.addEventListener('resize', () => {\n      if (\n        windowWidthRef.current !== window.innerWidth &&\n        window.innerWidth < 768\n      )\n        setHideSideMenu(true);\n    });\n  }, []);\n\n  return (\n    <>\n      <div\n        id='menu'\n        className={`group/menu dark bg-gray-900 fixed md:inset-y-0 md:flex md:w-[260px] md:flex-col transition-transform z-[999] top-0 left-0 h-full max-md:w-3/4 ${\n          hideSideMenu ? 'translate-x-[-100%]' : 'translate-x-[0%]'\n        }`}\n      >\n        <div className='flex h-full min-h-0 flex-col'>\n          <div className='flex h-full w-full flex-1 items-start border-white/20'>\n            <nav className='flex h-full flex-1 flex-col space-y-1 px-2 pt-2'>\n              <div className='flex gap-2'>\n                <NewChat />\n                <NewFolder />\n              </div>\n              <ChatHistoryList />\n              <MenuOptions />\n            </nav>\n          </div>\n        </div>\n        <div\n          id='menu-close'\n          className={`${\n            hideSideMenu ? 'hidden' : ''\n          } md:hidden absolute z-[999] right-0 translate-x-full top-10 bg-gray-900 p-2 cursor-pointer hover:bg-black text-white`}\n          onClick={() => {\n            setHideSideMenu(true);\n          }}\n        >\n          <CrossIcon2 />\n        </div>\n        <div\n          className={`${\n            hideSideMenu ? 'opacity-100' : 'opacity-0'\n          } group/menu md:group-hover/menu:opacity-100 max-md:hidden transition-opacity absolute z-[999] right-0 translate-x-full top-10 bg-gray-900 p-2 cursor-pointer hover:bg-black text-white ${\n            hideSideMenu ? '' : 'rotate-90'\n          }`}\n          onClick={() => {\n            setHideSideMenu(!hideSideMenu);\n          }}\n        >\n          {hideSideMenu ? (\n            <MenuIcon className='h-4 w-4' />\n          ) : (\n            <DownArrow className='h-4 w-4' />\n          )}\n        </div>\n      </div>\n      <div\n        id='menu-backdrop'\n        className={`${\n          hideSideMenu ? 'hidden' : ''\n        } md:hidden fixed top-0 left-0 h-full w-full z-[60] bg-gray-900/70`}\n        onClick={() => {\n          setHideSideMenu(true);\n        }}\n      />\n    </>\n  );\n};\n\nexport default Menu;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/Account.tsx",
    "content": "import React from 'react';\nimport PersonIcon from '@icon/PersonIcon';\n\nconst Account = () => {\n  return (\n    <a className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'>\n      <PersonIcon />\n      My account\n    </a>\n  );\n};\n\nexport default Account;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/Api.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport PersonIcon from '@icon/PersonIcon';\nimport ApiMenu from '@components/ApiMenu';\n\nconst Config = () => {\n  const { t } = useTranslation();\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  return (\n    <>\n      <a\n        className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n        id='api-menu'\n        onClick={() => setIsModalOpen(true)}\n      >\n        <PersonIcon />\n        {t('api')}\n      </a>\n      {isModalOpen && <ApiMenu setIsModalOpen={setIsModalOpen} />}\n    </>\n  );\n};\n\nexport default Config;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/ClearConversation.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport PopupModal from '@components/PopupModal';\nimport DeleteIcon from '@icon/DeleteIcon';\nimport useInitialiseNewChat from '@hooks/useInitialiseNewChat';\n\nconst ClearConversation = () => {\n  const { t } = useTranslation();\n\n  const initialiseNewChat = useInitialiseNewChat();\n  const setFolders = useStore((state) => state.setFolders);\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  const handleConfirm = () => {\n    setIsModalOpen(false);\n    initialiseNewChat();\n    setFolders({});\n  };\n\n  return (\n    <>\n      <button\n        className='btn btn-neutral'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n        aria-label={t('clearConversation') as string}\n      >\n        <DeleteIcon />\n        {t('clearConversation')}\n      </button>\n      {isModalOpen && (\n        <PopupModal\n          setIsModalOpen={setIsModalOpen}\n          title={t('warning') as string}\n          message={t('clearConversationWarning') as string}\n          handleConfirm={handleConfirm}\n        />\n      )}\n    </>\n  );\n};\n\nexport default ClearConversation;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/CollapseOptions.tsx",
    "content": "import ArrowBottom from '@icon/ArrowBottom';\nimport useStore from '@store/store';\n\nconst CollapseOptions = () => {\n  const setHideMenuOptions = useStore((state) => state.setHideMenuOptions);\n  const hideMenuOptions = useStore((state) => state.hideMenuOptions);\n\n  return (\n    <div\n      className={`fill-white hover:bg-gray-500/10 transition-colors duration-200 px-3 rounded-md cursor-pointer flex justify-center`}\n      onClick={() => setHideMenuOptions(!hideMenuOptions)}\n    >\n      <ArrowBottom\n        className={`h-3 w-3 transition-all duration-100 ${\n          hideMenuOptions ? 'rotate-180' : ''\n        }`}\n      />\n    </div>\n  );\n};\n\nexport default CollapseOptions;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/Logout.tsx",
    "content": "import React from 'react';\nimport LogoutIcon from '@icon/LogoutIcon';\n\nconst Logout = () => {\n  return (\n    <a className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'>\n      <LogoutIcon />\n      Log out\n    </a>\n  );\n};\n\nexport default Logout;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/Me.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport HeartIcon from '@icon/HeartIcon';\n\nconst Me = () => {\n  const { t } = useTranslation();\n  return (\n    <a\n      className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n      href='https://github.com/ztjhz/BetterChatGPT'\n      target='_blank'\n    >\n      <HeartIcon />\n      {t('author')}\n    </a>\n  );\n};\n\nexport default Me;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/MenuOptions.tsx",
    "content": "import React from 'react';\nimport useStore from '@store/store';\n\nimport Api from './Api';\nimport Me from './Me';\nimport AboutMenu from '@components/AboutMenu';\nimport ImportExportChat from '@components/ImportExportChat';\nimport SettingsMenu from '@components/SettingsMenu';\nimport CollapseOptions from './CollapseOptions';\nimport GoogleSync from '@components/GoogleSync';\nimport { TotalTokenCostDisplay } from '@components/SettingsMenu/TotalTokenCost';\n\nconst googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID || undefined;\n\nconst MenuOptions = () => {\n  const hideMenuOptions = useStore((state) => state.hideMenuOptions);\n  const countTotalTokens = useStore((state) => state.countTotalTokens);\n  return (\n    <>\n      <CollapseOptions />\n      <div\n        className={`${\n          hideMenuOptions ? 'max-h-0' : 'max-h-full'\n        } overflow-hidden transition-all`}\n      >\n        {countTotalTokens && <TotalTokenCostDisplay />}\n        {googleClientId && <GoogleSync clientId={googleClientId} />}\n        <AboutMenu />\n        <ImportExportChat />\n        <Api />\n        <SettingsMenu />\n        <Me />\n      </div>\n    </>\n  );\n};\n\nexport default MenuOptions;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/ThemeSwitcher.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport SunIcon from '@icon/SunIcon';\nimport MoonIcon from '@icon/MoonIcon';\nimport { Theme } from '@type/theme';\n\nconst getOppositeTheme = (theme: Theme): Theme => {\n  if (theme === 'dark') {\n    return 'light';\n  } else {\n    return 'dark';\n  }\n};\nconst ThemeSwitcher = () => {\n  const { t } = useTranslation();\n  const theme = useStore((state) => state.theme);\n  const setTheme = useStore((state) => state.setTheme);\n\n  const switchTheme = () => {\n    setTheme(getOppositeTheme(theme!));\n  };\n\n  useEffect(() => {\n    document.documentElement.className = theme;\n  }, [theme]);\n\n  return theme ? (\n    <button\n      className='items-center gap-3 btn btn-neutral'\n      onClick={switchTheme}\n      aria-label='toggle dark/light mode'\n    >\n      {theme === 'dark' ? <SunIcon /> : <MoonIcon />}\n      {t(getOppositeTheme(theme) + 'Mode')}\n    </button>\n  ) : (\n    <></>\n  );\n};\n\nexport default ThemeSwitcher;\n"
  },
  {
    "path": "src/components/Menu/MenuOptions/index.ts",
    "content": "export { default } from './MenuOptions';\n"
  },
  {
    "path": "src/components/Menu/NewChat.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport PlusIcon from '@icon/PlusIcon';\n\nimport useAddChat from '@hooks/useAddChat';\n\nconst NewChat = ({ folder }: { folder?: string }) => {\n  const { t } = useTranslation();\n  const addChat = useAddChat();\n  const generating = useStore((state) => state.generating);\n\n  return (\n    <a\n      className={`flex flex-1 items-center rounded-md hover:bg-gray-500/10 transition-all duration-200 text-white text-sm flex-shrink-0 ${\n        generating\n          ? 'cursor-not-allowed opacity-40'\n          : 'cursor-pointer opacity-100'\n      } ${\n        folder ? 'justify-start' : 'py-2 px-2 gap-3 mb-2 border border-white/20'\n      }`}\n      onClick={() => {\n        if (!generating) addChat(folder);\n      }}\n      title={folder ? String(t('newChat')) : ''}\n    >\n      {folder ? (\n        <div className='max-h-0 parent-sibling-hover:max-h-10 hover:max-h-10 parent-sibling-hover:py-2 hover:py-2 px-2 overflow-hidden transition-all duration-200 delay-500 text-sm flex gap-3 items-center text-gray-100'>\n          <PlusIcon /> {t('newChat')}\n        </div>\n      ) : (\n        <>\n          <PlusIcon />\n          <span className='inline-flex text-white text-sm'>{t('newChat')}</span>\n        </>\n      )}\n    </a>\n  );\n};\n\nexport default NewChat;\n"
  },
  {
    "path": "src/components/Menu/NewFolder.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { v4 as uuidv4 } from 'uuid';\nimport useStore from '@store/store';\n\nimport NewFolderIcon from '@icon/NewFolderIcon';\nimport { Folder, FolderCollection } from '@type/chat';\n\nconst NewFolder = () => {\n  const { t } = useTranslation();\n  const generating = useStore((state) => state.generating);\n  const setFolders = useStore((state) => state.setFolders);\n\n  const addFolder = () => {\n    let folderIndex = 1;\n    let name = `New Folder ${folderIndex}`;\n\n    const folders = useStore.getState().folders;\n\n    while (Object.values(folders).some((folder) => folder.name === name)) {\n      folderIndex += 1;\n      name = `New Folder ${folderIndex}`;\n    }\n\n    const updatedFolders: FolderCollection = JSON.parse(\n      JSON.stringify(folders)\n    );\n\n    const id = uuidv4();\n    const newFolder: Folder = {\n      id,\n      name,\n      expanded: false,\n      order: 0,\n    };\n\n    Object.values(updatedFolders).forEach((folder) => {\n      folder.order += 1;\n    });\n\n    setFolders({ [id]: newFolder, ...updatedFolders });\n  };\n\n  return (\n    <a\n      className={`flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white text-sm mb-2 flex-shrink-0 border border-white/20 transition-opacity ${\n        generating\n          ? 'cursor-not-allowed opacity-40'\n          : 'cursor-pointer opacity-100'\n      }`}\n      onClick={() => {\n        if (!generating) addFolder();\n      }}\n    >\n      <NewFolderIcon />\n    </a>\n  );\n};\n\nexport default NewFolder;\n"
  },
  {
    "path": "src/components/Menu/index.ts",
    "content": "export { default } from './Menu';\n"
  },
  {
    "path": "src/components/MobileBar/MobileBar.tsx",
    "content": "import React from 'react';\n\nimport useStore from '@store/store';\nimport PlusIcon from '@icon/PlusIcon';\nimport MenuIcon from '@icon/MenuIcon';\nimport useAddChat from '@hooks/useAddChat';\n\nconst MobileBar = () => {\n  const generating = useStore((state) => state.generating);\n  const setHideSideMenu = useStore((state) => state.setHideSideMenu);\n  const chatTitle = useStore((state) =>\n    state.chats &&\n    state.chats.length > 0 &&\n    state.currentChatIndex >= 0 &&\n    state.currentChatIndex < state.chats.length\n      ? state.chats[state.currentChatIndex].title\n      : 'New Chat'\n  );\n\n  const addChat = useAddChat();\n\n  return (\n    <div className='sticky top-0 left-0 w-full z-50 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden'>\n      <button\n        type='button'\n        className='-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white'\n        onClick={() => {\n          setHideSideMenu(false);\n        }}\n        aria-label='open sidebar'\n      >\n        <span className='sr-only'>Open sidebar</span>\n        <MenuIcon />\n      </button>\n      <h1 className='flex-1 text-center text-base font-normal px-2 max-h-20 overflow-y-auto'>\n        {chatTitle}\n      </h1>\n      <button\n        type='button'\n        className={`px-3 text-gray-400 transition-opacity ${\n          generating\n            ? 'cursor-not-allowed opacity-40'\n            : 'cursor-pointer opacity-100'\n        }`}\n        onClick={() => {\n          if (!generating) addChat();\n        }}\n        aria-label='new chat'\n      >\n        <PlusIcon className='h-6 w-6' />\n      </button>\n    </div>\n  );\n};\n\nexport default MobileBar;\n"
  },
  {
    "path": "src/components/MobileBar/index.ts",
    "content": "export { default } from './MobileBar';\n"
  },
  {
    "path": "src/components/PopupModal/PopupModal.tsx",
    "content": "import React, { useEffect } from 'react';\nimport ReactDOM from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport CrossIcon2 from '@icon/CrossIcon2';\n\nconst PopupModal = ({\n  title = 'Information',\n  message,\n  setIsModalOpen,\n  handleConfirm,\n  handleClose,\n  handleClickBackdrop,\n  cancelButton = true,\n  children,\n}: {\n  title?: string;\n  message?: string;\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  handleConfirm?: () => void;\n  handleClose?: () => void;\n  handleClickBackdrop?: () => void;\n  cancelButton?: boolean;\n  children?: React.ReactElement;\n}) => {\n  const modalRoot = document.getElementById('modal-root');\n  const { t } = useTranslation();\n\n  const _handleClose = () => {\n    handleClose && handleClose();\n    setIsModalOpen(false);\n  };\n\n  const _handleBackdropClose = () => {\n    if (handleClickBackdrop) handleClickBackdrop();\n    else _handleClose();\n  };\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (event.key === 'Escape') {\n      if (handleClickBackdrop) handleClickBackdrop();\n      else handleClose ? handleClose() : setIsModalOpen(false);\n    } else if (event.key === 'Enter') {\n      if (handleConfirm) handleConfirm();\n    }\n  };\n\n  useEffect(() => {\n    document.addEventListener('keydown', handleKeyDown);\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [handleConfirm, handleClose, handleClickBackdrop]);\n\n  if (modalRoot) {\n    return ReactDOM.createPortal(\n      <div className='fixed top-0 left-0 z-[999] w-full p-4 overflow-x-hidden overflow-y-auto h-full flex justify-center items-center'>\n        <div className='relative z-2 max-w-2xl md:h-auto flex justify-center max-h-full'>\n          <div className='relative bg-gray-50 rounded-lg shadow dark:bg-gray-700 max-h-full overflow-y-auto hide-scroll-bar'>\n            <div className='flex items-center justify-between p-4 border-b rounded-t dark:border-gray-600'>\n              <h3 className='ml-2 text-lg font-semibold text-gray-900 dark:text-white'>\n                {title}\n              </h3>\n              <button\n                type='button'\n                className='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white'\n                onClick={_handleClose}\n                aria-label='close modal'\n              >\n                <CrossIcon2 />\n              </button>\n            </div>\n\n            {message && (\n              <div className='p-6 border-b border-gray-200 dark:border-gray-600'>\n                <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>\n                  {message}\n                </div>\n              </div>\n            )}\n\n            {children}\n\n            <div className='flex items-center justify-center p-6 gap-4'>\n              {handleConfirm && (\n                <button\n                  type='button'\n                  className='btn btn-primary'\n                  onClick={handleConfirm}\n                  aria-label='confirm'\n                >\n                  {t('confirm')}\n                </button>\n              )}\n              {cancelButton && (\n                <button\n                  type='button'\n                  className='btn btn-neutral'\n                  onClick={_handleClose}\n                  aria-label='cancel'\n                >\n                  {t('cancel')}\n                </button>\n              )}\n            </div>\n          </div>\n        </div>\n        <div\n          className='bg-gray-800/90 absolute top-0 left-0 h-full w-full z-[-1]'\n          onClick={_handleBackdropClose}\n        />\n      </div>,\n      modalRoot\n    );\n  } else {\n    return null;\n  }\n};\n\nexport default PopupModal;\n"
  },
  {
    "path": "src/components/PopupModal/index.ts",
    "content": "export { default } from './PopupModal';\n"
  },
  {
    "path": "src/components/PromptLibraryMenu/ExportPrompt.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport { exportPrompts } from '@utils/prompt';\n\nconst ExportPrompt = () => {\n  const { t } = useTranslation();\n  const prompts = useStore.getState().prompts;\n\n  return (\n    <div className='mt-4'>\n      <div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {t('export')} (CSV)\n      </div>\n      <button\n        className='btn btn-small btn-primary'\n        onClick={() => {\n          exportPrompts(prompts);\n        }}\n        aria-label={t('export') as string}\n      >\n        {t('export')}\n      </button>\n    </div>\n  );\n};\n\nexport default ExportPrompt;\n"
  },
  {
    "path": "src/components/PromptLibraryMenu/ImportPrompt.tsx",
    "content": "import React, { useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { v4 as uuidv4 } from 'uuid';\nimport useStore from '@store/store';\n\nimport { importPromptCSV } from '@utils/prompt';\n\nconst ImportPrompt = () => {\n  const { t } = useTranslation();\n\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [alert, setAlert] = useState<{\n    message: string;\n    success: boolean;\n  } | null>(null);\n\n  const handleFileUpload = () => {\n    if (!inputRef || !inputRef.current) return;\n    const file = inputRef.current.files?.[0];\n    if (file) {\n      const reader = new FileReader();\n\n      reader.onload = (event) => {\n        const csvString = event.target?.result as string;\n\n        try {\n          const results = importPromptCSV(csvString);\n\n          const prompts = useStore.getState().prompts;\n          const setPrompts = useStore.getState().setPrompts;\n\n          const newPrompts = results.map((data) => {\n            const columns = Object.values(data);\n            return {\n              id: uuidv4(),\n              name: columns[0],\n              prompt: columns[1],\n            };\n          });\n\n          setPrompts(prompts.concat(newPrompts));\n\n          setAlert({ message: 'Succesfully imported!', success: true });\n        } catch (error: unknown) {\n          setAlert({ message: (error as Error).message, success: false });\n        }\n      };\n\n      reader.readAsText(file);\n    }\n  };\n\n  return (\n    <div>\n      <label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {t('import')} (CSV)\n      </label>\n      <input\n        className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'\n        type='file'\n        ref={inputRef}\n      />\n      <button\n        className='btn btn-small btn-primary mt-3'\n        onClick={handleFileUpload}\n        aria-label={t('import') as string}\n      >\n        {t('import')}\n      </button>\n      {alert && (\n        <div\n          className={`relative py-2 px-3 w-full mt-3 border rounded-md text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap ${\n            alert.success\n              ? 'border-green-500 bg-green-500/10'\n              : 'border-red-500 bg-red-500/10'\n          }`}\n        >\n          {alert.message}\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default ImportPrompt;\n"
  },
  {
    "path": "src/components/PromptLibraryMenu/PromptLibraryMenu.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport useStore from '@store/store';\nimport { useTranslation } from 'react-i18next';\n\nimport PopupModal from '@components/PopupModal';\nimport { Prompt } from '@type/prompt';\nimport PlusIcon from '@icon/PlusIcon';\nimport CrossIcon from '@icon/CrossIcon';\nimport { v4 as uuidv4 } from 'uuid';\nimport ImportPrompt from './ImportPrompt';\nimport ExportPrompt from './ExportPrompt';\n\nconst PromptLibraryMenu = () => {\n  const { t } = useTranslation();\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n  return (\n    <div>\n      <button\n        className='btn btn-neutral'\n        onClick={() => setIsModalOpen(true)}\n        aria-label={t('promptLibrary') as string}\n      >\n        {t('promptLibrary')}\n      </button>\n      {isModalOpen && (\n        <PromptLibraryMenuPopUp setIsModalOpen={setIsModalOpen} />\n      )}\n    </div>\n  );\n};\n\nconst PromptLibraryMenuPopUp = ({\n  setIsModalOpen,\n}: {\n  setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  const { t } = useTranslation();\n\n  const setPrompts = useStore((state) => state.setPrompts);\n  const prompts = useStore((state) => state.prompts);\n\n  const [_prompts, _setPrompts] = useState<Prompt[]>(\n    JSON.parse(JSON.stringify(prompts))\n  );\n  const container = useRef<HTMLDivElement>(null);\n\n  const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    e.target.style.height = 'auto';\n    e.target.style.height = `${e.target.scrollHeight}px`;\n    e.target.style.maxHeight = `${e.target.scrollHeight}px`;\n  };\n\n  const handleSave = () => {\n    setPrompts(_prompts);\n    setIsModalOpen(false);\n  };\n\n  const addPrompt = () => {\n    const updatedPrompts: Prompt[] = JSON.parse(JSON.stringify(_prompts));\n    updatedPrompts.push({\n      id: uuidv4(),\n      name: '',\n      prompt: '',\n    });\n    _setPrompts(updatedPrompts);\n  };\n\n  const deletePrompt = (index: number) => {\n    const updatedPrompts: Prompt[] = JSON.parse(JSON.stringify(_prompts));\n    updatedPrompts.splice(index, 1);\n    _setPrompts(updatedPrompts);\n  };\n\n  const clearPrompts = () => {\n    _setPrompts([]);\n  };\n\n  const handleOnFocus = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {\n    e.target.style.height = 'auto';\n    e.target.style.height = `${e.target.scrollHeight}px`;\n    e.target.style.maxHeight = `${e.target.scrollHeight}px`;\n  };\n\n  const handleOnBlur = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {\n    e.target.style.height = 'auto';\n    e.target.style.maxHeight = '2.5rem';\n  };\n\n  useEffect(() => {\n    _setPrompts(prompts);\n  }, [prompts]);\n\n  return (\n    <PopupModal\n      title={t('promptLibrary') as string}\n      setIsModalOpen={setIsModalOpen}\n      handleConfirm={handleSave}\n    >\n      <div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'>\n        <div className='border px-4 py-2 rounded border-gray-200 dark:border-gray-600'>\n          <ImportPrompt />\n          <ExportPrompt />\n        </div>\n        <div className='flex flex-col p-2 max-w-full' ref={container}>\n          <div className='flex font-bold border-b border-gray-500/50 mb-1 p-1'>\n            <div className='sm:w-1/4 max-sm:flex-1'>{t('name')}</div>\n            <div className='flex-1'>{t('prompt')}</div>\n          </div>\n          {_prompts.map((prompt, index) => (\n            <div\n              key={prompt.id}\n              className='flex items-center border-b border-gray-500/50 mb-1 p-1'\n            >\n              <div className='sm:w-1/4 max-sm:flex-1'>\n                <textarea\n                  className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 focus:ring-1 focus:ring-blue w-full max-h-10 transition-all'\n                  onFocus={handleOnFocus}\n                  onBlur={handleOnBlur}\n                  onChange={(e) => {\n                    _setPrompts((prev) => {\n                      const newPrompts = [...prev];\n                      newPrompts[index].name = e.target.value;\n                      return newPrompts;\n                    });\n                  }}\n                  onInput={handleInput}\n                  value={prompt.name}\n                  rows={1}\n                ></textarea>\n              </div>\n              <div className='flex-1'>\n                <textarea\n                  className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 focus:ring-1 focus:ring-blue w-full max-h-10 transition-all'\n                  onFocus={handleOnFocus}\n                  onBlur={handleOnBlur}\n                  onChange={(e) => {\n                    _setPrompts((prev) => {\n                      const newPrompts = [...prev];\n                      newPrompts[index].prompt = e.target.value;\n                      return newPrompts;\n                    });\n                  }}\n                  onInput={handleInput}\n                  value={prompt.prompt}\n                  rows={1}\n                ></textarea>\n              </div>\n              <div\n                className='cursor-pointer'\n                onClick={() => deletePrompt(index)}\n              >\n                <CrossIcon />\n              </div>\n            </div>\n          ))}\n        </div>\n        <div className='flex justify-center cursor-pointer' onClick={addPrompt}>\n          <PlusIcon />\n        </div>\n        <div className='flex justify-center mt-2'>\n          <div\n            className='btn btn-neutral cursor-pointer text-xs'\n            onClick={clearPrompts}\n          >\n            {t('clearPrompts')}\n          </div>\n        </div>\n        <div className='mt-6 px-2'>\n          {t('morePrompts')}\n          <a\n            href='https://github.com/f/awesome-chatgpt-prompts'\n            target='_blank'\n            className='link'\n          >\n            awesome-chatgpt-prompts\n          </a>\n        </div>\n      </div>\n    </PopupModal>\n  );\n};\n\nexport default PromptLibraryMenu;\n"
  },
  {
    "path": "src/components/PromptLibraryMenu/index.ts",
    "content": "export { default } from './PromptLibraryMenu';\n"
  },
  {
    "path": "src/components/SearchBar/SearchBar.tsx",
    "content": "import React from 'react';\nimport { useTranslation } from 'react-i18next';\n\nconst SearchBar = ({\n  value,\n  handleChange,\n  className,\n  disabled,\n}: {\n  value: string;\n  handleChange: React.ChangeEventHandler<HTMLInputElement>;\n  className?: React.HTMLAttributes<HTMLDivElement>['className'];\n  disabled?: boolean;\n}) => {\n  const { t } = useTranslation();\n\n  return (\n    <div className={className}>\n      <input\n        disabled={disabled}\n        type='text'\n        className='text-gray-800 dark:text-white p-3 text-sm bg-transparent disabled:opacity-40  disabled:cursor-not-allowed transition-opacity m-0 w-full h-full focus:outline-none rounded border border-white/20'\n        placeholder={t('search') as string}\n        value={value}\n        onChange={(e) => {\n          handleChange(e);\n        }}\n      />\n    </div>\n  );\n};\n\nexport default SearchBar;\n"
  },
  {
    "path": "src/components/SearchBar/index.ts",
    "content": "export { default } from './SearchBar';\n"
  },
  {
    "path": "src/components/SettingsMenu/AdvencedModeToggle.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport Toggle from '@components/Toggle';\n\nconst AdvancedModeToggle = () => {\n  const { t } = useTranslation();\n\n  const setAdvancedMode = useStore((state) => state.setAdvancedMode);\n\n  const [isChecked, setIsChecked] = useState<boolean>(\n    useStore.getState().advancedMode\n  );\n\n  useEffect(() => {\n    setAdvancedMode(isChecked);\n  }, [isChecked]);\n\n  return (\n    <Toggle\n      label={t('advancedMode') as string}\n      isChecked={isChecked}\n      setIsChecked={setIsChecked}\n    />\n  );\n};\n\nexport default AdvancedModeToggle;\n"
  },
  {
    "path": "src/components/SettingsMenu/AutoTitleToggle.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport Toggle from '@components/Toggle';\n\nconst AutoTitleToggle = () => {\n  const { t } = useTranslation();\n\n  const setAutoTitle = useStore((state) => state.setAutoTitle);\n\n  const [isChecked, setIsChecked] = useState<boolean>(\n    useStore.getState().autoTitle\n  );\n\n  useEffect(() => {\n    setAutoTitle(isChecked);\n  }, [isChecked]);\n\n  return (\n    <Toggle\n      label={t('autoTitle') as string}\n      isChecked={isChecked}\n      setIsChecked={setIsChecked}\n    />\n  );\n};\n\nexport default AutoTitleToggle;\n"
  },
  {
    "path": "src/components/SettingsMenu/EnterToSubmitToggle.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport Toggle from '@components/Toggle';\n\nconst EnterToSubmitToggle = () => {\n  const { t } = useTranslation();\n\n  const setEnterToSubmit = useStore((state) => state.setEnterToSubmit);\n\n  const [isChecked, setIsChecked] = useState<boolean>(\n    useStore.getState().enterToSubmit\n  );\n\n  useEffect(() => {\n    setEnterToSubmit(isChecked);\n  }, [isChecked]);\n\n  return (\n    <Toggle\n      label={t('enterToSubmit') as string}\n      isChecked={isChecked}\n      setIsChecked={setIsChecked}\n    />\n  );\n};\n\nexport default EnterToSubmitToggle;\n"
  },
  {
    "path": "src/components/SettingsMenu/InlineLatexToggle.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport Toggle from '@components/Toggle';\n\nconst InlineLatexToggle = () => {\n  const { t } = useTranslation();\n\n  const setInlineLatex = useStore((state) => state.setInlineLatex);\n\n  const [isChecked, setIsChecked] = useState<boolean>(\n    useStore.getState().inlineLatex\n  );\n\n  useEffect(() => {\n    setInlineLatex(isChecked);\n  }, [isChecked]);\n\n  return (\n    <Toggle\n      label={t('inlineLatex') as string}\n      isChecked={isChecked}\n      setIsChecked={setIsChecked}\n    />\n  );\n};\n\nexport default InlineLatexToggle;\n"
  },
  {
    "path": "src/components/SettingsMenu/SettingsMenu.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\nimport useCloudAuthStore from '@store/cloud-auth-store';\n\nimport PopupModal from '@components/PopupModal';\nimport SettingIcon from '@icon/SettingIcon';\nimport ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher';\nimport LanguageSelector from '@components/LanguageSelector';\nimport AutoTitleToggle from './AutoTitleToggle';\nimport AdvancedModeToggle from './AdvencedModeToggle';\nimport InlineLatexToggle from './InlineLatexToggle';\n\nimport PromptLibraryMenu from '@components/PromptLibraryMenu';\nimport ChatConfigMenu from '@components/ChatConfigMenu';\nimport EnterToSubmitToggle from './EnterToSubmitToggle';\nimport TotalTokenCost, { TotalTokenCostToggle } from './TotalTokenCost';\nimport ClearConversation from '@components/Menu/MenuOptions/ClearConversation';\n\nconst SettingsMenu = () => {\n  const { t } = useTranslation();\n\n  const theme = useStore.getState().theme;\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  useEffect(() => {\n    document.documentElement.className = theme;\n  }, [theme]);\n  return (\n    <>\n      <a\n        className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n      >\n        <SettingIcon className='w-4 h-4' /> {t('setting') as string}\n      </a>\n      {isModalOpen && (\n        <PopupModal\n          setIsModalOpen={setIsModalOpen}\n          title={t('setting') as string}\n          cancelButton={false}\n        >\n          <div className='p-6 border-b border-gray-200 dark:border-gray-600 flex flex-col items-center gap-4'>\n            <LanguageSelector />\n            <ThemeSwitcher />\n            <div className='flex flex-col gap-3'>\n              <AutoTitleToggle />\n              <EnterToSubmitToggle />\n              <InlineLatexToggle />\n              <AdvancedModeToggle />\n              <TotalTokenCostToggle />\n            </div>\n            <ClearConversation />\n            <PromptLibraryMenu />\n            <ChatConfigMenu />\n            <TotalTokenCost />\n          </div>\n        </PopupModal>\n      )}\n    </>\n  );\n};\n\nexport default SettingsMenu;\n"
  },
  {
    "path": "src/components/SettingsMenu/TotalTokenCost.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport useStore from '@store/store';\n\nimport { modelCost } from '@constants/chat';\nimport Toggle from '@components/Toggle/Toggle';\n\nimport { ModelOptions, TotalTokenUsed } from '@type/chat';\n\nimport CalculatorIcon from '@icon/CalculatorIcon';\n\ntype CostMapping = { model: string; cost: number }[];\n\nconst tokenCostToCost = (\n  tokenCost: TotalTokenUsed[ModelOptions],\n  model: ModelOptions\n) => {\n  if (!tokenCost) return 0;\n  const { prompt, completion } = modelCost[model as keyof typeof modelCost];\n  const completionCost =\n    (completion.price / completion.unit) * tokenCost.completionTokens;\n  const promptCost = (prompt.price / prompt.unit) * tokenCost.promptTokens;\n  return completionCost + promptCost;\n};\n\nconst TotalTokenCost = () => {\n  const { t } = useTranslation(['main', 'model']);\n\n  const totalTokenUsed = useStore((state) => state.totalTokenUsed);\n  const setTotalTokenUsed = useStore((state) => state.setTotalTokenUsed);\n  const countTotalTokens = useStore((state) => state.countTotalTokens);\n\n  const [costMapping, setCostMapping] = useState<CostMapping>([]);\n\n  const resetCost = () => {\n    setTotalTokenUsed({});\n  };\n\n  useEffect(() => {\n    const updatedCostMapping: CostMapping = [];\n    Object.entries(totalTokenUsed).forEach(([model, tokenCost]) => {\n      const cost = tokenCostToCost(tokenCost, model as ModelOptions);\n      updatedCostMapping.push({ model, cost });\n    });\n\n    setCostMapping(updatedCostMapping);\n  }, [totalTokenUsed]);\n\n  return countTotalTokens ? (\n    <div className='flex flex-col items-center gap-2'>\n      <div className='relative overflow-x-auto shadow-md sm:rounded-lg'>\n        <table className='w-full text-sm text-left text-gray-500 dark:text-gray-400'>\n          <thead className='text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400'>\n            <tr>\n              <th className='px-4 py-2'>{t('model', { ns: 'model' })}</th>\n              <th className='px-4 py-2'>USD</th>\n            </tr>\n          </thead>\n          <tbody>\n            {costMapping.map(({ model, cost }) => (\n              <tr\n                key={model}\n                className='bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700'\n              >\n                <td className='px-4 py-2'>{model}</td>\n                <td className='px-4 py-2'>{cost.toPrecision(3)}</td>\n              </tr>\n            ))}\n            <tr className='bg-white border-b dark:bg-gray-800 dark:border-gray-700 font-bold'>\n              <td className='px-4 py-2'>{t('total', { ns: 'main' })}</td>\n              <td className='px-4 py-2'>\n                {costMapping\n                  .reduce((prev, curr) => prev + curr.cost, 0)\n                  .toPrecision(3)}\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n      <div className='btn btn-neutral cursor-pointer' onClick={resetCost}>\n        {t('resetCost', { ns: 'main' })}\n      </div>\n    </div>\n  ) : (\n    <></>\n  );\n};\n\nexport const TotalTokenCostToggle = () => {\n  const { t } = useTranslation('main');\n\n  const setCountTotalTokens = useStore((state) => state.setCountTotalTokens);\n\n  const [isChecked, setIsChecked] = useState<boolean>(\n    useStore.getState().countTotalTokens\n  );\n\n  useEffect(() => {\n    setCountTotalTokens(isChecked);\n  }, [isChecked]);\n\n  return (\n    <Toggle\n      label={t('countTotalTokens') as string}\n      isChecked={isChecked}\n      setIsChecked={setIsChecked}\n    />\n  );\n};\n\nexport const TotalTokenCostDisplay = () => {\n  const { t } = useTranslation();\n  const totalTokenUsed = useStore((state) => state.totalTokenUsed);\n\n  const [totalCost, setTotalCost] = useState<number>(0);\n\n  useEffect(() => {\n    let updatedTotalCost = 0;\n\n    Object.entries(totalTokenUsed).forEach(([model, tokenCost]) => {\n      updatedTotalCost += tokenCostToCost(tokenCost, model as ModelOptions);\n    });\n\n    setTotalCost(updatedTotalCost);\n  }, [totalTokenUsed]);\n\n  return (\n    <a className='flex py-2 px-2 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white text-sm'>\n      <CalculatorIcon />\n      {`USD ${totalCost.toPrecision(3)}`}\n    </a>\n  );\n};\n\nexport default TotalTokenCost;\n"
  },
  {
    "path": "src/components/SettingsMenu/index.ts",
    "content": "export { default } from './SettingsMenu';\n"
  },
  {
    "path": "src/components/ShareGPT/ShareGPT.tsx",
    "content": "import React, { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport useStore from '@store/store';\n\nimport PopupModal from '@components/PopupModal';\nimport { submitShareGPT } from '@api/api';\nimport { ShareGPTSubmitBodyInterface } from '@type/api';\n\nconst ShareGPT = React.memo(() => {\n  const { t } = useTranslation();\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n\n  const handleConfirm = async () => {\n    const chats = useStore.getState().chats;\n    const currentChatIndex = useStore.getState().currentChatIndex;\n    if (chats) {\n      try {\n        const items: ShareGPTSubmitBodyInterface['items'] = [];\n        const messages = document.querySelectorAll('.share-gpt-message');\n\n        messages.forEach((message, index) => {\n          items.push({\n            from: 'gpt',\n            value: `<p><b>${t(\n              chats[currentChatIndex].messages[index].role\n            )}</b></p>${message.innerHTML}`,\n          });\n        });\n\n        await submitShareGPT({\n          avatarUrl: '',\n          items,\n        });\n        setIsModalOpen(false);\n      } catch (e: unknown) {\n        console.log(e);\n      }\n    }\n  };\n\n  return (\n    <>\n      <button\n        className='btn btn-neutral'\n        onClick={() => {\n          setIsModalOpen(true);\n        }}\n        aria-label={t('postOnShareGPT.title') as string}\n      >\n        {t('postOnShareGPT.title')}\n      </button>\n      {isModalOpen && (\n        <PopupModal\n          setIsModalOpen={setIsModalOpen}\n          title={t('postOnShareGPT.title') as string}\n          message={t('postOnShareGPT.warning') as string}\n          handleConfirm={handleConfirm}\n        />\n      )}\n    </>\n  );\n});\n\nexport default ShareGPT;\n"
  },
  {
    "path": "src/components/ShareGPT/index.ts",
    "content": "export { default } from './ShareGPT';\n"
  },
  {
    "path": "src/components/StopGeneratingButton/StopGeneratingButton.tsx",
    "content": "import React from 'react';\nimport useStore from '@store/store';\n\nconst StopGeneratingButton = () => {\n  const setGenerating = useStore((state) => state.setGenerating);\n  const generating = useStore((state) => state.generating);\n\n  return generating ? (\n    <div\n      className='absolute bottom-6 left-0 right-0 m-auto flex md:w-full md:m-auto gap-0 md:gap-2 justify-center'\n      onClick={() => setGenerating(false)}\n    >\n      <button\n        className='btn relative btn-neutral border-0 md:border'\n        aria-label='stop generating'\n      >\n        <div className='flex w-full items-center justify-center gap-2'>\n          <svg\n            stroke='currentColor'\n            fill='none'\n            strokeWidth='1.5'\n            viewBox='0 0 24 24'\n            strokeLinecap='round'\n            strokeLinejoin='round'\n            className='h-3 w-3'\n            height='1em'\n            width='1em'\n            xmlns='http://www.w3.org/2000/svg'\n          >\n            <rect x='3' y='3' width='18' height='18' rx='2' ry='2'></rect>\n          </svg>\n          Stop generating\n        </div>\n      </button>\n    </div>\n  ) : (\n    <></>\n  );\n};\n\nexport default StopGeneratingButton;\n"
  },
  {
    "path": "src/components/Toast/Toast.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport useStore from '@store/store';\n\nexport type ToastStatus = 'success' | 'error' | 'warning';\n\nconst Toast = () => {\n  const message = useStore((state) => state.toastMessage);\n  const status = useStore((state) => state.toastStatus);\n  const toastShow = useStore((state) => state.toastShow);\n  const setToastShow = useStore((state) => state.setToastShow);\n\n  const [timeoutID, setTimeoutID] = useState<number>();\n\n  useEffect(() => {\n    if (toastShow) {\n      window.clearTimeout(timeoutID);\n\n      const newTimeoutID = window.setTimeout(() => {\n        setToastShow(false);\n      }, 5000);\n\n      setTimeoutID(newTimeoutID);\n    }\n  }, [toastShow, status, message]);\n\n  return toastShow ? (\n    <div\n      className={`flex fixed right-5 bottom-5 z-[1000] items-center w-3/4 md:w-full max-w-xs p-4 mb-4 text-gray-500 dark:text-gray-400 rounded-lg shadow-md border border-gray-400/30 animate-bounce`}\n      role='alert'\n    >\n      <StatusIcon status={status} />\n      <div className='ml-3 text-sm font-normal'>{message}</div>\n      <button\n        type='button'\n        className='ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700'\n        aria-label='Close'\n        onClick={() => {\n          setToastShow(false);\n        }}\n      >\n        <CloseIcon />\n      </button>\n    </div>\n  ) : (\n    <></>\n  );\n};\n\nconst StatusIcon = ({ status }: { status: ToastStatus }) => {\n  const statusToIcon = {\n    success: <CheckIcon />,\n    error: <ErrorIcon />,\n    warning: <WarningIcon />,\n  };\n  return statusToIcon[status] || null;\n};\n\nconst CloseIcon = () => (\n  <>\n    <span className='sr-only'>Close</span>\n    <svg\n      aria-hidden='true'\n      className='w-5 h-5'\n      fill='currentColor'\n      viewBox='0 0 20 20'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        fillRule='evenodd'\n        d='M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z'\n        clipRule='evenodd'\n      ></path>\n    </svg>\n  </>\n);\n\nconst CheckIcon = () => (\n  <div className='inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200'>\n    <svg\n      aria-hidden='true'\n      className='w-5 h-5'\n      fill='currentColor'\n      viewBox='0 0 20 20'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        fillRule='evenodd'\n        d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'\n        clipRule='evenodd'\n      ></path>\n    </svg>\n    <span className='sr-only'>Check icon</span>\n  </div>\n);\n\nconst ErrorIcon = () => (\n  <div className='inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200'>\n    <svg\n      aria-hidden='true'\n      className='w-5 h-5'\n      fill='currentColor'\n      viewBox='0 0 20 20'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        fillRule='evenodd'\n        d='M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z'\n        clipRule='evenodd'\n      ></path>\n    </svg>\n    <span className='sr-only'>Error icon</span>\n  </div>\n);\n\nconst WarningIcon = () => (\n  <div className='inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200'>\n    <svg\n      aria-hidden='true'\n      className='w-5 h-5'\n      fill='currentColor'\n      viewBox='0 0 20 20'\n      xmlns='http://www.w3.org/2000/svg'\n    >\n      <path\n        fillRule='evenodd'\n        d='M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z'\n        clipRule='evenodd'\n      ></path>\n    </svg>\n    <span className='sr-only'>Warning icon</span>\n  </div>\n);\n\nexport default Toast;\n"
  },
  {
    "path": "src/components/Toast/index.ts",
    "content": "export { default } from './Toast';\n"
  },
  {
    "path": "src/components/Toggle/Toggle.tsx",
    "content": "import React from 'react';\n\nconst Toggle = ({\n  label,\n  isChecked,\n  setIsChecked,\n}: {\n  label: string;\n  isChecked: boolean;\n  setIsChecked: React.Dispatch<React.SetStateAction<boolean>>;\n}) => {\n  return (\n    <label className='relative flex items-center cursor-pointer'>\n      <input\n        type='checkbox'\n        className='sr-only peer'\n        checked={isChecked}\n        onChange={() => {\n          setIsChecked((prev) => !prev);\n        }}\n      />\n      <div className=\"w-9 h-5 bg-gray-200 dark:bg-gray-600 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-green-500/70\"></div>\n      <span className='ml-3 text-sm font-medium text-gray-900 dark:text-gray-300'>\n        {label}\n      </span>\n    </label>\n  );\n};\n\nexport default Toggle;\n"
  },
  {
    "path": "src/components/Toggle/index.ts",
    "content": "export { default } from './Toggle';\n"
  },
  {
    "path": "src/components/TokenCount/TokenCount.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react';\nimport useStore from '@store/store';\nimport { shallow } from 'zustand/shallow';\n\nimport countTokens from '@utils/messageUtils';\nimport { modelCost } from '@constants/chat';\n\nconst TokenCount = React.memo(() => {\n  const [tokenCount, setTokenCount] = useState<number>(0);\n  const generating = useStore((state) => state.generating);\n  const messages = useStore(\n    (state) =>\n      state.chats ? state.chats[state.currentChatIndex].messages : [],\n    shallow\n  );\n\n  const model = useStore((state) =>\n    state.chats\n      ? state.chats[state.currentChatIndex].config.model\n      : 'gpt-3.5-turbo'\n  );\n\n  const cost = useMemo(() => {\n    const price =\n      modelCost[model].prompt.price *\n      (tokenCount / modelCost[model].prompt.unit);\n    return price.toPrecision(3);\n  }, [model, tokenCount]);\n\n  useEffect(() => {\n    if (!generating) setTokenCount(countTokens(messages, model));\n  }, [messages, generating]);\n\n  return (\n    <div className='absolute top-[-16px] right-0'>\n      <div className='text-xs italic text-gray-900 dark:text-gray-300'>\n        Tokens: {tokenCount} (${cost})\n      </div>\n    </div>\n  );\n});\n\nexport default TokenCount;\n"
  },
  {
    "path": "src/components/TokenCount/index.ts",
    "content": "export { default } from './TokenCount';\n"
  },
  {
    "path": "src/constants/auth.ts",
    "content": "export const officialAPIEndpoint = 'https://api.openai.com/v1/chat/completions';\nconst customAPIEndpoint =\n  import.meta.env.VITE_CUSTOM_API_ENDPOINT || 'https://chatgpt-api.shn.hk/v1/';\nexport const defaultAPIEndpoint =\n  import.meta.env.VITE_DEFAULT_API_ENDPOINT || officialAPIEndpoint;\n\nexport const availableEndpoints = [officialAPIEndpoint, customAPIEndpoint];\n"
  },
  {
    "path": "src/constants/chat.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport { ChatInterface, ConfigInterface, ModelOptions } from '@type/chat';\nimport useStore from '@store/store';\n\nconst date = new Date();\nconst dateString =\n  date.getFullYear() +\n  '-' +\n  ('0' + (date.getMonth() + 1)).slice(-2) +\n  '-' +\n  ('0' + date.getDate()).slice(-2);\n\n// default system message obtained using the following method: https://twitter.com/DeminDimin/status/1619935545144279040\nexport const _defaultSystemMessage =\n  import.meta.env.VITE_DEFAULT_SYSTEM_MESSAGE ??\n  `You are ChatGPT, a large language model trained by OpenAI.\nCarefully heed the user's instructions. \nRespond using Markdown.`;\n\nexport const modelOptions: ModelOptions[] = [\n  'gpt-3.5-turbo',\n  'gpt-3.5-turbo-16k',\n  'gpt-3.5-turbo-1106',\n  'gpt-3.5-turbo-0125',\n  'gpt-4',\n  'gpt-4-32k',\n  'gpt-4-1106-preview',\n  'gpt-4-0125-preview',\n  'gpt-4-turbo',\n  'gpt-4-turbo-2024-04-09',\n  'gpt-4o',\n  'gpt-4o-2024-05-13',\n  // 'gpt-3.5-turbo-0301',\n  // 'gpt-4-0314',\n  // 'gpt-4-32k-0314',\n];\n\nexport const defaultModel = 'gpt-3.5-turbo';\n\nexport const modelMaxToken = {\n  'gpt-3.5-turbo': 4096,\n  'gpt-3.5-turbo-0301': 4096,\n  'gpt-3.5-turbo-0613': 4096,\n  'gpt-3.5-turbo-16k': 16384,\n  'gpt-3.5-turbo-16k-0613': 16384,\n  'gpt-3.5-turbo-1106': 16384,\n  'gpt-3.5-turbo-0125': 16384,\n  'gpt-4': 8192,\n  'gpt-4-0314': 8192,\n  'gpt-4-0613': 8192,\n  'gpt-4-32k': 32768,\n  'gpt-4-32k-0314': 32768,\n  'gpt-4-32k-0613': 32768,\n  'gpt-4-1106-preview': 128000,\n  'gpt-4-0125-preview': 128000,\n  'gpt-4-turbo': 128000,\n  'gpt-4-turbo-2024-04-09': 128000,\n  'gpt-4o': 128000,\n  'gpt-4o-2024-05-13': 128000,\n};\n\nexport const modelCost = {\n  'gpt-3.5-turbo': {\n    prompt: { price: 0.0015, unit: 1000 },\n    completion: { price: 0.002, unit: 1000 },\n  },\n  'gpt-3.5-turbo-0301': {\n    prompt: { price: 0.0015, unit: 1000 },\n    completion: { price: 0.002, unit: 1000 },\n  },\n  'gpt-3.5-turbo-0613': {\n    prompt: { price: 0.0015, unit: 1000 },\n    completion: { price: 0.002, unit: 1000 },\n  },\n  'gpt-3.5-turbo-16k': {\n    prompt: { price: 0.003, unit: 1000 },\n    completion: { price: 0.004, unit: 1000 },\n  },\n  'gpt-3.5-turbo-16k-0613': {\n    prompt: { price: 0.003, unit: 1000 },\n    completion: { price: 0.004, unit: 1000 },\n  },\n  'gpt-3.5-turbo-1106': {\n    prompt: { price: 0.001, unit: 1000 },\n    completion: { price: 0.0015, unit: 1000 },\n  },\n  'gpt-3.5-turbo-0125': {\n    prompt: { price: 0.0005, unit: 1000 },\n    completion: { price: 0.0015, unit: 1000 },\n  },\n  'gpt-4': {\n    prompt: { price: 0.03, unit: 1000 },\n    completion: { price: 0.06, unit: 1000 },\n  },\n  'gpt-4-0314': {\n    prompt: { price: 0.03, unit: 1000 },\n    completion: { price: 0.06, unit: 1000 },\n  },\n  'gpt-4-0613': {\n    prompt: { price: 0.03, unit: 1000 },\n    completion: { price: 0.06, unit: 1000 },\n  },\n  'gpt-4-32k': {\n    prompt: { price: 0.06, unit: 1000 },\n    completion: { price: 0.12, unit: 1000 },\n  },\n  'gpt-4-32k-0314': {\n    prompt: { price: 0.06, unit: 1000 },\n    completion: { price: 0.12, unit: 1000 },\n  },\n  'gpt-4-32k-0613': {\n    prompt: { price: 0.06, unit: 1000 },\n    completion: { price: 0.12, unit: 1000 },\n  },\n  'gpt-4-1106-preview': {\n    prompt: { price: 0.01, unit: 1000 },\n    completion: { price: 0.03, unit: 1000 },\n  },\n  'gpt-4-0125-preview': {\n    prompt: { price: 0.01, unit: 1000 },\n    completion: { price: 0.03, unit: 1000 },\n  },\n  'gpt-4-turbo': {\n    prompt: { price: 0.01, unit: 1000 },\n    completion: { price: 0.03, unit: 1000 },\n  },\n  'gpt-4-turbo-2024-04-09': {\n    prompt: { price: 0.01, unit: 1000 },\n    completion: { price: 0.03, unit: 1000 },\n  },\n  'gpt-4o': {\n    prompt: { price: 0.005, unit: 1000 },\n    completion: { price: 0.015, unit: 1000 },\n  },\n  'gpt-4o-2024-05-13': {\n    prompt: { price: 0.005, unit: 1000 },\n    completion: { price: 0.015, unit: 1000 },\n  },\n};\n\nexport const defaultUserMaxToken = 4000;\n\nexport const _defaultChatConfig: ConfigInterface = {\n  model: defaultModel,\n  max_tokens: defaultUserMaxToken,\n  temperature: 1,\n  presence_penalty: 0,\n  top_p: 1,\n  frequency_penalty: 0,\n};\n\nexport const generateDefaultChat = (\n  title?: string,\n  folder?: string\n): ChatInterface => ({\n  id: uuidv4(),\n  title: title ? title : 'New Chat',\n  messages:\n    useStore.getState().defaultSystemMessage.length > 0\n      ? [{ role: 'system', content: useStore.getState().defaultSystemMessage }]\n      : [],\n  config: { ...useStore.getState().defaultChatConfig },\n  titleSet: false,\n  folder,\n});\n\nexport const codeLanguageSubset = [\n  'python',\n  'javascript',\n  'java',\n  'go',\n  'bash',\n  'c',\n  'cpp',\n  'csharp',\n  'css',\n  'diff',\n  'graphql',\n  'json',\n  'kotlin',\n  'less',\n  'lua',\n  'makefile',\n  'markdown',\n  'objectivec',\n  'perl',\n  'php',\n  'php-template',\n  'plaintext',\n  'python-repl',\n  'r',\n  'ruby',\n  'rust',\n  'scss',\n  'shell',\n  'sql',\n  'swift',\n  'typescript',\n  'vbnet',\n  'wasm',\n  'xml',\n  'yaml',\n];\n"
  },
  {
    "path": "src/constants/color.ts",
    "content": "export const folderColorOptions = [\n  '#be123c', // rose-700\n  '#6d28d9', // violet-700\n  '#0369a1', // sky-700\n  '#047857', // emerald-700\n  '#b45309', // amber-700\n];\n"
  },
  {
    "path": "src/constants/language.ts",
    "content": "// languages that have translation files in `public/locales`\nexport const i18nLanguages = [\n  // 'ar',\n  'da',\n  'de',\n  'en',\n  'en-GB',\n  'en-US',\n  'es',\n  'fr',\n  'fr-FR',\n  'it',\n  'ja',\n  'ms',\n  'nb',\n  'ro',\n  'ru',\n  'sv',\n  // 'ug',\n  'yue',\n  'zh',\n  'zh-CN',\n  'zh-HK',\n  'zh-TW',\n] as const;\n\n// languages that are selectable on the web page\nexport const selectableLanguages = [\n  // 'ar',\n  'da',\n  'de',\n  // 'en',\n  'en-GB',\n  'en-US',\n  'es',\n  // 'fr',\n  'fr-FR',\n  'it',\n  'ja',\n  'ms',\n  'nb',\n  'ro',\n  'ru',\n  'sv',\n  // 'ug',\n  'yue',\n  // 'zh',\n  'zh-CN',\n  // 'zh-HK',\n  'zh-TW',\n] as const;\n\nexport const languageCodeToName = {\n  // 'ar': 'العربية',\n  'da': 'Dansk',\n  'de': 'Deutsch',\n  'en': 'English',\n  'en-GB': 'English (UK)',\n  'en-US': 'English (US)',\n  'es': 'Español',\n  'fr': 'Français',\n  'fr-FR': 'Français', // Français (France). no need to include \"France\" at this time, as there is currently only one variant\n  'it': 'Italiano',\n  'ja': '日本語',\n  'ms': 'Bahasa Melayu',\n  'nb': 'Norsk bokmål',\n  'ro': 'Română',\n  'ru': 'Русский',\n  'sv': 'Svenska',\n  // 'ug': 'ئۇيغۇرچە',\n  'yue': '廣東話',\n  'zh': '中文',\n  'zh-CN': '中文（简体）',\n  'zh-HK': '廣東話', // 中文（香港）. currently there is no support for `zh-HK`, so `zh-HK` will be regarded as `yue`\n  'zh-TW': '中文（台灣）',\n};\n"
  },
  {
    "path": "src/constants/prompt.ts",
    "content": "import { Prompt } from '@type/prompt';\n\n// prompts from https://github.com/f/awesome-chatgpt-prompts\nconst defaultPrompts: Prompt[] = [\n  {\n    id: '0d3e9cb7-b585-43fa-acc3-840c189f6b93',\n    name: 'English Translator',\n    prompt:\n      'I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. Do you understand?',\n  },\n];\n\nexport default defaultPrompts;\n"
  },
  {
    "path": "src/fonts/LICENSE.txt",
    "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "src/hooks/useAddChat.ts",
    "content": "import React from 'react';\nimport useStore from '@store/store';\nimport { generateDefaultChat } from '@constants/chat';\nimport { ChatInterface } from '@type/chat';\n\nconst useAddChat = () => {\n  const setChats = useStore((state) => state.setChats);\n  const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n\n  const addChat = (folder?:string) => {\n    const chats = useStore.getState().chats;\n    if (chats) {\n      const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));\n      let titleIndex = 1;\n      let title = `New Chat ${titleIndex}`;\n\n      while (chats.some((chat) => chat.title === title)) {\n        titleIndex += 1;\n        title = `New Chat ${titleIndex}`;\n      }\n\n      updatedChats.unshift(generateDefaultChat(title, folder));\n      setChats(updatedChats);\n      setCurrentChatIndex(0);\n    }\n  };\n\n  return addChat;\n};\n\nexport default useAddChat;\n"
  },
  {
    "path": "src/hooks/useHideOnOutsideClick.ts",
    "content": "import React, { useEffect, useRef, useState } from 'react';\n\nconst useHideOnOutsideClick = (): [\n  boolean,\n  React.Dispatch<React.SetStateAction<boolean>>,\n  React.RefObject<HTMLDivElement>\n] => {\n  const elementRef = useRef<HTMLDivElement>(null);\n  const [showElement, setShowElement] = useState<boolean>(false);\n\n  const handleClickOutside = (event: MouseEvent) => {\n    if (\n      elementRef.current &&\n      !elementRef.current.contains(event.target as Node)\n    ) {\n      setShowElement(false);\n    }\n  };\n\n  useEffect(() => {\n    // Bind the event listener only if the element is show.\n    if (showElement) {\n      document.addEventListener('mousedown', handleClickOutside);\n    } else {\n      document.removeEventListener('mousedown', handleClickOutside);\n    }\n\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [showElement, elementRef]);\n\n  return [showElement, setShowElement, elementRef];\n};\n\nexport default useHideOnOutsideClick;\n"
  },
  {
    "path": "src/hooks/useInitialiseNewChat.ts",
    "content": "import React from 'react';\nimport useStore from '@store/store';\nimport { MessageInterface } from '@type/chat';\nimport { generateDefaultChat } from '@constants/chat';\n\nconst useInitialiseNewChat = () => {\n  const setChats = useStore((state) => state.setChats);\n  const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);\n\n  const initialiseNewChat = () => {\n    setChats([generateDefaultChat()]);\n    setCurrentChatIndex(0);\n  };\n\n  return initialiseNewChat;\n};\n\nexport default useInitialiseNewChat;\n"
  },
  {
    "path": "src/hooks/useSaveToLocalStorage.ts",
    "content": "import React, { useEffect, useRef } from 'react';\nimport useStore from '@store/store';\n\nconst useSaveToLocalStorage = () => {\n  const chatsRef = useRef(useStore.getState().chats);\n\n  useEffect(() => {\n    const unsubscribe = useStore.subscribe((state) => {\n      if (chatsRef && chatsRef.current !== state.chats) {\n        chatsRef.current = state.chats;\n        localStorage.setItem('chats', JSON.stringify(state.chats));\n      }\n    });\n\n    return unsubscribe;\n  }, []);\n};\n\nexport default useSaveToLocalStorage;\n"
  },
  {
    "path": "src/hooks/useSubmit.ts",
    "content": "import React from 'react';\nimport useStore from '@store/store';\nimport { useTranslation } from 'react-i18next';\nimport { ChatInterface, MessageInterface } from '@type/chat';\nimport { getChatCompletion, getChatCompletionStream } from '@api/api';\nimport { parseEventSource } from '@api/helper';\nimport { limitMessageTokens, updateTotalTokenUsed } from '@utils/messageUtils';\nimport { _defaultChatConfig } from '@constants/chat';\nimport { officialAPIEndpoint } from '@constants/auth';\n\nconst useSubmit = () => {\n  const { t, i18n } = useTranslation('api');\n  const error = useStore((state) => state.error);\n  const setError = useStore((state) => state.setError);\n  const apiEndpoint = useStore((state) => state.apiEndpoint);\n  const apiKey = useStore((state) => state.apiKey);\n  const setGenerating = useStore((state) => state.setGenerating);\n  const generating = useStore((state) => state.generating);\n  const currentChatIndex = useStore((state) => state.currentChatIndex);\n  const setChats = useStore((state) => state.setChats);\n\n  const generateTitle = async (\n    message: MessageInterface[]\n  ): Promise<string> => {\n    let data;\n    try {\n      if (!apiKey || apiKey.length === 0) {\n        // official endpoint\n        if (apiEndpoint === officialAPIEndpoint) {\n          throw new Error(t('noApiKeyWarning') as string);\n        }\n\n        // other endpoints\n        data = await getChatCompletion(\n          useStore.getState().apiEndpoint,\n          message,\n          _defaultChatConfig\n        );\n      } else if (apiKey) {\n        // own apikey\n        data = await getChatCompletion(\n          useStore.getState().apiEndpoint,\n          message,\n          _defaultChatConfig,\n          apiKey\n        );\n      }\n    } catch (error: unknown) {\n      throw new Error(`Error generating title!\\n${(error as Error).message}`);\n    }\n    return data.choices[0].message.content;\n  };\n\n  const handleSubmit = async () => {\n    const chats = useStore.getState().chats;\n    if (generating || !chats) return;\n\n    const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));\n\n    updatedChats[currentChatIndex].messages.push({\n      role: 'assistant',\n      content: '',\n    });\n\n    setChats(updatedChats);\n    setGenerating(true);\n\n    try {\n      let stream;\n      if (chats[currentChatIndex].messages.length === 0)\n        throw new Error('No messages submitted!');\n\n      const messages = limitMessageTokens(\n        chats[currentChatIndex].messages,\n        chats[currentChatIndex].config.max_tokens,\n        chats[currentChatIndex].config.model\n      );\n      if (messages.length === 0) throw new Error('Message exceed max token!');\n\n      // no api key (free)\n      if (!apiKey || apiKey.length === 0) {\n        // official endpoint\n        if (apiEndpoint === officialAPIEndpoint) {\n          throw new Error(t('noApiKeyWarning') as string);\n        }\n\n        // other endpoints\n        stream = await getChatCompletionStream(\n          useStore.getState().apiEndpoint,\n          messages,\n          chats[currentChatIndex].config\n        );\n      } else if (apiKey) {\n        // own apikey\n        stream = await getChatCompletionStream(\n          useStore.getState().apiEndpoint,\n          messages,\n          chats[currentChatIndex].config,\n          apiKey\n        );\n      }\n\n      if (stream) {\n        if (stream.locked)\n          throw new Error(\n            'Oops, the stream is locked right now. Please try again'\n          );\n        const reader = stream.getReader();\n        let reading = true;\n        let partial = '';\n        while (reading && useStore.getState().generating) {\n          const { done, value } = await reader.read();\n          const result = parseEventSource(\n            partial + new TextDecoder().decode(value)\n          );\n          partial = '';\n\n          if (result === '[DONE]' || done) {\n            reading = false;\n          } else {\n            const resultString = result.reduce((output: string, curr) => {\n              if (typeof curr === 'string') {\n                partial += curr;\n              } else {\n                const content = curr.choices[0]?.delta?.content ?? null;\n                if (content) output += content;\n              }\n              return output;\n            }, '');\n\n            const updatedChats: ChatInterface[] = JSON.parse(\n              JSON.stringify(useStore.getState().chats)\n            );\n            const updatedMessages = updatedChats[currentChatIndex].messages;\n            updatedMessages[updatedMessages.length - 1].content += resultString;\n            setChats(updatedChats);\n          }\n        }\n        if (useStore.getState().generating) {\n          reader.cancel('Cancelled by user');\n        } else {\n          reader.cancel('Generation completed');\n        }\n        reader.releaseLock();\n        stream.cancel();\n      }\n\n      // update tokens used in chatting\n      const currChats = useStore.getState().chats;\n      const countTotalTokens = useStore.getState().countTotalTokens;\n\n      if (currChats && countTotalTokens) {\n        const model = currChats[currentChatIndex].config.model;\n        const messages = currChats[currentChatIndex].messages;\n        updateTotalTokenUsed(\n          model,\n          messages.slice(0, -1),\n          messages[messages.length - 1]\n        );\n      }\n\n      // generate title for new chats\n      if (\n        useStore.getState().autoTitle &&\n        currChats &&\n        !currChats[currentChatIndex]?.titleSet\n      ) {\n        const messages_length = currChats[currentChatIndex].messages.length;\n        const assistant_message =\n          currChats[currentChatIndex].messages[messages_length - 1].content;\n        const user_message =\n          currChats[currentChatIndex].messages[messages_length - 2].content;\n\n        const message: MessageInterface = {\n          role: 'user',\n          content: `Generate a title in less than 6 words for the following message (language: ${i18n.language}):\\n\"\"\"\\nUser: ${user_message}\\nAssistant: ${assistant_message}\\n\"\"\"`,\n        };\n\n        let title = (await generateTitle([message])).trim();\n        if (title.startsWith('\"') && title.endsWith('\"')) {\n          title = title.slice(1, -1);\n        }\n        const updatedChats: ChatInterface[] = JSON.parse(\n          JSON.stringify(useStore.getState().chats)\n        );\n        updatedChats[currentChatIndex].title = title;\n        updatedChats[currentChatIndex].titleSet = true;\n        setChats(updatedChats);\n\n        // update tokens used for generating title\n        if (countTotalTokens) {\n          const model = _defaultChatConfig.model;\n          updateTotalTokenUsed(model, [message], {\n            role: 'assistant',\n            content: title,\n          });\n        }\n      }\n    } catch (e: unknown) {\n      const err = (e as Error).message;\n      console.log(err);\n      setError(err);\n    }\n    setGenerating(false);\n  };\n\n  return { handleSubmit, error };\n};\n\nexport default useSubmit;\n"
  },
  {
    "path": "src/i18n.ts",
    "content": "import i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\n\nimport Backend from 'i18next-http-backend';\nimport LanguageDetector from 'i18next-browser-languagedetector';\n\nconst googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID || undefined;\n\nconst namespace = ['main', 'api', 'about', 'model'];\nif (googleClientId) namespace.push('drive');\n\ni18n\n  .use(Backend)\n  .use(LanguageDetector)\n  .use(initReactI18next)\n  .init({\n    backend: {\n      loadPath: 'locales/{{lng}}/{{ns}}.json',\n    },\n    fallbackLng: {\n      default: ['en'],\n    },\n    ns: namespace,\n    defaultNS: 'main',\n  });\n\nexport default i18n;\n"
  },
  {
    "path": "src/main.css",
    "content": "@import url(Roboto.css);\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  * {\n    box-sizing: border-box;\n  }\n\n  body,\n  html {\n    height: 100%;\n  }\n\n  body {\n    line-height: inherit;\n    margin: 0;\n  }\n\n  .dark body,\n  .dark html {\n    --tw-bg-opacity: 1;\n    background-color: rgba(52, 53, 65, var(--tw-bg-opacity));\n  }\n\n  #root {\n    height: 100%;\n  }\n\n  .markdown table {\n    --tw-border-spacing-x: 0px;\n    --tw-border-spacing-y: 0px;\n    border-collapse: separate;\n    border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);\n    width: 100%;\n  }\n  .markdown th {\n    background-color: rgba(236, 236, 241, 0.2);\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-width: 1px;\n    padding: 0.25rem 0.75rem;\n  }\n  .markdown th:first-child {\n    border-top-left-radius: 0.375rem;\n  }\n  .markdown th:last-child {\n    border-right-width: 1px;\n    border-top-right-radius: 0.375rem;\n  }\n  .markdown td {\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    padding: 0.25rem 0.75rem;\n  }\n  .markdown td:last-child {\n    border-right-width: 1px;\n  }\n  .markdown tbody tr:last-child td:first-child {\n    border-bottom-left-radius: 0.375rem;\n  }\n  .markdown tbody tr:last-child td:last-child {\n    border-bottom-right-radius: 0.375rem;\n  }\n\n  img {\n    @apply inline-block;\n  }\n\n  input[type='range']::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    @apply w-4;\n    @apply h-4;\n    @apply rounded-full;\n    background: rgba(16, 163, 127);\n  }\n\n  ::-webkit-scrollbar {\n    height: 1rem;\n    width: 0.5rem;\n  }\n\n  @media screen and (max-width: 768px) {\n    ::-webkit-scrollbar {\n      display: none;\n      scrollbar-width: none; /* Firefox */\n    }\n  }\n\n  .hide-scroll-bar::-webkit-scrollbar {\n    display: none;\n    scrollbar-width: none; /* Firefox */\n  }\n\n  ::-webkit-scrollbar-thumb {\n    --tw-border-opacity: 1;\n    background-color: rgba(217, 217, 227, 0.8);\n    border-color: rgba(255, 255, 255, var(--tw-border-opacity));\n    border-radius: 9999px;\n    border-width: 1px;\n  }\n\n  ::-webkit-scrollbar-thumb:hover {\n    background-color: rgba(217, 217, 227, 0.6);\n  }\n\n  .dark ::-webkit-scrollbar-thumb {\n    --tw-bg-opacity: 1;\n    background-color: rgba(86, 88, 105, var(--tw-bg-opacity));\n  }\n\n  .dark ::-webkit-scrollbar-thumb:hover {\n    background-color: rgba(217, 217, 227, 0.8);\n  }\n\n  ::-webkit-scrollbar-track {\n    background-color: transparent;\n    border-radius: 9999px;\n  }\n\n  pre ::-webkit-scrollbar-thumb {\n    display: none;\n  }\n  pre {\n    scrollbar-width: 0;\n  }\n\n  textarea:focus {\n    outline: none;\n  }\n\n  a.link {\n    @apply underline dark:hover:text-white hover:text-black;\n  }\n}\n\n@layer components {\n  .btn {\n    align-items: center;\n    border-color: transparent;\n    border-radius: 0.25rem;\n    border-width: 1px;\n    display: inline-flex;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    padding: 0.5rem 0.75rem;\n    pointer-events: auto;\n  }\n\n  .btn-neutral {\n    --tw-bg-opacity: 1;\n    --tw-text-opacity: 1;\n    background-color: rgba(255, 255, 255, var(--tw-bg-opacity));\n    border-color: rgba(0, 0, 0, 0.1);\n    border-width: 1px;\n    color: rgba(64, 65, 79, var(--tw-text-opacity));\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n  }\n\n  .btn-neutral:hover {\n    --tw-bg-opacity: 1;\n    background-color: rgba(236, 236, 241, var(--tw-bg-opacity));\n  }\n\n  .dark .btn-neutral {\n    --tw-border-opacity: 1;\n    --tw-bg-opacity: 1;\n    --tw-text-opacity: 1;\n    background-color: rgba(52, 53, 65, var(--tw-bg-opacity));\n    border-color: rgba(86, 88, 105, var(--tw-border-opacity));\n    color: rgba(217, 217, 227, var(--tw-text-opacity));\n  }\n\n  .dark .btn-neutral:hover {\n    --tw-bg-opacity: 1;\n    background-color: rgba(64, 65, 79, var(--tw-bg-opacity));\n  }\n\n  .btn-dark {\n    --tw-border-opacity: 1;\n    --tw-bg-opacity: 1;\n    --tw-text-opacity: 1;\n    background-color: rgba(52, 53, 65, var(--tw-bg-opacity));\n    border-color: rgba(86, 88, 105, var(--tw-border-opacity));\n    border-width: 1px;\n    color: rgba(255, 255, 255, var(--tw-text-opacity));\n  }\n\n  .btn-primary {\n    --tw-bg-opacity: 1;\n    --tw-text-opacity: 1;\n    background-color: rgba(16, 163, 127, var(--tw-bg-opacity));\n    color: rgba(255, 255, 255, var(--tw-text-opacity));\n  }\n\n  .btn-primary:hover {\n    --tw-bg-opacity: 1;\n    background-color: rgba(26, 127, 100, var(--tw-bg-opacity));\n  }\n\n  .btn-small {\n    padding: 0.25rem 0.5rem;\n  }\n\n  button.scroll-convo {\n    display: none;\n  }\n\n  .markdown ol,\n  .markdown ul {\n    display: flex;\n    flex-direction: column;\n    padding-left: 1rem;\n  }\n\n  .markdown ol li,\n  .markdown ol li > p,\n  .markdown ol ol,\n  .markdown ol ul,\n  .markdown ul li,\n  .markdown ul li > p,\n  .markdown ul ol,\n  .markdown ul ul {\n    margin: 0;\n  }\n\n  .markdown ul li:before {\n    content: '•';\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    margin-left: -1rem;\n    position: absolute;\n  }\n}\n\n:not(pre) > code.hljs,\n:not(pre) > code[class*='language-'] {\n  border-radius: 0.3em;\n  white-space: normal;\n}\n.hljs-comment {\n  color: hsla(0, 0%, 100%, 0.5);\n}\n.hljs-meta {\n  color: hsla(0, 0%, 100%, 0.6);\n}\n.hljs-built_in,\n.hljs-class .hljs-title {\n  color: #e9950c;\n}\n.hljs-doctag,\n.hljs-formula,\n.hljs-keyword,\n.hljs-literal {\n  color: #2e95d3;\n}\n.hljs-addition,\n.hljs-attribute,\n.hljs-meta-string,\n.hljs-regexp,\n.hljs-string {\n  color: #00a67d;\n}\n.hljs-attr,\n.hljs-number,\n.hljs-selector-attr,\n.hljs-selector-class,\n.hljs-selector-pseudo,\n.hljs-template-variable,\n.hljs-type,\n.hljs-variable {\n  color: #df3079;\n}\n.hljs-bullet,\n.hljs-link,\n.hljs-selector-id,\n.hljs-symbol,\n.hljs-title {\n  color: #f22c3d;\n}\n"
  },
  {
    "path": "src/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\nimport './main.css';\nawait import('katex/dist/katex.min.css');\n\nimport './i18n';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n"
  },
  {
    "path": "src/store/auth-slice.ts",
    "content": "import { defaultAPIEndpoint } from '@constants/auth';\nimport { StoreSlice } from './store';\n\nexport interface AuthSlice {\n  apiKey?: string;\n  apiEndpoint: string;\n  firstVisit: boolean;\n  setApiKey: (apiKey: string) => void;\n  setApiEndpoint: (apiEndpoint: string) => void;\n  setFirstVisit: (firstVisit: boolean) => void;\n}\n\nexport const createAuthSlice: StoreSlice<AuthSlice> = (set, get) => ({\n  apiKey: import.meta.env.VITE_OPENAI_API_KEY || undefined,\n  apiEndpoint: defaultAPIEndpoint,\n  firstVisit: true,\n  setApiKey: (apiKey: string) => {\n    set((prev: AuthSlice) => ({\n      ...prev,\n      apiKey: apiKey,\n    }));\n  },\n  setApiEndpoint: (apiEndpoint: string) => {\n    set((prev: AuthSlice) => ({\n      ...prev,\n      apiEndpoint: apiEndpoint,\n    }));\n  },\n  setFirstVisit: (firstVisit: boolean) => {\n    set((prev: AuthSlice) => ({\n      ...prev,\n      firstVisit: firstVisit,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/chat-slice.ts",
    "content": "import { StoreSlice } from './store';\nimport { ChatInterface, FolderCollection, MessageInterface } from '@type/chat';\n\nexport interface ChatSlice {\n  messages: MessageInterface[];\n  chats?: ChatInterface[];\n  currentChatIndex: number;\n  generating: boolean;\n  error: string;\n  folders: FolderCollection;\n  setMessages: (messages: MessageInterface[]) => void;\n  setChats: (chats: ChatInterface[]) => void;\n  setCurrentChatIndex: (currentChatIndex: number) => void;\n  setGenerating: (generating: boolean) => void;\n  setError: (error: string) => void;\n  setFolders: (folders: FolderCollection) => void;\n}\n\nexport const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({\n  messages: [],\n  currentChatIndex: -1,\n  generating: false,\n  error: '',\n  folders: {},\n  setMessages: (messages: MessageInterface[]) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      messages: messages,\n    }));\n  },\n  setChats: (chats: ChatInterface[]) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      chats: chats,\n    }));\n  },\n  setCurrentChatIndex: (currentChatIndex: number) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      currentChatIndex: currentChatIndex,\n    }));\n  },\n  setGenerating: (generating: boolean) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      generating: generating,\n    }));\n  },\n  setError: (error: string) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      error: error,\n    }));\n  },\n  setFolders: (folders: FolderCollection) => {\n    set((prev: ChatSlice) => ({\n      ...prev,\n      folders: folders,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/cloud-auth-slice.ts",
    "content": "import { SyncStatus } from '@type/google-api';\nimport { StoreSlice } from './cloud-auth-store';\n\nexport interface CloudAuthSlice {\n  googleAccessToken?: string;\n  googleRefreshToken?: string;\n  cloudSync: boolean;\n  syncStatus: SyncStatus;\n  fileId?: string;\n  setGoogleAccessToken: (googleAccessToken?: string) => void;\n  setGoogleRefreshToken: (googleRefreshToken?: string) => void;\n  setFileId: (fileId?: string) => void;\n  setCloudSync: (cloudSync: boolean) => void;\n  setSyncStatus: (syncStatus: SyncStatus) => void;\n}\n\nexport const createCloudAuthSlice: StoreSlice<CloudAuthSlice> = (set, get) => ({\n  cloudSync: false,\n  syncStatus: 'unauthenticated',\n  setGoogleAccessToken: (googleAccessToken?: string) => {\n    set((prev: CloudAuthSlice) => ({\n      ...prev,\n      googleAccessToken: googleAccessToken,\n    }));\n  },\n  setGoogleRefreshToken: (googleRefreshToken?: string) => {\n    set((prev: CloudAuthSlice) => ({\n      ...prev,\n      googleRefreshToken: googleRefreshToken,\n    }));\n  },\n  setFileId: (fileId?: string) => {\n    set((prev: CloudAuthSlice) => ({\n      ...prev,\n      fileId: fileId,\n    }));\n  },\n  setCloudSync: (cloudSync: boolean) => {\n    set((prev: CloudAuthSlice) => ({\n      ...prev,\n      cloudSync: cloudSync,\n    }));\n  },\n  setSyncStatus: (syncStatus: SyncStatus) => {\n    set((prev: CloudAuthSlice) => ({\n      ...prev,\n      syncStatus: syncStatus,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/cloud-auth-store.ts",
    "content": "import { StoreApi, create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport { CloudAuthSlice, createCloudAuthSlice } from './cloud-auth-slice';\n\nexport type StoreState = CloudAuthSlice;\n\nexport type StoreSlice<T> = (\n  set: StoreApi<StoreState>['setState'],\n  get: StoreApi<StoreState>['getState']\n) => T;\n\nconst useCloudAuthStore = create<StoreState>()(\n  persist(\n    (set, get) => ({\n      ...createCloudAuthSlice(set, get),\n    }),\n    {\n      name: 'cloud',\n      partialize: (state) => ({\n        cloudSync: state.cloudSync,\n        fileId: state.fileId,\n      }),\n      version: 1,\n    }\n  )\n);\n\nexport default useCloudAuthStore;\n"
  },
  {
    "path": "src/store/config-slice.ts",
    "content": "import { StoreSlice } from './store';\nimport { Theme } from '@type/theme';\nimport { ConfigInterface, TotalTokenUsed } from '@type/chat';\nimport { _defaultChatConfig, _defaultSystemMessage } from '@constants/chat';\n\nexport interface ConfigSlice {\n  openConfig: boolean;\n  theme: Theme;\n  autoTitle: boolean;\n  hideMenuOptions: boolean;\n  advancedMode: boolean;\n  defaultChatConfig: ConfigInterface;\n  defaultSystemMessage: string;\n  hideSideMenu: boolean;\n  enterToSubmit: boolean;\n  inlineLatex: boolean;\n  markdownMode: boolean;\n  countTotalTokens: boolean;\n  totalTokenUsed: TotalTokenUsed;\n  setOpenConfig: (openConfig: boolean) => void;\n  setTheme: (theme: Theme) => void;\n  setAutoTitle: (autoTitle: boolean) => void;\n  setAdvancedMode: (advancedMode: boolean) => void;\n  setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => void;\n  setDefaultSystemMessage: (defaultSystemMessage: string) => void;\n  setHideMenuOptions: (hideMenuOptions: boolean) => void;\n  setHideSideMenu: (hideSideMenu: boolean) => void;\n  setEnterToSubmit: (enterToSubmit: boolean) => void;\n  setInlineLatex: (inlineLatex: boolean) => void;\n  setMarkdownMode: (markdownMode: boolean) => void;\n  setCountTotalTokens: (countTotalTokens: boolean) => void;\n  setTotalTokenUsed: (totalTokenUsed: TotalTokenUsed) => void;\n}\n\nexport const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({\n  openConfig: false,\n  theme: 'dark',\n  hideMenuOptions: false,\n  hideSideMenu: false,\n  autoTitle: false,\n  enterToSubmit: true,\n  advancedMode: true,\n  defaultChatConfig: _defaultChatConfig,\n  defaultSystemMessage: _defaultSystemMessage,\n  inlineLatex: false,\n  markdownMode: true,\n  countTotalTokens: false,\n  totalTokenUsed: {},\n  setOpenConfig: (openConfig: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      openConfig: openConfig,\n    }));\n  },\n  setTheme: (theme: Theme) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      theme: theme,\n    }));\n  },\n  setAutoTitle: (autoTitle: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      autoTitle: autoTitle,\n    }));\n  },\n  setAdvancedMode: (advancedMode: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      advancedMode: advancedMode,\n    }));\n  },\n  setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      defaultChatConfig: defaultChatConfig,\n    }));\n  },\n  setDefaultSystemMessage: (defaultSystemMessage: string) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      defaultSystemMessage: defaultSystemMessage,\n    }));\n  },\n  setHideMenuOptions: (hideMenuOptions: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      hideMenuOptions: hideMenuOptions,\n    }));\n  },\n  setHideSideMenu: (hideSideMenu: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      hideSideMenu: hideSideMenu,\n    }));\n  },\n  setEnterToSubmit: (enterToSubmit: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      enterToSubmit: enterToSubmit,\n    }));\n  },\n  setInlineLatex: (inlineLatex: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      inlineLatex: inlineLatex,\n    }));\n  },\n  setMarkdownMode: (markdownMode: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      markdownMode: markdownMode,\n    }));\n  },\n  setCountTotalTokens: (countTotalTokens: boolean) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      countTotalTokens: countTotalTokens,\n    }));\n  },\n  setTotalTokenUsed: (totalTokenUsed: TotalTokenUsed) => {\n    set((prev: ConfigSlice) => ({\n      ...prev,\n      totalTokenUsed: totalTokenUsed,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/input-slice.ts",
    "content": "import { StoreSlice } from './store';\nimport { Role } from '@type/chat';\n\nexport interface InputSlice {\n  inputRole: Role;\n  setInputRole: (inputRole: Role) => void;\n}\n\nexport const createInputSlice: StoreSlice<InputSlice> = (set, get) => ({\n  inputRole: 'user',\n  setInputRole: (inputRole: Role) => {\n    set((prev: InputSlice) => ({\n      ...prev,\n      inputRole: inputRole,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/migrate.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\n\nimport {\n  Folder,\n  FolderCollection,\n  LocalStorageInterfaceV0ToV1,\n  LocalStorageInterfaceV1ToV2,\n  LocalStorageInterfaceV2ToV3,\n  LocalStorageInterfaceV3ToV4,\n  LocalStorageInterfaceV4ToV5,\n  LocalStorageInterfaceV5ToV6,\n  LocalStorageInterfaceV6ToV7,\n  LocalStorageInterfaceV7oV8,\n} from '@type/chat';\nimport {\n  _defaultChatConfig,\n  defaultModel,\n  defaultUserMaxToken,\n} from '@constants/chat';\nimport { officialAPIEndpoint } from '@constants/auth';\nimport defaultPrompts from '@constants/prompt';\n\nexport const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => {\n  persistedState.chats.forEach((chat) => {\n    chat.titleSet = false;\n    if (!chat.config) chat.config = { ..._defaultChatConfig };\n  });\n};\n\nexport const migrateV1 = (persistedState: LocalStorageInterfaceV1ToV2) => {\n  if (persistedState.apiFree) {\n    persistedState.apiEndpoint = persistedState.apiFreeEndpoint;\n  } else {\n    persistedState.apiEndpoint = officialAPIEndpoint;\n  }\n};\n\nexport const migrateV2 = (persistedState: LocalStorageInterfaceV2ToV3) => {\n  persistedState.chats.forEach((chat) => {\n    chat.config = {\n      ...chat.config,\n      top_p: _defaultChatConfig.top_p,\n      frequency_penalty: _defaultChatConfig.frequency_penalty,\n    };\n  });\n  persistedState.autoTitle = false;\n};\n\nexport const migrateV3 = (persistedState: LocalStorageInterfaceV3ToV4) => {\n  persistedState.prompts = defaultPrompts;\n};\n\nexport const migrateV4 = (persistedState: LocalStorageInterfaceV4ToV5) => {\n  persistedState.chats.forEach((chat) => {\n    chat.config = {\n      ...chat.config,\n      model: defaultModel,\n    };\n  });\n};\n\nexport const migrateV5 = (persistedState: LocalStorageInterfaceV5ToV6) => {\n  persistedState.chats.forEach((chat) => {\n    chat.config = {\n      ...chat.config,\n      max_tokens: defaultUserMaxToken,\n    };\n  });\n};\n\nexport const migrateV6 = (persistedState: LocalStorageInterfaceV6ToV7) => {\n  if (\n    persistedState.apiEndpoint ===\n    'https://sharegpt.churchless.tech/share/v1/chat'\n  ) {\n    persistedState.apiEndpoint = 'https://chatgpt-api.shn.hk/v1/';\n  }\n  if (!persistedState.apiKey || persistedState.apiKey.length === 0)\n    persistedState.apiKey = '';\n};\n\nexport const migrateV7 = (persistedState: LocalStorageInterfaceV7oV8) => {\n  let folders: FolderCollection = {};\n  const folderNameToIdMap: Record<string, string> = {};\n\n  // convert foldersExpanded and foldersName to folders\n  persistedState.foldersName.forEach((name, index) => {\n    const id = uuidv4();\n    const folder: Folder = {\n      id,\n      name,\n      expanded: persistedState.foldersExpanded[index],\n      order: index,\n    };\n\n    folders = { [id]: folder, ...folders };\n    folderNameToIdMap[name] = id;\n  });\n  persistedState.folders = folders;\n\n  // change the chat.folder from name to id\n  persistedState.chats.forEach((chat) => {\n    if (chat.folder) chat.folder = folderNameToIdMap[chat.folder];\n    chat.id = uuidv4();\n  });\n};\n"
  },
  {
    "path": "src/store/prompt-slice.ts",
    "content": "import { StoreSlice } from './store';\nimport { Prompt } from '@type/prompt';\nimport defaultPrompts from '@constants/prompt';\n\nexport interface PromptSlice {\n  prompts: Prompt[];\n  setPrompts: (commandPrompt: Prompt[]) => void;\n}\n\nexport const createPromptSlice: StoreSlice<PromptSlice> = (set, get) => ({\n  prompts: defaultPrompts,\n  setPrompts: (prompts: Prompt[]) => {\n    set((prev: PromptSlice) => ({\n      ...prev,\n      prompts: prompts,\n    }));\n  },\n});\n"
  },
  {
    "path": "src/store/storage/GoogleCloudStorage.ts",
    "content": "import { PersistStorage, StorageValue, StateStorage } from 'zustand/middleware';\nimport useCloudAuthStore from '@store/cloud-auth-store';\nimport useStore from '@store/store';\nimport {\n  deleteDriveFile,\n  getDriveFile,\n  updateDriveFileDebounced,\n  validateGoogleOath2AccessToken,\n} from '@api/google-api';\n\nconst createGoogleCloudStorage = <S>(): PersistStorage<S> | undefined => {\n  const accessToken = useCloudAuthStore.getState().googleAccessToken;\n  const fileId = useCloudAuthStore.getState().fileId;\n  if (!accessToken || !fileId) return;\n\n  try {\n    const authenticated = validateGoogleOath2AccessToken(accessToken);\n    if (!authenticated) return;\n  } catch (e) {\n    // prevent error if the storage is not defined (e.g. when server side rendering a page)\n    return;\n  }\n  const persistStorage: PersistStorage<S> = {\n    getItem: async (name) => {\n      useCloudAuthStore.getState().setSyncStatus('syncing');\n      try {\n        const accessToken = useCloudAuthStore.getState().googleAccessToken;\n        const fileId = useCloudAuthStore.getState().fileId;\n        if (!accessToken || !fileId) return null;\n\n        const data: StorageValue<S> = await getDriveFile(fileId, accessToken);\n        useCloudAuthStore.getState().setSyncStatus('synced');\n        return data;\n      } catch (e: unknown) {\n        useCloudAuthStore.getState().setSyncStatus('unauthenticated');\n        useStore.getState().setToastMessage((e as Error).message);\n        useStore.getState().setToastShow(true);\n        useStore.getState().setToastStatus('error');\n        return null;\n      }\n    },\n    setItem: async (name, newValue): Promise<void> => {\n      const accessToken = useCloudAuthStore.getState().googleAccessToken;\n      const fileId = useCloudAuthStore.getState().fileId;\n      if (!accessToken || !fileId) return;\n\n      const blob = new Blob([JSON.stringify(newValue)], {\n        type: 'application/json',\n      });\n      const file = new File([blob], 'better-chatgpt.json', {\n        type: 'application/json',\n      });\n\n      if (useCloudAuthStore.getState().syncStatus !== 'unauthenticated') {\n        useCloudAuthStore.getState().setSyncStatus('syncing');\n\n        await updateDriveFileDebounced(file, fileId, accessToken);\n      }\n    },\n\n    removeItem: async (name): Promise<void> => {\n      const accessToken = useCloudAuthStore.getState().googleAccessToken;\n      const fileId = useCloudAuthStore.getState().fileId;\n      if (!accessToken || !fileId) return;\n\n      await deleteDriveFile(accessToken, fileId);\n    },\n  };\n  return persistStorage;\n};\n\nexport default createGoogleCloudStorage;\n"
  },
  {
    "path": "src/store/store.ts",
    "content": "import { StoreApi, create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport { ChatSlice, createChatSlice } from './chat-slice';\nimport { InputSlice, createInputSlice } from './input-slice';\nimport { AuthSlice, createAuthSlice } from './auth-slice';\nimport { ConfigSlice, createConfigSlice } from './config-slice';\nimport { PromptSlice, createPromptSlice } from './prompt-slice';\nimport { ToastSlice, createToastSlice } from './toast-slice';\nimport {\n  LocalStorageInterfaceV0ToV1,\n  LocalStorageInterfaceV1ToV2,\n  LocalStorageInterfaceV2ToV3,\n  LocalStorageInterfaceV3ToV4,\n  LocalStorageInterfaceV4ToV5,\n  LocalStorageInterfaceV5ToV6,\n  LocalStorageInterfaceV6ToV7,\n  LocalStorageInterfaceV7oV8,\n} from '@type/chat';\nimport {\n  migrateV0,\n  migrateV1,\n  migrateV2,\n  migrateV3,\n  migrateV4,\n  migrateV5,\n  migrateV6,\n  migrateV7,\n} from './migrate';\n\nexport type StoreState = ChatSlice &\n  InputSlice &\n  AuthSlice &\n  ConfigSlice &\n  PromptSlice &\n  ToastSlice;\n\nexport type StoreSlice<T> = (\n  set: StoreApi<StoreState>['setState'],\n  get: StoreApi<StoreState>['getState']\n) => T;\n\nexport const createPartializedState = (state: StoreState) => ({\n  chats: state.chats,\n  currentChatIndex: state.currentChatIndex,\n  apiKey: state.apiKey,\n  apiEndpoint: state.apiEndpoint,\n  theme: state.theme,\n  autoTitle: state.autoTitle,\n  advancedMode: state.advancedMode,\n  prompts: state.prompts,\n  defaultChatConfig: state.defaultChatConfig,\n  defaultSystemMessage: state.defaultSystemMessage,\n  hideMenuOptions: state.hideMenuOptions,\n  firstVisit: state.firstVisit,\n  hideSideMenu: state.hideSideMenu,\n  folders: state.folders,\n  enterToSubmit: state.enterToSubmit,\n  inlineLatex: state.inlineLatex,\n  markdownMode: state.markdownMode,\n  totalTokenUsed: state.totalTokenUsed,\n  countTotalTokens: state.countTotalTokens,\n});\n\nconst useStore = create<StoreState>()(\n  persist(\n    (set, get) => ({\n      ...createChatSlice(set, get),\n      ...createInputSlice(set, get),\n      ...createAuthSlice(set, get),\n      ...createConfigSlice(set, get),\n      ...createPromptSlice(set, get),\n      ...createToastSlice(set, get),\n    }),\n    {\n      name: 'free-chat-gpt',\n      partialize: (state) => createPartializedState(state),\n      version: 8,\n      migrate: (persistedState, version) => {\n        switch (version) {\n          case 0:\n            migrateV0(persistedState as LocalStorageInterfaceV0ToV1);\n          case 1:\n            migrateV1(persistedState as LocalStorageInterfaceV1ToV2);\n          case 2:\n            migrateV2(persistedState as LocalStorageInterfaceV2ToV3);\n          case 3:\n            migrateV3(persistedState as LocalStorageInterfaceV3ToV4);\n          case 4:\n            migrateV4(persistedState as LocalStorageInterfaceV4ToV5);\n          case 5:\n            migrateV5(persistedState as LocalStorageInterfaceV5ToV6);\n          case 6:\n            migrateV6(persistedState as LocalStorageInterfaceV6ToV7);\n          case 7:\n            migrateV7(persistedState as LocalStorageInterfaceV7oV8);\n            break;\n        }\n        return persistedState as StoreState;\n      },\n    }\n  )\n);\n\nexport default useStore;\n"
  },
  {
    "path": "src/store/toast-slice.ts",
    "content": "import { ToastStatus } from '@components/Toast/Toast';\nimport { StoreSlice } from './store';\n\nexport interface ToastSlice {\n  toastShow: boolean;\n  toastMessage: string;\n  toastStatus: ToastStatus;\n  setToastShow: (toastShow: boolean) => void;\n  setToastMessage: (toastMessage: string) => void;\n  setToastStatus: (toastStatus: ToastStatus) => void;\n}\n\nexport const createToastSlice: StoreSlice<ToastSlice> = (set, get) => ({\n  toastShow: false,\n  toastMessage: '',\n  toastStatus: 'success',\n  setToastShow: (toastShow: boolean) => {\n    set((prev) => ({ ...prev, toastShow }));\n  },\n  setToastMessage: (toastMessage: string) => {\n    set((prev: ToastSlice) => ({ ...prev, toastMessage }));\n  },\n  setToastStatus: (toastStatus: ToastStatus) => {\n    set((prev: ToastSlice) => ({ ...prev, toastStatus }));\n  },\n});\n"
  },
  {
    "path": "src/types/api.ts",
    "content": "export interface EventSourceDataInterface {\n  choices: EventSourceDataChoices[];\n  created: number;\n  id: string;\n  model: string;\n  object: string;\n}\n\nexport type EventSourceData = EventSourceDataInterface | '[DONE]';\n\nexport interface EventSourceDataChoices {\n  delta: {\n    content?: string;\n    role?: string;\n  };\n  finish_reason?: string;\n  index: number;\n}\n\nexport interface ShareGPTSubmitBodyInterface {\n  avatarUrl: string;\n  items: {\n    from: 'gpt' | 'human';\n    value: string;\n  }[];\n}\n"
  },
  {
    "path": "src/types/chat.ts",
    "content": "import { Prompt } from './prompt';\nimport { Theme } from './theme';\n\nexport type Role = 'user' | 'assistant' | 'system';\nexport const roles: Role[] = ['user', 'assistant', 'system'];\n\nexport interface MessageInterface {\n  role: Role;\n  content: string;\n}\n\nexport interface ChatInterface {\n  id: string;\n  title: string;\n  folder?: string;\n  messages: MessageInterface[];\n  config: ConfigInterface;\n  titleSet: boolean;\n}\n\nexport interface ConfigInterface {\n  model: ModelOptions;\n  max_tokens: number;\n  temperature: number;\n  presence_penalty: number;\n  top_p: number;\n  frequency_penalty: number;\n}\n\nexport interface ChatHistoryInterface {\n  title: string;\n  index: number;\n  id: string;\n}\n\nexport interface ChatHistoryFolderInterface {\n  [folderId: string]: ChatHistoryInterface[];\n}\n\nexport interface FolderCollection {\n  [folderId: string]: Folder;\n}\n\nexport interface Folder {\n  id: string;\n  name: string;\n  expanded: boolean;\n  order: number;\n  color?: string;\n}\n\nexport type ModelOptions =\n  | 'gpt-4o'\n  | 'gpt-4o-2024-05-13'\n  | 'gpt-4'\n  | 'gpt-4-32k'\n  | 'gpt-4-1106-preview'\n  | 'gpt-4-0125-preview'\n  | 'gpt-4-turbo'\n  | 'gpt-4-turbo-2024-04-09'\n  | 'gpt-3.5-turbo'\n  | 'gpt-3.5-turbo-16k'\n  | 'gpt-3.5-turbo-1106'\n  | 'gpt-3.5-turbo-0125';\n// | 'gpt-3.5-turbo-0301';\n// | 'gpt-4-0314'\n// | 'gpt-4-32k-0314'\n\nexport type TotalTokenUsed = {\n  [model in ModelOptions]?: {\n    promptTokens: number;\n    completionTokens: number;\n  };\n};\nexport interface LocalStorageInterfaceV0ToV1 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  theme: Theme;\n}\n\nexport interface LocalStorageInterfaceV1ToV2 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  apiEndpoint?: string;\n  theme: Theme;\n}\n\nexport interface LocalStorageInterfaceV2ToV3 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  apiEndpoint?: string;\n  theme: Theme;\n  autoTitle: boolean;\n}\nexport interface LocalStorageInterfaceV3ToV4 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  apiEndpoint?: string;\n  theme: Theme;\n  autoTitle: boolean;\n  prompts: Prompt[];\n}\n\nexport interface LocalStorageInterfaceV4ToV5 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  apiEndpoint?: string;\n  theme: Theme;\n  autoTitle: boolean;\n  prompts: Prompt[];\n}\n\nexport interface LocalStorageInterfaceV5ToV6 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiKey: string;\n  apiFree: boolean;\n  apiFreeEndpoint: string;\n  apiEndpoint?: string;\n  theme: Theme;\n  autoTitle: boolean;\n  prompts: Prompt[];\n}\n\nexport interface LocalStorageInterfaceV6ToV7 {\n  chats: ChatInterface[];\n  currentChatIndex: number;\n  apiFree?: boolean;\n  apiKey: string;\n  apiEndpoint: string;\n  theme: Theme;\n  autoTitle: boolean;\n  prompts: Prompt[];\n  defaultChatConfig: ConfigInterface;\n  defaultSystemMessage: string;\n  hideMenuOptions: boolean;\n  firstVisit: boolean;\n  hideSideMenu: boolean;\n}\n\nexport interface LocalStorageInterfaceV7oV8\n  extends LocalStorageInterfaceV6ToV7 {\n  foldersName: string[];\n  foldersExpanded: boolean[];\n  folders: FolderCollection;\n}\n"
  },
  {
    "path": "src/types/export.ts",
    "content": "import { ChatInterface, FolderCollection, Role } from './chat';\n\nexport interface ExportBase {\n  version: number;\n}\n\nexport interface ExportV1 extends ExportBase {\n  chats?: ChatInterface[];\n  folders: FolderCollection;\n}\n\nexport type OpenAIChat = {\n  title: string;\n  mapping: {\n    [key: string]: {\n      id: string;\n      message?: {\n        author: {\n          role: Role;\n        };\n        content: {\n          parts?: string[];\n        };\n      } | null;\n      parent: string | null;\n      children: string[];\n    };\n  };\n  current_node: string;\n};\n\nexport default ExportV1;\n"
  },
  {
    "path": "src/types/google-api.ts",
    "content": "export interface GoogleFileResource {\n  kind: string;\n  id: string;\n  name: string;\n  mimeType: string;\n}\n\nexport interface GoogleTokenInfo {\n  azp: string;\n  aud: string;\n  sub: string;\n  scope: string;\n  exp: string;\n  expires_in: string;\n  email: string;\n  email_verified: string;\n  access_type: string;\n}\n\nexport interface GoogleFileList {\n  nextPageToken?: string;\n  kind: string;\n  incompleteSearch: boolean;\n  files: GoogleFileResource[];\n}\n\nexport type SyncStatus = 'unauthenticated' | 'syncing' | 'synced';\n"
  },
  {
    "path": "src/types/persist.ts",
    "content": "import { LocalStorageInterfaceV7oV8 } from './chat';\n\ninterface PersistStorageState extends LocalStorageInterfaceV7oV8 {}\n\nexport default PersistStorageState;\n"
  },
  {
    "path": "src/types/prompt.ts",
    "content": "export interface Prompt {\n  id: string;\n  name: string;\n  prompt: string;\n}\n"
  },
  {
    "path": "src/types/theme.ts",
    "content": "export type Theme = 'light' | 'dark';\n"
  },
  {
    "path": "src/utils/api.ts",
    "content": "export const isAzureEndpoint = (endpoint: string) => {\n  return endpoint.includes('openai.azure.com');\n};\n"
  },
  {
    "path": "src/utils/chat.ts",
    "content": "import html2canvas from 'html2canvas';\nimport { ChatInterface } from '@type/chat';\n\n// Function to convert HTML to an image using html2canvas\nexport const htmlToImg = async (html: HTMLDivElement) => {\n  const needResize = window.innerWidth >= 1024;\n  const initialWidth = html.style.width;\n  if (needResize) {\n    html.style.width = '1023px';\n  }\n  const canvas = await html2canvas(html);\n  if (needResize) html.style.width = initialWidth;\n  const dataURL = canvas.toDataURL('image/png');\n  return dataURL;\n};\n\n// Function to download the image as a file\nexport const downloadImg = (imgData: string, fileName: string) => {\n  const link = document.createElement('a');\n  link.href = imgData;\n  link.download = fileName;\n  link.click();\n  link.remove();\n};\n\n// Function to convert a chat object to markdown format\nexport const chatToMarkdown = (chat: ChatInterface) => {\n  let markdown = `# ${chat.title}\\n\\n`;\n  chat.messages.forEach((message) => {\n    markdown += `### **${message.role}**:\\n\\n${message.content}\\n\\n---\\n\\n`;\n  });\n  return markdown;\n};\n\n// Function to download the markdown content as a file\nexport const downloadMarkdown = (markdown: string, fileName: string) => {\n  const link = document.createElement('a');\n  const markdownFile = new Blob([markdown], { type: 'text/markdown' });\n  link.href = URL.createObjectURL(markdownFile);\n  link.download = fileName;\n  link.click();\n  link.remove();\n};\n"
  },
  {
    "path": "src/utils/date.ts",
    "content": "export const getToday = () => {\n  const date = new Date();\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, '0');\n  const day = String(date.getDate()).padStart(2, '0');\n\n  return `${year}-${month}-${day}`;\n};\n"
  },
  {
    "path": "src/utils/downloadFile.ts",
    "content": "const downloadFile = (data: object, filename: string) => {\n  const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });\n  const url = URL.createObjectURL(blob);\n  const link = document.createElement('a');\n  link.href = url;\n  link.download = filename;\n  link.click();\n  link.remove();\n};\n\nexport default downloadFile;\n"
  },
  {
    "path": "src/utils/google-api.ts",
    "content": "import { listDriveFiles } from '@api/google-api';\n\nimport useStore, { createPartializedState } from '@store/store';\nimport useCloudAuthStore from '@store/cloud-auth-store';\n\nexport const getFiles = async (googleAccessToken: string) => {\n  try {\n    const driveFiles = await listDriveFiles(googleAccessToken);\n    return driveFiles.files;\n  } catch (e: unknown) {\n    useCloudAuthStore.getState().setSyncStatus('unauthenticated');\n    useStore.getState().setToastMessage((e as Error).message);\n    useStore.getState().setToastShow(true);\n    useStore.getState().setToastStatus('error');\n    return;\n  }\n};\n\nexport const getFileID = async (\n  googleAccessToken: string\n): Promise<string | null> => {\n  const driveFiles = await listDriveFiles(googleAccessToken);\n  if (driveFiles.files.length === 0) return null;\n  return driveFiles.files[0].id;\n};\n\nexport const stateToFile = () => {\n  const partializedState = createPartializedState(useStore.getState());\n\n  const blob = new Blob([JSON.stringify(partializedState)], {\n    type: 'application/json',\n  });\n  const file = new File([blob], 'better-chatgpt.json', {\n    type: 'application/json',\n  });\n\n  return file;\n};\n"
  },
  {
    "path": "src/utils/import.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\n\nimport {\n  ChatInterface,\n  ConfigInterface,\n  FolderCollection,\n  MessageInterface,\n} from '@type/chat';\nimport { roles } from '@type/chat';\nimport {\n  defaultModel,\n  modelOptions,\n  _defaultChatConfig,\n} from '@constants/chat';\nimport { ExportV1, OpenAIChat } from '@type/export';\n\nexport const validateAndFixChats = (chats: any): chats is ChatInterface[] => {\n  if (!Array.isArray(chats)) return false;\n\n  for (const chat of chats) {\n    if (!(typeof chat.id === 'string')) chat.id = uuidv4();\n    if (!(typeof chat.title === 'string') || chat.title === '') return false;\n\n    if (chat.titleSet === undefined) chat.titleSet = false;\n    if (!(typeof chat.titleSet === 'boolean')) return false;\n\n    if (!validateMessage(chat.messages)) return false;\n    if (!validateAndFixChatConfig(chat.config)) return false;\n  }\n\n  return true;\n};\n\nconst validateMessage = (messages: MessageInterface[]) => {\n  if (!Array.isArray(messages)) return false;\n  for (const message of messages) {\n    if (!(typeof message.content === 'string')) return false;\n    if (!(typeof message.role === 'string')) return false;\n    if (!roles.includes(message.role)) return false;\n  }\n  return true;\n};\n\nconst validateAndFixChatConfig = (config: ConfigInterface) => {\n  if (config === undefined) config = _defaultChatConfig;\n  if (!(typeof config === 'object')) return false;\n\n  if (!config.temperature) config.temperature = _defaultChatConfig.temperature;\n  if (!(typeof config.temperature === 'number')) return false;\n\n  if (!config.presence_penalty)\n    config.presence_penalty = _defaultChatConfig.presence_penalty;\n  if (!(typeof config.presence_penalty === 'number')) return false;\n\n  if (!config.top_p) config.top_p = _defaultChatConfig.top_p;\n  if (!(typeof config.top_p === 'number')) return false;\n\n  if (!config.frequency_penalty)\n    config.frequency_penalty = _defaultChatConfig.frequency_penalty;\n  if (!(typeof config.frequency_penalty === 'number')) return false;\n\n  if (!config.model) config.model = defaultModel;\n  if (!modelOptions.includes(config.model)) return false;\n\n  return true;\n};\n\nexport const isLegacyImport = (importedData: any) => {\n  if (Array.isArray(importedData)) return true;\n  return false;\n};\n\nexport const validateFolders = (\n  folders: FolderCollection\n): folders is FolderCollection => {\n  if (typeof folders !== 'object') return false;\n\n  for (const folderId in folders) {\n    if (typeof folders[folderId].id !== 'string') return false;\n    if (typeof folders[folderId].name !== 'string') return false;\n    if (typeof folders[folderId].order !== 'number') return false;\n    if (typeof folders[folderId].expanded !== 'boolean') return false;\n  }\n\n  return true;\n};\n\nexport const validateExportV1 = (data: ExportV1): data is ExportV1 => {\n  return validateAndFixChats(data.chats) && validateFolders(data.folders);\n};\n\n// Convert OpenAI chat format to BetterChatGPT format\nexport const convertOpenAIToBetterChatGPTFormat = (\n  openAIChat: OpenAIChat\n): ChatInterface => {\n  const messages: MessageInterface[] = [];\n\n  // Traverse the chat tree and collect messages\n  const traverseTree = (id: string) => {\n    const node = openAIChat.mapping[id];\n\n    // Extract message if it exists\n    if (node.message) {\n      const { role } = node.message.author;\n      const content = node.message.content.parts?.join('') || '';\n      if (content.length > 0) messages.push({ role, content });\n    }\n\n    // Traverse the last child node if any children exist\n    if (node.children.length > 0) {\n      traverseTree(node.children[node.children.length - 1]);\n    }\n  };\n\n  // Start traversing the tree from the root node\n  const rootNode = openAIChat.mapping[Object.keys(openAIChat.mapping)[0]].id;\n  traverseTree(rootNode);\n\n  // Return the chat interface object\n  return {\n    id: uuidv4(),\n    title: openAIChat.title,\n    messages,\n    config: _defaultChatConfig,\n    titleSet: true,\n  };\n};\n\n// Import OpenAI chat data and convert it to BetterChatGPT format\nexport const importOpenAIChatExport = (openAIChatExport: OpenAIChat[]) => {\n  return openAIChatExport.map(convertOpenAIToBetterChatGPTFormat);\n};\n"
  },
  {
    "path": "src/utils/messageUtils.ts",
    "content": "import { MessageInterface, ModelOptions, TotalTokenUsed } from '@type/chat';\n\nimport useStore from '@store/store';\n\nimport { Tiktoken } from '@dqbd/tiktoken/lite';\nconst cl100k_base = await import('@dqbd/tiktoken/encoders/cl100k_base.json');\n\nconst encoder = new Tiktoken(\n  cl100k_base.bpe_ranks,\n  {\n    ...cl100k_base.special_tokens,\n    '<|im_start|>': 100264,\n    '<|im_end|>': 100265,\n    '<|im_sep|>': 100266,\n  },\n  cl100k_base.pat_str\n);\n\n// https://github.com/dqbd/tiktoken/issues/23#issuecomment-1483317174\nexport const getChatGPTEncoding = (\n  messages: MessageInterface[],\n  model: ModelOptions\n) => {\n  const isGpt3 = model === 'gpt-3.5-turbo';\n\n  const msgSep = isGpt3 ? '\\n' : '';\n  const roleSep = isGpt3 ? '\\n' : '<|im_sep|>';\n\n  const serialized = [\n    messages\n      .map(({ role, content }) => {\n        return `<|im_start|>${role}${roleSep}${content}<|im_end|>`;\n      })\n      .join(msgSep),\n    `<|im_start|>assistant${roleSep}`,\n  ].join(msgSep);\n\n  return encoder.encode(serialized, 'all');\n};\n\nconst countTokens = (messages: MessageInterface[], model: ModelOptions) => {\n  if (messages.length === 0) return 0;\n  return getChatGPTEncoding(messages, model).length;\n};\n\nexport const limitMessageTokens = (\n  messages: MessageInterface[],\n  limit: number = 4096,\n  model: ModelOptions\n): MessageInterface[] => {\n  const limitedMessages: MessageInterface[] = [];\n  let tokenCount = 0;\n\n  const isSystemFirstMessage = messages[0]?.role === 'system';\n  let retainSystemMessage = false;\n\n  // Check if the first message is a system message and if it fits within the token limit\n  if (isSystemFirstMessage) {\n    const systemTokenCount = countTokens([messages[0]], model);\n    if (systemTokenCount < limit) {\n      tokenCount += systemTokenCount;\n      retainSystemMessage = true;\n    }\n  }\n\n  // Iterate through messages in reverse order, adding them to the limitedMessages array\n  // until the token limit is reached (excludes first message)\n  for (let i = messages.length - 1; i >= 1; i--) {\n    const count = countTokens([messages[i]], model);\n    if (count + tokenCount > limit) break;\n    tokenCount += count;\n    limitedMessages.unshift({ ...messages[i] });\n  }\n\n  // Process first message\n  if (retainSystemMessage) {\n    // Insert the system message in the third position from the end\n    limitedMessages.splice(-3, 0, { ...messages[0] });\n  } else if (!isSystemFirstMessage) {\n    // Check if the first message (non-system) can fit within the limit\n    const firstMessageTokenCount = countTokens([messages[0]], model);\n    if (firstMessageTokenCount + tokenCount < limit) {\n      limitedMessages.unshift({ ...messages[0] });\n    }\n  }\n\n  return limitedMessages;\n};\n\nexport const updateTotalTokenUsed = (\n  model: ModelOptions,\n  promptMessages: MessageInterface[],\n  completionMessage: MessageInterface\n) => {\n  const setTotalTokenUsed = useStore.getState().setTotalTokenUsed;\n  const updatedTotalTokenUsed: TotalTokenUsed = JSON.parse(\n    JSON.stringify(useStore.getState().totalTokenUsed)\n  );\n\n  const newPromptTokens = countTokens(promptMessages, model);\n  const newCompletionTokens = countTokens([completionMessage], model);\n  const { promptTokens = 0, completionTokens = 0 } =\n    updatedTotalTokenUsed[model] ?? {};\n\n  updatedTotalTokenUsed[model] = {\n    promptTokens: promptTokens + newPromptTokens,\n    completionTokens: completionTokens + newCompletionTokens,\n  };\n  setTotalTokenUsed(updatedTotalTokenUsed);\n};\n\nexport default countTokens;\n"
  },
  {
    "path": "src/utils/prompt.ts",
    "content": "import { Prompt } from '@type/prompt';\nimport { getToday } from './date';\n\nimport Papa from 'papaparse';\n\nexport const importPromptCSV = (csvString: string, header: boolean = true) => {\n  const results = Papa.parse(csvString, {\n    header,\n    delimiter: ',',\n    newline: '\\n',\n    skipEmptyLines: true,\n  });\n\n  return results.data as Record<string, string>[];\n};\n\nexport const exportPrompts = (prompts: Prompt[]) => {\n  const csvString = Papa.unparse(\n    prompts.map((prompt) => ({ name: prompt.name, prompt: prompt.prompt }))\n  );\n\n  const blob = new Blob([csvString], {\n    type: 'text/csv;charset=utf-8;',\n  });\n  const url = URL.createObjectURL(blob);\n  const link = document.createElement('a');\n  link.href = url;\n  link.download = `${getToday()}.csv`;\n  link.click();\n  link.remove();\n};\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tailwind.config.cjs",
    "content": "/** @type {import('tailwindcss').Config} */\n\nfunction parentSiblingHoverPlugin({ addVariant, e }) {\n  addVariant('parent-sibling-hover', ({ modifySelectors, separator }) => {\n    modifySelectors(({ className }) => {\n      return `.parent-sibling:hover ~ .parent .${e(\n        `parent-sibling-hover${separator}${className}`\n      )}`;\n    });\n  });\n}\n\nmodule.exports = {\n  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    fontFamily: {\n      sans: [\n        'Söhne',\n        'Roboto',\n        'ui-sans-serif',\n        'system-ui',\n        '-apple-system',\n        'Ubuntu',\n        'Cantarell',\n        'Noto Sans',\n        'sans-serif',\n        'Helvetica Neue',\n        'Arial',\n        'Apple Color Emoji',\n        'Segoe UI Emoji',\n        'Segoe UI Symbol',\n        'Noto Color Emoji',\n      ],\n      mono: [\n        'Söhne Mono',\n        'Monaco',\n        'Andale Mono',\n        'Ubuntu Mono',\n        'Consolas',\n        'monospace',\n      ],\n    },\n    extend: {\n      typography: {\n        DEFAULT: {\n          css: {\n            pre: { padding: 0, margin: 0 },\n            ul: {\n              'list-style-type': 'none',\n            },\n          },\n        },\n      },\n      colors: {\n        gray: {\n          50: '#f7f7f8',\n          100: '#ececf1',\n          200: '#d9d9e3',\n          300: '#d1d5db',\n          400: '#acacbe',\n          500: '#8e8ea0',\n          600: '#4b5563',\n          650: '#444654',\n          700: '#40414f',\n          800: '#343541',\n          850: '#2A2B32',\n          900: '#202123',\n        },\n      },\n    },\n  },\n  plugins: [require('@tailwindcss/typography'), parentSiblingHoverPlugin],\n  darkMode: 'class',\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@icon/*\": [\"./src/assets/icons/*\"],\n      \"@type/*\": [\"./src/types/*\"],\n      \"@store/*\": [\"./src/store/*\"],\n      \"@hooks/*\": [\"./src/hooks/*\"],\n      \"@constants/*\": [\"./src/constants/*\"],\n      \"@api/*\": [\"./src/api/*\"],\n      \"@components/*\": [\"./src/components/*\"],\n      \"@utils/*\": [\"./src/utils/*\"],\n      \"@src/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\", \"electron/index.cjs\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react-swc';\nimport wasm from 'vite-plugin-wasm';\nimport topLevelAwait from 'vite-plugin-top-level-await';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react(), wasm(), topLevelAwait()],\n  resolve: {\n    alias: {\n      '@icon/': new URL('./src/assets/icons/', import.meta.url).pathname,\n      '@type/': new URL('./src/types/', import.meta.url).pathname,\n      '@store/': new URL('./src/store/', import.meta.url).pathname,\n      '@hooks/': new URL('./src/hooks/', import.meta.url).pathname,\n      '@constants/': new URL('./src/constants/', import.meta.url).pathname,\n      '@api/': new URL('./src/api/', import.meta.url).pathname,\n      '@components/': new URL('./src/components/', import.meta.url).pathname,\n      '@utils/': new URL('./src/utils/', import.meta.url).pathname,\n      '@src/': new URL('./src/', import.meta.url).pathname,\n    },\n  },\n  base: './',\n});\n"
  }
]