├── .babelrc ├── .env.template ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── 功能建议.md │ └── 反馈问题.md ├── dependabot.yml └── workflows │ ├── app.yml │ ├── docker.yml │ ├── issue-translator.yml │ └── sync.yml ├── .gitignore ├── .gitpod.yml ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_CN.md ├── app ├── api │ ├── auth.ts │ ├── common.ts │ ├── config │ │ └── route.ts │ ├── cors │ │ └── [...path] │ │ │ └── route.ts │ └── openai │ │ └── [...path] │ │ └── route.ts ├── azure.ts ├── client │ ├── api.ts │ ├── controller.ts │ └── platforms │ │ └── openai.ts ├── command.ts ├── components │ ├── auth.module.scss │ ├── auth.tsx │ ├── button.module.scss │ ├── button.tsx │ ├── chat-list.tsx │ ├── chat.module.scss │ ├── chat.tsx │ ├── emoji.tsx │ ├── error.tsx │ ├── exporter.module.scss │ ├── exporter.tsx │ ├── home.module.scss │ ├── home.tsx │ ├── input-range.module.scss │ ├── input-range.tsx │ ├── markdown.tsx │ ├── mask.module.scss │ ├── mask.tsx │ ├── message-selector.module.scss │ ├── message-selector.tsx │ ├── model-config.tsx │ ├── new-chat.module.scss │ ├── new-chat.tsx │ ├── settings.module.scss │ ├── settings.tsx │ ├── sidebar.tsx │ ├── ui-lib.module.scss │ └── ui-lib.tsx ├── config │ ├── build.ts │ ├── client.ts │ └── server.ts ├── constant.ts ├── global.d.ts ├── icons │ ├── add.svg │ ├── auto.svg │ ├── black-bot.svg │ ├── bot.png │ ├── bot.svg │ ├── bottom.svg │ ├── brain.svg │ ├── break.svg │ ├── cancel.svg │ ├── chat-settings.svg │ ├── chat.svg │ ├── chatgpt.png │ ├── chatgpt.svg │ ├── clear.svg │ ├── close.svg │ ├── cloud-fail.svg │ ├── cloud-success.svg │ ├── config.svg │ ├── confirm.svg │ ├── connection.svg │ ├── copy.svg │ ├── dark.svg │ ├── delete.svg │ ├── down.svg │ ├── download.svg │ ├── drag.svg │ ├── edit.svg │ ├── export.svg │ ├── eye-off.svg │ ├── eye.svg │ ├── github.svg │ ├── left.svg │ ├── light.svg │ ├── lightning.svg │ ├── mask.svg │ ├── max.svg │ ├── menu.svg │ ├── min.svg │ ├── pause.svg │ ├── pin.svg │ ├── plugin.svg │ ├── prompt.svg │ ├── reload.svg │ ├── rename.svg │ ├── return.svg │ ├── robot.svg │ ├── send-white.svg │ ├── settings.svg │ ├── share.svg │ ├── three-dots.svg │ └── upload.svg ├── layout.tsx ├── locales │ ├── ar.ts │ ├── bn.ts │ ├── cn.ts │ ├── cs.ts │ ├── de.ts │ ├── en.ts │ ├── es.ts │ ├── fr.ts │ ├── id.ts │ ├── index.ts │ ├── it.ts │ ├── jp.ts │ ├── ko.ts │ ├── no.ts │ ├── ru.ts │ ├── tr.ts │ ├── tw.ts │ └── vi.ts ├── masks │ ├── cn.ts │ ├── en.ts │ ├── index.ts │ └── typing.ts ├── page.tsx ├── polyfill.ts ├── store │ ├── access.ts │ ├── chat.ts │ ├── config.ts │ ├── index.ts │ ├── mask.ts │ ├── prompt.ts │ ├── sync.ts │ └── update.ts ├── styles │ ├── animation.scss │ ├── globals.scss │ ├── highlight.scss │ ├── markdown.scss │ └── window.scss ├── typing.ts ├── utils.ts └── utils │ ├── clone.ts │ ├── cloud │ ├── index.ts │ ├── upstash.ts │ └── webdav.ts │ ├── cors.ts │ ├── format.ts │ ├── hooks.ts │ ├── merge.ts │ ├── model.ts │ ├── store.ts │ ├── sync.ts │ └── token.ts ├── docker-compose.yml ├── docs ├── cloudflare-pages-cn.md ├── cloudflare-pages-en.md ├── cloudflare-pages-es.md ├── cloudflare-pages-ja.md ├── cloudflare-pages-ko.md ├── faq-cn.md ├── faq-en.md ├── faq-es.md ├── faq-ja.md ├── faq-ko.md ├── images │ ├── cover.png │ ├── enable-actions-sync.jpg │ ├── enable-actions.jpg │ ├── icon.svg │ ├── more.png │ ├── settings.png │ ├── upstash-1.png │ ├── upstash-2.png │ ├── upstash-3.png │ ├── upstash-4.png │ ├── upstash-5.png │ ├── upstash-6.png │ ├── upstash-7.png │ └── vercel │ │ ├── vercel-create-1.jpg │ │ ├── vercel-create-2.jpg │ │ ├── vercel-create-3.jpg │ │ ├── vercel-env-edit.jpg │ │ └── vercel-redeploy.jpg ├── synchronise-chat-logs-cn.md ├── synchronise-chat-logs-en.md ├── synchronise-chat-logs-es.md ├── synchronise-chat-logs-ja.md ├── synchronise-chat-logs-ko.md ├── translation.md ├── user-manual-cn.md ├── vercel-cn.md ├── vercel-es.md ├── vercel-ja.md └── vercel-ko.md ├── next.config.mjs ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── macos.png ├── prompts.json ├── robots.txt ├── serviceWorker.js ├── serviceWorkerRegister.js └── site.webmanifest ├── scripts ├── .gitignore ├── fetch-prompts.mjs ├── init-proxy.sh ├── proxychains.template.conf └── setup.sh ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json ├── tsconfig.json ├── vercel.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-env": { 7 | "targets": { 8 | "browsers": ["> 0.25%, not dead"] 9 | } 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | 2 | # Your openai api key. (required) 3 | OPENAI_API_KEY=sk-xxxx 4 | 5 | # Access passsword, separated by comma. (optional) 6 | CODE=your-password 7 | 8 | # You can start service behind a proxy 9 | PROXY_URL=http://localhost:7890 10 | 11 | # Override openai api request base url. (optional) 12 | # Default: https://api.openai.com 13 | # Examples: http://your-openai-proxy.com 14 | BASE_URL= 15 | 16 | # Specify OpenAI organization ID.(optional) 17 | # Default: Empty 18 | OPENAI_ORG_ID= 19 | 20 | # (optional) 21 | # Default: Empty 22 | # If you do not want users to use GPT-4, set this value to 1. 23 | DISABLE_GPT4= 24 | 25 | # (optional) 26 | # Default: Empty 27 | # If you do not want users to input their own API key, set this value to 1. 28 | HIDE_USER_API_KEY= 29 | 30 | # (optional) 31 | # Default: Empty 32 | # If you do want users to query balance, set this value to 1. 33 | ENABLE_BALANCE_QUERY= 34 | 35 | # (optional) 36 | # Default: Empty 37 | # If you want to disable parse settings from url, set this value to 1. 38 | DISABLE_FAST_LINK= 39 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/serviceWorker.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["prettier"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Deployment** 27 | - [ ] Docker 28 | - [ ] Vercel 29 | - [ ] Server 30 | 31 | **Desktop (please complete the following information):** 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | 36 | **Smartphone (please complete the following information):** 37 | - Device: [e.g. iPhone6] 38 | - OS: [e.g. iOS8.1] 39 | - Browser [e.g. stock browser, safari] 40 | - Version [e.g. 22] 41 | 42 | **Additional Logs** 43 | Add any logs about the problem here. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/功能建议.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能建议 3 | about: 请告诉我们你的灵光一闪 4 | title: "[Feature] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | > 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 11 | 12 | > [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) 13 | 14 | **这个功能与现有的问题有关吗?** 15 | 如果有关,请在此列出链接或者描述问题。 16 | 17 | **你想要什么功能或者有什么建议?** 18 | 尽管告诉我们。 19 | 20 | **有没有可以参考的同类竞品?** 21 | 可以给出参考产品的链接或者截图。 22 | 23 | **其他信息** 24 | 可以说说你的其他考虑。 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/反馈问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 反馈问题 3 | about: 请告诉我们你遇到的问题 4 | title: "[Bug] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | > 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 11 | 12 | > [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) 13 | 14 | **反馈须知** 15 | 16 | ⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。 17 | 18 | 请在下方中括号内输入 x 来表示你已经知晓相关内容。 19 | - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; 20 | - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 21 | - [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。 22 | 23 | **描述问题** 24 | 请在此描述你遇到了什么问题。 25 | 26 | **如何复现** 27 | 请告诉我们你是通过什么操作触发的该问题。 28 | 29 | **截图** 30 | 请在此提供控制台截图、屏幕截图或者服务端的 log 截图。 31 | 32 | **一些必要的信息** 33 | - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16] 34 | - 浏览器: [比如 chrome, safari] 35 | - 版本: [填写设置页面的版本号] 36 | - 部署方式:[比如 vercel、docker 或者服务器部署] 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/app.yml: -------------------------------------------------------------------------------- 1 | name: Release App 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | create-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | outputs: 14 | release_id: ${{ steps.create-release.outputs.result }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: setup node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | - name: get version 23 | run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV 24 | - name: create release 25 | id: create-release 26 | uses: actions/github-script@v6 27 | with: 28 | script: | 29 | const { data } = await github.rest.repos.getLatestRelease({ 30 | owner: context.repo.owner, 31 | repo: context.repo.repo, 32 | }) 33 | return data.id 34 | 35 | build-tauri: 36 | needs: create-release 37 | permissions: 38 | contents: write 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | config: 43 | - os: ubuntu-latest 44 | arch: x86_64 45 | rust_target: x86_64-unknown-linux-gnu 46 | - os: macos-latest 47 | arch: x86_64 48 | rust_target: x86_64-apple-darwin 49 | - os: macos-latest 50 | arch: aarch64 51 | rust_target: aarch64-apple-darwin 52 | - os: windows-latest 53 | arch: x86_64 54 | rust_target: x86_64-pc-windows-msvc 55 | 56 | runs-on: ${{ matrix.config.os }} 57 | steps: 58 | - uses: actions/checkout@v3 59 | - name: setup node 60 | uses: actions/setup-node@v3 61 | with: 62 | node-version: 16 63 | - name: install Rust stable 64 | uses: dtolnay/rust-toolchain@stable 65 | with: 66 | targets: ${{ matrix.config.rust_target }} 67 | - uses: Swatinem/rust-cache@v2 68 | with: 69 | key: ${{ matrix.config.rust_target }} 70 | - name: install dependencies (ubuntu only) 71 | if: matrix.config.os == 'ubuntu-latest' 72 | run: | 73 | sudo apt-get update 74 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 75 | - name: install frontend dependencies 76 | run: yarn install # change this to npm or pnpm depending on which one you use 77 | - uses: tauri-apps/tauri-action@v0 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 81 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 82 | with: 83 | releaseId: ${{ needs.create-release.outputs.release_id }} 84 | 85 | publish-release: 86 | permissions: 87 | contents: write 88 | runs-on: ubuntu-latest 89 | needs: [create-release, build-tauri] 90 | 91 | steps: 92 | - name: publish release 93 | id: publish-release 94 | uses: actions/github-script@v6 95 | env: 96 | release_id: ${{ needs.create-release.outputs.release_id }} 97 | with: 98 | script: | 99 | github.rest.repos.updateRelease({ 100 | owner: context.repo.owner, 101 | repo: context.repo.repo, 102 | release_id: process.env.release_id, 103 | draft: false, 104 | prerelease: false 105 | }) 106 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Check out the repo 15 | uses: actions/checkout@v3 16 | - 17 | name: Log in to Docker Hub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | 23 | - 24 | name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@v4 27 | with: 28 | images: yidadaa/chatgpt-next-web 29 | tags: | 30 | type=raw,value=latest 31 | type=ref,event=tag 32 | 33 | - 34 | name: Set up QEMU 35 | uses: docker/setup-qemu-action@v2 36 | 37 | - 38 | name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - 42 | name: Build and push Docker image 43 | uses: docker/build-push-action@v4 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | cache-from: type=gha 51 | cache-to: type=gha,mode=max 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yml: -------------------------------------------------------------------------------- 1 | name: Issue Translator 2 | on: 3 | issue_comment: 4 | types: [created] 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: usthe/issues-translate-action@v2.7 13 | with: 14 | IS_MODIFY_TITLE: false 15 | CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 16 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Upstream Sync 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" # every day 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sync_latest_from_upstream: 13 | name: Sync latest commits from upstream repo 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.repository.fork }} 16 | 17 | steps: 18 | # Step 1: run a standard checkout action 19 | - name: Checkout target repo 20 | uses: actions/checkout@v3 21 | 22 | # Step 2: run the sync action 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: Yidadaa/ChatGPT-Next-Web 28 | upstream_sync_branch: main 29 | target_sync_branch: main 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set 31 | 32 | # Set test_mode true to run tests instead of the true action!! 33 | test_mode: false 34 | 35 | - name: Sync check 36 | if: failure() 37 | run: | 38 | echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" 39 | echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" 40 | exit 1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | dev 38 | 39 | .vscode 40 | .idea 41 | 42 | # docker-compose env files 43 | .env 44 | 45 | *.key 46 | *.key.pub -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: yarn install && yarn run dev 9 | command: yarn run dev 10 | 11 | 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | arrowParens: 'always', 10 | }; 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | FROM base AS deps 4 | 5 | RUN apk add --no-cache libc6-compat 6 | 7 | WORKDIR /app 8 | 9 | COPY package.json yarn.lock ./ 10 | 11 | RUN yarn config set registry 'https://registry.npmmirror.com/' 12 | RUN yarn install 13 | 14 | FROM base AS builder 15 | 16 | RUN apk update && apk add --no-cache git 17 | 18 | ENV OPENAI_API_KEY="" 19 | ENV CODE="" 20 | 21 | WORKDIR /app 22 | COPY --from=deps /app/node_modules ./node_modules 23 | COPY . . 24 | 25 | RUN yarn build 26 | 27 | FROM base AS runner 28 | WORKDIR /app 29 | 30 | RUN apk add proxychains-ng 31 | 32 | ENV PROXY_URL="" 33 | ENV OPENAI_API_KEY="" 34 | ENV CODE="" 35 | 36 | COPY --from=builder /app/public ./public 37 | COPY --from=builder /app/.next/standalone ./ 38 | COPY --from=builder /app/.next/static ./.next/static 39 | COPY --from=builder /app/.next/server ./.next/server 40 | 41 | EXPOSE 3000 42 | 43 | CMD if [ -n "$PROXY_URL" ]; then \ 44 | export HOSTNAME="127.0.0.1"; \ 45 | protocol=$(echo $PROXY_URL | cut -d: -f1); \ 46 | host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ 47 | port=$(echo $PROXY_URL | cut -d: -f3); \ 48 | conf=/etc/proxychains.conf; \ 49 | echo "strict_chain" > $conf; \ 50 | echo "proxy_dns" >> $conf; \ 51 | echo "remote_dns_subnet 224" >> $conf; \ 52 | echo "tcp_read_time_out 15000" >> $conf; \ 53 | echo "tcp_connect_time_out 8000" >> $conf; \ 54 | echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ 55 | echo "localnet ::1/128" >> $conf; \ 56 | echo "[ProxyList]" >> $conf; \ 57 | echo "$protocol $host $port" >> $conf; \ 58 | cat /etc/proxychains.conf; \ 59 | proxychains -f $conf node server.js; \ 60 | else \ 61 | node server.js; \ 62 | fi 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zhang Yifei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { getServerSideConfig } from "../config/server"; 3 | import md5 from "spark-md5"; 4 | import { ACCESS_CODE_PREFIX } from "../constant"; 5 | 6 | function getIP(req: NextRequest) { 7 | let ip = req.ip ?? req.headers.get("x-real-ip"); 8 | const forwardedFor = req.headers.get("x-forwarded-for"); 9 | 10 | if (!ip && forwardedFor) { 11 | ip = forwardedFor.split(",").at(0) ?? ""; 12 | } 13 | 14 | return ip; 15 | } 16 | 17 | function parseApiKey(bearToken: string) { 18 | const token = bearToken.trim().replaceAll("Bearer ", "").trim(); 19 | const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX); 20 | 21 | return { 22 | accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length), 23 | apiKey: isOpenAiKey ? token : "", 24 | }; 25 | } 26 | 27 | export function auth(req: NextRequest) { 28 | const authToken = req.headers.get("Authorization") ?? ""; 29 | 30 | // check if it is openai api key or user token 31 | const { accessCode, apiKey } = parseApiKey(authToken); 32 | 33 | const hashedCode = md5.hash(accessCode ?? "").trim(); 34 | 35 | const serverConfig = getServerSideConfig(); 36 | console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); 37 | console.log("[Auth] got access code:", accessCode); 38 | console.log("[Auth] hashed access code:", hashedCode); 39 | console.log("[User IP] ", getIP(req)); 40 | console.log("[Time] ", new Date().toLocaleString()); 41 | 42 | if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { 43 | return { 44 | error: true, 45 | msg: !accessCode ? "empty access code" : "wrong access code", 46 | }; 47 | } 48 | 49 | if (serverConfig.hideUserApiKey && !!apiKey) { 50 | return { 51 | error: true, 52 | msg: "you are not allowed to access openai with your own api key", 53 | }; 54 | } 55 | 56 | // if user does not provide an api key, inject system api key 57 | if (!apiKey) { 58 | const serverApiKey = serverConfig.isAzure 59 | ? serverConfig.azureApiKey 60 | : serverConfig.apiKey; 61 | 62 | if (serverApiKey) { 63 | console.log("[Auth] use system api key"); 64 | req.headers.set( 65 | "Authorization", 66 | `${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`, 67 | ); 68 | } else { 69 | console.log("[Auth] admin did not provide an api key"); 70 | } 71 | } else { 72 | console.log("[Auth] use user api key"); 73 | } 74 | 75 | return { 76 | error: false, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /app/api/common.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getServerSideConfig } from "../config/server"; 3 | import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; 4 | import { collectModelTable } from "../utils/model"; 5 | import { makeAzurePath } from "../azure"; 6 | 7 | const serverConfig = getServerSideConfig(); 8 | 9 | export async function requestOpenai(req: NextRequest) { 10 | const controller = new AbortController(); 11 | 12 | const authValue = req.headers.get("Authorization") ?? ""; 13 | const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization"; 14 | 15 | let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( 16 | "/api/openai/", 17 | "", 18 | ); 19 | 20 | let baseUrl = 21 | serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL; 22 | 23 | if (!baseUrl.startsWith("http")) { 24 | baseUrl = `https://${baseUrl}`; 25 | } 26 | 27 | if (baseUrl.endsWith("/")) { 28 | baseUrl = baseUrl.slice(0, -1); 29 | } 30 | 31 | console.log("[Proxy] ", path); 32 | console.log("[Base Url]", baseUrl); 33 | console.log("[Org ID]", serverConfig.openaiOrgId); 34 | 35 | const timeoutId = setTimeout( 36 | () => { 37 | controller.abort(); 38 | }, 39 | 10 * 60 * 1000, 40 | ); 41 | 42 | if (serverConfig.isAzure) { 43 | if (!serverConfig.azureApiVersion) { 44 | return NextResponse.json({ 45 | error: true, 46 | message: `missing AZURE_API_VERSION in server env vars`, 47 | }); 48 | } 49 | path = makeAzurePath(path, serverConfig.azureApiVersion); 50 | } 51 | 52 | const fetchUrl = `${baseUrl}/${path}`; 53 | const fetchOptions: RequestInit = { 54 | headers: { 55 | "Content-Type": "application/json", 56 | "Cache-Control": "no-store", 57 | [authHeaderName]: authValue, 58 | ...(serverConfig.openaiOrgId && { 59 | "OpenAI-Organization": serverConfig.openaiOrgId, 60 | }), 61 | }, 62 | method: req.method, 63 | body: req.body, 64 | // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body 65 | redirect: "manual", 66 | // @ts-ignore 67 | duplex: "half", 68 | signal: controller.signal, 69 | }; 70 | 71 | // #1815 try to refuse gpt4 request 72 | if (serverConfig.customModels && req.body) { 73 | try { 74 | const modelTable = collectModelTable( 75 | DEFAULT_MODELS, 76 | serverConfig.customModels, 77 | ); 78 | const clonedBody = await req.text(); 79 | fetchOptions.body = clonedBody; 80 | 81 | const jsonBody = JSON.parse(clonedBody) as { model?: string }; 82 | 83 | // not undefined and is false 84 | if (modelTable[jsonBody?.model ?? ""].available === false) { 85 | return NextResponse.json( 86 | { 87 | error: true, 88 | message: `you are not allowed to use ${jsonBody?.model} model`, 89 | }, 90 | { 91 | status: 403, 92 | }, 93 | ); 94 | } 95 | } catch (e) { 96 | console.error("[OpenAI] gpt4 filter", e); 97 | } 98 | } 99 | 100 | try { 101 | const res = await fetch(fetchUrl, fetchOptions); 102 | 103 | // to prevent browser prompt for credentials 104 | const newHeaders = new Headers(res.headers); 105 | newHeaders.delete("www-authenticate"); 106 | // to disable nginx buffering 107 | newHeaders.set("X-Accel-Buffering", "no"); 108 | 109 | return new Response(res.body, { 110 | status: res.status, 111 | statusText: res.statusText, 112 | headers: newHeaders, 113 | }); 114 | } finally { 115 | clearTimeout(timeoutId); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/api/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import { getServerSideConfig } from "../../config/server"; 4 | 5 | const serverConfig = getServerSideConfig(); 6 | 7 | // Danger! Do not hard code any secret value here! 8 | // 警告!不要在这里写入任何敏感信息! 9 | const DANGER_CONFIG = { 10 | needCode: serverConfig.needCode, 11 | hideUserApiKey: serverConfig.hideUserApiKey, 12 | disableGPT4: serverConfig.disableGPT4, 13 | hideBalanceQuery: serverConfig.hideBalanceQuery, 14 | disableFastLink: serverConfig.disableFastLink, 15 | customModels: serverConfig.customModels, 16 | }; 17 | 18 | declare global { 19 | type DangerConfig = typeof DANGER_CONFIG; 20 | } 21 | 22 | async function handle() { 23 | return NextResponse.json(DANGER_CONFIG); 24 | } 25 | 26 | export const GET = handle; 27 | export const POST = handle; 28 | 29 | export const runtime = "edge"; 30 | -------------------------------------------------------------------------------- /app/api/cors/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | async function handle( 4 | req: NextRequest, 5 | { params }: { params: { path: string[] } }, 6 | ) { 7 | if (req.method === "OPTIONS") { 8 | return NextResponse.json({ body: "OK" }, { status: 200 }); 9 | } 10 | 11 | const [protocol, ...subpath] = params.path; 12 | const targetUrl = `${protocol}://${subpath.join("/")}`; 13 | 14 | const method = req.headers.get("method") ?? undefined; 15 | const shouldNotHaveBody = ["get", "head"].includes( 16 | method?.toLowerCase() ?? "", 17 | ); 18 | 19 | const fetchOptions: RequestInit = { 20 | headers: { 21 | authorization: req.headers.get("authorization") ?? "", 22 | }, 23 | body: shouldNotHaveBody ? null : req.body, 24 | method, 25 | // @ts-ignore 26 | duplex: "half", 27 | }; 28 | 29 | const fetchResult = await fetch(targetUrl, fetchOptions); 30 | 31 | console.log("[Any Proxy]", targetUrl, { 32 | status: fetchResult.status, 33 | statusText: fetchResult.statusText, 34 | }); 35 | 36 | return fetchResult; 37 | } 38 | 39 | export const POST = handle; 40 | export const GET = handle; 41 | export const OPTIONS = handle; 42 | 43 | export const runtime = "nodejs"; 44 | -------------------------------------------------------------------------------- /app/api/openai/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; 2 | import { getServerSideConfig } from "@/app/config/server"; 3 | import { OpenaiPath } from "@/app/constant"; 4 | import { prettyObject } from "@/app/utils/format"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | import { auth } from "../../auth"; 7 | import { requestOpenai } from "../../common"; 8 | 9 | const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); 10 | 11 | function getModels(remoteModelRes: OpenAIListModelResponse) { 12 | const config = getServerSideConfig(); 13 | 14 | if (config.disableGPT4) { 15 | remoteModelRes.data = remoteModelRes.data.filter( 16 | (m) => !m.id.startsWith("gpt-4"), 17 | ); 18 | } 19 | 20 | return remoteModelRes; 21 | } 22 | 23 | async function handle( 24 | req: NextRequest, 25 | { params }: { params: { path: string[] } }, 26 | ) { 27 | console.log("[OpenAI Route] params ", params); 28 | 29 | if (req.method === "OPTIONS") { 30 | return NextResponse.json({ body: "OK" }, { status: 200 }); 31 | } 32 | 33 | const subpath = params.path.join("/"); 34 | 35 | if (!ALLOWD_PATH.has(subpath)) { 36 | console.log("[OpenAI Route] forbidden path ", subpath); 37 | return NextResponse.json( 38 | { 39 | error: true, 40 | msg: "you are not allowed to request " + subpath, 41 | }, 42 | { 43 | status: 403, 44 | }, 45 | ); 46 | } 47 | 48 | const authResult = auth(req); 49 | if (authResult.error) { 50 | return NextResponse.json(authResult, { 51 | status: 401, 52 | }); 53 | } 54 | 55 | try { 56 | const response = await requestOpenai(req); 57 | 58 | // list models 59 | if (subpath === OpenaiPath.ListModelPath && response.status === 200) { 60 | const resJson = (await response.json()) as OpenAIListModelResponse; 61 | const availableModels = getModels(resJson); 62 | return NextResponse.json(availableModels, { 63 | status: response.status, 64 | }); 65 | } 66 | 67 | return response; 68 | } catch (e) { 69 | console.error("[OpenAI] ", e); 70 | return NextResponse.json(prettyObject(e)); 71 | } 72 | } 73 | 74 | export const GET = handle; 75 | export const POST = handle; 76 | 77 | export const runtime = "edge"; 78 | -------------------------------------------------------------------------------- /app/azure.ts: -------------------------------------------------------------------------------- 1 | export function makeAzurePath(path: string, apiVersion: string) { 2 | // should omit /v1 prefix 3 | path = path.replaceAll("v1/", ""); 4 | 5 | // should add api-key to query string 6 | path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`; 7 | 8 | return path; 9 | } 10 | -------------------------------------------------------------------------------- /app/client/controller.ts: -------------------------------------------------------------------------------- 1 | // To store message streaming controller 2 | export const ChatControllerPool = { 3 | controllers: {} as Record, 4 | 5 | addController( 6 | sessionId: string, 7 | messageId: string, 8 | controller: AbortController, 9 | ) { 10 | const key = this.key(sessionId, messageId); 11 | this.controllers[key] = controller; 12 | return key; 13 | }, 14 | 15 | stop(sessionId: string, messageId: string) { 16 | const key = this.key(sessionId, messageId); 17 | const controller = this.controllers[key]; 18 | controller?.abort(); 19 | }, 20 | 21 | stopAll() { 22 | Object.values(this.controllers).forEach((v) => v.abort()); 23 | }, 24 | 25 | hasPending() { 26 | return Object.values(this.controllers).length > 0; 27 | }, 28 | 29 | remove(sessionId: string, messageId: string) { 30 | const key = this.key(sessionId, messageId); 31 | delete this.controllers[key]; 32 | }, 33 | 34 | key(sessionId: string, messageIndex: string) { 35 | return `${sessionId},${messageIndex}`; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /app/command.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSearchParams } from "react-router-dom"; 3 | import Locale from "./locales"; 4 | 5 | type Command = (param: string) => void; 6 | interface Commands { 7 | fill?: Command; 8 | submit?: Command; 9 | mask?: Command; 10 | code?: Command; 11 | settings?: Command; 12 | } 13 | 14 | export function useCommand(commands: Commands = {}) { 15 | const [searchParams, setSearchParams] = useSearchParams(); 16 | 17 | useEffect(() => { 18 | let shouldUpdate = false; 19 | searchParams.forEach((param, name) => { 20 | const commandName = name as keyof Commands; 21 | if (typeof commands[commandName] === "function") { 22 | commands[commandName]!(param); 23 | searchParams.delete(name); 24 | shouldUpdate = true; 25 | } 26 | }); 27 | 28 | if (shouldUpdate) { 29 | setSearchParams(searchParams); 30 | } 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [searchParams, commands]); 33 | } 34 | 35 | interface ChatCommands { 36 | new?: Command; 37 | newm?: Command; 38 | next?: Command; 39 | prev?: Command; 40 | clear?: Command; 41 | del?: Command; 42 | } 43 | 44 | export const ChatCommandPrefix = ":"; 45 | 46 | export function useChatCommand(commands: ChatCommands = {}) { 47 | function extract(userInput: string) { 48 | return ( 49 | userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput 50 | ) as keyof ChatCommands; 51 | } 52 | 53 | function search(userInput: string) { 54 | const input = extract(userInput); 55 | const desc = Locale.Chat.Commands; 56 | return Object.keys(commands) 57 | .filter((c) => c.startsWith(input)) 58 | .map((c) => ({ 59 | title: desc[c as keyof ChatCommands], 60 | content: ChatCommandPrefix + c, 61 | })); 62 | } 63 | 64 | function match(userInput: string) { 65 | const command = extract(userInput); 66 | const matched = typeof commands[command] === "function"; 67 | 68 | return { 69 | matched, 70 | invoke: () => matched && commands[command]!(userInput), 71 | }; 72 | } 73 | 74 | return { match, search }; 75 | } 76 | -------------------------------------------------------------------------------- /app/components/auth.module.scss: -------------------------------------------------------------------------------- 1 | .auth-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | width: 100%; 7 | flex-direction: column; 8 | 9 | .auth-logo { 10 | transform: scale(1.4); 11 | } 12 | 13 | .auth-title { 14 | font-size: 24px; 15 | font-weight: bold; 16 | line-height: 2; 17 | } 18 | 19 | .auth-tips { 20 | font-size: 14px; 21 | } 22 | 23 | .auth-input { 24 | margin: 3vh 0; 25 | } 26 | 27 | .auth-actions { 28 | display: flex; 29 | justify-content: center; 30 | flex-direction: column; 31 | 32 | button:not(:last-child) { 33 | margin-bottom: 10px; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/components/auth.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./auth.module.scss"; 2 | import { IconButton } from "./button"; 3 | 4 | import { useNavigate } from "react-router-dom"; 5 | import { Path } from "../constant"; 6 | import { useAccessStore } from "../store"; 7 | import Locale from "../locales"; 8 | 9 | import BotIcon from "../icons/bot.svg"; 10 | import { useEffect } from "react"; 11 | import { getClientConfig } from "../config/client"; 12 | 13 | export function AuthPage() { 14 | const navigate = useNavigate(); 15 | const accessStore = useAccessStore(); 16 | 17 | const goHome = () => navigate(Path.Home); 18 | const goChat = () => navigate(Path.Chat); 19 | const resetAccessCode = () => { 20 | accessStore.update((access) => { 21 | access.openaiApiKey = ""; 22 | access.accessCode = ""; 23 | }); 24 | }; // Reset access code to empty string 25 | 26 | useEffect(() => { 27 | if (getClientConfig()?.isApp) { 28 | navigate(Path.Settings); 29 | } 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, []); 32 | 33 | return ( 34 |
35 |
36 | 37 |
38 | 39 |
{Locale.Auth.Title}
40 |
{Locale.Auth.Tips}
41 | 42 | { 48 | accessStore.update( 49 | (access) => (access.accessCode = e.currentTarget.value), 50 | ); 51 | }} 52 | /> 53 | {!accessStore.hideUserApiKey ? ( 54 | <> 55 |
{Locale.Auth.SubTips}
56 | { 62 | accessStore.update( 63 | (access) => (access.openaiApiKey = e.currentTarget.value), 64 | ); 65 | }} 66 | /> 67 | 68 | ) : null} 69 | 70 |
71 | 76 | { 79 | resetAccessCode(); 80 | goHome(); 81 | }} 82 | /> 83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /app/components/button.module.scss: -------------------------------------------------------------------------------- 1 | .icon-button { 2 | background-color: var(--white); 3 | border-radius: 10px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | padding: 10px; 8 | 9 | cursor: pointer; 10 | transition: all 0.3s ease; 11 | overflow: hidden; 12 | user-select: none; 13 | outline: none; 14 | border: none; 15 | color: var(--black); 16 | 17 | &[disabled] { 18 | cursor: not-allowed; 19 | opacity: 0.5; 20 | } 21 | 22 | &.primary { 23 | background-color: var(--primary); 24 | color: white; 25 | 26 | path { 27 | fill: white !important; 28 | } 29 | } 30 | 31 | &.danger { 32 | color: rgba($color: red, $alpha: 0.8); 33 | border-color: rgba($color: red, $alpha: 0.5); 34 | background-color: rgba($color: red, $alpha: 0.05); 35 | 36 | &:hover { 37 | border-color: red; 38 | background-color: rgba($color: red, $alpha: 0.1); 39 | } 40 | 41 | path { 42 | fill: red !important; 43 | } 44 | } 45 | 46 | &:hover, 47 | &:focus { 48 | border-color: var(--primary); 49 | } 50 | } 51 | 52 | .shadow { 53 | box-shadow: var(--card-shadow); 54 | } 55 | 56 | .border { 57 | border: var(--border-in-light); 58 | } 59 | 60 | .icon-button-icon { 61 | width: 16px; 62 | height: 16px; 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | } 67 | 68 | @media only screen and (max-width: 600px) { 69 | .icon-button { 70 | padding: 16px; 71 | } 72 | } 73 | 74 | .icon-button-text { 75 | font-size: 12px; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | 80 | &:not(:first-child) { 81 | margin-left: 5px; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import styles from "./button.module.scss"; 4 | 5 | export type ButtonType = "primary" | "danger" | null; 6 | 7 | export function IconButton(props: { 8 | onClick?: () => void; 9 | icon?: JSX.Element; 10 | type?: ButtonType; 11 | text?: string; 12 | bordered?: boolean; 13 | shadow?: boolean; 14 | className?: string; 15 | title?: string; 16 | disabled?: boolean; 17 | tabIndex?: number; 18 | autoFocus?: boolean; 19 | }) { 20 | return ( 21 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/components/emoji.tsx: -------------------------------------------------------------------------------- 1 | import EmojiPicker, { 2 | Emoji, 3 | EmojiStyle, 4 | Theme as EmojiTheme, 5 | } from "emoji-picker-react"; 6 | 7 | import { ModelType } from "../store"; 8 | 9 | import BotIcon from "../icons/bot.svg"; 10 | import BlackBotIcon from "../icons/black-bot.svg"; 11 | 12 | export function getEmojiUrl(unified: string, style: EmojiStyle) { 13 | return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; 14 | } 15 | 16 | export function AvatarPicker(props: { 17 | onEmojiClick: (emojiId: string) => void; 18 | }) { 19 | return ( 20 | { 25 | props.onEmojiClick(e.unified); 26 | }} 27 | /> 28 | ); 29 | } 30 | 31 | export function Avatar(props: { model?: ModelType; avatar?: string }) { 32 | if (props.model) { 33 | return ( 34 |
35 | {props.model?.startsWith("gpt-4") ? ( 36 | 37 | ) : ( 38 | 39 | )} 40 |
41 | ); 42 | } 43 | 44 | return ( 45 |
46 | {props.avatar && } 47 |
48 | ); 49 | } 50 | 51 | export function EmojiAvatar(props: { avatar: string; size?: number }) { 52 | return ( 53 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/components/error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IconButton } from "./button"; 3 | import GithubIcon from "../icons/github.svg"; 4 | import ResetIcon from "../icons/reload.svg"; 5 | import { ISSUE_URL } from "../constant"; 6 | import Locale from "../locales"; 7 | import { showConfirm } from "./ui-lib"; 8 | import { useSyncStore } from "../store/sync"; 9 | 10 | interface IErrorBoundaryState { 11 | hasError: boolean; 12 | error: Error | null; 13 | info: React.ErrorInfo | null; 14 | } 15 | 16 | export class ErrorBoundary extends React.Component { 17 | constructor(props: any) { 18 | super(props); 19 | this.state = { hasError: false, error: null, info: null }; 20 | } 21 | 22 | componentDidCatch(error: Error, info: React.ErrorInfo) { 23 | // Update state with error details 24 | this.setState({ hasError: true, error, info }); 25 | } 26 | 27 | clearAndSaveData() { 28 | try { 29 | useSyncStore.getState().export(); 30 | } finally { 31 | localStorage.clear(); 32 | location.reload(); 33 | } 34 | } 35 | 36 | render() { 37 | if (this.state.hasError) { 38 | // Render error message 39 | return ( 40 |
41 |

Oops, something went wrong!

42 |
43 |             {this.state.error?.toString()}
44 |             {this.state.info?.componentStack}
45 |           
46 | 47 |
48 | 49 | } 52 | bordered 53 | /> 54 | 55 | } 57 | text="Clear All Data" 58 | onClick={async () => { 59 | if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) { 60 | this.clearAndSaveData(); 61 | } 62 | }} 63 | bordered 64 | /> 65 |
66 |
67 | ); 68 | } 69 | // if no error occurred, render children 70 | return this.props.children; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/components/input-range.module.scss: -------------------------------------------------------------------------------- 1 | .input-range { 2 | border: var(--border-in-light); 3 | border-radius: 10px; 4 | padding: 5px 10px 5px 10px; 5 | font-size: 12px; 6 | display: flex; 7 | justify-content: space-between; 8 | max-width: 40%; 9 | 10 | input[type="range"] { 11 | max-width: calc(100% - 34px); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/components/input-range.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styles from "./input-range.module.scss"; 3 | 4 | interface InputRangeProps { 5 | onChange: React.ChangeEventHandler; 6 | title?: string; 7 | value: number | string; 8 | className?: string; 9 | min: string; 10 | max: string; 11 | step: string; 12 | } 13 | 14 | export function InputRange({ 15 | onChange, 16 | title, 17 | value, 18 | className, 19 | min, 20 | max, 21 | step, 22 | }: InputRangeProps) { 23 | return ( 24 |
25 | {title || value} 26 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/components/mask.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | .mask-page { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | 7 | .mask-page-body { 8 | padding: 20px; 9 | overflow-y: auto; 10 | 11 | .mask-filter { 12 | width: 100%; 13 | max-width: 100%; 14 | margin-bottom: 20px; 15 | animation: slide-in ease 0.3s; 16 | height: 40px; 17 | 18 | display: flex; 19 | 20 | .search-bar { 21 | flex-grow: 1; 22 | max-width: 100%; 23 | min-width: 0; 24 | } 25 | 26 | .mask-filter-lang { 27 | height: 100%; 28 | margin-left: 10px; 29 | } 30 | 31 | .mask-create { 32 | height: 100%; 33 | margin-left: 10px; 34 | box-sizing: border-box; 35 | min-width: 80px; 36 | } 37 | } 38 | 39 | .mask-item { 40 | display: flex; 41 | justify-content: space-between; 42 | padding: 20px; 43 | border: var(--border-in-light); 44 | animation: slide-in ease 0.3s; 45 | 46 | &:not(:last-child) { 47 | border-bottom: 0; 48 | } 49 | 50 | &:first-child { 51 | border-top-left-radius: 10px; 52 | border-top-right-radius: 10px; 53 | } 54 | 55 | &:last-child { 56 | border-bottom-left-radius: 10px; 57 | border-bottom-right-radius: 10px; 58 | } 59 | 60 | .mask-header { 61 | display: flex; 62 | align-items: center; 63 | 64 | .mask-icon { 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | margin-right: 10px; 69 | } 70 | 71 | .mask-title { 72 | .mask-name { 73 | font-size: 14px; 74 | font-weight: bold; 75 | } 76 | .mask-info { 77 | font-size: 12px; 78 | } 79 | } 80 | } 81 | 82 | .mask-actions { 83 | display: flex; 84 | flex-wrap: nowrap; 85 | transition: all ease 0.3s; 86 | } 87 | 88 | @media screen and (max-width: 600px) { 89 | display: flex; 90 | flex-direction: column; 91 | padding-bottom: 10px; 92 | border-radius: 10px; 93 | margin-bottom: 20px; 94 | box-shadow: var(--card-shadow); 95 | 96 | &:not(:last-child) { 97 | border-bottom: var(--border-in-light); 98 | } 99 | 100 | .mask-actions { 101 | width: 100%; 102 | justify-content: space-between; 103 | padding-top: 10px; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/components/message-selector.module.scss: -------------------------------------------------------------------------------- 1 | .message-selector { 2 | .message-filter { 3 | display: flex; 4 | 5 | .search-bar { 6 | max-width: unset; 7 | flex-grow: 1; 8 | margin-right: 10px; 9 | } 10 | 11 | .actions { 12 | display: flex; 13 | 14 | button:not(:last-child) { 15 | margin-right: 10px; 16 | } 17 | } 18 | 19 | @media screen and (max-width: 600px) { 20 | flex-direction: column; 21 | 22 | .search-bar { 23 | margin-right: 0; 24 | } 25 | 26 | .actions { 27 | margin-top: 20px; 28 | 29 | button { 30 | flex-grow: 1; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .messages { 37 | margin-top: 20px; 38 | border-radius: 10px; 39 | border: var(--border-in-light); 40 | overflow: hidden; 41 | 42 | .message { 43 | display: flex; 44 | align-items: center; 45 | padding: 8px 10px; 46 | cursor: pointer; 47 | 48 | &-selected { 49 | background-color: var(--second); 50 | } 51 | 52 | &:not(:last-child) { 53 | border-bottom: var(--border-in-light); 54 | } 55 | 56 | .avatar { 57 | margin-right: 10px; 58 | } 59 | 60 | .body { 61 | flex: 1; 62 | max-width: calc(100% - 80px); 63 | 64 | .date { 65 | font-size: 12px; 66 | line-height: 1.2; 67 | opacity: 0.5; 68 | } 69 | 70 | .content { 71 | font-size: 12px; 72 | } 73 | } 74 | 75 | .checkbox { 76 | display: flex; 77 | justify-content: flex-end; 78 | flex: 1; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/components/new-chat.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .new-chat { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | flex-direction: column; 10 | 11 | .mask-header { 12 | display: flex; 13 | justify-content: space-between; 14 | width: 100%; 15 | padding: 10px; 16 | box-sizing: border-box; 17 | animation: slide-in-from-top ease 0.3s; 18 | } 19 | 20 | .mask-cards { 21 | display: flex; 22 | margin-top: 5vh; 23 | margin-bottom: 20px; 24 | animation: slide-in ease 0.3s; 25 | 26 | .mask-card { 27 | padding: 20px 10px; 28 | border: var(--border-in-light); 29 | box-shadow: var(--card-shadow); 30 | border-radius: 14px; 31 | background-color: var(--white); 32 | transform: scale(1); 33 | 34 | &:first-child { 35 | transform: rotate(-15deg) translateY(5px); 36 | } 37 | 38 | &:last-child { 39 | transform: rotate(15deg) translateY(5px); 40 | } 41 | } 42 | } 43 | 44 | .title { 45 | font-size: 32px; 46 | font-weight: bolder; 47 | margin-bottom: 1vh; 48 | animation: slide-in ease 0.35s; 49 | } 50 | 51 | .sub-title { 52 | animation: slide-in ease 0.4s; 53 | } 54 | 55 | .actions { 56 | margin-top: 5vh; 57 | margin-bottom: 2vh; 58 | animation: slide-in ease 0.45s; 59 | display: flex; 60 | justify-content: center; 61 | font-size: 12px; 62 | 63 | .skip { 64 | margin-left: 10px; 65 | } 66 | } 67 | 68 | .masks { 69 | flex-grow: 1; 70 | width: 100%; 71 | overflow: auto; 72 | align-items: center; 73 | padding-top: 20px; 74 | 75 | $linear: linear-gradient( 76 | to bottom, 77 | rgba(0, 0, 0, 0), 78 | rgba(0, 0, 0, 1), 79 | rgba(0, 0, 0, 0) 80 | ); 81 | 82 | -webkit-mask-image: $linear; 83 | mask-image: $linear; 84 | 85 | animation: slide-in ease 0.5s; 86 | 87 | .mask-row { 88 | display: flex; 89 | // justify-content: center; 90 | margin-bottom: 10px; 91 | 92 | @for $i from 1 to 10 { 93 | &:nth-child(#{$i * 2}) { 94 | margin-left: 50px; 95 | } 96 | } 97 | 98 | .mask { 99 | display: flex; 100 | align-items: center; 101 | padding: 10px 14px; 102 | border: var(--border-in-light); 103 | box-shadow: var(--card-shadow); 104 | background-color: var(--white); 105 | border-radius: 10px; 106 | margin-right: 10px; 107 | max-width: 8em; 108 | transform: scale(1); 109 | cursor: pointer; 110 | transition: all ease 0.3s; 111 | 112 | &:hover { 113 | transform: translateY(-5px) scale(1.1); 114 | z-index: 999; 115 | border-color: var(--primary); 116 | } 117 | 118 | .mask-name { 119 | margin-left: 10px; 120 | font-size: 14px; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/components/settings.module.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 20px; 3 | overflow: auto; 4 | } 5 | 6 | .avatar { 7 | cursor: pointer; 8 | } 9 | 10 | .edit-prompt-modal { 11 | display: flex; 12 | flex-direction: column; 13 | 14 | .edit-prompt-title { 15 | max-width: unset; 16 | margin-bottom: 20px; 17 | text-align: left; 18 | } 19 | .edit-prompt-content { 20 | max-width: unset; 21 | } 22 | } 23 | 24 | .user-prompt-modal { 25 | min-height: 40vh; 26 | 27 | .user-prompt-search { 28 | width: 100%; 29 | max-width: 100%; 30 | margin-bottom: 10px; 31 | background-color: var(--gray); 32 | } 33 | 34 | .user-prompt-list { 35 | border: var(--border-in-light); 36 | border-radius: 10px; 37 | 38 | .user-prompt-item { 39 | display: flex; 40 | justify-content: space-between; 41 | padding: 10px; 42 | 43 | &:not(:last-child) { 44 | border-bottom: var(--border-in-light); 45 | } 46 | 47 | .user-prompt-header { 48 | max-width: calc(100% - 100px); 49 | 50 | .user-prompt-title { 51 | font-size: 14px; 52 | line-height: 2; 53 | font-weight: bold; 54 | } 55 | .user-prompt-content { 56 | font-size: 12px; 57 | } 58 | } 59 | 60 | .user-prompt-buttons { 61 | display: flex; 62 | align-items: center; 63 | column-gap: 2px; 64 | 65 | .user-prompt-button { 66 | //height: 100%; 67 | padding: 7px; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/config/build.ts: -------------------------------------------------------------------------------- 1 | import tauriConfig from "../../src-tauri/tauri.conf.json"; 2 | 3 | export const getBuildConfig = () => { 4 | if (typeof process === "undefined") { 5 | throw Error( 6 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 7 | ); 8 | } 9 | 10 | const buildMode = process.env.BUILD_MODE ?? "standalone"; 11 | const isApp = !!process.env.BUILD_APP; 12 | const version = "v" + tauriConfig.package.version; 13 | 14 | const commitInfo = (() => { 15 | try { 16 | const childProcess = require("child_process"); 17 | const commitDate: string = childProcess 18 | .execSync('git log -1 --format="%at000" --date=unix') 19 | .toString() 20 | .trim(); 21 | const commitHash: string = childProcess 22 | .execSync('git log --pretty=format:"%H" -n 1') 23 | .toString() 24 | .trim(); 25 | 26 | return { commitDate, commitHash }; 27 | } catch (e) { 28 | console.error("[Build Config] No git or not from git repo."); 29 | return { 30 | commitDate: "unknown", 31 | commitHash: "unknown", 32 | }; 33 | } 34 | })(); 35 | 36 | return { 37 | version, 38 | ...commitInfo, 39 | buildMode, 40 | isApp, 41 | }; 42 | }; 43 | 44 | export type BuildConfig = ReturnType; 45 | -------------------------------------------------------------------------------- /app/config/client.ts: -------------------------------------------------------------------------------- 1 | import { BuildConfig, getBuildConfig } from "./build"; 2 | 3 | export function getClientConfig() { 4 | if (typeof document !== "undefined") { 5 | // client side 6 | return JSON.parse(queryMeta("config")) as BuildConfig; 7 | } 8 | 9 | if (typeof process !== "undefined") { 10 | // server side 11 | return getBuildConfig(); 12 | } 13 | } 14 | 15 | function queryMeta(key: string, defaultValue?: string): string { 16 | let ret: string; 17 | if (document) { 18 | const meta = document.head.querySelector( 19 | `meta[name='${key}']`, 20 | ) as HTMLMetaElement; 21 | ret = meta?.content ?? ""; 22 | } else { 23 | ret = defaultValue ?? ""; 24 | } 25 | 26 | return ret; 27 | } 28 | -------------------------------------------------------------------------------- /app/config/server.ts: -------------------------------------------------------------------------------- 1 | import md5 from "spark-md5"; 2 | import { DEFAULT_MODELS } from "../constant"; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | PROXY_URL?: string; // docker only 8 | 9 | OPENAI_API_KEY?: string; 10 | CODE?: string; 11 | 12 | BASE_URL?: string; 13 | OPENAI_ORG_ID?: string; // openai only 14 | 15 | VERCEL?: string; 16 | BUILD_MODE?: "standalone" | "export"; 17 | BUILD_APP?: string; // is building desktop app 18 | 19 | HIDE_USER_API_KEY?: string; // disable user's api key input 20 | DISABLE_GPT4?: string; // allow user to use gpt-4 or not 21 | ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not 22 | DISABLE_FAST_LINK?: string; // disallow parse settings from url or not 23 | CUSTOM_MODELS?: string; // to control custom models 24 | 25 | // azure only 26 | AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name} 27 | AZURE_API_KEY?: string; 28 | AZURE_API_VERSION?: string; 29 | } 30 | } 31 | } 32 | 33 | const ACCESS_CODES = (function getAccessCodes(): Set { 34 | const code = process.env.CODE; 35 | 36 | try { 37 | const codes = (code?.split(",") ?? []) 38 | .filter((v) => !!v) 39 | .map((v) => md5.hash(v.trim())); 40 | return new Set(codes); 41 | } catch (e) { 42 | return new Set(); 43 | } 44 | })(); 45 | 46 | export const getServerSideConfig = () => { 47 | if (typeof process === "undefined") { 48 | throw Error( 49 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 50 | ); 51 | } 52 | 53 | const disableGPT4 = !!process.env.DISABLE_GPT4; 54 | let customModels = process.env.CUSTOM_MODELS ?? ""; 55 | 56 | if (disableGPT4) { 57 | if (customModels) customModels += ","; 58 | customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4")) 59 | .map((m) => "-" + m.name) 60 | .join(","); 61 | } 62 | 63 | const isAzure = !!process.env.AZURE_URL; 64 | 65 | return { 66 | baseUrl: process.env.BASE_URL, 67 | apiKey: process.env.OPENAI_API_KEY, 68 | openaiOrgId: process.env.OPENAI_ORG_ID, 69 | 70 | isAzure, 71 | azureUrl: process.env.AZURE_URL, 72 | azureApiKey: process.env.AZURE_API_KEY, 73 | azureApiVersion: process.env.AZURE_API_VERSION, 74 | 75 | needCode: ACCESS_CODES.size > 0, 76 | code: process.env.CODE, 77 | codes: ACCESS_CODES, 78 | 79 | proxyUrl: process.env.PROXY_URL, 80 | isVercel: !!process.env.VERCEL, 81 | 82 | hideUserApiKey: !!process.env.HIDE_USER_API_KEY, 83 | disableGPT4, 84 | hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, 85 | disableFastLink: !!process.env.DISABLE_FAST_LINK, 86 | customModels, 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | declare module "*.png"; 3 | declare module "*.woff2"; 4 | declare module "*.woff"; 5 | declare module "*.ttf"; 6 | declare module "*.scss" { 7 | const content: Record; 8 | export default content; 9 | } 10 | 11 | declare module "*.svg"; 12 | 13 | declare interface Window { 14 | __TAURI__?: { 15 | writeText(text: string): Promise; 16 | invoke(command: string, payload?: Record): Promise; 17 | dialog: { 18 | save(options?: Record): Promise; 19 | }; 20 | fs: { 21 | writeBinaryFile(path: string, data: Uint8Array): Promise; 22 | }; 23 | notification:{ 24 | requestPermission(): Promise; 25 | isPermissionGranted(): Promise; 26 | sendNotification(options: string | Options): void; 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /app/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/auto.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/black-bot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/app/icons/bot.png -------------------------------------------------------------------------------- /app/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/chat.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/icons/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/app/icons/chatgpt.png -------------------------------------------------------------------------------- /app/icons/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/close.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/confirm.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/drag.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/max.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/plugin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/prompt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/reload.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/icons/rename.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/return.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/three-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-page-custom-font */ 2 | import "./styles/globals.scss"; 3 | import "./styles/markdown.scss"; 4 | import "./styles/highlight.scss"; 5 | import { getClientConfig } from "./config/client"; 6 | import { type Metadata } from "next"; 7 | 8 | export const metadata: Metadata = { 9 | title: "ChatGPT Next Web", 10 | description: "Your personal ChatGPT Chat Bot.", 11 | viewport: { 12 | width: "device-width", 13 | initialScale: 1, 14 | maximumScale: 1, 15 | }, 16 | themeColor: [ 17 | { media: "(prefers-color-scheme: light)", color: "#fafafa" }, 18 | { media: "(prefers-color-scheme: dark)", color: "#151515" }, 19 | ], 20 | appleWebApp: { 21 | title: "ChatGPT Next Web", 22 | statusBarStyle: "default", 23 | }, 24 | }; 25 | 26 | export default function RootLayout({ 27 | children, 28 | }: { 29 | children: React.ReactNode; 30 | }) { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/locales/index.ts: -------------------------------------------------------------------------------- 1 | import cn from "./cn"; 2 | import en from "./en"; 3 | import tw from "./tw"; 4 | import id from "./id"; 5 | import fr from "./fr"; 6 | import es from "./es"; 7 | import it from "./it"; 8 | import tr from "./tr"; 9 | import jp from "./jp"; 10 | import de from "./de"; 11 | import vi from "./vi"; 12 | import ru from "./ru"; 13 | import no from "./no"; 14 | import cs from "./cs"; 15 | import ko from "./ko"; 16 | import ar from "./ar"; 17 | import bn from "./bn"; 18 | import { merge } from "../utils/merge"; 19 | 20 | import type { LocaleType } from "./cn"; 21 | export type { LocaleType, PartialLocaleType } from "./cn"; 22 | 23 | const ALL_LANGS = { 24 | cn, 25 | en, 26 | tw, 27 | jp, 28 | ko, 29 | id, 30 | fr, 31 | es, 32 | it, 33 | tr, 34 | de, 35 | vi, 36 | ru, 37 | cs, 38 | no, 39 | ar, 40 | bn, 41 | }; 42 | 43 | export type Lang = keyof typeof ALL_LANGS; 44 | 45 | export const AllLangs = Object.keys(ALL_LANGS) as Lang[]; 46 | 47 | export const ALL_LANG_OPTIONS: Record = { 48 | cn: "简体中文", 49 | en: "English", 50 | tw: "繁體中文", 51 | jp: "日本語", 52 | ko: "한국어", 53 | id: "Indonesia", 54 | fr: "Français", 55 | es: "Español", 56 | it: "Italiano", 57 | tr: "Türkçe", 58 | de: "Deutsch", 59 | vi: "Tiếng Việt", 60 | ru: "Русский", 61 | cs: "Čeština", 62 | no: "Nynorsk", 63 | ar: "العربية", 64 | bn: "বাংলা", 65 | }; 66 | 67 | const LANG_KEY = "lang"; 68 | const DEFAULT_LANG = "en"; 69 | 70 | const fallbackLang = en; 71 | const targetLang = ALL_LANGS[getLang()] as LocaleType; 72 | 73 | // if target lang missing some fields, it will use fallback lang string 74 | merge(fallbackLang, targetLang); 75 | 76 | export default fallbackLang as LocaleType; 77 | 78 | function getItem(key: string) { 79 | try { 80 | return localStorage.getItem(key); 81 | } catch { 82 | return null; 83 | } 84 | } 85 | 86 | function setItem(key: string, value: string) { 87 | try { 88 | localStorage.setItem(key, value); 89 | } catch {} 90 | } 91 | 92 | function getLanguage() { 93 | try { 94 | return navigator.language.toLowerCase(); 95 | } catch { 96 | return DEFAULT_LANG; 97 | } 98 | } 99 | 100 | export function getLang(): Lang { 101 | const savedLang = getItem(LANG_KEY); 102 | 103 | if (AllLangs.includes((savedLang ?? "") as Lang)) { 104 | return savedLang as Lang; 105 | } 106 | 107 | const lang = getLanguage(); 108 | 109 | for (const option of AllLangs) { 110 | if (lang.includes(option)) { 111 | return option; 112 | } 113 | } 114 | 115 | return DEFAULT_LANG; 116 | } 117 | 118 | export function changeLang(lang: Lang) { 119 | setItem(LANG_KEY, lang); 120 | location.reload(); 121 | } 122 | 123 | export function getISOLang() { 124 | const isoLangString: Record = { 125 | cn: "zh-Hans", 126 | tw: "zh-Hant", 127 | }; 128 | 129 | const lang = getLang(); 130 | return isoLangString[lang] ?? lang; 131 | } 132 | -------------------------------------------------------------------------------- /app/masks/index.ts: -------------------------------------------------------------------------------- 1 | import { Mask } from "../store/mask"; 2 | import { CN_MASKS } from "./cn"; 3 | import { EN_MASKS } from "./en"; 4 | 5 | import { type BuiltinMask } from "./typing"; 6 | export { type BuiltinMask } from "./typing"; 7 | 8 | export const BUILTIN_MASK_ID = 100000; 9 | 10 | export const BUILTIN_MASK_STORE = { 11 | buildinId: BUILTIN_MASK_ID, 12 | masks: {} as Record, 13 | get(id?: string) { 14 | if (!id) return undefined; 15 | return this.masks[id] as Mask | undefined; 16 | }, 17 | add(m: BuiltinMask) { 18 | const mask = { ...m, id: this.buildinId++, builtin: true }; 19 | this.masks[mask.id] = mask; 20 | return mask; 21 | }, 22 | }; 23 | 24 | export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...EN_MASKS].map( 25 | (m) => BUILTIN_MASK_STORE.add(m), 26 | ); 27 | -------------------------------------------------------------------------------- /app/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { ModelConfig } from "../store"; 2 | import { type Mask } from "../store/mask"; 3 | 4 | export type BuiltinMask = Omit & { 5 | builtin: Boolean; 6 | modelConfig: Partial; 7 | }; 8 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | 3 | import { Home } from "./components/home"; 4 | 5 | import { getServerSideConfig } from "./config/server"; 6 | 7 | const serverConfig = getServerSideConfig(); 8 | 9 | export default async function App() { 10 | return ( 11 | <> 12 | 13 | {serverConfig?.isVercel && } 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/polyfill.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Array { 3 | at(index: number): T | undefined; 4 | } 5 | } 6 | 7 | if (!Array.prototype.at) { 8 | Array.prototype.at = function (index: number) { 9 | // Get the length of the array 10 | const length = this.length; 11 | 12 | // Convert negative index to a positive index 13 | if (index < 0) { 14 | index = length + index; 15 | } 16 | 17 | // Return undefined if the index is out of range 18 | if (index < 0 || index >= length) { 19 | return undefined; 20 | } 21 | 22 | // Use Array.prototype.slice method to get value at the specified index 23 | return Array.prototype.slice.call(this, index, index + 1)[0]; 24 | }; 25 | } 26 | 27 | export {}; 28 | -------------------------------------------------------------------------------- /app/store/access.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiPath, 3 | DEFAULT_API_HOST, 4 | ServiceProvider, 5 | StoreKey, 6 | } from "../constant"; 7 | import { getHeaders } from "../client/api"; 8 | import { getClientConfig } from "../config/client"; 9 | import { createPersistStore } from "../utils/store"; 10 | import { ensure } from "../utils/clone"; 11 | 12 | let fetchState = 0; // 0 not fetch, 1 fetching, 2 done 13 | 14 | const DEFAULT_OPENAI_URL = 15 | getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI; 16 | 17 | const DEFAULT_ACCESS_STATE = { 18 | accessCode: "", 19 | useCustomConfig: false, 20 | 21 | provider: ServiceProvider.OpenAI, 22 | 23 | // openai 24 | openaiUrl: DEFAULT_OPENAI_URL, 25 | openaiApiKey: "", 26 | 27 | // azure 28 | azureUrl: "", 29 | azureApiKey: "", 30 | azureApiVersion: "2023-08-01-preview", 31 | 32 | // server config 33 | needCode: true, 34 | hideUserApiKey: false, 35 | hideBalanceQuery: false, 36 | disableGPT4: false, 37 | disableFastLink: false, 38 | customModels: "", 39 | }; 40 | 41 | export const useAccessStore = createPersistStore( 42 | { ...DEFAULT_ACCESS_STATE }, 43 | 44 | (set, get) => ({ 45 | enabledAccessControl() { 46 | this.fetch(); 47 | 48 | return get().needCode; 49 | }, 50 | 51 | isValidOpenAI() { 52 | return ensure(get(), ["openaiApiKey"]); 53 | }, 54 | 55 | isValidAzure() { 56 | return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]); 57 | }, 58 | 59 | isAuthorized() { 60 | this.fetch(); 61 | 62 | // has token or has code or disabled access control 63 | return ( 64 | this.isValidOpenAI() || 65 | this.isValidAzure() || 66 | !this.enabledAccessControl() || 67 | (this.enabledAccessControl() && ensure(get(), ["accessCode"])) 68 | ); 69 | }, 70 | fetch() { 71 | if (fetchState > 0 || getClientConfig()?.buildMode === "export") return; 72 | fetchState = 1; 73 | fetch("/api/config", { 74 | method: "post", 75 | body: null, 76 | headers: { 77 | ...getHeaders(), 78 | }, 79 | }) 80 | .then((res) => res.json()) 81 | .then((res: DangerConfig) => { 82 | console.log("[Config] got config from server", res); 83 | set(() => ({ ...res })); 84 | }) 85 | .catch(() => { 86 | console.error("[Config] failed to fetch config"); 87 | }) 88 | .finally(() => { 89 | fetchState = 2; 90 | }); 91 | }, 92 | }), 93 | { 94 | name: StoreKey.Access, 95 | version: 2, 96 | migrate(persistedState, version) { 97 | if (version < 2) { 98 | const state = persistedState as { 99 | token: string; 100 | openaiApiKey: string; 101 | azureApiVersion: string; 102 | }; 103 | state.openaiApiKey = state.token; 104 | state.azureApiVersion = "2023-08-01-preview"; 105 | } 106 | 107 | return persistedState as any; 108 | }, 109 | }, 110 | ); 111 | -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | export * from "./config"; 5 | -------------------------------------------------------------------------------- /app/store/mask.ts: -------------------------------------------------------------------------------- 1 | import { BUILTIN_MASKS } from "../masks"; 2 | import { getLang, Lang } from "../locales"; 3 | import { DEFAULT_TOPIC, ChatMessage } from "./chat"; 4 | import { ModelConfig, useAppConfig } from "./config"; 5 | import { StoreKey } from "../constant"; 6 | import { nanoid } from "nanoid"; 7 | import { createPersistStore } from "../utils/store"; 8 | 9 | export type Mask = { 10 | id: string; 11 | createdAt: number; 12 | avatar: string; 13 | name: string; 14 | hideContext?: boolean; 15 | context: ChatMessage[]; 16 | syncGlobalConfig?: boolean; 17 | modelConfig: ModelConfig; 18 | lang: Lang; 19 | builtin: boolean; 20 | }; 21 | 22 | export const DEFAULT_MASK_STATE = { 23 | masks: {} as Record, 24 | }; 25 | 26 | export type MaskState = typeof DEFAULT_MASK_STATE; 27 | 28 | export const DEFAULT_MASK_AVATAR = "gpt-bot"; 29 | export const createEmptyMask = () => 30 | ({ 31 | id: nanoid(), 32 | avatar: DEFAULT_MASK_AVATAR, 33 | name: DEFAULT_TOPIC, 34 | context: [], 35 | syncGlobalConfig: true, // use global config as default 36 | modelConfig: { ...useAppConfig.getState().modelConfig }, 37 | lang: getLang(), 38 | builtin: false, 39 | createdAt: Date.now(), 40 | }) as Mask; 41 | 42 | export const useMaskStore = createPersistStore( 43 | { ...DEFAULT_MASK_STATE }, 44 | 45 | (set, get) => ({ 46 | create(mask?: Partial) { 47 | const masks = get().masks; 48 | const id = nanoid(); 49 | masks[id] = { 50 | ...createEmptyMask(), 51 | ...mask, 52 | id, 53 | builtin: false, 54 | }; 55 | 56 | set(() => ({ masks })); 57 | get().markUpdate(); 58 | 59 | return masks[id]; 60 | }, 61 | updateMask(id: string, updater: (mask: Mask) => void) { 62 | const masks = get().masks; 63 | const mask = masks[id]; 64 | if (!mask) return; 65 | const updateMask = { ...mask }; 66 | updater(updateMask); 67 | masks[id] = updateMask; 68 | set(() => ({ masks })); 69 | get().markUpdate(); 70 | }, 71 | delete(id: string) { 72 | const masks = get().masks; 73 | delete masks[id]; 74 | set(() => ({ masks })); 75 | get().markUpdate(); 76 | }, 77 | 78 | get(id?: string) { 79 | return get().masks[id ?? 1145141919810]; 80 | }, 81 | getAll() { 82 | const userMasks = Object.values(get().masks).sort( 83 | (a, b) => b.createdAt - a.createdAt, 84 | ); 85 | const config = useAppConfig.getState(); 86 | if (config.hideBuiltinMasks) return userMasks; 87 | const buildinMasks = BUILTIN_MASKS.map( 88 | (m) => 89 | ({ 90 | ...m, 91 | modelConfig: { 92 | ...config.modelConfig, 93 | ...m.modelConfig, 94 | }, 95 | }) as Mask, 96 | ); 97 | return userMasks.concat(buildinMasks); 98 | }, 99 | search(text: string) { 100 | return Object.values(get().masks); 101 | }, 102 | }), 103 | { 104 | name: StoreKey.Mask, 105 | version: 3.1, 106 | 107 | migrate(state, version) { 108 | const newState = JSON.parse(JSON.stringify(state)) as MaskState; 109 | 110 | // migrate mask id to nanoid 111 | if (version < 3) { 112 | Object.values(newState.masks).forEach((m) => (m.id = nanoid())); 113 | } 114 | 115 | if (version < 3.1) { 116 | const updatedMasks: Record = {}; 117 | Object.values(newState.masks).forEach((m) => { 118 | updatedMasks[m.id] = m; 119 | }); 120 | newState.masks = updatedMasks; 121 | } 122 | 123 | return newState as any; 124 | }, 125 | }, 126 | ); 127 | -------------------------------------------------------------------------------- /app/store/sync.ts: -------------------------------------------------------------------------------- 1 | import { getClientConfig } from "../config/client"; 2 | import { Updater } from "../typing"; 3 | import { ApiPath, STORAGE_KEY, StoreKey } from "../constant"; 4 | import { createPersistStore } from "../utils/store"; 5 | import { 6 | AppState, 7 | getLocalAppState, 8 | GetStoreState, 9 | mergeAppState, 10 | setLocalAppState, 11 | } from "../utils/sync"; 12 | import { downloadAs, readFromFile } from "../utils"; 13 | import { showToast } from "../components/ui-lib"; 14 | import Locale from "../locales"; 15 | import { createSyncClient, ProviderType } from "../utils/cloud"; 16 | import { corsPath } from "../utils/cors"; 17 | 18 | export interface WebDavConfig { 19 | server: string; 20 | username: string; 21 | password: string; 22 | } 23 | 24 | const isApp = !!getClientConfig()?.isApp; 25 | export type SyncStore = GetStoreState; 26 | 27 | const DEFAULT_SYNC_STATE = { 28 | provider: ProviderType.WebDAV, 29 | useProxy: true, 30 | proxyUrl: corsPath(ApiPath.Cors), 31 | 32 | webdav: { 33 | endpoint: "", 34 | username: "", 35 | password: "", 36 | }, 37 | 38 | upstash: { 39 | endpoint: "", 40 | username: STORAGE_KEY, 41 | apiKey: "", 42 | }, 43 | 44 | lastSyncTime: 0, 45 | lastProvider: "", 46 | }; 47 | 48 | export const useSyncStore = createPersistStore( 49 | DEFAULT_SYNC_STATE, 50 | (set, get) => ({ 51 | coundSync() { 52 | const config = get()[get().provider]; 53 | return Object.values(config).every((c) => c.toString().length > 0); 54 | }, 55 | 56 | markSyncTime() { 57 | set({ lastSyncTime: Date.now(), lastProvider: get().provider }); 58 | }, 59 | 60 | export() { 61 | const state = getLocalAppState(); 62 | const datePart = isApp 63 | ? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}` 64 | : new Date().toLocaleString(); 65 | 66 | const fileName = `Backup-${datePart}.json`; 67 | downloadAs(JSON.stringify(state), fileName); 68 | }, 69 | 70 | async import() { 71 | const rawContent = await readFromFile(); 72 | 73 | try { 74 | const remoteState = JSON.parse(rawContent) as AppState; 75 | const localState = getLocalAppState(); 76 | mergeAppState(localState, remoteState); 77 | setLocalAppState(localState); 78 | location.reload(); 79 | } catch (e) { 80 | console.error("[Import]", e); 81 | showToast(Locale.Settings.Sync.ImportFailed); 82 | } 83 | }, 84 | 85 | getClient() { 86 | const provider = get().provider; 87 | const client = createSyncClient(provider, get()); 88 | return client; 89 | }, 90 | 91 | async sync() { 92 | const localState = getLocalAppState(); 93 | const provider = get().provider; 94 | const config = get()[provider]; 95 | const client = this.getClient(); 96 | 97 | try { 98 | const remoteState = JSON.parse( 99 | await client.get(config.username), 100 | ) as AppState; 101 | mergeAppState(localState, remoteState); 102 | setLocalAppState(localState); 103 | } catch (e) { 104 | console.log("[Sync] failed to get remote state", e); 105 | } 106 | 107 | await client.set(config.username, JSON.stringify(localState)); 108 | 109 | this.markSyncTime(); 110 | }, 111 | 112 | async check() { 113 | const client = this.getClient(); 114 | return await client.check(); 115 | }, 116 | }), 117 | { 118 | name: StoreKey.Sync, 119 | version: 1.1, 120 | 121 | migrate(persistedState, version) { 122 | const newState = persistedState as typeof DEFAULT_SYNC_STATE; 123 | 124 | if (version < 1.1) { 125 | newState.upstash.username = STORAGE_KEY; 126 | } 127 | 128 | return newState as any; 129 | }, 130 | }, 131 | ); 132 | -------------------------------------------------------------------------------- /app/styles/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes slide-in { 2 | from { 3 | opacity: 0; 4 | transform: translateY(20px); 5 | } 6 | 7 | to { 8 | opacity: 1; 9 | transform: translateY(0px); 10 | } 11 | } 12 | 13 | @keyframes slide-in-from-top { 14 | from { 15 | opacity: 0; 16 | transform: translateY(-20px); 17 | } 18 | 19 | to { 20 | opacity: 1; 21 | transform: translateY(0px); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/styles/highlight.scss: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | pre { 3 | padding: 0; 4 | } 5 | 6 | pre, 7 | code { 8 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 9 | } 10 | 11 | pre code { 12 | display: block; 13 | overflow-x: auto; 14 | padding: 1em; 15 | } 16 | 17 | code { 18 | padding: 3px 5px; 19 | } 20 | 21 | .hljs, 22 | pre { 23 | background: #1a1b26; 24 | color: #cbd2ea; 25 | } 26 | 27 | /*! 28 | Theme: Tokyo-night-Dark 29 | origin: https://github.com/enkia/tokyo-night-vscode-theme 30 | Description: Original highlight.js style 31 | Author: (c) Henri Vandersleyen 32 | License: see project LICENSE 33 | Touched: 2022 34 | */ 35 | .hljs-comment, 36 | .hljs-meta { 37 | color: #565f89; 38 | } 39 | 40 | .hljs-deletion, 41 | .hljs-doctag, 42 | .hljs-regexp, 43 | .hljs-selector-attr, 44 | .hljs-selector-class, 45 | .hljs-selector-id, 46 | .hljs-selector-pseudo, 47 | .hljs-tag, 48 | .hljs-template-tag, 49 | .hljs-variable.language_ { 50 | color: #f7768e; 51 | } 52 | 53 | .hljs-link, 54 | .hljs-literal, 55 | .hljs-number, 56 | .hljs-params, 57 | .hljs-template-variable, 58 | .hljs-type, 59 | .hljs-variable { 60 | color: #ff9e64; 61 | } 62 | 63 | .hljs-attribute, 64 | .hljs-built_in { 65 | color: #e0af68; 66 | } 67 | 68 | .hljs-keyword, 69 | .hljs-property, 70 | .hljs-subst, 71 | .hljs-title, 72 | .hljs-title.class_, 73 | .hljs-title.class_.inherited__, 74 | .hljs-title.function_ { 75 | color: #7dcfff; 76 | } 77 | 78 | .hljs-selector-tag { 79 | color: #73daca; 80 | } 81 | 82 | .hljs-addition, 83 | .hljs-bullet, 84 | .hljs-quote, 85 | .hljs-string, 86 | .hljs-symbol { 87 | color: #9ece6a; 88 | } 89 | 90 | .hljs-code, 91 | .hljs-formula, 92 | .hljs-section { 93 | color: #7aa2f7; 94 | } 95 | 96 | .hljs-attr, 97 | .hljs-char.escape_, 98 | .hljs-keyword, 99 | .hljs-name, 100 | .hljs-operator { 101 | color: #bb9af7; 102 | } 103 | 104 | .hljs-punctuation { 105 | color: #c0caf5; 106 | } 107 | 108 | .hljs-emphasis { 109 | font-style: italic; 110 | } 111 | 112 | .hljs-strong { 113 | font-weight: 700; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/styles/window.scss: -------------------------------------------------------------------------------- 1 | .window-header { 2 | padding: 14px 20px; 3 | border-bottom: rgba(0, 0, 0, 0.1) 1px solid; 4 | position: relative; 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .window-header-title { 12 | max-width: calc(100% - 100px); 13 | overflow: hidden; 14 | 15 | .window-header-main-title { 16 | font-size: 20px; 17 | font-weight: bolder; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | display: block; 22 | max-width: 50vw; 23 | } 24 | 25 | .window-header-sub-title { 26 | font-size: 14px; 27 | } 28 | } 29 | 30 | .window-actions { 31 | display: inline-flex; 32 | } 33 | 34 | .window-action-button:not(:first-child) { 35 | margin-left: 10px; 36 | } 37 | -------------------------------------------------------------------------------- /app/typing.ts: -------------------------------------------------------------------------------- 1 | export type Updater = (updater: (value: T) => void) => void; 2 | -------------------------------------------------------------------------------- /app/utils/clone.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(obj: T) { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | 5 | export function ensure( 6 | obj: T, 7 | keys: Array<[keyof T][number]>, 8 | ) { 9 | return keys.every( 10 | (k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "", 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/utils/cloud/index.ts: -------------------------------------------------------------------------------- 1 | import { createWebDavClient } from "./webdav"; 2 | import { createUpstashClient } from "./upstash"; 3 | 4 | export enum ProviderType { 5 | WebDAV = "webdav", 6 | UpStash = "upstash", 7 | } 8 | 9 | export const SyncClients = { 10 | [ProviderType.UpStash]: createUpstashClient, 11 | [ProviderType.WebDAV]: createWebDavClient, 12 | } as const; 13 | 14 | type SyncClientConfig = { 15 | [K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends ( 16 | _: infer C, 17 | ) => any 18 | ? C 19 | : never; 20 | }; 21 | 22 | export type SyncClient = { 23 | get: (key: string) => Promise; 24 | set: (key: string, value: string) => Promise; 25 | check: () => Promise; 26 | }; 27 | 28 | export function createSyncClient( 29 | provider: T, 30 | config: SyncClientConfig[T], 31 | ): SyncClient { 32 | return SyncClients[provider](config as any) as any; 33 | } 34 | -------------------------------------------------------------------------------- /app/utils/cloud/upstash.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEY } from "@/app/constant"; 2 | import { SyncStore } from "@/app/store/sync"; 3 | import { corsFetch } from "../cors"; 4 | import { chunks } from "../format"; 5 | 6 | export type UpstashConfig = SyncStore["upstash"]; 7 | export type UpStashClient = ReturnType; 8 | 9 | export function createUpstashClient(store: SyncStore) { 10 | const config = store.upstash; 11 | const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username; 12 | const chunkCountKey = `${storeKey}-chunk-count`; 13 | const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`; 14 | 15 | const proxyUrl = 16 | store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; 17 | 18 | return { 19 | async check() { 20 | try { 21 | const res = await corsFetch(this.path(`get/${storeKey}`), { 22 | method: "GET", 23 | headers: this.headers(), 24 | proxyUrl, 25 | }); 26 | console.log("[Upstash] check", res.status, res.statusText); 27 | return [200].includes(res.status); 28 | } catch (e) { 29 | console.error("[Upstash] failed to check", e); 30 | } 31 | return false; 32 | }, 33 | 34 | async redisGet(key: string) { 35 | const res = await corsFetch(this.path(`get/${key}`), { 36 | method: "GET", 37 | headers: this.headers(), 38 | proxyUrl, 39 | }); 40 | 41 | console.log("[Upstash] get key = ", key, res.status, res.statusText); 42 | const resJson = (await res.json()) as { result: string }; 43 | 44 | return resJson.result; 45 | }, 46 | 47 | async redisSet(key: string, value: string) { 48 | const res = await corsFetch(this.path(`set/${key}`), { 49 | method: "POST", 50 | headers: this.headers(), 51 | body: value, 52 | proxyUrl, 53 | }); 54 | 55 | console.log("[Upstash] set key = ", key, res.status, res.statusText); 56 | }, 57 | 58 | async get() { 59 | const chunkCount = Number(await this.redisGet(chunkCountKey)); 60 | if (!Number.isInteger(chunkCount)) return; 61 | 62 | const chunks = await Promise.all( 63 | new Array(chunkCount) 64 | .fill(0) 65 | .map((_, i) => this.redisGet(chunkIndexKey(i))), 66 | ); 67 | console.log("[Upstash] get full chunks", chunks); 68 | return chunks.join(""); 69 | }, 70 | 71 | async set(_: string, value: string) { 72 | // upstash limit the max request size which is 1Mb for “Free” and “Pay as you go” 73 | // so we need to split the data to chunks 74 | let index = 0; 75 | for await (const chunk of chunks(value)) { 76 | await this.redisSet(chunkIndexKey(index), chunk); 77 | index += 1; 78 | } 79 | await this.redisSet(chunkCountKey, index.toString()); 80 | }, 81 | 82 | headers() { 83 | return { 84 | Authorization: `Bearer ${config.apiKey}`, 85 | }; 86 | }, 87 | path(path: string) { 88 | let url = config.endpoint; 89 | 90 | if (!url.endsWith("/")) { 91 | url += "/"; 92 | } 93 | 94 | if (path.startsWith("/")) { 95 | path = path.slice(1); 96 | } 97 | 98 | return url + path; 99 | }, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /app/utils/cloud/webdav.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEY } from "@/app/constant"; 2 | import { SyncStore } from "@/app/store/sync"; 3 | import { corsFetch } from "../cors"; 4 | 5 | export type WebDAVConfig = SyncStore["webdav"]; 6 | export type WebDavClient = ReturnType; 7 | 8 | export function createWebDavClient(store: SyncStore) { 9 | const folder = STORAGE_KEY; 10 | const fileName = `${folder}/backup.json`; 11 | const config = store.webdav; 12 | const proxyUrl = 13 | store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; 14 | 15 | return { 16 | async check() { 17 | try { 18 | const res = await corsFetch(this.path(folder), { 19 | method: "MKCOL", 20 | headers: this.headers(), 21 | proxyUrl, 22 | }); 23 | console.log("[WebDav] check", res.status, res.statusText); 24 | return [201, 200, 404, 301, 302, 307, 308].includes(res.status); 25 | } catch (e) { 26 | console.error("[WebDav] failed to check", e); 27 | } 28 | 29 | return false; 30 | }, 31 | 32 | async get(key: string) { 33 | const res = await corsFetch(this.path(fileName), { 34 | method: "GET", 35 | headers: this.headers(), 36 | proxyUrl, 37 | }); 38 | 39 | console.log("[WebDav] get key = ", key, res.status, res.statusText); 40 | 41 | return await res.text(); 42 | }, 43 | 44 | async set(key: string, value: string) { 45 | const res = await corsFetch(this.path(fileName), { 46 | method: "PUT", 47 | headers: this.headers(), 48 | body: value, 49 | proxyUrl, 50 | }); 51 | 52 | console.log("[WebDav] set key = ", key, res.status, res.statusText); 53 | }, 54 | 55 | headers() { 56 | const auth = btoa(config.username + ":" + config.password); 57 | 58 | return { 59 | authorization: `Basic ${auth}`, 60 | }; 61 | }, 62 | path(path: string) { 63 | let url = config.endpoint; 64 | 65 | if (!url.endsWith("/")) { 66 | url += "/"; 67 | } 68 | 69 | if (path.startsWith("/")) { 70 | path = path.slice(1); 71 | } 72 | 73 | return url + path; 74 | }, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /app/utils/cors.ts: -------------------------------------------------------------------------------- 1 | import { getClientConfig } from "../config/client"; 2 | import { ApiPath, DEFAULT_CORS_HOST } from "../constant"; 3 | 4 | export function corsPath(path: string) { 5 | const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : ""; 6 | 7 | if (!path.startsWith("/")) { 8 | path = "/" + path; 9 | } 10 | 11 | if (!path.endsWith("/")) { 12 | path += "/"; 13 | } 14 | 15 | return `${baseUrl}${path}`; 16 | } 17 | 18 | export function corsFetch( 19 | url: string, 20 | options: RequestInit & { 21 | proxyUrl?: string; 22 | }, 23 | ) { 24 | if (!url.startsWith("http")) { 25 | throw Error("[CORS Fetch] url must starts with http/https"); 26 | } 27 | 28 | let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors); 29 | if (!proxyUrl.endsWith("/")) { 30 | proxyUrl += "/"; 31 | } 32 | 33 | url = url.replace("://", "/"); 34 | 35 | const corsOptions = { 36 | ...options, 37 | method: "POST", 38 | headers: options.method 39 | ? { 40 | ...options.headers, 41 | method: options.method, 42 | } 43 | : options.headers, 44 | }; 45 | 46 | const corsUrl = proxyUrl + url; 47 | console.info("[CORS] target = ", corsUrl); 48 | 49 | return fetch(corsUrl, corsOptions); 50 | } 51 | -------------------------------------------------------------------------------- /app/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function prettyObject(msg: any) { 2 | const obj = msg; 3 | if (typeof msg !== "string") { 4 | msg = JSON.stringify(msg, null, " "); 5 | } 6 | if (msg === "{}") { 7 | return obj.toString(); 8 | } 9 | if (msg.startsWith("```json")) { 10 | return msg; 11 | } 12 | return ["```json", msg, "```"].join("\n"); 13 | } 14 | 15 | export function* chunks(s: string, maxBytes = 1000 * 1000) { 16 | const decoder = new TextDecoder("utf-8"); 17 | let buf = new TextEncoder().encode(s); 18 | while (buf.length) { 19 | let i = buf.lastIndexOf(32, maxBytes + 1); 20 | // If no space found, try forward search 21 | if (i < 0) i = buf.indexOf(32, maxBytes); 22 | // If there's no space at all, take all 23 | if (i < 0) i = buf.length; 24 | // This is a safe cut-off point; never half-way a multi-byte 25 | yield decoder.decode(buf.slice(0, i)); 26 | buf = buf.slice(i + 1); // Skip space (if any) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useAccessStore, useAppConfig } from "../store"; 3 | import { collectModels } from "./model"; 4 | 5 | export function useAllModels() { 6 | const accessStore = useAccessStore(); 7 | const configStore = useAppConfig(); 8 | const models = useMemo(() => { 9 | return collectModels( 10 | configStore.models, 11 | [configStore.customModels, accessStore.customModels].join(","), 12 | ); 13 | }, [accessStore.customModels, configStore.customModels, configStore.models]); 14 | 15 | return models; 16 | } 17 | -------------------------------------------------------------------------------- /app/utils/merge.ts: -------------------------------------------------------------------------------- 1 | export function merge(target: any, source: any) { 2 | Object.keys(source).forEach(function (key) { 3 | if ( 4 | source.hasOwnProperty(key) && // Check if the property is not inherited 5 | source[key] && 6 | typeof source[key] === "object" || key === "__proto__" || key === "constructor" 7 | ) { 8 | merge((target[key] = target[key] || {}), source[key]); 9 | return; 10 | } 11 | target[key] = source[key]; 12 | }); 13 | } -------------------------------------------------------------------------------- /app/utils/model.ts: -------------------------------------------------------------------------------- 1 | import { LLMModel } from "../client/api"; 2 | 3 | export function collectModelTable( 4 | models: readonly LLMModel[], 5 | customModels: string, 6 | ) { 7 | const modelTable: Record< 8 | string, 9 | { available: boolean; name: string; displayName: string } 10 | > = {}; 11 | 12 | // default models 13 | models.forEach( 14 | (m) => 15 | (modelTable[m.name] = { 16 | ...m, 17 | displayName: m.name, 18 | }), 19 | ); 20 | 21 | // server custom models 22 | customModels 23 | .split(",") 24 | .filter((v) => !!v && v.length > 0) 25 | .map((m) => { 26 | const available = !m.startsWith("-"); 27 | const nameConfig = 28 | m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; 29 | const [name, displayName] = nameConfig.split(":"); 30 | modelTable[name] = { 31 | name, 32 | displayName: displayName || name, 33 | available, 34 | }; 35 | }); 36 | return modelTable; 37 | } 38 | 39 | /** 40 | * Generate full model table. 41 | */ 42 | export function collectModels( 43 | models: readonly LLMModel[], 44 | customModels: string, 45 | ) { 46 | const modelTable = collectModelTable(models, customModels); 47 | const allModels = Object.values(modelTable); 48 | 49 | return allModels; 50 | } 51 | -------------------------------------------------------------------------------- /app/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { combine, persist } from "zustand/middleware"; 3 | import { Updater } from "../typing"; 4 | import { deepClone } from "./clone"; 5 | 6 | type SecondParam = T extends ( 7 | _f: infer _F, 8 | _s: infer S, 9 | ...args: infer _U 10 | ) => any 11 | ? S 12 | : never; 13 | 14 | type MakeUpdater = { 15 | lastUpdateTime: number; 16 | 17 | markUpdate: () => void; 18 | update: Updater; 19 | }; 20 | 21 | type SetStoreState = ( 22 | partial: T | Partial | ((state: T) => T | Partial), 23 | replace?: boolean | undefined, 24 | ) => void; 25 | 26 | export function createPersistStore( 27 | state: T, 28 | methods: ( 29 | set: SetStoreState>, 30 | get: () => T & MakeUpdater, 31 | ) => M, 32 | persistOptions: SecondParam>>, 33 | ) { 34 | return create( 35 | persist( 36 | combine( 37 | { 38 | ...state, 39 | lastUpdateTime: 0, 40 | }, 41 | (set, get) => { 42 | return { 43 | ...methods(set, get as any), 44 | 45 | markUpdate() { 46 | set({ lastUpdateTime: Date.now() } as Partial< 47 | T & M & MakeUpdater 48 | >); 49 | }, 50 | update(updater) { 51 | const state = deepClone(get()); 52 | updater(state); 53 | set({ 54 | ...state, 55 | lastUpdateTime: Date.now(), 56 | }); 57 | }, 58 | } as M & MakeUpdater; 59 | }, 60 | ), 61 | persistOptions as any, 62 | ), 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /app/utils/token.ts: -------------------------------------------------------------------------------- 1 | export function estimateTokenLength(input: string): number { 2 | let tokenLength = 0; 3 | 4 | for (let i = 0; i < input.length; i++) { 5 | const charCode = input.charCodeAt(i); 6 | 7 | if (charCode < 128) { 8 | // ASCII character 9 | if (charCode <= 122 && charCode >= 65) { 10 | // a-Z 11 | tokenLength += 0.25; 12 | } else { 13 | tokenLength += 0.5; 14 | } 15 | } else { 16 | // Unicode character 17 | tokenLength += 1.5; 18 | } 19 | } 20 | 21 | return tokenLength; 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | chatgpt-next-web: 4 | profiles: ["no-proxy"] 5 | container_name: chatgpt-next-web 6 | image: yidadaa/chatgpt-next-web 7 | ports: 8 | - 3000:3000 9 | environment: 10 | - OPENAI_API_KEY=$OPENAI_API_KEY 11 | - CODE=$CODE 12 | - BASE_URL=$BASE_URL 13 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 14 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 15 | - DISABLE_GPT4=$DISABLE_GPT4 16 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 17 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 18 | - OPENAI_SB=$OPENAI_SB 19 | 20 | chatgpt-next-web-proxy: 21 | profiles: ["proxy"] 22 | container_name: chatgpt-next-web-proxy 23 | image: yidadaa/chatgpt-next-web 24 | ports: 25 | - 3000:3000 26 | environment: 27 | - OPENAI_API_KEY=$OPENAI_API_KEY 28 | - CODE=$CODE 29 | - PROXY_URL=$PROXY_URL 30 | - BASE_URL=$BASE_URL 31 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 32 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 33 | - DISABLE_GPT4=$DISABLE_GPT4 34 | - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY 35 | - DISABLE_FAST_LINK=$DISABLE_FAST_LINK 36 | - OPENAI_SB=$OPENAI_SB 37 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-cn.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 部署指南 2 | 3 | ## 如何新建项目 4 | 5 | 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 6 | 7 | 1. 点击 "Create a project"。 8 | 2. 选择 "Connect to Git"。 9 | 3. 关联 Cloudflare Pages 和你的 GitHub 账号。 10 | 4. 选中你 fork 的此项目。 11 | 5. 点击 "Begin setup"。 12 | 6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。 13 | 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 14 | 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: 15 | ``` 16 | npx @cloudflare/next-on-pages@1.5.0 17 | ``` 18 | 9. 对于 "Build output directory",使用默认值并且不要修改。 19 | 10. 不要修改 "Root Directory"。 20 | 11. 对于 "Environment variables",点击 ">" 然后点击 "Add variable"。按照以下信息填写: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=你自己的API Key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | 根据实际需要,可以选择填写以下选项: 29 | 30 | - `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 31 | - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 32 | - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 33 | - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能` 35 | - `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置` 36 | 37 | 12. 点击 "Save and Deploy"。 38 | 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 39 | 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 40 | 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 41 | 16. 前往 "Deployments",点击 "Retry deployment"。 42 | 17. Enjoy. 43 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-en.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages Deployment Guide 2 | 3 | ## How to create a new project 4 | 5 | Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 6 | 7 | 1. Click "Create a project". 8 | 2. Choose "Connect to Git". 9 | 3. Connect Cloudflare Pages to your GitHub account. 10 | 4. Select the forked project. 11 | 5. Click "Begin setup". 12 | 6. For "Project name" and "Production branch", use the default values or change them as needed. 13 | 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 14 | 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: 15 | ``` 16 | npx @cloudflare/next-on-pages --experimental-minify 17 | ``` 18 | 9. For "Build output directory", use the default value and do not modify it. 19 | 10. Do not modify "Root Directory". 20 | 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: 21 | 22 | - `NODE_VERSION=20.1` 23 | - `NEXT_TELEMETRY_DISABLE=1` 24 | - `OPENAI_API_KEY=your_own_API_key` 25 | - `YARN_VERSION=1.22.19` 26 | - `PHP_VERSION=7.4` 27 | 28 | Optionally fill in the following based on your needs: 29 | 30 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 31 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 32 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 33 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 34 | - `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance` 35 | - `DISABLE_FAST_LINK=1 Optional, disable parse settings from url` 36 | - `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API` 37 | 38 | 12. Click "Save and Deploy". 39 | 13. Click "Cancel deployment" because you need to fill in Compatibility flags. 40 | 14. Go to "Build settings", "Functions", and find "Compatibility flags". 41 | 15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". 42 | 16. Go to "Deployments" and click "Retry deployment". 43 | 17. Enjoy. 44 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-es.md: -------------------------------------------------------------------------------- 1 | # Guía de implementación de Cloudflare Pages 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Bifurca el proyecto en Github, luego inicia sesión en dash.cloudflare.com y ve a Pages. 6 | 7 | 1. Haga clic en "Crear un proyecto". 8 | 2. Selecciona Conectar a Git. 9 | 3. Vincula páginas de Cloudflare a tu cuenta de GitHub. 10 | 4. Seleccione este proyecto que bifurcó. 11 | 5. Haga clic en "Comenzar configuración". 12 | 6. Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario. 13 | 7. En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js. 14 | 8. Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando: 15 | npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify 16 | 9. Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique. 17 | 10. No modifique el "Directorio raíz". 18 | 11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información: 19 | 20 | * `NODE_VERSION=20.1` 21 | * `NEXT_TELEMETRY_DISABLE=1` 22 | * `OPENAI_API_KEY=你自己的API Key` 23 | * `YARN_VERSION=1.22.19` 24 | * `PHP_VERSION=7.4` 25 | 26 | Dependiendo de sus necesidades reales, puede completar opcionalmente las siguientes opciones: 27 | 28 | * `CODE= 可选填,访问密码,可以使用逗号隔开多个密码` 29 | * `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` 30 | * `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` 31 | * `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 32 | 12. Haga clic en "Guardar e implementar". 33 | 13. Haga clic en "Cancelar implementación" porque necesita rellenar los indicadores de compatibilidad. 34 | 14. Vaya a "Configuración de compilación", "Funciones" y busque "Indicadores de compatibilidad". 35 | 15. Rellene "nodejs_compat" en "Configurar indicador de compatibilidad de producción" y "Configurar indicador de compatibilidad de vista previa". 36 | 16. Vaya a "Implementaciones" y haga clic en "Reintentar implementación". 37 | 17. Disfrutar. 38 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ja.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 導入ガイド 2 | 3 | ## 新規プロジェクトの作成方法 4 | GitHub でこのプロジェクトをフォークし、dash.cloudflare.com にログインして Pages にアクセスします。 5 | 6 | 1. "Create a project" をクリックする。 7 | 2. "Connect to Git" を選択する。 8 | 3. Cloudflare Pages を GitHub アカウントに接続します。 9 | 4. フォークしたプロジェクトを選択します。 10 | 5. "Begin setup" をクリックする。 11 | 6. "Project name" と "Production branch" はデフォルト値を使用するか、必要に応じて変更してください。 12 | 7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 13 | 8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: 14 | ``` 15 | npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify 16 | ``` 17 | 9. "Build output directory" はデフォルト値を使用し、変更しない。 18 | 10. "Root Directory" を変更しない。 19 | 11. "Environment variables" は、">" をクリックし、"Add variable" をクリックします。そして以下の情報を入力します: 20 | - `NODE_VERSION=20.1` 21 | - `NEXT_TELEMETRY_DISABLE=1` 22 | - `OPENAI_API_KEY=your_own_API_key` 23 | - `YARN_VERSION=1.22.19` 24 | - `PHP_VERSION=7.4` 25 | 26 | 必要に応じて、以下の項目を入力してください: 27 | 28 | - `CODE= Optional, access passwords, multiple passwords can be separated by commas` 29 | - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` 30 | - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` 31 | - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` 32 | 33 | 12. "Save and Deploy" をクリックする。 34 | 13. 互換性フラグを記入する必要があるため、"Cancel deployment" をクリックする。 35 | 14. "Build settings" の "Functions" から "Compatibility flags" を見つける。 36 | 15. "Configure Production compatibility flag" と "Configure Preview compatibility flag" の両方に "nodejs_compat "を記入する。 37 | 16. "Deployments" に移動し、"Retry deployment" をクリックします。 38 | 17. お楽しみください。 39 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-ko.md: -------------------------------------------------------------------------------- 1 | ## Cloudflare 페이지 배포 가이드 2 | 3 | ## 새 프로젝트를 만드는 방법 4 | 이 프로젝트를 Github에서 포크한 다음 dash.cloudflare.com에 로그인하고 페이지로 이동합니다. 5 | 6 | 1. "프로젝트 만들기"를 클릭합니다. 7 | 2. "Git에 연결"을 선택합니다. 8 | 3. Cloudflare 페이지를 GitHub 계정과 연결합니다. 9 | 4. 포크한 프로젝트를 선택합니다. 10 | 5. "설정 시작"을 클릭합니다. 11 | 6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. 12 | 7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. 13 | 8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: 14 | `` 15 | npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental- minify 16 | ``` 17 | 9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. 18 | 10. "루트 디렉토리"는 수정하지 마십시오. 19 | 11. "환경 변수"의 경우 ">"를 클릭한 다음 "변수 추가"를 클릭합니다. 다음에 따라 정보를 입력합니다: 20 | 21 | - node_version=20.1`. 22 | - next_telemetry_disable=1`. 23 | - `OPENAI_API_KEY=자신의 API 키` 24 | - ``yarn_version=1.22.19`` 25 | - ``php_version=7.4``. 26 | 27 | 실제 필요에 따라 다음 옵션을 선택적으로 입력합니다: 28 | 29 | - `CODE= 선택적으로 액세스 비밀번호를 입력하며 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다`. 30 | - `OPENAI_ORG_ID= 선택 사항, OpenAI에서 조직 ID 지정` 31 | - `HIDE_USER_API_KEY=1 선택 사항, 사용자가 API 키를 입력하지 못하도록 합니다. 32 | - `DISABLE_GPT4=1 옵션, 사용자가 GPT-4를 사용하지 못하도록 설정` 12. 33 | 34 | 12. "저장 후 배포"를 클릭합니다. 35 | 13. 호환성 플래그를 입력해야 하므로 "배포 취소"를 클릭합니다. 36 | 14. "빌드 설정", "기능"으로 이동하여 "호환성 플래그"를 찾습니다. 37 | "프로덕션 호환성 플래그 구성" 및 "프리뷰 호환성 플래그 구성"에서 "nodejs_compat"를 입력합니다. 38 | 16. "배포"로 이동하여 "배포 다시 시도"를 클릭합니다. 39 | 17. 즐기세요! -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/enable-actions-sync.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/enable-actions-sync.jpg -------------------------------------------------------------------------------- /docs/images/enable-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/enable-actions.jpg -------------------------------------------------------------------------------- /docs/images/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/more.png -------------------------------------------------------------------------------- /docs/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/settings.png -------------------------------------------------------------------------------- /docs/images/upstash-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-1.png -------------------------------------------------------------------------------- /docs/images/upstash-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-2.png -------------------------------------------------------------------------------- /docs/images/upstash-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-3.png -------------------------------------------------------------------------------- /docs/images/upstash-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-4.png -------------------------------------------------------------------------------- /docs/images/upstash-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-5.png -------------------------------------------------------------------------------- /docs/images/upstash-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-6.png -------------------------------------------------------------------------------- /docs/images/upstash-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/upstash-7.png -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/vercel/vercel-create-1.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/vercel/vercel-create-2.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/vercel/vercel-create-3.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-env-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/vercel/vercel-env-edit.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-redeploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/docs/images/vercel/vercel-redeploy.jpg -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-cn.md: -------------------------------------------------------------------------------- 1 | # 同步聊天记录 2 | ## 准备工作 3 | - GitHub账号 4 | - 拥有自己搭建过的ChatGPT-Next-Web的服务器 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 开始教程 8 | 1. 注册UpStash账号 9 | 2. 创建数据库 10 | 11 | ![注册登录](./images/upstash-1.png) 12 | 13 | ![创建数据库](./images/upstash-2.png) 14 | 15 | ![选择服务器](./images/upstash-3.png) 16 | 17 | 3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) 18 | 19 | ![复制](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** 22 | 23 | ![同步1](./images/upstash-5.png) 24 | 25 | 如果没什么问题,那就成功了 26 | 27 | ![同步可用性完成的样子](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![好耶~!](./images/upstash-7.png) 32 | -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-en.md: -------------------------------------------------------------------------------- 1 | # Synchronize Chat Logs with UpStash 2 | ## Prerequisites 3 | - GitHub account 4 | - Your own ChatGPT-Next-Web server set up 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Getting Started 8 | 1. Register for an UpStash account. 9 | 2. Create a database. 10 | 11 | ![Register and Login](./images/upstash-1.png) 12 | 13 | ![Create Database](./images/upstash-2.png) 14 | 15 | ![Select Server](./images/upstash-3.png) 16 | 17 | 3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!) 18 | 19 | ![Copy](./images/upstash-4.png) 20 | 21 | 4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**. 22 | 23 | ![Synchronize 1](./images/upstash-5.png) 24 | 25 | If everything is in order, you've successfully completed this step. 26 | 27 | ![Sync Availability Check Completed](./images/upstash-6.png) 28 | 29 | 5. Success! 30 | 31 | ![Great job~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-es.md: -------------------------------------------------------------------------------- 1 | # Sincronizzare i Log delle Chat con UpStash 2 | ## Prerequisiti 3 | - Account GitHub 4 | - Server ChatGPT-Next-Web di propria configurazione 5 | - [UpStash](https://upstash.com) 6 | 7 | ## Per iniziare 8 | 1. Registrarsi per un account UpStash. 9 | 2. Creare un database. 10 | 11 | ![Registrarsi ed Accedere](./images/upstash-1.png) 12 | 13 | ![Creare un Database](./images/upstash-2.png) 14 | 15 | ![Selezionare il Server](./images/upstash-3.png) 16 | 17 | 3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!) 18 | 19 | ![Copia](./images/upstash-4.png) 20 | 21 | 4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**. 22 | 23 | ![Sincronizzazione 1](./images/upstash-5.png) 24 | 25 | Se tutto è in ordine, hai completato con successo questa fase. 26 | 27 | ![Verifica la Disponibilità della Sincronizzazione Completata](./images/upstash-6.png) 28 | 29 | 5. Successo! 30 | 31 | ![Ottimo lavoro~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ja.md: -------------------------------------------------------------------------------- 1 | # UpStashを使用してチャットログを同期する 2 | ## 事前準備 3 | - GitHubアカウント 4 | - 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 始める 8 | 1. UpStashアカウントを登録します。 9 | 2. データベースを作成します。 10 | 11 | ![登録とログイン](./images/upstash-1.png) 12 | 13 | ![データベースの作成](./images/upstash-2.png) 14 | 15 | ![サーバーの選択](./images/upstash-3.png) 16 | 17 | 3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします(⚠重要⚠:トークンを共有しないでください!) 18 | 19 | ![コピー](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。 22 | 23 | ![同期1](./images/upstash-5.png) 24 | 25 | すべてが正常であれば、このステップは成功です。 26 | 27 | ![同期可用性チェックが完了しました](./images/upstash-6.png) 28 | 29 | 5. 成功! 30 | 31 | ![お疲れ様でした~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /docs/synchronise-chat-logs-ko.md: -------------------------------------------------------------------------------- 1 | # UpStash를 사용하여 채팅 기록 동기화 2 | ## 사전 준비물 3 | - GitHub 계정 4 | - 자체 ChatGPT-Next-Web 서버 설정 5 | - [UpStash](https://upstash.com) 6 | 7 | ## 시작하기 8 | 1. UpStash 계정 등록 9 | 2. 데이터베이스 생성 10 | 11 | ![등록 및 로그인](./images/upstash-1.png) 12 | 13 | ![데이터베이스 생성](./images/upstash-2.png) 14 | 15 | ![서버 선택](./images/upstash-3.png) 16 | 17 | 3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!) 18 | 19 | ![복사](./images/upstash-4.png) 20 | 21 | 4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다. 22 | 23 | ![동기화 1](./images/upstash-5.png) 24 | 25 | 모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다. 26 | 27 | ![동기화 가용성 확인 완료](./images/upstash-6.png) 28 | 29 | 5. 성공! 30 | 31 | ![잘 했어요~!](./images/upstash-7.png) -------------------------------------------------------------------------------- /docs/translation.md: -------------------------------------------------------------------------------- 1 | # How to add a new translation? 2 | 3 | Assume that we are adding a new translation for `new`. 4 | 5 | 1. copy `app/locales/en.ts` to `app/locales/new.ts`; 6 | 2. edit `new.ts`, change `const en: LocaleType = ` to `const new: PartialLocaleType`, and `export default new;`; 7 | 3. edit `app/locales/index.ts`: 8 | 4. `import new from './new.ts'`; 9 | 5. add `new` to `ALL_LANGS`; 10 | 6. add `new: "new lang"` to `ALL_LANG_OPTIONS`; 11 | 7. translate the strings in `new.ts`; 12 | 8. submit a pull request, and the author will merge it. 13 | -------------------------------------------------------------------------------- /docs/user-manual-cn.md: -------------------------------------------------------------------------------- 1 | # 用户手册 User Manual 2 | 3 | > No english version yet, please read this doc with ChatGPT or other translation tools. 4 | 5 | 本文档用于解释 ChatGPT Next Web 的部分功能介绍和设计原则。 6 | 7 | ## 面具 (Mask) 8 | 9 | ### 什么是面具?它和提示词的区别是什么? 10 | 11 | 面具 = 多个预设提示词 + 模型设置 + 对话设置。 12 | 13 | 其中预设提示词(Contextual Prompts)一般用于 In-Context Learning,用于让 ChatGPT 生成更加符合要求的输出,也可以增加系统约束或者输入有限的额外知识。 14 | 15 | 模型设置则顾名思义,使用此面具创建的对话都会默认使用对应的模型参数。 16 | 17 | 对话设置是与对话体验相关的一系列设置,我们会在下方的章节中依次介绍。 18 | 19 | ### 如何添加一个预设面具? 20 | 21 | 目前仅能够通过编辑源代码的方式添加预设面具,请根据需要编辑 [mask](../app/masks/) 目录下对应语言的文件即可。 22 | 23 | 编辑步骤如下: 24 | 25 | 1. 在 ChatGPT Next Web 中配置好一个面具; 26 | 2. 使用面具编辑页面的下载按钮,将面具保存为 JSON 格式; 27 | 3. 让 ChatGPT 帮你将 json 文件格式化为对应的 ts 代码; 28 | 4. 放入对应的 .ts 文件。 29 | 30 | 后续会增加使用旁加载的方式加载面具。 31 | 32 | ## 对话 (Chat) 33 | 34 | ### 对话框上方的按钮的作用 35 | 36 | 在默认状态下,将鼠标移动到按钮上,即可查看按钮的文字说明,我们依次介绍: 37 | 38 | - 对话设置:当前对话的设置,它与全局设置的关系,请查看下一小节的说明; 39 | - 颜色主题:点击即可在自动、暗黑、浅色之间轮换; 40 | - 快捷指令:项目内置的快捷填充预设提示词,也可以在对话框中输入 / 进行搜索; 41 | - 所有面具:进入面具页面; 42 | - 清除聊天:插入一个清除标记,标记上方的聊天将不会发给 GPT,效果相当于清除了当前对话,当然,你也可以再次点击该按钮,可取消清除; 43 | - 模型设置:更改当前对话的模型,注意,此按钮只会修改当前对话的模型,并不会修改全局默认模型。 44 | 45 | ### 对话内设置与全局设置的关系 46 | 47 | 目前有两处设置入口: 48 | 49 | 1. 页面左下角的设置按钮,进入后是全局设置页; 50 | 2. 对话框上方的设置按钮,进入后是对话设置页。 51 | 52 | 在新建对话后,该对话的设置默认与全局设置保持同步,修改全局设置,则新建对话的对话内设置也会被同步修改。 53 | 54 | 一旦用户手动更改过对话内设置,则对话内设置将与全局设置断开同步,此时更改全局设置,将不会对该对话生效。 55 | 56 | 如果想恢复两者的同步关系,可以将“对话内设置 -> 使用全局设置”选项勾选。 57 | 58 | ### 对话内设置项的含义 59 | 60 | 点开对话框上方的按钮,进入对话内设置,内容从上到下依次为: 61 | 62 | - 预设提示词列表:可以增加、删除、排序预设提示词 63 | - 角色头像:顾名思义 64 | - 角色名称:顾名思义 65 | - 隐藏预设对话:隐藏后,预设提示词不会出现在聊天界面 66 | - 使用全局设置:用于表示当前对话是否使用全局对话设置 67 | - 模型设置选项:剩余的选项与全局设置选项含义一致,见下一小节 68 | 69 | ### 全局设置项的含义 70 | 71 | - model / temperature / top_p / max_tokens / presence_penalty / frequency_penalty 均为 ChatGPT 的设置参数,详情请查阅 OpenAI 官方文档,再次不再赘述; 72 | - 注入系统级提示信息、用户输入预处理:详情请看 [https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144) 73 | - 附带历史消息数:用户每次输入消息并发送时,所携带的最近 n 条消息数量; 74 | - 历史消息长度压缩阈值:当已经产生的聊天字数达到该数值以后,则自动触发历史摘要功能; 75 | - 历史摘要:是否启用历史摘要功能。 76 | 77 | ### 什么是历史摘要? 78 | 79 | 历史摘要功能,也是历史消息压缩功能,是保证长对话场景下保持历史记忆的关键,合理使用该功能可以在不丢失历史话题信息的情况下,节省所使用的 token。 80 | 81 | 由于 ChatGPT API 的长度限制,我们以 3.5 模型为例,它只能接受小于 4096 tokens 的对话消息,一旦超出这个数值,就会报错。 82 | 83 | 同时为了让 ChatGPT 理解我们对话的上下文,往往会携带多条历史消息来提供上下文信息,而当对话进行一段时间之后,很容易就会触发长度限制。 84 | 85 | 为了解决此问题,我们增加了历史记录压缩功能,假设阈值为 1000 字符,那么每次用户产生的聊天记录超过 1000 字符时,都会将没有被总结过的消息,发送给 ChatGPT,让其产生一个 100 字所有的摘要。 86 | 87 | 这样,历史信息就从 1000 字压缩到了 100 字,这是一种有损压缩,但已能满足大多数使用场景。 88 | 89 | ### 什么时候应该关闭历史摘要? 90 | 91 | 历史摘要可能会影响 ChatGPT 的对话质量,所以如果对话场景是翻译、信息提取等一次性对话场景,请直接关闭历史摘要功能,并将历史消息数设置为 0。 92 | 93 | ### 当用户发送一条消息时,有哪些信息被发送出去了? 94 | 95 | 当用户在对话框输入了一条消息后,发送给 ChatGPT 的消息,包含以下几个部分: 96 | 97 | 1. 系统级提示词:用于尽可能贴近 ChatGPT 官方 WebUI 的使用体验,可在设置中关闭此信息; 98 | 2. 历史摘要:作为长期记忆,提供长久但模糊的上下文信息; 99 | 3. 预设提示词:当前对话内设置的预设提示词,用于 In-Context Learning 或者注入系统级限制; 100 | 4. 最近 n 条对话记录:作为短期记忆,提供短暂但精确的上下文信息; 101 | 5. 用户当前输入的消息。 102 | -------------------------------------------------------------------------------- /docs/vercel-cn.md: -------------------------------------------------------------------------------- 1 | # Vercel 的使用说明 2 | 3 | ## 如何新建项目 4 | 当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. 进入 Vercel 控制台首页; 8 | 2. 点击 Add New; 9 | 3. 选择 Project。 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. 在 Import Git Repository 处,搜索 chatgpt-next-web; 13 | 2. 选中新 fork 的项目,点击 Import。 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 在项目配置页,点开 Environmane Variables 开始配置环境变量; 17 | 2. 依次新增名为 OPENAI_API_KEY 和 CODE ([访问密码](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)) 的环境变量; 18 | 3. 填入环境变量对应的值; 19 | 4. 点击 Add 确认增加环境变量; 20 | 5. 请确保你添加了 OPENAI_API_KEY,否则无法使用; 21 | 6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。 22 | 23 | ## 如何增加自定义域名 24 | [TODO] 25 | 26 | ## 如何更改环境变量 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮; 29 | 2. 点击左侧的 Environment Variables; 30 | 3. 点击已有条目的右侧按钮; 31 | 4. 选择 Edit 进行编辑,然后保存即可。 32 | 33 | ⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效! 34 | 35 | ## 如何重新部署 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮; 38 | 2. 选择列表最顶部一条的右侧按钮; 39 | 3. 点击 Redeploy 即可重新部署。 40 | -------------------------------------------------------------------------------- /docs/vercel-es.md: -------------------------------------------------------------------------------- 1 | # Instrucciones de uso de Verbel 2 | 3 | ## Cómo crear un nuevo proyecto 4 | 5 | Cuando bifurca este proyecto desde Github y necesita crear un nuevo proyecto de Vercel en Vercel para volver a implementarlo, debe seguir los pasos a continuación. 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vaya a la página de inicio de la consola de Vercel; 10 | 2. Haga clic en Agregar nuevo; 11 | 3. Seleccione Proyecto. 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. En Import Git Repository, busque chatgpt-next-web; 16 | 2. Seleccione el proyecto de la nueva bifurcación y haga clic en Importar. 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. En la página de configuración del proyecto, haga clic en Variables de entorno para configurar las variables de entorno; 21 | 2. Agregar variables de entorno denominadas OPENAI_API_KEY y CODE; 22 | 3. Rellenar los valores correspondientes a las variables de entorno; 23 | 4. Haga clic en Agregar para confirmar la adición de variables de entorno; 24 | 5. Asegúrese de agregar OPENAI_API_KEY, de lo contrario no funcionará; 25 | 6. Haga clic en Implementar, créelo y espere pacientemente unos 5 minutos a que se complete la implementación. 26 | 27 | ## Cómo agregar un nombre de dominio personalizado 28 | 29 | \[TODO] 30 | 31 | ## Cómo cambiar las variables de entorno 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Configuración en la parte superior; 36 | 2. Haga clic en Variables de entorno a la izquierda; 37 | 3. Haga clic en el botón a la derecha de una entrada existente; 38 | 4. Seleccione Editar para editarlo y, a continuación, guárdelo. 39 | 40 | ⚠️️ Nota: Lo necesita cada vez que modifique las variables de entorno[Volver a implementar el proyecto](#如何重新部署)para que los cambios surtan efecto! 41 | 42 | ## Cómo volver a implementar 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Implementaciones en la parte superior; 47 | 2. Seleccione el botón derecho del artículo superior de la lista; 48 | 3. Haga clic en Volver a implementar para volver a implementar. 49 | -------------------------------------------------------------------------------- /docs/vercel-ja.md: -------------------------------------------------------------------------------- 1 | # Vercel 使用説明書 2 | 3 | ## 新規プロジェクトの作成方法 4 | 5 | このプロジェクトを GitHub からフォークし、Vercel で新しい Vercel プロジェクトを作成して再デプロイする必要がある場合は、以下の手順に従ってください。 6 | 7 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 8 | 9 | 1. Vercel コンソールのホームページにアクセスします; 10 | 2. 新規追加をクリックする; 11 | 3. プロジェクトを選択します。 12 | 13 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 14 | 15 | 1. Git リポジトリのインポートで、chatgpt-next-web を検索します; 16 | 2 .新しいフォークプロジェクトを選択し、インポートをクリックします。 17 | 18 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 19 | 20 | 1. Project Settings ページで、Environment Variables をクリックして環境変数を設定する; 21 | 2. OPENAI_API_KEY と CODE という名前の環境変数を追加します; 22 | 3. 環境変数に対応する値を入力します; 23 | 4. Add をクリックして、環境変数の追加を確認する; 24 | 5. OPENAI_API_KEY を必ず追加してください; 25 | 6. Deploy をクリックして作成し、デプロイが完了するまで約 5 分間辛抱強く待つ。 26 | 27 | ## カスタムドメイン名の追加方法 28 | 29 | \[TODO] 30 | 31 | ## 環境変数の変更方法 32 | 33 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 34 | 35 | 1. 内部 Vercel プロジェクトコンソールに移動し、上部の設定ボタンをクリックします; 36 | 2. 左側の Environment Variables をクリックします; 37 | 3. 既存のエントリーの右側のボタンをクリックします; 38 | 4. 編集を選択して編集し、保存する。 39 | 40 | ⚠️️ 注意: [プロジェクトの再デプロイ](#再実装の方法)環境変数を変更するたびに、変更を有効にするために必要です! 41 | 42 | ## 再実装の方法 43 | 44 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 45 | 46 | 1. Vercelプロジェクトの内部コンソールに移動し、一番上のDeploymentsボタンをクリックします; 47 | 2. リストの一番上の項目の右のボタンを選択します; 48 | 3. 再デプロイをクリックして再デプロイします。 49 | -------------------------------------------------------------------------------- /docs/vercel-ko.md: -------------------------------------------------------------------------------- 1 | # Vercel 사용 방법 2 | 3 | ## 새 프로젝트 생성 방법 4 | 이 프로젝트를 Github에서 포크한 후, 다시 배포하려면 Vercel에서 새로운 Vercel 프로젝트를 생성해야 하며, 다음 단계를 따라야 합니다. 5 | 6 | ![vercel-create-1](./images/vercel/vercel-create-1.jpg) 7 | 1. Vercel 콘솔 홈 페이지로 이동합니다; 8 | 2. 새로 추가를 클릭합니다; 9 | 3. 프로젝트를 선택합니다. 10 | 11 | ![vercel-create-2](./images/vercel/vercel-create-2.jpg) 12 | 1. Git 리포지토리 가져오기에서 chatgpt-next-web을 검색합니다. 2. 새 포크를 선택합니다; 13 | 2. 새로 포크된 프로젝트를 선택하고 가져오기를 클릭합니다. 14 | 15 | ![vercel-create-3](./images/vercel/vercel-create-3.jpg) 16 | 1. 프로젝트 구성 페이지에서 환경 변수 설정을 클릭하여 환경 변수 설정을 시작합니다; 17 | 2. OPENAI_API_KEY, CODE ([Access Code](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)). 환경 변수를 설정합니다; 18 | 3. 환경 변수의 값을 입력합니다; 19 | 4. 추가를 클릭하여 환경 변수 추가를 확인합니다; 20 | 5. OPENAI_API_KEY를 추가해야 하며, 그렇지 않으면 작동하지 않습니다; 21 | 6. 배포를 클릭하여 도메인 이름 생성을 완료하고 배포가 완료될 때까지 약 5분간 기다립니다. 22 | 23 | ## 사용자 정의 도메인 네임 추가 방법 24 | [TODO] 25 | 26 | ## 환경 변수 변경 방법 27 | ![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) 28 | 1. 버셀 프로젝트의 내부 콘솔로 이동하여 상단의 설정 버튼을 클릭합니다; 29 | 2. 왼쪽의 환경 변수를 클릭합니다; 30 | 3. 기존 항목 오른쪽에 있는 버튼을 클릭합니다; 31 | 4. 편집을 선택하여 수정하고 저장합니다. 32 | 33 | ⚠️️ 참고: 환경 변수를 변경할 때마다 [프로젝트를 재배포](#如何重新部署)해야 변경 사항을 적용할 수 있습니다! 34 | 35 | ## 재배포 방법 36 | ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) 37 | 1. 버셀 내부 프로젝트 콘솔로 이동하여 상단의 배포 버튼을 클릭합니다; 38 | 2. 목록에서 맨 위 항목 오른쪽에 있는 버튼을 선택합니다; 39 | 3. 재배포를 클릭하여 재배포합니다. -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | const mode = process.env.BUILD_MODE ?? "standalone"; 4 | console.log("[Next] build mode", mode); 5 | 6 | const disableChunk = !!process.env.DISABLE_CHUNK || mode === "export"; 7 | console.log("[Next] build with chunk: ", !disableChunk); 8 | 9 | /** @type {import('next').NextConfig} */ 10 | const nextConfig = { 11 | webpack(config) { 12 | config.module.rules.push({ 13 | test: /\.svg$/, 14 | use: ["@svgr/webpack"], 15 | }); 16 | 17 | if (disableChunk) { 18 | config.plugins.push( 19 | new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }), 20 | ); 21 | } 22 | 23 | config.resolve.fallback = { 24 | child_process: false, 25 | }; 26 | 27 | return config; 28 | }, 29 | output: mode, 30 | images: { 31 | unoptimized: mode === "export", 32 | }, 33 | experimental: { 34 | forceSwcTransforms: true, 35 | }, 36 | }; 37 | 38 | const CorsHeaders = [ 39 | { key: "Access-Control-Allow-Credentials", value: "true" }, 40 | { key: "Access-Control-Allow-Origin", value: "*" }, 41 | { 42 | key: "Access-Control-Allow-Methods", 43 | value: "*", 44 | }, 45 | { 46 | key: "Access-Control-Allow-Headers", 47 | value: "*", 48 | }, 49 | { 50 | key: "Access-Control-Max-Age", 51 | value: "86400", 52 | }, 53 | ]; 54 | 55 | if (mode !== "export") { 56 | nextConfig.headers = async () => { 57 | return [ 58 | { 59 | source: "/api/:path*", 60 | headers: CorsHeaders, 61 | }, 62 | ]; 63 | }; 64 | 65 | nextConfig.rewrites = async () => { 66 | const ret = [ 67 | { 68 | source: "/api/proxy/:path*", 69 | destination: "https://api.openai.com/:path*", 70 | }, 71 | { 72 | source: "/google-fonts/:path*", 73 | destination: "https://fonts.googleapis.com/:path*", 74 | }, 75 | { 76 | source: "/sharegpt", 77 | destination: "https://sharegpt.com/api/conversations", 78 | }, 79 | ]; 80 | 81 | return { 82 | beforeFiles: ret, 83 | }; 84 | }; 85 | } 86 | 87 | export default nextConfig; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-next-web", 3 | "private": false, 4 | "license": "mit", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "cross-env BUILD_MODE=standalone next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "export": "cross-env BUILD_MODE=export BUILD_APP=1 next build", 11 | "export:dev": "cross-env BUILD_MODE=export BUILD_APP=1 next dev", 12 | "app:dev": "yarn tauri dev", 13 | "app:build": "yarn tauri build", 14 | "prompts": "node ./scripts/fetch-prompts.mjs", 15 | "prepare": "husky install", 16 | "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" 17 | }, 18 | "dependencies": { 19 | "@fortaine/fetch-event-source": "^3.0.6", 20 | "@hello-pangea/dnd": "^16.3.0", 21 | "@svgr/webpack": "^6.5.1", 22 | "@vercel/analytics": "^0.1.11", 23 | "emoji-picker-react": "^4.5.15", 24 | "fuse.js": "^6.6.2", 25 | "html-to-image": "^1.11.11", 26 | "mermaid": "^10.6.1", 27 | "nanoid": "^5.0.3", 28 | "next": "^13.4.9", 29 | "node-fetch": "^3.3.1", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-markdown": "^8.0.7", 33 | "react-router-dom": "^6.15.0", 34 | "rehype-highlight": "^6.0.0", 35 | "rehype-katex": "^6.0.3", 36 | "remark-breaks": "^3.0.2", 37 | "remark-gfm": "^3.0.1", 38 | "remark-math": "^5.1.1", 39 | "sass": "^1.59.2", 40 | "spark-md5": "^3.0.2", 41 | "use-debounce": "^9.0.4", 42 | "zustand": "^4.3.8" 43 | }, 44 | "devDependencies": { 45 | "@tauri-apps/cli": "^1.4.0", 46 | "@types/node": "^20.9.0", 47 | "@types/react": "^18.2.14", 48 | "@types/react-dom": "^18.2.7", 49 | "@types/react-katex": "^3.0.0", 50 | "@types/spark-md5": "^3.0.4", 51 | "cross-env": "^7.0.3", 52 | "eslint": "^8.49.0", 53 | "eslint-config-next": "13.4.19", 54 | "eslint-config-prettier": "^8.8.0", 55 | "eslint-plugin-prettier": "^4.2.1", 56 | "husky": "^8.0.0", 57 | "lint-staged": "^13.2.2", 58 | "prettier": "^3.0.2", 59 | "typescript": "5.2.2", 60 | "webpack": "^5.88.1" 61 | }, 62 | "resolutions": { 63 | "lint-staged/yaml": "^2.2.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/favicon.ico -------------------------------------------------------------------------------- /public/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/public/macos.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | User-agent: vitals.vercel-insights.com 4 | Allow: / -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; 2 | 3 | self.addEventListener("activate", function (event) { 4 | console.log("ServiceWorker activated."); 5 | }); 6 | 7 | self.addEventListener("install", function (event) { 8 | event.waitUntil( 9 | caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) { 10 | return cache.addAll([]); 11 | }), 12 | ); 13 | }); 14 | 15 | self.addEventListener("fetch", (e) => {}); 16 | -------------------------------------------------------------------------------- /public/serviceWorkerRegister.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('load', function () { 3 | navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) { 4 | console.log('ServiceWorker registration successful with scope: ', registration.scope); 5 | }, function (err) { 6 | console.error('ServiceWorker registration failed: ', err); 7 | }); 8 | }); 9 | } -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT Next Web", 3 | "short_name": "ChatGPT", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } 21 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | proxychains.conf 2 | -------------------------------------------------------------------------------- /scripts/fetch-prompts.mjs: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import fs from "fs/promises"; 3 | 4 | const RAW_FILE_URL = "https://raw.githubusercontent.com/"; 5 | const MIRRORF_FILE_URL = "http://raw.fgit.ml/"; 6 | 7 | const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json"; 8 | const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL; 9 | const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv"; 10 | const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL; 11 | const FILE = "./public/prompts.json"; 12 | 13 | const ignoreWords = ["涩涩", "魅魔"]; 14 | 15 | const timeoutPromise = (timeout) => { 16 | return new Promise((resolve, reject) => { 17 | setTimeout(() => { 18 | reject(new Error("Request timeout")); 19 | }, timeout); 20 | }); 21 | }; 22 | 23 | async function fetchCN() { 24 | console.log("[Fetch] fetching cn prompts..."); 25 | try { 26 | const response = await Promise.race([fetch(CN_URL), timeoutPromise(5000)]); 27 | const raw = await response.json(); 28 | return raw 29 | .map((v) => [v.act, v.prompt]) 30 | .filter( 31 | (v) => 32 | v[0] && 33 | v[1] && 34 | ignoreWords.every((w) => !v[0].includes(w) && !v[1].includes(w)), 35 | ); 36 | } catch (error) { 37 | console.error("[Fetch] failed to fetch cn prompts", error); 38 | return []; 39 | } 40 | } 41 | 42 | async function fetchEN() { 43 | console.log("[Fetch] fetching en prompts..."); 44 | try { 45 | // const raw = await (await fetch(EN_URL)).text(); 46 | const response = await Promise.race([fetch(EN_URL), timeoutPromise(5000)]); 47 | const raw = await response.text(); 48 | return raw 49 | .split("\n") 50 | .slice(1) 51 | .map((v) => 52 | v 53 | .split('","') 54 | .map((v) => v.replace(/^"|"$/g, "").replaceAll('""', '"')) 55 | .filter((v) => v[0] && v[1]), 56 | ); 57 | } catch (error) { 58 | console.error("[Fetch] failed to fetch en prompts", error); 59 | return []; 60 | } 61 | } 62 | 63 | async function main() { 64 | Promise.all([fetchCN(), fetchEN()]) 65 | .then(([cn, en]) => { 66 | fs.writeFile(FILE, JSON.stringify({ cn, en })); 67 | }) 68 | .catch((e) => { 69 | console.error("[Fetch] failed to fetch prompts"); 70 | fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] })); 71 | }) 72 | .finally(() => { 73 | console.log("[Fetch] saved to " + FILE); 74 | }); 75 | } 76 | 77 | main(); 78 | -------------------------------------------------------------------------------- /scripts/init-proxy.sh: -------------------------------------------------------------------------------- 1 | dir="$(dirname "$0")" 2 | config=$dir/proxychains.conf 3 | host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //') 4 | echo "proxying to $host_ip" 5 | cp $dir/proxychains.template.conf $config 6 | sed -i "\$s/.*/http $host_ip 7890/" $config 7 | -------------------------------------------------------------------------------- /scripts/proxychains.template.conf: -------------------------------------------------------------------------------- 1 | strict_chain 2 | proxy_dns 3 | 4 | remote_dns_subnet 224 5 | 6 | tcp_read_time_out 15000 7 | tcp_connect_time_out 8000 8 | 9 | localnet 127.0.0.0/255.0.0.0 10 | 11 | [ProxyList] 12 | socks4 127.0.0.1 9050 13 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running on a supported system 4 | case "$(uname -s)" in 5 | Linux) 6 | if [[ -f "/etc/lsb-release" ]]; then 7 | . /etc/lsb-release 8 | if [[ "$DISTRIB_ID" != "Ubuntu" ]]; then 9 | echo "This script only works on Ubuntu, not $DISTRIB_ID." 10 | exit 1 11 | fi 12 | else 13 | if [[ !"$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")|(ID=\"debian\")$ ]]; then 14 | echo "Unsupported Linux distribution." 15 | exit 1 16 | fi 17 | fi 18 | ;; 19 | Darwin) 20 | echo "Running on MacOS." 21 | ;; 22 | *) 23 | echo "Unsupported operating system." 24 | exit 1 25 | ;; 26 | esac 27 | 28 | # Check if needed dependencies are installed and install if necessary 29 | if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then 30 | case "$(uname -s)" in 31 | Linux) 32 | if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=ubuntu" ]]; then 33 | sudo apt-get update 34 | sudo apt-get -y install nodejs git yarn 35 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=debian" ]]; then 36 | sudo apt-get update 37 | sudo apt-get -y install nodejs git yarn 38 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=centos" ]]; then 39 | sudo yum -y install epel-release 40 | sudo yum -y install nodejs git yarn 41 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=arch" ]]; then 42 | sudo pacman -Syu -y 43 | sudo pacman -S -y nodejs git yarn 44 | else 45 | echo "Unsupported Linux distribution" 46 | exit 1 47 | fi 48 | ;; 49 | Darwin) 50 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 51 | brew install node git yarn 52 | ;; 53 | esac 54 | fi 55 | 56 | # Clone the repository and install dependencies 57 | git clone https://github.com/Yidadaa/ChatGPT-Next-Web 58 | cd ChatGPT-Next-Web 59 | yarn install 60 | 61 | # Prompt user for environment variables 62 | read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY 63 | read -p "Enter CODE: " CODE 64 | read -p "Enter PORT: " PORT 65 | 66 | # Build and run the project using the environment variables 67 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build 68 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start 69 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chatgpt-next-web" 3 | version = "0.1.0" 4 | description = "A cross platform app for LLM ChatBot." 5 | authors = ["Yidadaa"] 6 | license = "mit" 7 | repository = "" 8 | default-run = "chatgpt-next-web" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.3.0", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.3.0", features = ["notification-all", "fs-all", "clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] } 21 | tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 22 | 23 | [features] 24 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 25 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 26 | # DO NOT REMOVE!! 27 | custom-protocol = [ "tauri/custom-protocol" ] 28 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/ChatGPT-Next-Web/9876a1aeca71610841af5585d7baeba3126a8df9/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri::Builder::default() 6 | .plugin(tauri_plugin_window_state::Builder::default().build()) 7 | .run(tauri::generate_context!()) 8 | .expect("error while running tauri application"); 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "yarn export", 5 | "beforeDevCommand": "yarn export:dev", 6 | "devPath": "http://localhost:3000", 7 | "distDir": "../out", 8 | "withGlobalTauri": true 9 | }, 10 | "package": { 11 | "productName": "ChatGPT Next Web", 12 | "version": "2.9.11" 13 | }, 14 | "tauri": { 15 | "allowlist": { 16 | "all": false, 17 | "shell": { 18 | "all": false, 19 | "open": true 20 | }, 21 | "dialog": { 22 | "all": true, 23 | "ask": true, 24 | "confirm": true, 25 | "message": true, 26 | "open": true, 27 | "save": true 28 | }, 29 | "clipboard": { 30 | "all": true, 31 | "writeText": true, 32 | "readText": true 33 | }, 34 | "window": { 35 | "all": false, 36 | "close": true, 37 | "hide": true, 38 | "maximize": true, 39 | "minimize": true, 40 | "setIcon": true, 41 | "setIgnoreCursorEvents": true, 42 | "setResizable": true, 43 | "show": true, 44 | "startDragging": true, 45 | "unmaximize": true, 46 | "unminimize": true 47 | }, 48 | "fs": { 49 | "all": true 50 | }, 51 | "notification": { 52 | "all": true 53 | } 54 | }, 55 | "bundle": { 56 | "active": true, 57 | "category": "DeveloperTool", 58 | "copyright": "2023, Zhang Yifei All Rights Reserved.", 59 | "deb": { 60 | "depends": [] 61 | }, 62 | "externalBin": [], 63 | "icon": [ 64 | "icons/32x32.png", 65 | "icons/128x128.png", 66 | "icons/128x128@2x.png", 67 | "icons/icon.icns", 68 | "icons/icon.ico" 69 | ], 70 | "identifier": "com.yida.chatgpt.next.web", 71 | "longDescription": "ChatGPT Next Web is a cross-platform ChatGPT client, including Web/Win/Linux/OSX/PWA.", 72 | "macOS": { 73 | "entitlements": null, 74 | "exceptionDomain": "", 75 | "frameworks": [], 76 | "providerShortName": null, 77 | "signingIdentity": null 78 | }, 79 | "resources": [], 80 | "shortDescription": "ChatGPT Next Web App", 81 | "targets": "all", 82 | "windows": { 83 | "certificateThumbprint": null, 84 | "digestAlgorithm": "sha256", 85 | "timestampUrl": "" 86 | } 87 | }, 88 | "security": { 89 | "csp": null 90 | }, 91 | "updater": { 92 | "active": true, 93 | "endpoints": [ 94 | "https://github.com/Yidadaa/ChatGPT-Next-Web/releases/latest/download/latest.json" 95 | ], 96 | "dialog": false, 97 | "windows": { 98 | "installMode": "passive" 99 | }, 100 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFNDE4MENFM0Y1RTZBOTQKUldTVWFsNC96b0JCM3RqM2NmMnlFTmxIaStRaEJrTHNOU2VqRVlIV1hwVURoWUdVdEc1eDcxVEYK" 101 | }, 102 | "windows": [ 103 | { 104 | "fullscreen": false, 105 | "height": 600, 106 | "resizable": true, 107 | "title": "ChatGPT Next Web", 108 | "width": 960, 109 | "hiddenTitle": true, 110 | "titleBarStyle": "Overlay" 111 | } 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | }, 5 | "headers": [ 6 | { 7 | "source": "/(.*)", 8 | "headers": [ 9 | { 10 | "key": "X-Real-IP", 11 | "value": "$remote_addr" 12 | }, 13 | { 14 | "key": "X-Forwarded-For", 15 | "value": "$proxy_add_x_forwarded_for" 16 | }, 17 | { 18 | "key": "Host", 19 | "value": "$http_host" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | --------------------------------------------------------------------------------