├── .env.template ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── 功能建议.md │ └── 反馈问题.md ├── dependabot.yml └── workflows │ ├── app.yml │ ├── docker.yml │ └── sync.yml ├── .gitignore ├── .gitpod.yml ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_CN.md ├── README_ES.md ├── app ├── api │ ├── auth.ts │ ├── common.ts │ ├── config │ │ └── route.ts │ ├── dify │ │ └── [...path] │ │ │ └── route.ts │ └── openai │ │ └── [...path] │ │ └── route.ts ├── client │ ├── api.ts │ ├── controller.ts │ └── platforms │ │ ├── dify.ts │ │ └── openai.ts ├── command.ts ├── components │ ├── auth.module.scss │ ├── auth.tsx │ ├── button.module.scss │ ├── button.tsx │ ├── chat-list.tsx │ ├── chat.module.scss │ ├── chat.tsx │ ├── dify-new-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 │ ├── chat-settings.svg │ ├── chat.svg │ ├── chatgpt.png │ ├── chatgpt.svg │ ├── clear.svg │ ├── close.svg │ ├── copy.svg │ ├── dark.svg │ ├── delete.svg │ ├── down.svg │ ├── download.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 │ ├── plugin.svg │ ├── prompt.svg │ ├── reload.svg │ ├── rename.svg │ ├── return.svg │ ├── select.svg │ ├── send-white.svg │ ├── settings.svg │ ├── share.svg │ ├── three-dots.svg │ └── upload.svg ├── layout.tsx ├── locales │ ├── cn.ts │ ├── cs.ts │ ├── de.ts │ ├── en.ts │ ├── es.ts │ ├── fr.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 │ ├── difyKey.ts │ ├── index.ts │ ├── mask.ts │ ├── prompt.ts │ └── update.ts ├── styles │ ├── animation.scss │ ├── globals.scss │ ├── highlight.scss │ ├── markdown.scss │ └── window.scss ├── typing.ts ├── utils.ts └── utils │ ├── format.ts │ ├── merge.ts │ └── token.ts ├── docker-compose.yml ├── docs ├── cloudflare-pages-cn.md ├── cloudflare-pages-en.md ├── cloudflare-pages-es.md ├── faq-cn.md ├── faq-en.md ├── faq-es.md ├── images │ ├── cover.png │ ├── enable-actions-sync.jpg │ ├── enable-actions.jpg │ ├── icon.svg │ ├── more.png │ ├── settings.png │ └── vercel │ │ ├── vercel-create-1.jpg │ │ ├── vercel-create-2.jpg │ │ ├── vercel-create-3.jpg │ │ ├── vercel-env-edit.jpg │ │ └── vercel-redeploy.jpg ├── vercel-cn.md └── vercel-es.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 ├── 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 /.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 | # If you do not want users to input their own API key, set this value to 1. 19 | OPENAI_ORG_ID= 20 | 21 | # (optional) 22 | # Default: Empty 23 | # If you do not want users to input their own API key, set this value to 1. 24 | HIDE_USER_API_KEY= 25 | 26 | # (optional) 27 | # Default: Empty 28 | # If you do not want users to use GPT-4, set this value to 1. 29 | DISABLE_GPT4= 30 | -------------------------------------------------------------------------------- /.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-20.04 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 | platform: [macos-latest, ubuntu-20.04, windows-latest] 43 | 44 | runs-on: ${{ matrix.platform }} 45 | steps: 46 | - uses: actions/checkout@v3 47 | - name: setup node 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: 16 51 | - name: install Rust stable 52 | uses: dtolnay/rust-toolchain@stable 53 | - name: install dependencies (ubuntu only) 54 | if: matrix.platform == 'ubuntu-20.04' 55 | run: | 56 | sudo apt-get update 57 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 58 | - name: install frontend dependencies 59 | run: yarn install # change this to npm or pnpm depending on which one you use 60 | - uses: tauri-apps/tauri-action@v0 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 64 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 65 | with: 66 | releaseId: ${{ needs.create-release.outputs.release_id }} 67 | 68 | publish-release: 69 | permissions: 70 | contents: write 71 | runs-on: ubuntu-20.04 72 | needs: [create-release, build-tauri] 73 | 74 | steps: 75 | - name: publish release 76 | id: publish-release 77 | uses: actions/github-script@v6 78 | env: 79 | release_id: ${{ needs.create-release.outputs.release_id }} 80 | with: 81 | script: | 82 | github.rest.repos.updateRelease({ 83 | owner: context.repo.owner, 84 | repo: context.repo.repo, 85 | release_id: process.env.release_id, 86 | draft: false, 87 | prerelease: false 88 | }) 89 | -------------------------------------------------------------------------------- /.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/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 | 版权所有(c)<2023> 2 | 3 | 反996许可证版本1.0 4 | 5 | 在符合下列条件的情况下, 6 | 特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品” 7 | )的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使 8 | 用、复制,修改,衍生利用、散布,发布和再许可: 9 | 10 | 11 | 1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不 12 | 得自行修改。 13 | 2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或 14 | 经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和 15 | 标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可 16 | 执行,则个人或法人实体必须遵守国际劳工标准的核心公约。 17 | 3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同 18 | 意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和 19 | 标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该 20 | 等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规 21 | 情况的有关当局报告或投诉上述违反许可证的行为的权利。 22 | 23 | 该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用 24 | 性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均 25 | 不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。 26 | 27 | 28 | ------------------------- ENGLISH ------------------------------ 29 | 30 | 31 | Copyright (c) <2023> 32 | 33 | Anti 996 License Version 1.0 (Draft) 34 | 35 | Permission is hereby granted to any individual or legal entity obtaining a copy 36 | of this licensed work (including the source code, documentation and/or related 37 | items, hereinafter collectively referred to as the "licensed work"), free of 38 | charge, to deal with the licensed work for any purpose, including without 39 | limitation, the rights to use, reproduce, modify, prepare derivative works of, 40 | publish, distribute and sublicense the licensed work, subject to the following 41 | conditions: 42 | 43 | 1. The individual or the legal entity must conspicuously display, without 44 | modification, this License on each redistributed or derivative copy of the 45 | Licensed Work. 46 | 47 | 2. The individual or the legal entity must strictly comply with all applicable 48 | laws, regulations, rules and standards of the jurisdiction relating to 49 | labor and employment where the individual is physically located or where 50 | the individual was born or naturalized; or where the legal entity is 51 | registered or is operating (whichever is stricter). In case that the 52 | jurisdiction has no such laws, regulations, rules and standards or its 53 | laws, regulations, rules and standards are unenforceable, the individual 54 | or the legal entity are required to comply with Core International Labor 55 | Standards. 56 | 57 | 3. The individual or the legal entity shall not induce or force its 58 | employee(s), whether full-time or part-time, or its independent 59 | contractor(s), in any methods, to agree in oral or written form, 60 | to directly or indirectly restrict, weaken or relinquish his or 61 | her rights or remedies under such laws, regulations, rules and 62 | standards relating to labor and employment as mentioned above, 63 | no matter whether such written or oral agreement are enforceable 64 | under the laws of the said jurisdiction, nor shall such individual 65 | or the legal entity limit, in any methods, the rights of its employee(s) 66 | or independent contractor(s) from reporting or complaining to the copyright 67 | holder or relevant authorities monitoring the compliance of the license 68 | about its violation(s) of the said license. 69 | 70 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 72 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT 73 | HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 74 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION 75 | WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | icon 3 | 4 |

Dify Next Web

5 | 6 | English / [简体中文](./README_CN.md) 7 | 8 | 本项目基于Dify和ChatGPT-Next-Web两个项目创建,用于构建大语言模型在垂直细分领域的应用 9 | 10 | 您可以点击下方“Buy Me a Coffee”为ChatGPT-Next-Web的开发者Yidadaa提供支持 11 | 12 | 很可惜没有找到Dify开发者的支持方式,各位可以多多关注Dify项目 13 | 14 | [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) 15 | 16 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) 17 | 18 | ![cover](./docs/images/cover.png) 19 | 20 |
21 | 22 | ## Dify Next Web主要功能和实现 23 | 24 | - Dify Next Web提供链接Dify的服务,且设置了Dify Key的列表以存储多个API KEY以方便切换,构建在垂直细分领域的应用 25 | - 本项目提供一个基于Llamaindex的chrome插件,用于链接本地数据,奈何Llamaindex的release频率太高,且有一点点技术要求,暂不开放 26 | ### 组成 27 | - ChatGPT-Next-Web 是一个用于部署ChatGPT web UI的项目,它的主要功能包括完整的Markdown支持、流式响应、Mask和Prompt支持和上下文摘要等 28 | - 关于部署教程参阅[简体中文](./README_CN.md) 29 | - Dify 是一个LLMOps平台,旨在使更多的人能够创建可持续的、AI原生的应用程序。它提供了数据集的集成,以及用于提示工程、可视化分析和持续改进的单一接口 30 | - 关于部署教程参阅[Dify文档](https://docs.dify.ai/v/zh-hans/getting-started/intro-to-dify) 31 | ### 必备的API KEY 32 | - 想要使用Dify Next Web服务,需要获取两个API 33 | - 获取OpenAI API,需要注册OpenAI的API服务,点击右边的View API keys. 然后点击Create new secret key 即可生成新的API Key. 34 | - 获取Dify Api,包括以下两种办法:①使用Dify官方提供的云服务创建Dify API,访问 [Dify.ai](https://cloud.dify.ai) ②自己Docker部署后端服务 35 | ### 最简易的食用指南 36 | - Vercel部署,记得添加环境变量哦 37 | 38 | 39 | ## 开发计划 40 | 41 | - 暂无开发计划可言,也许以后会聚合更多的功能 42 | 43 | ### 一定不会开发的功能 44 | 45 | - 界面文字自定义 46 | - 用户登录、账号管理、消息云同步 47 | 48 | ## LICENSE 49 | 50 | [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN) 51 | -------------------------------------------------------------------------------- /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: token } = 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) && !token) { 43 | return { 44 | error: true, 45 | msg: !accessCode ? "empty access code" : "wrong access code", 46 | }; 47 | } 48 | 49 | // if user does not provide an api key, inject system api key 50 | if (!token) { 51 | const apiKey = serverConfig.apiKey; 52 | if (apiKey) { 53 | console.log("[Auth] use system api key"); 54 | req.headers.set("Authorization", `Bearer ${apiKey}`); 55 | } else { 56 | console.log("[Auth] admin did not provide an api key"); 57 | } 58 | } else { 59 | console.log("[Auth] use user api key"); 60 | } 61 | 62 | return { 63 | error: false, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /app/api/common.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export const OPENAI_URL = "api.openai.com"; 4 | export const DIFY_URL = "api.dify.ai/v1"; 5 | const DEFAULT_PROTOCOL = "https"; 6 | const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL; 7 | const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; 8 | const DISABLE_GPT4 = !!process.env.DISABLE_GPT4; 9 | const DIFY_BASE_URL = process.env.DIFY_BASE_URL ?? DIFY_URL; 10 | 11 | export async function requestOpenai(req: NextRequest) { 12 | const controller = new AbortController(); 13 | const authValue = req.headers.get("Authorization") ?? ""; 14 | const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( 15 | "/api/openai/", 16 | "", 17 | ); 18 | 19 | let baseUrl = BASE_URL; 20 | 21 | if (!baseUrl.startsWith("http")) { 22 | baseUrl = `${PROTOCOL}://${baseUrl}`; 23 | } 24 | 25 | console.log("[Proxy] ", openaiPath); 26 | console.log("[Base Url]", baseUrl); 27 | 28 | if (process.env.OPENAI_ORG_ID) { 29 | console.log("[Org ID]", process.env.OPENAI_ORG_ID); 30 | } 31 | 32 | const timeoutId = setTimeout(() => { 33 | controller.abort(); 34 | }, 10 * 60 * 1000); 35 | 36 | const fetchUrl = `${baseUrl}/${openaiPath}`; 37 | const fetchOptions: RequestInit = { 38 | headers: { 39 | "Content-Type": "application/json", 40 | Authorization: authValue, 41 | ...(process.env.OPENAI_ORG_ID && { 42 | "OpenAI-Organization": process.env.OPENAI_ORG_ID, 43 | }), 44 | }, 45 | cache: "no-store", 46 | method: req.method, 47 | body: req.body, 48 | signal: controller.signal, 49 | }; 50 | 51 | // #1815 try to refuse gpt4 request 52 | if (DISABLE_GPT4 && req.body) { 53 | try { 54 | const clonedBody = await req.text(); 55 | fetchOptions.body = clonedBody; 56 | 57 | const jsonBody = JSON.parse(clonedBody); 58 | 59 | if ((jsonBody?.model ?? "").includes("gpt-4")) { 60 | return NextResponse.json( 61 | { 62 | error: true, 63 | message: "you are not allowed to use gpt-4 model", 64 | }, 65 | { 66 | status: 403, 67 | }, 68 | ); 69 | } 70 | } catch (e) { 71 | console.error("[OpenAI] gpt4 filter", e); 72 | } 73 | } 74 | 75 | try { 76 | const res = await fetch(fetchUrl, fetchOptions); 77 | 78 | // to prevent browser prompt for credentials 79 | const newHeaders = new Headers(res.headers); 80 | newHeaders.delete("www-authenticate"); 81 | 82 | // to disbale ngnix buffering 83 | newHeaders.set("X-Accel-Buffering", "no"); 84 | 85 | return new Response(res.body, { 86 | status: res.status, 87 | statusText: res.statusText, 88 | headers: newHeaders, 89 | }); 90 | } finally { 91 | clearTimeout(timeoutId); 92 | } 93 | } 94 | export async function requestDify(req: NextRequest) { 95 | const controller = new AbortController(); 96 | const authValue = req.headers.get("Authorization") ?? ""; 97 | const difyPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( 98 | "/api/dify/", 99 | "", 100 | ); 101 | 102 | let baseUrl = DIFY_BASE_URL; 103 | 104 | if (!baseUrl.startsWith("http")) { 105 | baseUrl = `${PROTOCOL}://${baseUrl}`; 106 | } 107 | console.log("[Proxy] ", difyPath); 108 | console.log("[Base Url]", baseUrl); 109 | // try { 110 | // const json = await req.json(); 111 | // console.log("[Dify Body]",{ json }); 112 | // } catch{} 113 | 114 | // if (process.env.OPENAI_ORG_ID) { 115 | // console.log("[Org ID]", process.env.OPENAI_ORG_ID); 116 | // } 117 | 118 | const timeoutId = setTimeout(() => { 119 | controller.abort(); 120 | }, 10 * 60 * 1000); 121 | 122 | const fetchUrl = `${baseUrl}/${difyPath}`; 123 | const fetchOptions: RequestInit = { 124 | headers: { 125 | "Content-Type": "application/json", 126 | Authorization: authValue, 127 | }, 128 | cache: "no-store", 129 | method: req.method, 130 | body: req.body, 131 | signal: controller.signal, 132 | }; 133 | 134 | try { 135 | const res = await fetch(fetchUrl, fetchOptions); 136 | 137 | if (res.status === 401) { 138 | // to prevent browser prompt for credentials 139 | const newHeaders = new Headers(res.headers); 140 | newHeaders.delete("www-authenticate"); 141 | return new Response(res.body, { 142 | status: res.status, 143 | statusText: res.statusText, 144 | headers: newHeaders, 145 | }); 146 | } 147 | 148 | return res; 149 | } finally { 150 | clearTimeout(timeoutId); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /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! Don not write any secret value here! 8 | // 警告!不要在这里写入任何敏感信息! 9 | const DANGER_CONFIG = { 10 | needCode: serverConfig.needCode, 11 | hideUserApiKey: serverConfig.hideUserApiKey, 12 | enableGPT4: serverConfig.enableGPT4, 13 | }; 14 | 15 | declare global { 16 | type DangerConfig = typeof DANGER_CONFIG; 17 | } 18 | 19 | async function handle() { 20 | return NextResponse.json(DANGER_CONFIG); 21 | } 22 | 23 | export const GET = handle; 24 | export const POST = handle; 25 | 26 | export const runtime = "edge"; 27 | -------------------------------------------------------------------------------- /app/api/dify/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { prettyObject } from "@/app/utils/format"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { requestDify } from "../../common"; 4 | import { DIFY_KEY_PREFIX } from "@/app/constant"; 5 | import { getServerSideConfig } from "../../../config/server"; 6 | 7 | async function handle( 8 | req: NextRequest, 9 | { params }: { params: { path: string[] } }, 10 | ) { 11 | console.log("[Dify Route] params ", params); 12 | 13 | const authResult = authDify(req); 14 | if (authResult.error) { 15 | return NextResponse.json(authResult, { 16 | status: 401, 17 | }); 18 | } 19 | 20 | try { 21 | return await requestDify(req); 22 | } catch (e) { 23 | console.error("[Dify] ", e); 24 | return NextResponse.json(prettyObject(e)); 25 | } 26 | } 27 | 28 | export const GET = handle; 29 | export const POST = handle; 30 | 31 | export const runtime = "edge"; 32 | 33 | function authDify(req: NextRequest) { 34 | const bearer = req.headers.get("Authorization") ?? ""; 35 | const token = bearer.trim(); 36 | const serverConfig = getServerSideConfig(); 37 | console.log("[Time] ", new Date().toLocaleString()); 38 | 39 | // if user does not provide an api key, inject system api key 40 | if (!token) { 41 | const apiKey = serverConfig.difyApiKey; 42 | if (apiKey) { 43 | console.log("[Dify Auth] use system api key"); 44 | req.headers.set("Authorization", `Bearer ${apiKey}`); 45 | } else { 46 | console.log("[Dify Auth] admin did not provide an api key"); 47 | } 48 | } else { 49 | console.log("[Dify Auth] use user api key"); 50 | } 51 | 52 | return { 53 | error: false, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /app/api/openai/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { OpenaiPath } from "@/app/constant"; 2 | import { prettyObject } from "@/app/utils/format"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { auth } from "../../auth"; 5 | import { requestOpenai } from "../../common"; 6 | 7 | const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); 8 | 9 | async function handle( 10 | req: NextRequest, 11 | { params }: { params: { path: string[] } }, 12 | ) { 13 | console.log("[OpenAI Route] params ", params); 14 | 15 | if (req.method === "OPTIONS") { 16 | return NextResponse.json({ body: "OK" }, { status: 200 }); 17 | } 18 | 19 | const subpath = params.path.join("/"); 20 | 21 | if (!ALLOWD_PATH.has(subpath)) { 22 | console.log("[OpenAI Route] forbidden path ", subpath); 23 | return NextResponse.json( 24 | { 25 | error: true, 26 | msg: "you are not allowed to request " + subpath, 27 | }, 28 | { 29 | status: 403, 30 | }, 31 | ); 32 | } 33 | 34 | const authResult = auth(req); 35 | if (authResult.error) { 36 | return NextResponse.json(authResult, { 37 | status: 401, 38 | }); 39 | } 40 | 41 | try { 42 | return await requestOpenai(req); 43 | } catch (e) { 44 | console.error("[OpenAI] ", e); 45 | return NextResponse.json(prettyObject(e)); 46 | } 47 | } 48 | 49 | export const GET = handle; 50 | export const POST = handle; 51 | 52 | export const runtime = "edge"; 53 | -------------------------------------------------------------------------------- /app/client/api.ts: -------------------------------------------------------------------------------- 1 | import { getClientConfig } from "../config/client"; 2 | import { ACCESS_CODE_PREFIX, DIFY_KEY_PREFIX } from "../constant"; 3 | import { ChatMessage, ModelType, useAccessStore } from "../store"; 4 | import { ChatGPTApi } from "./platforms/openai"; 5 | import { DifyAPI } from "./platforms/dify"; 6 | 7 | export const ROLES = ["system", "user", "assistant"] as const; 8 | export type MessageRole = (typeof ROLES)[number]; 9 | 10 | export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; 11 | export type ChatModel = ModelType; 12 | 13 | export interface RequestMessage { 14 | role: MessageRole; 15 | content: string; 16 | } 17 | 18 | export interface LLMConfig { 19 | model: string; 20 | temperature?: number; 21 | top_p?: number; 22 | stream?: boolean; 23 | presence_penalty?: number; 24 | frequency_penalty?: number; 25 | } 26 | 27 | export interface ChatOptions { 28 | messages: RequestMessage[]; 29 | config: LLMConfig; 30 | 31 | onUpdate?: (message: string, chunk: string) => void; 32 | onFinish: (message: string) => void; 33 | onError?: (err: Error) => void; 34 | onController?: (controller: AbortController) => void; 35 | } 36 | 37 | export interface LLMUsage { 38 | used: number; 39 | total: number; 40 | } 41 | 42 | export abstract class LLMApi { 43 | abstract chat(options: ChatOptions): Promise; 44 | abstract usage(): Promise; 45 | } 46 | 47 | type ProviderName = "openai" | "azure" | "claude" | "palm"; 48 | 49 | interface Model { 50 | name: string; 51 | provider: ProviderName; 52 | ctxlen: number; 53 | } 54 | 55 | interface ChatProvider { 56 | name: ProviderName; 57 | apiConfig: { 58 | baseUrl: string; 59 | apiKey: string; 60 | summaryModel: Model; 61 | }; 62 | models: Model[]; 63 | 64 | chat: () => void; 65 | usage: () => void; 66 | } 67 | 68 | export class ClientApi { 69 | public llm: LLMApi; 70 | public dify: DifyAPI; 71 | 72 | constructor() { 73 | this.llm = new ChatGPTApi(); 74 | this.dify = new DifyAPI(); 75 | } 76 | 77 | config() {} 78 | 79 | prompts() {} 80 | 81 | masks() {} 82 | 83 | async share(messages: ChatMessage[], avatarUrl: string | null = null) { 84 | const msgs = messages 85 | .map((m) => ({ 86 | from: m.role === "user" ? "human" : "gpt", 87 | value: m.content, 88 | })) 89 | .concat([ 90 | { 91 | from: "human", 92 | value: 93 | "Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web", 94 | }, 95 | ]); 96 | // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用 97 | // Please do not modify this message 98 | 99 | console.log("[Share]", msgs); 100 | const clientConfig = getClientConfig(); 101 | const proxyUrl = "/sharegpt"; 102 | const rawUrl = "https://sharegpt.com/api/conversations"; 103 | const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl; 104 | const res = await fetch(shareUrl, { 105 | body: JSON.stringify({ 106 | avatarUrl, 107 | items: msgs, 108 | }), 109 | headers: { 110 | "Content-Type": "application/json", 111 | }, 112 | method: "POST", 113 | }); 114 | 115 | const resJson = await res.json(); 116 | console.log("[Share]", resJson); 117 | if (resJson.id) { 118 | return `https://shareg.pt/${resJson.id}`; 119 | } 120 | } 121 | } 122 | 123 | export const api = new ClientApi(); 124 | 125 | export function getHeaders() { 126 | const accessStore = useAccessStore.getState(); 127 | let headers: Record = { 128 | "Content-Type": "application/json", 129 | "x-requested-with": "XMLHttpRequest", 130 | }; 131 | 132 | const makeBearer = (token: string) => `Bearer ${token.trim()}`; 133 | const validString = (x: string) => x && x.length > 0; 134 | 135 | // use user's api key first 136 | if (validString(accessStore.token)) { 137 | headers.Authorization = makeBearer(accessStore.token); 138 | } else if ( 139 | accessStore.enabledAccessControl() && 140 | validString(accessStore.accessCode) 141 | ) { 142 | headers.Authorization = makeBearer( 143 | ACCESS_CODE_PREFIX + accessStore.accessCode, 144 | ); 145 | } 146 | 147 | return headers; 148 | } 149 | 150 | export function getDifyHeaders(difyKey: string) { 151 | // const accessStore = useAccessStore.getState(); 152 | let headers: Record = { 153 | "Content-Type": "application/json", 154 | "x-requested-with": "XMLHttpRequest", 155 | }; 156 | 157 | const makeBearer = (token: string) => `Bearer ${token.trim()}`; 158 | const validString = (x: string) => x && x.length > 0; 159 | 160 | // use user's api key first 161 | if (validString(difyKey)) { 162 | headers.Authorization = makeBearer(difyKey); 163 | } 164 | 165 | return headers; 166 | } 167 | -------------------------------------------------------------------------------- /app/client/controller.ts: -------------------------------------------------------------------------------- 1 | // To store message streaming controller 2 | export const ChatControllerPool = { 3 | controllers: {} as Record, 4 | 5 | addController( 6 | sessionIndex: number, 7 | messageId: number, 8 | controller: AbortController, 9 | ) { 10 | const key = this.key(sessionIndex, messageId); 11 | this.controllers[key] = controller; 12 | return key; 13 | }, 14 | 15 | stop(sessionIndex: number, messageId: number) { 16 | const key = this.key(sessionIndex, 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(sessionIndex: number, messageId: number) { 30 | const key = this.key(sessionIndex, messageId); 31 | delete this.controllers[key]; 32 | }, 33 | 34 | key(sessionIndex: number, messageIndex: number) { 35 | return `${sessionIndex},${messageIndex}`; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /app/command.ts: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from "react-router-dom"; 2 | 3 | type Command = (param: string) => void; 4 | interface Commands { 5 | fill?: Command; 6 | submit?: Command; 7 | mask?: Command; 8 | } 9 | 10 | export function useCommand(commands: Commands = {}) { 11 | const [searchParams, setSearchParams] = useSearchParams(); 12 | 13 | if (commands === undefined) return; 14 | 15 | let shouldUpdate = false; 16 | searchParams.forEach((param, name) => { 17 | const commandName = name as keyof Commands; 18 | if (typeof commands[commandName] === "function") { 19 | commands[commandName]!(param); 20 | searchParams.delete(name); 21 | shouldUpdate = true; 22 | } 23 | }); 24 | 25 | if (shouldUpdate) { 26 | setSearchParams(searchParams); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | 11 | export function AuthPage() { 12 | const navigate = useNavigate(); 13 | const access = useAccessStore(); 14 | 15 | const goHome = () => navigate(Path.Home); 16 | 17 | return ( 18 |
19 |
20 | 21 |
22 | 23 |
{Locale.Auth.Title}
24 |
{Locale.Auth.Tips}
25 | 26 | { 32 | access.updateCode(e.currentTarget.value); 33 | }} 34 | /> 35 | 36 |
37 | 42 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /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 | 32 | .shadow { 33 | box-shadow: var(--card-shadow); 34 | } 35 | 36 | .border { 37 | border: var(--border-in-light); 38 | } 39 | 40 | .icon-button:hover { 41 | border-color: var(--primary); 42 | } 43 | 44 | .icon-button-icon { 45 | width: 16px; 46 | height: 16px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | } 51 | 52 | @media only screen and (max-width: 600px) { 53 | .icon-button { 54 | padding: 16px; 55 | } 56 | } 57 | 58 | .icon-button-text { 59 | margin-left: 5px; 60 | font-size: 12px; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | } 65 | -------------------------------------------------------------------------------- /app/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import styles from "./button.module.scss"; 4 | 5 | export function IconButton(props: { 6 | onClick?: () => void; 7 | icon?: JSX.Element; 8 | type?: "primary" | "danger"; 9 | text?: string; 10 | bordered?: boolean; 11 | shadow?: boolean; 12 | className?: string; 13 | title?: string; 14 | disabled?: boolean; 15 | }) { 16 | return ( 17 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import DeleteIcon from "../icons/delete.svg"; 2 | import BotIcon from "../icons/bot.svg"; 3 | 4 | import styles from "./home.module.scss"; 5 | import { 6 | DragDropContext, 7 | Droppable, 8 | Draggable, 9 | OnDragEndResponder, 10 | } from "@hello-pangea/dnd"; 11 | 12 | import { useChatStore } from "../store"; 13 | 14 | import Locale from "../locales"; 15 | import { Link, useNavigate } from "react-router-dom"; 16 | import { Path } from "../constant"; 17 | import { MaskAvatar } from "./mask"; 18 | import { Mask } from "../store/mask"; 19 | import { useRef, useEffect } from "react"; 20 | 21 | export function ChatItem(props: { 22 | onClick?: () => void; 23 | onDelete?: () => void; 24 | title: string; 25 | count: number; 26 | time: string; 27 | selected: boolean; 28 | id: number; 29 | index: number; 30 | narrow?: boolean; 31 | mask: Mask; 32 | }) { 33 | const draggableRef = useRef(null); 34 | useEffect(() => { 35 | if (props.selected && draggableRef.current) { 36 | draggableRef.current?.scrollIntoView({ 37 | block: "center", 38 | }); 39 | } 40 | }, [props.selected]); 41 | return ( 42 | 43 | {(provided) => ( 44 |
{ 50 | draggableRef.current = ele; 51 | provided.innerRef(ele); 52 | }} 53 | {...provided.draggableProps} 54 | {...provided.dragHandleProps} 55 | title={`${props.title}\n${Locale.ChatItem.ChatItemCount( 56 | props.count, 57 | )}`} 58 | > 59 | {props.narrow ? ( 60 |
61 |
62 | 63 |
64 |
65 | {props.count} 66 |
67 |
68 | ) : ( 69 | <> 70 |
{props.title}
71 |
72 |
73 | {Locale.ChatItem.ChatItemCount(props.count)} 74 |
75 |
{props.time}
76 |
77 | 78 | )} 79 | 80 |
84 | 85 |
86 |
87 | )} 88 |
89 | ); 90 | } 91 | 92 | export function ChatList(props: { narrow?: boolean }) { 93 | const [sessions, selectedIndex, selectSession, moveSession] = useChatStore( 94 | (state) => [ 95 | state.sessions, 96 | state.currentSessionIndex, 97 | state.selectSession, 98 | state.moveSession, 99 | ], 100 | ); 101 | const chatStore = useChatStore(); 102 | const navigate = useNavigate(); 103 | 104 | const onDragEnd: OnDragEndResponder = (result) => { 105 | const { destination, source } = result; 106 | if (!destination) { 107 | return; 108 | } 109 | 110 | if ( 111 | destination.droppableId === source.droppableId && 112 | destination.index === source.index 113 | ) { 114 | return; 115 | } 116 | 117 | moveSession(source.index, destination.index); 118 | }; 119 | 120 | return ( 121 | 122 | 123 | {(provided) => ( 124 |
129 | {sessions.map((item, i) => ( 130 | { 139 | navigate(Path.Chat); 140 | selectSession(i); 141 | }} 142 | onDelete={() => { 143 | if (!props.narrow || confirm(Locale.Home.DeleteChat)) { 144 | chatStore.deleteSession(i); 145 | } 146 | }} 147 | narrow={props.narrow} 148 | mask={item.mask} 149 | /> 150 | ))} 151 | {provided.placeholder} 152 |
153 | )} 154 |
155 |
156 | ); 157 | } 158 | -------------------------------------------------------------------------------- /app/components/chat.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .chat-input-actions { 4 | display: flex; 5 | flex-wrap: wrap; 6 | 7 | .chat-input-action { 8 | display: inline-flex; 9 | border-radius: 20px; 10 | font-size: 12px; 11 | background-color: var(--white); 12 | color: var(--black); 13 | border: var(--border-in-light); 14 | padding: 4px 10px; 15 | animation: slide-in ease 0.3s; 16 | box-shadow: var(--card-shadow); 17 | transition: all ease 0.3s; 18 | margin-bottom: 10px; 19 | align-items: center; 20 | height: 16px; 21 | width: var(--icon-width); 22 | 23 | &:not(:last-child) { 24 | margin-right: 5px; 25 | } 26 | 27 | .text { 28 | white-space: nowrap; 29 | padding-left: 5px; 30 | opacity: 0; 31 | transform: translateX(-5px); 32 | transition: all ease 0.3s; 33 | transition-delay: 0.1s; 34 | pointer-events: none; 35 | } 36 | 37 | &:hover { 38 | width: var(--full-width); 39 | 40 | .text { 41 | opacity: 1; 42 | transform: translate(0); 43 | } 44 | } 45 | 46 | .text, 47 | .icon { 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | } 52 | } 53 | } 54 | 55 | .prompt-toast { 56 | position: absolute; 57 | bottom: -50px; 58 | z-index: 999; 59 | display: flex; 60 | justify-content: center; 61 | width: calc(100% - 40px); 62 | 63 | .prompt-toast-inner { 64 | display: flex; 65 | justify-content: center; 66 | align-items: center; 67 | font-size: 12px; 68 | background-color: var(--white); 69 | color: var(--black); 70 | 71 | border: var(--border-in-light); 72 | box-shadow: var(--card-shadow); 73 | padding: 10px 20px; 74 | border-radius: 100px; 75 | 76 | animation: slide-in-from-top ease 0.3s; 77 | 78 | .prompt-toast-content { 79 | margin-left: 10px; 80 | } 81 | } 82 | } 83 | 84 | .section-title { 85 | font-size: 12px; 86 | font-weight: bold; 87 | margin-bottom: 10px; 88 | display: flex; 89 | justify-content: space-between; 90 | align-items: center; 91 | 92 | .section-title-action { 93 | display: flex; 94 | align-items: center; 95 | } 96 | } 97 | 98 | .context-prompt { 99 | .context-prompt-row { 100 | display: flex; 101 | justify-content: center; 102 | width: 100%; 103 | margin-bottom: 10px; 104 | 105 | .context-role { 106 | margin-right: 10px; 107 | } 108 | 109 | .context-content { 110 | flex: 1; 111 | max-width: 100%; 112 | text-align: left; 113 | } 114 | 115 | .context-delete-button { 116 | margin-left: 10px; 117 | } 118 | } 119 | 120 | .context-prompt-button { 121 | flex: 1; 122 | } 123 | } 124 | 125 | .memory-prompt { 126 | margin: 20px 0; 127 | 128 | .memory-prompt-content { 129 | background-color: var(--white); 130 | color: var(--black); 131 | border: var(--border-in-light); 132 | border-radius: 10px; 133 | padding: 10px; 134 | font-size: 12px; 135 | user-select: text; 136 | } 137 | } 138 | 139 | .clear-context { 140 | margin: 20px 0 0 0; 141 | padding: 4px 0; 142 | 143 | border-top: var(--border-in-light); 144 | border-bottom: var(--border-in-light); 145 | box-shadow: var(--card-shadow) inset; 146 | 147 | display: flex; 148 | justify-content: center; 149 | align-items: center; 150 | 151 | color: var(--black); 152 | transition: all ease 0.3s; 153 | cursor: pointer; 154 | overflow: hidden; 155 | position: relative; 156 | font-size: 12px; 157 | 158 | animation: slide-in ease 0.3s; 159 | 160 | $linear: linear-gradient( 161 | to right, 162 | rgba(0, 0, 0, 0), 163 | rgba(0, 0, 0, 1), 164 | rgba(0, 0, 0, 0) 165 | ); 166 | mask-image: $linear; 167 | 168 | @mixin show { 169 | transform: translateY(0); 170 | position: relative; 171 | transition: all ease 0.3s; 172 | opacity: 1; 173 | } 174 | 175 | @mixin hide { 176 | transform: translateY(-50%); 177 | position: absolute; 178 | transition: all ease 0.1s; 179 | opacity: 0; 180 | } 181 | 182 | &-tips { 183 | @include show; 184 | opacity: 0.5; 185 | } 186 | 187 | &-revert-btn { 188 | color: var(--primary); 189 | @include hide; 190 | } 191 | 192 | &:hover { 193 | opacity: 1; 194 | border-color: var(--primary); 195 | 196 | .clear-context-tips { 197 | @include hide; 198 | } 199 | 200 | .clear-context-revert-btn { 201 | @include show; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/components/dify-new-chat.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; 2 | 3 | import styles from "./settings.module.scss"; 4 | import SelectIcon from "../icons/select.svg"; 5 | 6 | import { 7 | Input, 8 | List, 9 | ListItem, 10 | Modal, 11 | PasswordInput, 12 | Popover, 13 | Select, 14 | } from "./ui-lib"; 15 | import { ModelConfigList } from "./model-config"; 16 | 17 | import { IconButton } from "./button"; 18 | import { 19 | SubmitKey, 20 | useChatStore, 21 | Theme, 22 | useUpdateStore, 23 | useAccessStore, 24 | useAppConfig, 25 | useDifyKeyStore, 26 | DifySearchService, 27 | } from "../store"; 28 | 29 | import Locale, { 30 | AllLangs, 31 | ALL_LANG_OPTIONS, 32 | changeLang, 33 | getLang, 34 | } from "../locales"; 35 | import { Prompt } from "../store/prompt"; 36 | import { useNavigate } from "react-router-dom"; 37 | import { Path } from "../constant"; 38 | export function DifyNewChat(props: { onClose?: () => void }) { 39 | const chatStore = useChatStore(); 40 | const difyKeyStore = useDifyKeyStore(); 41 | const allDifyKeys = difyKeyStore.getUserDifyKeys(); 42 | const [searchInput, setSearchInput] = useState(""); 43 | const [searchKeys, setSearchKeys] = useState([]); 44 | const difyKeys = searchInput.length > 0 ? searchKeys : allDifyKeys; 45 | const navigate = useNavigate(); 46 | 47 | useEffect(() => { 48 | if (searchInput.length > 0) { 49 | const searchResult = DifySearchService.search(searchInput); 50 | setSearchKeys(searchResult); 51 | } else { 52 | setSearchKeys([]); 53 | } 54 | }, [searchInput]); 55 | 56 | return ( 57 |
58 | props.onClose?.()} 61 | > 62 |
63 | setSearchInput(e.currentTarget.value)} 69 | > 70 | 71 |
72 | {difyKeys.map((key, _) => ( 73 |
77 |
78 |
{key.title}
79 |
80 | {key.content} 81 |
82 |
83 | 84 |
85 | } 87 | className={styles["user-prompt-button"]} 88 | text={Locale.Chat.SelectDifyKey} 89 | onClick={() => { 90 | chatStore.newSession(undefined, true, key.content); 91 | props.onClose?.(); 92 | navigate(Path.Chat); 93 | }} 94 | /> 95 |
96 |
97 | ))} 98 |
99 |
100 |
101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /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 { downloadAs } from "../utils"; 8 | 9 | interface IErrorBoundaryState { 10 | hasError: boolean; 11 | error: Error | null; 12 | info: React.ErrorInfo | null; 13 | } 14 | 15 | export class ErrorBoundary extends React.Component { 16 | constructor(props: any) { 17 | super(props); 18 | this.state = { hasError: false, error: null, info: null }; 19 | } 20 | 21 | componentDidCatch(error: Error, info: React.ErrorInfo) { 22 | // Update state with error details 23 | this.setState({ hasError: true, error, info }); 24 | } 25 | 26 | clearAndSaveData() { 27 | try { 28 | downloadAs( 29 | JSON.stringify(localStorage), 30 | "chatgpt-next-web-snapshot.json", 31 | ); 32 | } finally { 33 | localStorage.clear(); 34 | location.reload(); 35 | } 36 | } 37 | 38 | render() { 39 | if (this.state.hasError) { 40 | // Render error message 41 | return ( 42 |
43 |

Oops, something went wrong!

44 |
45 |             {this.state.error?.toString()}
46 |             {this.state.info?.componentStack}
47 |           
48 | 49 |
50 | 51 | } 54 | bordered 55 | /> 56 | 57 | } 59 | text="Clear All Data" 60 | onClick={() => 61 | confirm(Locale.Settings.Actions.ConfirmClearAll) && 62 | this.clearAndSaveData() 63 | } 64 | bordered 65 | /> 66 |
67 |
68 | ); 69 | } 70 | // if no error occurred, render children 71 | return this.props.children; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/exporter.module.scss: -------------------------------------------------------------------------------- 1 | .message-exporter { 2 | &-body { 3 | margin-top: 20px; 4 | } 5 | } 6 | 7 | .export-content { 8 | white-space: break-spaces; 9 | padding: 10px !important; 10 | } 11 | 12 | .steps { 13 | background-color: var(--gray); 14 | border-radius: 10px; 15 | overflow: hidden; 16 | padding: 5px; 17 | position: relative; 18 | box-shadow: var(--card-shadow) inset; 19 | 20 | .steps-progress { 21 | $padding: 5px; 22 | height: calc(100% - 2 * $padding); 23 | width: calc(100% - 2 * $padding); 24 | position: absolute; 25 | top: $padding; 26 | left: $padding; 27 | 28 | &-inner { 29 | box-sizing: border-box; 30 | box-shadow: var(--card-shadow); 31 | border: var(--border-in-light); 32 | content: ""; 33 | display: inline-block; 34 | width: 0%; 35 | height: 100%; 36 | background-color: var(--white); 37 | transition: all ease 0.3s; 38 | border-radius: 8px; 39 | } 40 | } 41 | 42 | .steps-inner { 43 | display: flex; 44 | transform: scale(1); 45 | 46 | .step { 47 | flex-grow: 1; 48 | padding: 5px 10px; 49 | font-size: 14px; 50 | color: var(--black); 51 | opacity: 0.5; 52 | transition: all ease 0.3s; 53 | 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | 58 | $radius: 8px; 59 | 60 | &-finished { 61 | opacity: 0.9; 62 | } 63 | 64 | &:hover { 65 | opacity: 0.8; 66 | } 67 | 68 | &-current { 69 | color: var(--primary); 70 | } 71 | 72 | .step-index { 73 | background-color: var(--gray); 74 | border: var(--border-in-light); 75 | border-radius: 6px; 76 | display: inline-block; 77 | padding: 0px 5px; 78 | font-size: 12px; 79 | margin-right: 8px; 80 | opacity: 0.8; 81 | } 82 | 83 | .step-name { 84 | font-size: 12px; 85 | } 86 | } 87 | } 88 | } 89 | 90 | .preview-actions { 91 | margin-bottom: 20px; 92 | display: flex; 93 | justify-content: space-between; 94 | 95 | button { 96 | flex-grow: 1; 97 | &:not(:last-child) { 98 | margin-right: 10px; 99 | } 100 | } 101 | } 102 | 103 | .image-previewer { 104 | .preview-body { 105 | border-radius: 10px; 106 | padding: 20px; 107 | box-shadow: var(--card-shadow) inset; 108 | background-color: var(--gray); 109 | 110 | .chat-info { 111 | background-color: var(--second); 112 | padding: 20px; 113 | border-radius: 10px; 114 | margin-bottom: 20px; 115 | display: flex; 116 | justify-content: space-between; 117 | align-items: flex-end; 118 | position: relative; 119 | overflow: hidden; 120 | 121 | @media screen and (max-width: 600px) { 122 | flex-direction: column; 123 | align-items: flex-start; 124 | 125 | .icons { 126 | margin-bottom: 20px; 127 | } 128 | } 129 | 130 | .logo { 131 | position: absolute; 132 | top: 0px; 133 | left: 0px; 134 | height: 50%; 135 | transform: scale(1.5); 136 | } 137 | 138 | .main-title { 139 | font-size: 20px; 140 | font-weight: bolder; 141 | } 142 | 143 | .sub-title { 144 | font-size: 12px; 145 | } 146 | 147 | .icons { 148 | margin-top: 10px; 149 | display: flex; 150 | align-items: center; 151 | 152 | .icon-space { 153 | font-size: 12px; 154 | margin: 0 10px; 155 | font-weight: bolder; 156 | color: var(--primary); 157 | } 158 | } 159 | 160 | .chat-info-item { 161 | font-size: 12px; 162 | color: var(--primary); 163 | padding: 2px 15px; 164 | border-radius: 10px; 165 | background-color: var(--white); 166 | box-shadow: var(--card-shadow); 167 | 168 | &:not(:last-child) { 169 | margin-bottom: 5px; 170 | } 171 | } 172 | } 173 | 174 | .message { 175 | margin-bottom: 20px; 176 | display: flex; 177 | 178 | .avatar { 179 | margin-right: 10px; 180 | } 181 | 182 | .body { 183 | border-radius: 10px; 184 | padding: 8px 10px; 185 | max-width: calc(100% - 104px); 186 | box-shadow: var(--card-shadow); 187 | border: var(--border-in-light); 188 | 189 | * { 190 | overflow: hidden; 191 | } 192 | } 193 | 194 | &-assistant { 195 | .body { 196 | background-color: var(--white); 197 | } 198 | } 199 | 200 | &-user { 201 | flex-direction: row-reverse; 202 | 203 | .avatar { 204 | margin-right: 0; 205 | } 206 | 207 | .body { 208 | background-color: var(--second); 209 | margin-right: 10px; 210 | } 211 | } 212 | } 213 | } 214 | 215 | .default-theme { 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /app/components/input-range.module.scss: -------------------------------------------------------------------------------- 1 | .input-range { 2 | border: var(--border-in-light); 3 | border-radius: 10px; 4 | padding: 5px 15px 5px 10px; 5 | font-size: 12px; 6 | display: flex; 7 | max-width: 40%; 8 | 9 | input[type="range"] { 10 | max-width: calc(100% - 50px); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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-grow: 1; 62 | max-width: calc(100% - 40px); 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 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 | export const getBuildConfig = () => { 2 | if (typeof process === "undefined") { 3 | throw Error( 4 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 5 | ); 6 | } 7 | 8 | const COMMIT_ID: string = (() => { 9 | try { 10 | const childProcess = require("child_process"); 11 | return childProcess 12 | .execSync('git log -1 --format="%at000" --date=unix') 13 | .toString() 14 | .trim(); 15 | } catch (e) { 16 | console.error("[Build Config] No git or not from git repo."); 17 | return "unknown"; 18 | } 19 | })(); 20 | 21 | return { 22 | commitId: COMMIT_ID, 23 | buildMode: process.env.BUILD_MODE ?? "standalone", 24 | isApp: !!process.env.BUILD_APP, 25 | }; 26 | }; 27 | 28 | export type BuildConfig = ReturnType; 29 | -------------------------------------------------------------------------------- /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 | 3 | declare global { 4 | namespace NodeJS { 5 | interface ProcessEnv { 6 | OPENAI_API_KEY?: string; 7 | CODE?: string; 8 | BASE_URL?: string; 9 | PROXY_URL?: string; 10 | VERCEL?: string; 11 | HIDE_USER_API_KEY?: string; // disable user's api key input 12 | DISABLE_GPT4?: string; // allow user to use gpt-4 or not 13 | BUILD_MODE?: "standalone" | "export"; 14 | BUILD_APP?: string; // is building desktop app 15 | } 16 | } 17 | } 18 | 19 | const ACCESS_CODES = (function getAccessCodes(): Set { 20 | const code = process.env.CODE; 21 | 22 | try { 23 | const codes = (code?.split(",") ?? []) 24 | .filter((v) => !!v) 25 | .map((v) => md5.hash(v.trim())); 26 | return new Set(codes); 27 | } catch (e) { 28 | return new Set(); 29 | } 30 | })(); 31 | 32 | export const getServerSideConfig = () => { 33 | if (typeof process === "undefined") { 34 | throw Error( 35 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 36 | ); 37 | } 38 | 39 | return { 40 | apiKey: process.env.OPENAI_API_KEY, 41 | difyApiKey: process.env.DIFY_API_KEY, 42 | code: process.env.CODE, 43 | codes: ACCESS_CODES, 44 | needCode: ACCESS_CODES.size > 0, 45 | baseUrl: process.env.BASE_URL, 46 | proxyUrl: process.env.PROXY_URL, 47 | isVercel: !!process.env.VERCEL, 48 | hideUserApiKey: !!process.env.HIDE_USER_API_KEY, 49 | enableGPT4: !process.env.DISABLE_GPT4, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /app/constant.ts: -------------------------------------------------------------------------------- 1 | export const OWNER = "Yidadaa"; 2 | export const REPO = "ChatGPT-Next-Web"; 3 | export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; 4 | export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; 5 | export const UPDATE_URL = `${REPO_URL}#keep-updated`; 6 | export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; 7 | export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; 8 | export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; 9 | export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; 10 | 11 | export enum Path { 12 | Home = "/", 13 | Chat = "/chat", 14 | Settings = "/settings", 15 | NewChat = "/new-chat", 16 | Masks = "/masks", 17 | Auth = "/auth", 18 | } 19 | 20 | export enum SlotID { 21 | AppBody = "app-body", 22 | } 23 | 24 | export enum FileName { 25 | Masks = "masks.json", 26 | Prompts = "prompts.json", 27 | } 28 | 29 | export enum StoreKey { 30 | Chat = "chat-next-web-store", 31 | Access = "access-control", 32 | Config = "app-config", 33 | Mask = "mask-store", 34 | Prompt = "prompt-store", 35 | Update = "chat-update", 36 | DifyKey = "dify-key-store", 37 | } 38 | 39 | export const MAX_SIDEBAR_WIDTH = 500; 40 | export const MIN_SIDEBAR_WIDTH = 230; 41 | export const NARROW_SIDEBAR_WIDTH = 100; 42 | 43 | export const ACCESS_CODE_PREFIX = "ak-"; 44 | export const DIFY_KEY_PREFIX = "app-"; 45 | 46 | export const LAST_INPUT_KEY = "last-input"; 47 | 48 | export const REQUEST_TIMEOUT_MS = 60000; 49 | 50 | export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; 51 | 52 | export const OpenaiPath = { 53 | ChatPath: "v1/chat/completions", 54 | UsagePath: "dashboard/billing/usage", 55 | SubsPath: "dashboard/billing/subscription", 56 | }; 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/app/icons/bot.png -------------------------------------------------------------------------------- /app/icons/bot.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/break.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/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/app/icons/chatgpt.png -------------------------------------------------------------------------------- /app/icons/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | 7 | export const metadata = { 8 | title: "ChatGPT Next Web", 9 | description: "Your personal ChatGPT Chat Bot.", 10 | viewport: { 11 | width: "device-width", 12 | initialScale: 1, 13 | maximumScale: 1, 14 | }, 15 | themeColor: [ 16 | { media: "(prefers-color-scheme: light)", color: "#fafafa" }, 17 | { media: "(prefers-color-scheme: dark)", color: "#151515" }, 18 | ], 19 | appleWebApp: { 20 | title: "ChatGPT Next Web", 21 | statusBarStyle: "default", 22 | }, 23 | }; 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: { 28 | children: React.ReactNode; 29 | }) { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | {children} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/locales/index.ts: -------------------------------------------------------------------------------- 1 | import cn from "./cn"; 2 | import en from "./en"; 3 | import tw from "./tw"; 4 | import fr from "./fr"; 5 | import es from "./es"; 6 | import it from "./it"; 7 | import tr from "./tr"; 8 | import jp from "./jp"; 9 | import de from "./de"; 10 | import vi from "./vi"; 11 | import ru from "./ru"; 12 | import no from "./no"; 13 | import cs from "./cs"; 14 | import ko from "./ko"; 15 | import { merge } from "../utils/merge"; 16 | 17 | import type { LocaleType } from "./cn"; 18 | export type { LocaleType, PartialLocaleType } from "./cn"; 19 | 20 | const ALL_LANGS = { 21 | cn, 22 | en, 23 | tw, 24 | jp, 25 | ko, 26 | fr, 27 | es, 28 | it, 29 | tr, 30 | de, 31 | vi, 32 | ru, 33 | cs, 34 | no, 35 | }; 36 | 37 | export type Lang = keyof typeof ALL_LANGS; 38 | 39 | export const AllLangs = Object.keys(ALL_LANGS) as Lang[]; 40 | 41 | export const ALL_LANG_OPTIONS: Record = { 42 | cn: "简体中文", 43 | en: "English", 44 | tw: "繁體中文", 45 | jp: "日本語", 46 | ko: "한국어", 47 | fr: "Français", 48 | es: "Español", 49 | it: "Italiano", 50 | tr: "Türkçe", 51 | de: "Deutsch", 52 | vi: "Tiếng Việt", 53 | ru: "Русский", 54 | cs: "Čeština", 55 | no: "Nynorsk", 56 | }; 57 | 58 | const LANG_KEY = "lang"; 59 | const DEFAULT_LANG = "en"; 60 | 61 | const fallbackLang = en; 62 | const targetLang = ALL_LANGS[getLang()] as LocaleType; 63 | 64 | // if target lang missing some fields, it will use fallback lang string 65 | merge(fallbackLang, targetLang); 66 | 67 | export default fallbackLang as LocaleType; 68 | 69 | function getItem(key: string) { 70 | try { 71 | return localStorage.getItem(key); 72 | } catch { 73 | return null; 74 | } 75 | } 76 | 77 | function setItem(key: string, value: string) { 78 | try { 79 | localStorage.setItem(key, value); 80 | } catch {} 81 | } 82 | 83 | function getLanguage() { 84 | try { 85 | return navigator.language.toLowerCase(); 86 | } catch { 87 | return DEFAULT_LANG; 88 | } 89 | } 90 | 91 | export function getLang(): Lang { 92 | const savedLang = getItem(LANG_KEY); 93 | 94 | if (AllLangs.includes((savedLang ?? "") as Lang)) { 95 | return savedLang as Lang; 96 | } 97 | 98 | const lang = getLanguage(); 99 | 100 | for (const option of AllLangs) { 101 | if (lang.includes(option)) { 102 | return option; 103 | } 104 | } 105 | 106 | return DEFAULT_LANG; 107 | } 108 | 109 | export function changeLang(lang: Lang) { 110 | setItem(LANG_KEY, lang); 111 | location.reload(); 112 | } 113 | -------------------------------------------------------------------------------- /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?: number) { 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: Mask[] = [...CN_MASKS, ...EN_MASKS].map((m) => 25 | BUILTIN_MASK_STORE.add(m), 26 | ); 27 | -------------------------------------------------------------------------------- /app/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { type Mask } from "../store/mask"; 2 | 3 | export type BuiltinMask = Omit & { 4 | builtin: true; 5 | }; 6 | -------------------------------------------------------------------------------- /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 { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { DEFAULT_API_HOST, StoreKey } from "../constant"; 4 | import { getHeaders } from "../client/api"; 5 | import { BOT_HELLO } from "./chat"; 6 | import { ALL_MODELS } from "./config"; 7 | import { getClientConfig } from "../config/client"; 8 | 9 | export interface AccessControlStore { 10 | accessCode: string; 11 | token: string; 12 | difyToken: string; 13 | 14 | needCode: boolean; 15 | hideUserApiKey: boolean; 16 | openaiUrl: string; 17 | difyUrl: string; 18 | 19 | updateToken: (_: string) => void; 20 | updateDifyToken: (_: string) => void; 21 | updateCode: (_: string) => void; 22 | updateOpenAiUrl: (_: string) => void; 23 | enabledAccessControl: () => boolean; 24 | isAuthorized: () => boolean; 25 | // isDifyAuthorized: () => boolean; 26 | isAuthorizedWithoutDify: () => boolean; 27 | fetch: () => void; 28 | } 29 | 30 | let fetchState = 0; // 0 not fetch, 1 fetching, 2 done 31 | 32 | const DEFAULT_OPENAI_URL = 33 | getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; 34 | console.log("[API] default openai url", DEFAULT_OPENAI_URL); 35 | 36 | export const useAccessStore = create()( 37 | persist( 38 | (set, get) => ({ 39 | token: "", 40 | difyToken: "", 41 | accessCode: "", 42 | needCode: true, 43 | hideUserApiKey: false, 44 | openaiUrl: DEFAULT_OPENAI_URL, 45 | difyUrl: "/api/dify", 46 | 47 | enabledAccessControl() { 48 | get().fetch(); 49 | 50 | return get().needCode; 51 | }, 52 | updateCode(code: string) { 53 | set(() => ({ accessCode: code })); 54 | }, 55 | updateToken(token: string) { 56 | set(() => ({ token })); 57 | }, 58 | updateDifyToken(difyToken: string) { 59 | set(() => ({ difyToken })); 60 | }, 61 | updateOpenAiUrl(url: string) { 62 | set(() => ({ openaiUrl: url })); 63 | }, 64 | isAuthorized() { 65 | get().fetch(); 66 | 67 | // has token or has code or disabled access control 68 | return ( 69 | !!get().difyToken || 70 | !!get().token || 71 | !!get().accessCode || 72 | !get().enabledAccessControl() 73 | ); 74 | }, 75 | // isDifyAuthorized() { 76 | // get().fetch(); 77 | // return !!get().difyToken || !get().enabledAccessControl(); 78 | // }, 79 | isAuthorizedWithoutDify() { 80 | get().fetch(); 81 | return ( 82 | !!get().token || !!get().accessCode || !get().enabledAccessControl() 83 | ); 84 | }, 85 | fetch() { 86 | if (fetchState > 0 || getClientConfig()?.buildMode === "export") return; 87 | fetchState = 1; 88 | fetch("/api/config", { 89 | method: "post", 90 | body: null, 91 | headers: { 92 | ...getHeaders(), 93 | }, 94 | }) 95 | .then((res) => res.json()) 96 | .then((res: DangerConfig) => { 97 | console.log("[Config] got config from server", res); 98 | set(() => ({ ...res })); 99 | 100 | if (!res.enableGPT4) { 101 | ALL_MODELS.forEach((model) => { 102 | if (model.name.startsWith("gpt-4")) { 103 | (model as any).available = false; 104 | } 105 | }); 106 | } 107 | 108 | if ((res as any).botHello) { 109 | BOT_HELLO.content = (res as any).botHello; 110 | } 111 | }) 112 | .catch(() => { 113 | console.error("[Config] failed to fetch config"); 114 | }) 115 | .finally(() => { 116 | fetchState = 2; 117 | }); 118 | }, 119 | }), 120 | { 121 | name: StoreKey.Access, 122 | version: 1, 123 | }, 124 | ), 125 | ); 126 | -------------------------------------------------------------------------------- /app/store/config.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { getClientConfig } from "../config/client"; 4 | import { StoreKey } from "../constant"; 5 | 6 | export enum SubmitKey { 7 | Enter = "Enter", 8 | CtrlEnter = "Ctrl + Enter", 9 | ShiftEnter = "Shift + Enter", 10 | AltEnter = "Alt + Enter", 11 | MetaEnter = "Meta + Enter", 12 | } 13 | 14 | export enum Theme { 15 | Auto = "auto", 16 | Dark = "dark", 17 | Light = "light", 18 | } 19 | 20 | export const DEFAULT_CONFIG = { 21 | submitKey: SubmitKey.CtrlEnter as SubmitKey, 22 | avatar: "1f603", 23 | fontSize: 14, 24 | theme: Theme.Auto as Theme, 25 | tightBorder: !!getClientConfig()?.isApp, 26 | sendPreviewBubble: true, 27 | sidebarWidth: 300, 28 | 29 | disablePromptHint: false, 30 | 31 | dontShowMaskSplashScreen: false, // dont show splash screen when create chat 32 | 33 | modelConfig: { 34 | model: "gpt-3.5-turbo" as ModelType, 35 | temperature: 0.5, 36 | max_tokens: 2000, 37 | presence_penalty: 0, 38 | sendMemory: true, 39 | historyMessageCount: 4, 40 | compressMessageLengthThreshold: 1000, 41 | IsDifyEnabled: true, 42 | }, 43 | }; 44 | 45 | export type ChatConfig = typeof DEFAULT_CONFIG; 46 | 47 | export type ChatConfigStore = ChatConfig & { 48 | reset: () => void; 49 | update: (updater: (config: ChatConfig) => void) => void; 50 | }; 51 | 52 | export type ModelConfig = ChatConfig["modelConfig"]; 53 | 54 | const ENABLE_GPT4 = true; 55 | 56 | export const ALL_MODELS = [ 57 | { 58 | name: "gpt-4", 59 | available: ENABLE_GPT4, 60 | }, 61 | { 62 | name: "gpt-4-0314", 63 | available: ENABLE_GPT4, 64 | }, 65 | { 66 | name: "gpt-4-0613", 67 | available: ENABLE_GPT4, 68 | }, 69 | { 70 | name: "gpt-4-32k", 71 | available: ENABLE_GPT4, 72 | }, 73 | { 74 | name: "gpt-4-32k-0314", 75 | available: ENABLE_GPT4, 76 | }, 77 | { 78 | name: "gpt-4-32k-0613", 79 | available: ENABLE_GPT4, 80 | }, 81 | { 82 | name: "gpt-3.5-turbo", 83 | available: true, 84 | }, 85 | { 86 | name: "gpt-3.5-turbo-0301", 87 | available: true, 88 | }, 89 | { 90 | name: "gpt-3.5-turbo-0613", 91 | available: true, 92 | }, 93 | { 94 | name: "gpt-3.5-turbo-16k", 95 | available: true, 96 | }, 97 | { 98 | name: "gpt-3.5-turbo-16k-0613", 99 | available: true, 100 | }, 101 | { 102 | name: "qwen-v1", // 通义千问 103 | available: false, 104 | }, 105 | { 106 | name: "ernie", // 文心一言 107 | available: false, 108 | }, 109 | { 110 | name: "spark", // 讯飞星火 111 | available: false, 112 | }, 113 | { 114 | name: "llama", // llama 115 | available: false, 116 | }, 117 | { 118 | name: "chatglm", // chatglm-6b 119 | available: false, 120 | }, 121 | ] as const; 122 | 123 | export type ModelType = (typeof ALL_MODELS)[number]["name"]; 124 | 125 | export function limitNumber( 126 | x: number, 127 | min: number, 128 | max: number, 129 | defaultValue: number, 130 | ) { 131 | if (typeof x !== "number" || isNaN(x)) { 132 | return defaultValue; 133 | } 134 | 135 | return Math.min(max, Math.max(min, x)); 136 | } 137 | 138 | export function limitModel(name: string) { 139 | return ALL_MODELS.some((m) => m.name === name && m.available) 140 | ? name 141 | : "gpt-3.5-turbo"; 142 | } 143 | 144 | export const ModalConfigValidator = { 145 | model(x: string) { 146 | return limitModel(x) as ModelType; 147 | }, 148 | max_tokens(x: number) { 149 | return limitNumber(x, 0, 32000, 2000); 150 | }, 151 | presence_penalty(x: number) { 152 | return limitNumber(x, -2, 2, 0); 153 | }, 154 | temperature(x: number) { 155 | return limitNumber(x, 0, 1, 1); 156 | }, 157 | }; 158 | 159 | export const useAppConfig = create()( 160 | persist( 161 | (set, get) => ({ 162 | ...DEFAULT_CONFIG, 163 | 164 | reset() { 165 | set(() => ({ ...DEFAULT_CONFIG })); 166 | }, 167 | 168 | update(updater) { 169 | const config = { ...get() }; 170 | updater(config); 171 | set(() => config); 172 | }, 173 | }), 174 | { 175 | name: StoreKey.Config, 176 | version: 2, 177 | migrate(persistedState, version) { 178 | if (version === 2) return persistedState as any; 179 | 180 | const state = persistedState as ChatConfig; 181 | state.modelConfig.sendMemory = true; 182 | state.modelConfig.historyMessageCount = 4; 183 | state.modelConfig.compressMessageLengthThreshold = 1000; 184 | state.dontShowMaskSplashScreen = false; 185 | 186 | return state; 187 | }, 188 | }, 189 | ), 190 | ); 191 | -------------------------------------------------------------------------------- /app/store/difyKey.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import Fuse from "fuse.js"; 4 | import { getLang } from "../locales"; 5 | import { StoreKey } from "../constant"; 6 | 7 | export interface DifyKey { 8 | id?: number; 9 | isUser?: boolean; 10 | title: string; 11 | content: string; 12 | } 13 | 14 | export interface DifyKeyStore { 15 | counter: number; 16 | latestId: number; 17 | difyKeys: Record; 18 | 19 | add: (aDifyKey: DifyKey) => number; 20 | get: (id: number) => DifyKey | undefined; 21 | remove: (id: number) => void; 22 | search: (text: string) => DifyKey[]; 23 | update: (id: number, updater: (prompt: DifyKey) => void) => void; 24 | 25 | getUserDifyKeys: () => DifyKey[]; 26 | } 27 | 28 | export const DifySearchService = { 29 | ready: false, 30 | // builtinEngine: new Fuse([], { keys: ["title"] }), 31 | userEngine: new Fuse([], { keys: ["title"] }), 32 | count: 0, 33 | userDifyKeys: [] as DifyKey[], 34 | 35 | init(userDifyKeys: DifyKey[]) { 36 | if (this.ready) { 37 | return; 38 | } 39 | (this.userDifyKeys = userDifyKeys.slice()), 40 | this.userEngine.setCollection(userDifyKeys); 41 | this.ready = true; 42 | }, 43 | 44 | remove(id: number) { 45 | this.userEngine.remove((doc) => doc.id === id); 46 | }, 47 | 48 | add(aDifyKey: DifyKey) { 49 | this.userEngine.add(aDifyKey); 50 | }, 51 | 52 | search(text: string) { 53 | const userResults = this.userEngine.search(text); 54 | return userResults.map((v) => v.item); 55 | }, 56 | }; 57 | 58 | export const useDifyKeyStore = create()( 59 | persist( 60 | (set, get) => ({ 61 | counter: 0, 62 | latestId: 0, 63 | difyKeys: {}, 64 | 65 | add(difyKey) { 66 | const difyKeys = get().difyKeys; 67 | difyKey.id = get().latestId + 1; 68 | difyKey.isUser = true; 69 | difyKeys[difyKey.id] = difyKey; 70 | 71 | set(() => ({ 72 | latestId: difyKey.id!, 73 | difyKeys: difyKeys, 74 | })); 75 | 76 | return difyKey.id!; 77 | }, 78 | 79 | get(id) { 80 | const targetPrompt = get().difyKeys[id]; 81 | 82 | // if (!targetPrompt) { 83 | // return SearchService.builtinPrompts.find((v) => v.id === id); 84 | // } 85 | 86 | return targetPrompt; 87 | }, 88 | 89 | remove(id) { 90 | const difyKeys = get().difyKeys; 91 | delete difyKeys[id]; 92 | DifySearchService.remove(id); 93 | 94 | set(() => ({ 95 | difyKeys: difyKeys, 96 | counter: get().counter + 1, 97 | })); 98 | }, 99 | 100 | getUserDifyKeys() { 101 | const userDifyKeys = Object.values(get().difyKeys ?? {}); 102 | userDifyKeys.sort((a, b) => (b.id && a.id ? b.id - a.id : 0)); 103 | return userDifyKeys; 104 | }, 105 | 106 | update(id: number, updater) { 107 | const prompt = get().difyKeys[id] ?? { 108 | title: "", 109 | content: "", 110 | id, 111 | }; 112 | 113 | DifySearchService.remove(id); 114 | updater(prompt); 115 | const prompts = get().difyKeys; 116 | prompts[id] = prompt; 117 | set(() => ({ difyKeys: prompts })); 118 | DifySearchService.add(prompt); 119 | }, 120 | 121 | search(text) { 122 | if (text.length === 0) { 123 | return DifySearchService.userDifyKeys; 124 | } 125 | return DifySearchService.search(text) as DifyKey[]; 126 | }, 127 | }), 128 | { 129 | name: StoreKey.DifyKey, 130 | version: 1, 131 | onRehydrateStorage() { 132 | const PROMPT_URL = "./prompts.json"; 133 | 134 | type PromptList = Array<[string, string]>; 135 | fetch(PROMPT_URL) 136 | .then((res) => res.json()) 137 | .then((res) => { 138 | let fetchPrompts = [res.en, res.cn]; 139 | if (getLang() === "cn") { 140 | fetchPrompts = fetchPrompts.reverse(); 141 | } 142 | // const builtinPrompts = fetchPrompts.map( 143 | // (promptList: PromptList) => { 144 | // return promptList.map( 145 | // ([title, content]) => 146 | // ({ 147 | // id: Math.random(), 148 | // title, 149 | // content, 150 | // } as Prompt), 151 | // ); 152 | }); 153 | setTimeout(() => {}, 2000); 154 | const userDifyKeys = [] as DifyKey[]; 155 | DifySearchService.count = userDifyKeys.length; 156 | DifySearchService.init(userDifyKeys); 157 | }, 158 | }, 159 | ), 160 | ); 161 | -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | export * from "./config"; 5 | export * from "./difyKey"; 6 | -------------------------------------------------------------------------------- /app/store/mask.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { BUILTIN_MASKS } from "../masks"; 4 | import { getLang, Lang } from "../locales"; 5 | import { DEFAULT_TOPIC, ChatMessage } from "./chat"; 6 | import { ModelConfig, ModelType, useAppConfig } from "./config"; 7 | import { StoreKey } from "../constant"; 8 | 9 | export type Mask = { 10 | id: number; 11 | avatar: string; 12 | name: string; 13 | hideContext?: boolean; 14 | context: ChatMessage[]; 15 | syncGlobalConfig?: boolean; 16 | modelConfig: Omit; 17 | lang: Lang; 18 | builtin: boolean; 19 | }; 20 | 21 | export const DEFAULT_MASK_STATE = { 22 | masks: {} as Record, 23 | globalMaskId: 0, 24 | }; 25 | 26 | export type MaskState = typeof DEFAULT_MASK_STATE; 27 | type MaskStore = MaskState & { 28 | create: (mask?: Partial) => Mask; 29 | update: (id: number, updater: (mask: Mask) => void) => void; 30 | delete: (id: number) => void; 31 | search: (text: string) => Mask[]; 32 | get: (id?: number) => Mask | null; 33 | getAll: () => Mask[]; 34 | }; 35 | 36 | export const DEFAULT_MASK_ID = 1145141919810; 37 | export const DEFAULT_MASK_AVATAR = "gpt-bot"; 38 | export const createEmptyMask = () => 39 | ({ 40 | id: DEFAULT_MASK_ID, 41 | avatar: DEFAULT_MASK_AVATAR, 42 | name: DEFAULT_TOPIC, 43 | context: [], 44 | syncGlobalConfig: true, // use global config as default 45 | modelConfig: { ...useAppConfig.getState().modelConfig }, 46 | lang: getLang(), 47 | builtin: false, 48 | } as Mask); 49 | 50 | export const useMaskStore = create()( 51 | persist( 52 | (set, get) => ({ 53 | ...DEFAULT_MASK_STATE, 54 | 55 | create(mask) { 56 | set(() => ({ globalMaskId: get().globalMaskId + 1 })); 57 | const id = get().globalMaskId; 58 | const masks = get().masks; 59 | masks[id] = { 60 | ...createEmptyMask(), 61 | ...mask, 62 | id, 63 | builtin: false, 64 | }; 65 | 66 | set(() => ({ masks })); 67 | 68 | return masks[id]; 69 | }, 70 | update(id, updater) { 71 | const masks = get().masks; 72 | const mask = masks[id]; 73 | if (!mask) return; 74 | const updateMask = { ...mask }; 75 | updater(updateMask); 76 | masks[id] = updateMask; 77 | set(() => ({ masks })); 78 | }, 79 | delete(id) { 80 | const masks = get().masks; 81 | delete masks[id]; 82 | set(() => ({ masks })); 83 | }, 84 | 85 | get(id) { 86 | return get().masks[id ?? 1145141919810]; 87 | }, 88 | getAll() { 89 | const userMasks = Object.values(get().masks).sort( 90 | (a, b) => b.id - a.id, 91 | ); 92 | return userMasks.concat(BUILTIN_MASKS); 93 | }, 94 | search(text) { 95 | return Object.values(get().masks); 96 | }, 97 | }), 98 | { 99 | name: StoreKey.Mask, 100 | version: 2, 101 | }, 102 | ), 103 | ); 104 | -------------------------------------------------------------------------------- /app/store/update.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { FETCH_COMMIT_URL, StoreKey } from "../constant"; 4 | import { api } from "../client/api"; 5 | import { getClientConfig } from "../config/client"; 6 | 7 | export interface UpdateStore { 8 | lastUpdate: number; 9 | remoteVersion: string; 10 | 11 | used?: number; 12 | subscription?: number; 13 | lastUpdateUsage: number; 14 | 15 | version: string; 16 | getLatestVersion: (force?: boolean) => Promise; 17 | updateUsage: (force?: boolean) => Promise; 18 | } 19 | 20 | const ONE_MINUTE = 60 * 1000; 21 | 22 | export const useUpdateStore = create()( 23 | persist( 24 | (set, get) => ({ 25 | lastUpdate: 0, 26 | remoteVersion: "", 27 | 28 | lastUpdateUsage: 0, 29 | 30 | version: "unknown", 31 | 32 | async getLatestVersion(force = false) { 33 | set(() => ({ version: getClientConfig()?.commitId ?? "unknown" })); 34 | 35 | const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE; 36 | if (!force && !overTenMins) return; 37 | 38 | set(() => ({ 39 | lastUpdate: Date.now(), 40 | })); 41 | 42 | try { 43 | const data = await (await fetch(FETCH_COMMIT_URL)).json(); 44 | const remoteCommitTime = data[0].commit.committer.date; 45 | const remoteId = new Date(remoteCommitTime).getTime().toString(); 46 | set(() => ({ 47 | remoteVersion: remoteId, 48 | })); 49 | console.log("[Got Upstream] ", remoteId); 50 | } catch (error) { 51 | console.error("[Fetch Upstream Commit Id]", error); 52 | } 53 | }, 54 | 55 | async updateUsage(force = false) { 56 | const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; 57 | if (!overOneMinute && !force) return; 58 | 59 | set(() => ({ 60 | lastUpdateUsage: Date.now(), 61 | })); 62 | 63 | try { 64 | const usage = await api.llm.usage(); 65 | 66 | if (usage) { 67 | set(() => ({ 68 | used: usage.used, 69 | subscription: usage.total, 70 | })); 71 | } 72 | } catch (e) { 73 | console.error((e as Error).message); 74 | } 75 | }, 76 | }), 77 | { 78 | name: StoreKey.Update, 79 | version: 1, 80 | }, 81 | ), 82 | ); 83 | -------------------------------------------------------------------------------- /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 | margin-top: 5px; 28 | } 29 | } 30 | 31 | .window-actions { 32 | display: inline-flex; 33 | } 34 | 35 | .window-action-button { 36 | margin-left: 10px; 37 | } 38 | -------------------------------------------------------------------------------- /app/typing.ts: -------------------------------------------------------------------------------- 1 | export type Updater = (updater: (value: T) => void) => void; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/utils/merge.ts: -------------------------------------------------------------------------------- 1 | export function merge(target: any, source: any) { 2 | Object.keys(source).forEach(function (key) { 3 | if (source[key] && typeof source[key] === "object") { 4 | merge((target[key] = target[key] || {}), source[key]); 5 | return; 6 | } 7 | target[key] = source[key]; 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /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 | 17 | chatgpt-next-web-proxy: 18 | profiles: ["proxy"] 19 | container_name: chatgpt-next-web-proxy 20 | image: yidadaa/chatgpt-next-web 21 | ports: 22 | - 3000:3000 23 | environment: 24 | - OPENAI_API_KEY=$OPENAI_API_KEY 25 | - CODE=$CODE 26 | - PROXY_URL=$PROXY_URL 27 | - BASE_URL=$BASE_URL 28 | - OPENAI_ORG_ID=$OPENAI_ORG_ID 29 | - HIDE_USER_API_KEY=$HIDE_USER_API_KEY 30 | - DISABLE_GPT4=$DISABLE_GPT4 31 | -------------------------------------------------------------------------------- /docs/cloudflare-pages-cn.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages 部署指南 2 | 3 | ## 如何新建项目 4 | 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 5 | 6 | 1. 点击 "Create a project"。 7 | 2. 选择 "Connect to Git"。 8 | 3. 关联 Cloudflare Pages 和你的 GitHub 账号。 9 | 4. 选中你 fork 的此项目。 10 | 5. 点击 "Begin setup"。 11 | 6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。 12 | 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 13 | 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "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 | 21 | - `NODE_VERSION=20.1` 22 | - `NEXT_TELEMETRY_DISABLE=1` 23 | - `OPENAI_API_KEY=你自己的API Key` 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 Key` 32 | - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` 33 | 34 | 12. 点击 "Save and Deploy"。 35 | 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 36 | 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 37 | 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 38 | 16. 前往 "Deployments",点击 "Retry deployment"。 39 | 17. Enjoy. -------------------------------------------------------------------------------- /docs/cloudflare-pages-en.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Pages Deployment Guide 2 | 3 | ## How to create a new project 4 | Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 5 | 6 | 1. Click "Create a project". 7 | 2. Choose "Connect to Git". 8 | 3. Connect Cloudflare Pages to your GitHub account. 9 | 4. Select the forked project. 10 | 5. Click "Begin setup". 11 | 6. For "Project name" and "Production branch", use the default values or change them as needed. 12 | 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 13 | 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following 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. For "Build output directory", use the default value and do not modify it. 18 | 10. Do not modify "Root Directory". 19 | 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: 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 | Optionally fill in the following based on your needs: 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. Click "Save and Deploy". 34 | 13. Click "Cancel deployment" because you need to fill in Compatibility flags. 35 | 14. Go to "Build settings", "Functions", and find "Compatibility flags". 36 | 15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". 37 | 16. Go to "Deployments" and click "Retry deployment". 38 | 17. Enjoy. -------------------------------------------------------------------------------- /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/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/cover.png -------------------------------------------------------------------------------- /docs/images/enable-actions-sync.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/enable-actions-sync.jpg -------------------------------------------------------------------------------- /docs/images/enable-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/enable-actions.jpg -------------------------------------------------------------------------------- /docs/images/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/more.png -------------------------------------------------------------------------------- /docs/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/settings.png -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/vercel/vercel-create-1.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/vercel/vercel-create-2.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/vercel/vercel-create-3.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-env-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/vercel/vercel-env-edit.jpg -------------------------------------------------------------------------------- /docs/images/vercel/vercel-redeploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/docs/images/vercel/vercel-redeploy.jpg -------------------------------------------------------------------------------- /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 的环境变量; 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 即可重新部署。 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | const mode = process.env.BUILD_MODE ?? "standalone"; 2 | console.log("[Next] build mode", mode); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | webpack(config) { 7 | config.module.rules.push({ 8 | test: /\.svg$/, 9 | use: ["@svgr/webpack"], 10 | }); 11 | 12 | return config; 13 | }, 14 | output: mode, 15 | images: { 16 | unoptimized: mode === "export", 17 | }, 18 | }; 19 | 20 | if (mode !== "export") { 21 | nextConfig.headers = async () => { 22 | return [ 23 | { 24 | source: "/api/:path*", 25 | headers: [ 26 | { key: "Access-Control-Allow-Credentials", value: "true" }, 27 | { key: "Access-Control-Allow-Origin", value: "*" }, 28 | { 29 | key: "Access-Control-Allow-Methods", 30 | value: "*", 31 | }, 32 | { 33 | key: "Access-Control-Allow-Headers", 34 | value: "*", 35 | }, 36 | { 37 | key: "Access-Control-Max-Age", 38 | value: "86400", 39 | }, 40 | ], 41 | }, 42 | ]; 43 | }; 44 | 45 | nextConfig.rewrites = async () => { 46 | const ret = [ 47 | { 48 | source: "/api/proxy/:path*", 49 | destination: "https://api.openai.com/:path*", 50 | }, 51 | { 52 | source: "/google-fonts/:path*", 53 | destination: "https://fonts.googleapis.com/:path*", 54 | }, 55 | { 56 | source: "/sharegpt", 57 | destination: "https://sharegpt.com/api/conversations", 58 | }, 59 | ]; 60 | 61 | const apiUrl = process.env.API_URL; 62 | if (apiUrl) { 63 | console.log("[Next] using api url ", apiUrl); 64 | ret.push({ 65 | source: "/api/:path*", 66 | destination: `${apiUrl}/:path*`, 67 | }); 68 | } 69 | 70 | return { 71 | beforeFiles: ret, 72 | }; 73 | }; 74 | } 75 | 76 | export default nextConfig; 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-next-web", 3 | "private": false, 4 | "license": "mit", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "export": "cross-env BUILD_MODE=export BUILD_APP=1 yarn build", 11 | "export:dev": "cross-env BUILD_MODE=export BUILD_APP=1 yarn 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.2.0", 21 | "@svgr/webpack": "^6.5.1", 22 | "@vercel/analytics": "^0.1.11", 23 | "emoji-picker-react": "^4.4.7", 24 | "fuse.js": "^6.6.2", 25 | "html-to-image": "^1.11.11", 26 | "nanoid": "^4.0.2", 27 | "mermaid": "^10.2.3", 28 | "next": "^13.4.3", 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.10.0", 34 | "rehype-highlight": "^6.0.0", 35 | "rehype-katex": "^6.0.2", 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.6" 43 | }, 44 | "devDependencies": { 45 | "@tauri-apps/cli": "^1.3.1", 46 | "@types/node": "^20.3.1", 47 | "@types/react": "^18.0.28", 48 | "@types/react-dom": "^18.0.11", 49 | "@types/react-katex": "^3.0.0", 50 | "@types/spark-md5": "^3.0.2", 51 | "cross-env": "^7.0.3", 52 | "eslint": "^8.36.0", 53 | "eslint-config-next": "13.2.3", 54 | "eslint-config-prettier": "^8.8.0", 55 | "eslint-plugin-prettier": "^4.2.1", 56 | "husky": "^8.0.0", 57 | "lint-staged": "^13.2.0", 58 | "prettier": "^2.8.7", 59 | "typescript": "4.9.5" 60 | }, 61 | "resolutions": { 62 | "lint-staged/yaml": "^2.2.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/public/favicon.ico -------------------------------------------------------------------------------- /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\")$ ]]; 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=centos" ]]; then 36 | sudo yum -y install epel-release 37 | sudo yum -y install nodejs git yarn 38 | elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=arch" ]]; then 39 | sudo pacman -Syu -y 40 | sudo pacman -S -y nodejs git yarn 41 | else 42 | echo "Unsupported Linux distribution" 43 | exit 1 44 | fi 45 | ;; 46 | Darwin) 47 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 48 | brew install node git yarn 49 | ;; 50 | esac 51 | fi 52 | 53 | # Clone the repository and install dependencies 54 | git clone https://github.com/Yidadaa/ChatGPT-Next-Web 55 | cd ChatGPT-Next-Web 56 | yarn install 57 | 58 | # Prompt user for environment variables 59 | read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY 60 | read -p "Enter CODE: " CODE 61 | read -p "Enter PORT: " PORT 62 | 63 | # Build and run the project using the environment variables 64 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build 65 | OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start 66 | -------------------------------------------------------------------------------- /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 = ["clipboard-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 | 22 | [features] 23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 24 | # 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. 25 | # DO NOT REMOVE!! 26 | custom-protocol = [ "tauri/custom-protocol" ] 27 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiangYain/Dify-Next-Web/4faf83634e2d04e8b053dbb0a74af0a3c79bbe50/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 | .run(tauri::generate_context!()) 7 | .expect("error while running tauri application"); 8 | } 9 | -------------------------------------------------------------------------------- /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 | }, 9 | "package": { 10 | "productName": "chatgpt-next-web", 11 | "version": "2.8.2" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "all": false, 16 | "shell": { 17 | "all": false, 18 | "open": true 19 | }, 20 | "clipboard": { 21 | "all": true 22 | }, 23 | "window": { 24 | "all": false, 25 | "close": true, 26 | "hide": true, 27 | "maximize": true, 28 | "minimize": true, 29 | "setIcon": true, 30 | "setIgnoreCursorEvents": true, 31 | "setResizable": true, 32 | "show": true, 33 | "startDragging": true, 34 | "unmaximize": true, 35 | "unminimize": true 36 | } 37 | }, 38 | "bundle": { 39 | "active": true, 40 | "category": "DeveloperTool", 41 | "copyright": "2023, Zhang Yifei All Rights Reserved.", 42 | "deb": { 43 | "depends": [] 44 | }, 45 | "externalBin": [], 46 | "icon": [ 47 | "icons/32x32.png", 48 | "icons/128x128.png", 49 | "icons/128x128@2x.png", 50 | "icons/icon.icns", 51 | "icons/icon.ico" 52 | ], 53 | "identifier": "com.yida.chatgpt.next.web", 54 | "longDescription": "ChatGPT Next Web is a cross-platform ChatGPT client, including Web/Win/Linux/OSX/PWA.", 55 | "macOS": { 56 | "entitlements": null, 57 | "exceptionDomain": "", 58 | "frameworks": [], 59 | "providerShortName": null, 60 | "signingIdentity": null 61 | }, 62 | "resources": [], 63 | "shortDescription": "ChatGPT Next Web App", 64 | "targets": "all", 65 | "windows": { 66 | "certificateThumbprint": null, 67 | "digestAlgorithm": "sha256", 68 | "timestampUrl": "" 69 | } 70 | }, 71 | "security": { 72 | "csp": null 73 | }, 74 | "updater": { 75 | "active": true, 76 | "endpoints": [ 77 | "https://github.com/Yidadaa/ChatGPT-Next-Web/releases/download/{{current_version}}/latest.json" 78 | ], 79 | "dialog": false, 80 | "windows": { 81 | "installMode": "passive" 82 | }, 83 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFNDE4MENFM0Y1RTZBOTQKUldTVWFsNC96b0JCM3RqM2NmMnlFTmxIaStRaEJrTHNOU2VqRVlIV1hwVURoWUdVdEc1eDcxVEYK" 84 | }, 85 | "windows": [ 86 | { 87 | "fullscreen": false, 88 | "height": 600, 89 | "resizable": true, 90 | "title": "ChatGPT Next Web", 91 | "width": 960, 92 | "hiddenTitle": true, 93 | "titleBarStyle": "Overlay" 94 | } 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------