├── .env.template ├── .gitignore ├── ChatGPT-Next-Web ├── .env.template ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitpod.yml ├── .husky │ └── pre-commit ├── .lintstagedrc.json ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_CN.md ├── app │ ├── client │ │ ├── api.ts │ │ ├── controller.ts │ │ └── platforms │ │ │ └── openai.ts │ ├── command.ts │ ├── components │ │ ├── button.module.scss │ │ ├── button.tsx │ │ ├── chat-list.tsx │ │ ├── chat.module.scss │ │ ├── chat.tsx │ │ ├── emoji.tsx │ │ ├── error.tsx │ │ ├── exporter.module.scss │ │ ├── exporter.tsx │ │ ├── home.module.scss │ │ ├── home.tsx │ │ ├── input-range.module.scss │ │ ├── input-range.tsx │ │ ├── markdown.tsx │ │ ├── mask.module.scss │ │ ├── mask.tsx │ │ ├── message-selector.module.scss │ │ ├── message-selector.tsx │ │ ├── model-config.tsx │ │ ├── new-chat.module.scss │ │ ├── new-chat.tsx │ │ ├── settings.module.scss │ │ ├── settings.tsx │ │ ├── sidebar.tsx │ │ ├── ui-lib.module.scss │ │ └── ui-lib.tsx │ ├── config │ │ ├── build.ts │ │ └── 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-py.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 │ │ ├── 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 │ │ ├── ru.ts │ │ ├── tr.ts │ │ ├── tw.ts │ │ └── vi.ts │ ├── masks │ │ ├── cn.ts │ │ ├── en.ts │ │ ├── index.ts │ │ └── typing.ts │ ├── page.tsx │ ├── polyfill.ts │ ├── store │ │ ├── access.ts │ │ ├── chat.ts │ │ ├── config.ts │ │ ├── index.ts │ │ ├── mask.ts │ │ ├── prompt.ts │ │ └── update.ts │ ├── styles │ │ ├── animation.scss │ │ ├── globals.scss │ │ ├── highlight.scss │ │ ├── markdown.scss │ │ └── window.scss │ ├── typing.ts │ ├── utils.ts │ └── utils │ │ ├── format.ts │ │ └── merge.ts ├── docker-compose.yml ├── docs │ ├── cloudflare-pages-cn.md │ ├── cloudflare-pages-en.md │ ├── faq-cn.md │ ├── faq-en.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 ├── 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 ├── tsconfig.json ├── vercel.json └── yarn.lock ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── __init__.py ├── auth.py ├── chat │ ├── __init__.py │ ├── chat.py │ ├── errors.py │ └── router.py └── config │ ├── __init__.py │ └── router.py ├── constant.py ├── docker-compose.yml ├── main.py ├── msg ├── __init__.py └── chat_messages.py ├── requirements.txt ├── settings ├── __init__.py └── config.py ├── utils ├── __init__.py └── all_utils.py └── vercel.json /.env.template: -------------------------------------------------------------------------------- 1 | 2 | # Your openai api key. (required) 3 | # OPENAI_API_KEY= 4 | 5 | # Access passsword, separated by comma. (optional) 6 | # CODE= 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=http://localhost:8000 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= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # 默认忽略的文件 163 | /shelf/ 164 | /workspace.xml 165 | # 基于编辑器的 HTTP 客户端请求 166 | /httpRequests/ 167 | # Datasource local storage ignored files 168 | /dataSources/ 169 | /dataSources.local.xml 170 | .idea 171 | venv 172 | .vercel 173 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.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 | # This is the new backend address. After setting it up, we will abandon the NodeJs backend. (optional) 12 | # Default: https://py.chatools.online Or NodeJS Default: https://api.opneai.com 13 | # Examples: http://your-openai-proxy.com 14 | BASE_URL= 15 | 16 | 17 | 18 | # Specify OpenAI organization ID.(optional) 19 | # Default: Empty 20 | # If you do not want users to input their own API key, set this value to 1. 21 | OPENAI_ORG_ID= 22 | 23 | # (optional) 24 | # Default: Empty 25 | # If you do not want users to input their own API key, set this value to 1. 26 | HIDE_USER_API_KEY= 27 | 28 | # (optional) 29 | # Default: Empty 30 | # If you do not want users to use GPT-4, set this value to 1. 31 | DISABLE_GPT4= 32 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.eslintignore: -------------------------------------------------------------------------------- 1 | public/serviceWorker.js -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["prettier"] 4 | } 5 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ 3 | "eslint --fix", 4 | "prettier --write" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/.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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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. -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/client/api.ts: -------------------------------------------------------------------------------- 1 | import { ACCESS_CODE_PREFIX } from "../constant"; 2 | import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store"; 3 | import { ChatGPTApi } from "./platforms/openai"; 4 | 5 | export const ROLES = ["system", "user", "assistant"] as const; 6 | export type MessageRole = (typeof ROLES)[number]; 7 | 8 | export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; 9 | export type ChatModel = ModelType; 10 | 11 | export interface RequestMessage { 12 | role: MessageRole; 13 | content: string; 14 | } 15 | 16 | export interface LLMConfig { 17 | model: string; 18 | temperature?: number; 19 | top_p?: number; 20 | stream?: boolean; 21 | presence_penalty?: number; 22 | frequency_penalty?: number; 23 | } 24 | 25 | export interface ChatOptions { 26 | messages: RequestMessage[]; 27 | config: LLMConfig; 28 | 29 | onUpdate?: (message: string, chunk: string) => void; 30 | onFinish: (message: string) => void; 31 | onError?: (err: Error) => void; 32 | onController?: (controller: AbortController) => void; 33 | } 34 | 35 | export interface LLMUsage { 36 | used: number; 37 | total: number; 38 | } 39 | 40 | export abstract class LLMApi { 41 | abstract chat(options: ChatOptions): Promise; 42 | abstract usage(): Promise; 43 | } 44 | 45 | export class ClientApi { 46 | public llm: LLMApi; 47 | 48 | constructor() { 49 | this.llm = new ChatGPTApi(); 50 | } 51 | 52 | config() {} 53 | 54 | prompts() {} 55 | 56 | masks() {} 57 | 58 | async share(messages: ChatMessage[], avatarUrl: string | null = null) { 59 | const msgs = messages 60 | .map((m) => ({ 61 | from: m.role === "user" ? "human" : "gpt", 62 | value: m.content, 63 | })) 64 | .concat([ 65 | { 66 | from: "human", 67 | value: 68 | "Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web", 69 | }, 70 | ]); 71 | // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用 72 | // Please do not modify this message 73 | 74 | console.log("[Share]", msgs); 75 | const res = await fetch("/sharegpt", { 76 | body: JSON.stringify({ 77 | avatarUrl, 78 | items: msgs, 79 | }), 80 | headers: { 81 | "Content-Type": "application/json", 82 | }, 83 | method: "POST", 84 | }); 85 | 86 | const resJson = await res.json(); 87 | console.log("[Share]", resJson); 88 | if (resJson.id) { 89 | return `https://shareg.pt/${resJson.id}`; 90 | } 91 | } 92 | } 93 | 94 | export const api = new ClientApi(); 95 | 96 | export function getHeaders() { 97 | const accessStore = useAccessStore.getState(); 98 | let headers: Record = { 99 | "Content-Type": "application/json", 100 | "x-requested-with": "XMLHttpRequest", 101 | }; 102 | 103 | const makeBearer = (token: string) => `Bearer ${token.trim()}`; 104 | const validString = (x: string) => x && x.length > 0; 105 | 106 | // use user's api key first 107 | if (validString(accessStore.token)) { 108 | headers.Authorization = makeBearer(accessStore.token); 109 | } else if ( 110 | accessStore.enabledAccessControl() && 111 | validString(accessStore.accessCode) 112 | ) { 113 | headers.Authorization = makeBearer( 114 | ACCESS_CODE_PREFIX + accessStore.accessCode, 115 | ); 116 | } 117 | 118 | return headers; 119 | } 120 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 |
76 | {new Date(props.time).toLocaleString()} 77 |
78 |
79 | 80 | )} 81 | 82 |
86 | 87 |
88 |
89 | )} 90 |
91 | ); 92 | } 93 | 94 | export function ChatList(props: { narrow?: boolean }) { 95 | const [sessions, selectedIndex, selectSession, moveSession] = useChatStore( 96 | (state) => [ 97 | state.sessions, 98 | state.currentSessionIndex, 99 | state.selectSession, 100 | state.moveSession, 101 | ], 102 | ); 103 | const chatStore = useChatStore(); 104 | const navigate = useNavigate(); 105 | 106 | const onDragEnd: OnDragEndResponder = (result) => { 107 | const { destination, source } = result; 108 | if (!destination) { 109 | return; 110 | } 111 | 112 | if ( 113 | destination.droppableId === source.droppableId && 114 | destination.index === source.index 115 | ) { 116 | return; 117 | } 118 | 119 | moveSession(source.index, destination.index); 120 | }; 121 | 122 | return ( 123 | 124 | 125 | {(provided) => ( 126 |
131 | {sessions.map((item, i) => ( 132 | { 141 | navigate(Path.Chat); 142 | selectSession(i); 143 | }} 144 | onDelete={() => { 145 | if (!props.narrow || confirm(Locale.Home.DeleteChat)) { 146 | chatStore.deleteSession(i); 147 | } 148 | }} 149 | narrow={props.narrow} 150 | mask={item.mask} 151 | /> 152 | ))} 153 | {provided.placeholder} 154 |
155 | )} 156 |
157 |
158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | 21 | &:not(:last-child) { 22 | margin-right: 5px; 23 | } 24 | } 25 | } 26 | 27 | .prompt-toast { 28 | position: absolute; 29 | bottom: -50px; 30 | z-index: 999; 31 | display: flex; 32 | justify-content: center; 33 | width: calc(100% - 40px); 34 | 35 | .prompt-toast-inner { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | font-size: 12px; 40 | background-color: var(--white); 41 | color: var(--black); 42 | 43 | border: var(--border-in-light); 44 | box-shadow: var(--card-shadow); 45 | padding: 10px 20px; 46 | border-radius: 100px; 47 | 48 | animation: slide-in-from-top ease 0.3s; 49 | 50 | .prompt-toast-content { 51 | margin-left: 10px; 52 | } 53 | } 54 | } 55 | 56 | .section-title { 57 | font-size: 12px; 58 | font-weight: bold; 59 | margin-bottom: 10px; 60 | display: flex; 61 | justify-content: space-between; 62 | align-items: center; 63 | 64 | .section-title-action { 65 | display: flex; 66 | align-items: center; 67 | } 68 | } 69 | 70 | .context-prompt { 71 | .context-prompt-row { 72 | display: flex; 73 | justify-content: center; 74 | width: 100%; 75 | margin-bottom: 10px; 76 | 77 | .context-role { 78 | margin-right: 10px; 79 | } 80 | 81 | .context-content { 82 | flex: 1; 83 | max-width: 100%; 84 | text-align: left; 85 | } 86 | 87 | .context-delete-button { 88 | margin-left: 10px; 89 | } 90 | } 91 | 92 | .context-prompt-button { 93 | flex: 1; 94 | } 95 | } 96 | 97 | .memory-prompt { 98 | margin: 20px 0; 99 | 100 | .memory-prompt-content { 101 | background-color: var(--white); 102 | color: var(--black); 103 | border: var(--border-in-light); 104 | border-radius: 10px; 105 | padding: 10px; 106 | font-size: 12px; 107 | user-select: text; 108 | } 109 | } 110 | 111 | .clear-context { 112 | margin: 20px 0 0 0; 113 | padding: 4px 0; 114 | 115 | border-top: var(--border-in-light); 116 | border-bottom: var(--border-in-light); 117 | box-shadow: var(--card-shadow) inset; 118 | 119 | display: flex; 120 | justify-content: center; 121 | align-items: center; 122 | 123 | color: var(--black); 124 | transition: all ease 0.3s; 125 | cursor: pointer; 126 | overflow: hidden; 127 | position: relative; 128 | font-size: 12px; 129 | 130 | animation: slide-in ease 0.3s; 131 | 132 | $linear: linear-gradient( 133 | to right, 134 | rgba(0, 0, 0, 0), 135 | rgba(0, 0, 0, 1), 136 | rgba(0, 0, 0, 0) 137 | ); 138 | mask-image: $linear; 139 | 140 | @mixin show { 141 | transform: translateY(0); 142 | position: relative; 143 | transition: all ease 0.3s; 144 | opacity: 1; 145 | } 146 | 147 | @mixin hide { 148 | transform: translateY(-50%); 149 | position: absolute; 150 | transition: all ease 0.1s; 151 | opacity: 0; 152 | } 153 | 154 | &-tips { 155 | @include show; 156 | opacity: 0.5; 157 | } 158 | 159 | &-revert-btn { 160 | color: var(--primary); 161 | @include hide; 162 | } 163 | 164 | &:hover { 165 | opacity: 1; 166 | border-color: var(--primary); 167 | 168 | .clear-context-tips { 169 | @include hide; 170 | } 171 | 172 | .clear-context-revert-btn { 173 | @include show; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/components/home.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | require("../polyfill"); 4 | 5 | import { useState, useEffect } from "react"; 6 | 7 | import styles from "./home.module.scss"; 8 | 9 | import BotIcon from "../icons/bot.svg"; 10 | import LoadingIcon from "../icons/three-dots.svg"; 11 | 12 | import { getCSSVar, useMobileScreen } from "../utils"; 13 | 14 | import dynamic from "next/dynamic"; 15 | import { Path, SlotID } from "../constant"; 16 | import { ErrorBoundary } from "./error"; 17 | 18 | import { 19 | HashRouter as Router, 20 | Routes, 21 | Route, 22 | useLocation, 23 | } from "react-router-dom"; 24 | import { SideBar } from "./sidebar"; 25 | import { useAppConfig } from "../store/config"; 26 | 27 | export function Loading(props: { noLogo?: boolean }) { 28 | return ( 29 |
30 | {!props.noLogo && } 31 | 32 |
33 | ); 34 | } 35 | 36 | const Settings = dynamic(async () => (await import("./settings")).Settings, { 37 | loading: () => , 38 | }); 39 | 40 | const Chat = dynamic(async () => (await import("./chat")).Chat, { 41 | loading: () => , 42 | }); 43 | 44 | const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, { 45 | loading: () => , 46 | }); 47 | 48 | const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, { 49 | loading: () => , 50 | }); 51 | 52 | export function useSwitchTheme() { 53 | const config = useAppConfig(); 54 | 55 | useEffect(() => { 56 | document.body.classList.remove("light"); 57 | document.body.classList.remove("dark"); 58 | 59 | if (config.theme === "dark") { 60 | document.body.classList.add("dark"); 61 | } else if (config.theme === "light") { 62 | document.body.classList.add("light"); 63 | } 64 | 65 | const metaDescriptionDark = document.querySelector( 66 | 'meta[name="theme-color"][media*="dark"]', 67 | ); 68 | const metaDescriptionLight = document.querySelector( 69 | 'meta[name="theme-color"][media*="light"]', 70 | ); 71 | 72 | if (config.theme === "auto") { 73 | metaDescriptionDark?.setAttribute("content", "#151515"); 74 | metaDescriptionLight?.setAttribute("content", "#fafafa"); 75 | } else { 76 | const themeColor = getCSSVar("--theme-color"); 77 | metaDescriptionDark?.setAttribute("content", themeColor); 78 | metaDescriptionLight?.setAttribute("content", themeColor); 79 | } 80 | }, [config.theme]); 81 | } 82 | 83 | const useHasHydrated = () => { 84 | const [hasHydrated, setHasHydrated] = useState(false); 85 | 86 | useEffect(() => { 87 | setHasHydrated(true); 88 | }, []); 89 | 90 | return hasHydrated; 91 | }; 92 | 93 | const loadAsyncGoogleFont = () => { 94 | const linkEl = document.createElement("link"); 95 | linkEl.rel = "stylesheet"; 96 | linkEl.href = 97 | "/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"; 98 | document.head.appendChild(linkEl); 99 | }; 100 | 101 | function Screen() { 102 | const config = useAppConfig(); 103 | const location = useLocation(); 104 | const isHome = location.pathname === Path.Home; 105 | const isMobileScreen = useMobileScreen(); 106 | 107 | useEffect(() => { 108 | loadAsyncGoogleFont(); 109 | }, []); 110 | 111 | return ( 112 |
122 | 123 | 124 |
125 | 126 | } /> 127 | } /> 128 | } /> 129 | } /> 130 | } /> 131 | 132 |
133 |
134 | ); 135 | } 136 | 137 | export function Home() { 138 | useSwitchTheme(); 139 | 140 | if (!useHasHydrated()) { 141 | return ; 142 | } 143 | 144 | return ( 145 | 146 | 147 | 148 | 149 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/components/model-config.tsx: -------------------------------------------------------------------------------- 1 | import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store"; 2 | 3 | import Locale from "../locales"; 4 | import { InputRange } from "./input-range"; 5 | import { List, ListItem, Select } from "./ui-lib"; 6 | 7 | export function ModelConfigList(props: { 8 | modelConfig: ModelConfig; 9 | updateConfig: (updater: (config: ModelConfig) => void) => void; 10 | }) { 11 | return ( 12 | <> 13 | 14 | 31 | 32 | 36 | { 42 | props.updateConfig( 43 | (config) => 44 | (config.temperature = ModalConfigValidator.temperature( 45 | e.currentTarget.valueAsNumber, 46 | )), 47 | ); 48 | }} 49 | > 50 | 51 | 55 | 61 | props.updateConfig( 62 | (config) => 63 | (config.max_tokens = ModalConfigValidator.max_tokens( 64 | e.currentTarget.valueAsNumber, 65 | )), 66 | ) 67 | } 68 | > 69 | 70 | 74 | { 80 | props.updateConfig( 81 | (config) => 82 | (config.presence_penalty = 83 | ModalConfigValidator.presence_penalty( 84 | e.currentTarget.valueAsNumber, 85 | )), 86 | ); 87 | }} 88 | > 89 | 90 | 91 | 95 | 102 | props.updateConfig( 103 | (config) => (config.historyMessageCount = e.target.valueAsNumber), 104 | ) 105 | } 106 | > 107 | 108 | 109 | 113 | 119 | props.updateConfig( 120 | (config) => 121 | (config.compressMessageLengthThreshold = 122 | e.currentTarget.valueAsNumber), 123 | ) 124 | } 125 | > 126 | 127 | 128 | 132 | props.updateConfig( 133 | (config) => (config.sendMemory = e.currentTarget.checked), 134 | ) 135 | } 136 | > 137 | 138 | 139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/components/ui-lib.module.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/animation.scss"; 2 | 3 | .card { 4 | background-color: var(--white); 5 | border-radius: 10px; 6 | box-shadow: var(--card-shadow); 7 | padding: 10px; 8 | } 9 | 10 | .popover { 11 | position: relative; 12 | z-index: 2; 13 | } 14 | 15 | .popover-content { 16 | position: absolute; 17 | animation: slide-in 0.3s ease; 18 | right: 0; 19 | top: calc(100% + 10px); 20 | } 21 | 22 | .popover-mask { 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | width: 100vw; 27 | height: 100vh; 28 | } 29 | 30 | .list-item { 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | min-height: 40px; 35 | border-bottom: var(--border-in-light); 36 | padding: 10px 20px; 37 | animation: slide-in ease 0.6s; 38 | 39 | .list-header { 40 | display: flex; 41 | align-items: center; 42 | 43 | .list-icon { 44 | margin-right: 10px; 45 | } 46 | 47 | .list-item-title { 48 | font-size: 14px; 49 | font-weight: bolder; 50 | } 51 | 52 | .list-item-sub-title { 53 | font-size: 12px; 54 | font-weight: normal; 55 | } 56 | } 57 | } 58 | 59 | .list { 60 | border: var(--border-in-light); 61 | border-radius: 10px; 62 | box-shadow: var(--card-shadow); 63 | margin-bottom: 20px; 64 | animation: slide-in ease 0.3s; 65 | } 66 | 67 | .list .list-item:last-child { 68 | border: 0; 69 | } 70 | 71 | .modal-container { 72 | box-shadow: var(--card-shadow); 73 | background-color: var(--white); 74 | border-radius: 12px; 75 | width: 60vw; 76 | animation: slide-in ease 0.3s; 77 | 78 | --modal-padding: 20px; 79 | 80 | .modal-header { 81 | padding: var(--modal-padding); 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-between; 85 | border-bottom: var(--border-in-light); 86 | 87 | .modal-title { 88 | font-weight: bolder; 89 | font-size: 16px; 90 | } 91 | 92 | .modal-close-btn { 93 | cursor: pointer; 94 | 95 | &:hover { 96 | filter: brightness(1.2); 97 | } 98 | } 99 | } 100 | 101 | .modal-content { 102 | max-height: 40vh; 103 | padding: var(--modal-padding); 104 | overflow: auto; 105 | } 106 | 107 | .modal-footer { 108 | padding: var(--modal-padding); 109 | display: flex; 110 | justify-content: flex-end; 111 | border-top: var(--border-in-light); 112 | box-shadow: var(--shadow); 113 | 114 | .modal-actions { 115 | display: flex; 116 | align-items: center; 117 | 118 | .modal-action { 119 | &:not(:last-child) { 120 | margin-right: 20px; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | @media screen and (max-width: 600px) { 128 | .modal-container { 129 | width: 100vw; 130 | border-bottom-left-radius: 0; 131 | border-bottom-right-radius: 0; 132 | 133 | .modal-content { 134 | max-height: 50vh; 135 | } 136 | } 137 | } 138 | 139 | .show { 140 | opacity: 1; 141 | transition: all ease 0.3s; 142 | transform: translateY(0); 143 | position: fixed; 144 | left: 0; 145 | bottom: 0; 146 | animation: slide-in ease 0.6s; 147 | z-index: 99999; 148 | } 149 | 150 | .hide { 151 | opacity: 0; 152 | transition: all ease 0.3s; 153 | transform: translateY(20px); 154 | } 155 | 156 | .toast-container { 157 | position: fixed; 158 | bottom: 5vh; 159 | left: 0; 160 | width: 100vw; 161 | display: flex; 162 | justify-content: center; 163 | pointer-events: none; 164 | 165 | .toast-content { 166 | max-width: 80vw; 167 | word-break: break-all; 168 | font-size: 14px; 169 | background-color: var(--white); 170 | box-shadow: var(--card-shadow); 171 | border: var(--border-in-light); 172 | color: var(--black); 173 | padding: 10px 20px; 174 | border-radius: 50px; 175 | margin-bottom: 20px; 176 | display: flex; 177 | align-items: center; 178 | pointer-events: all; 179 | 180 | .toast-action { 181 | padding-left: 20px; 182 | color: var(--primary); 183 | opacity: 0.8; 184 | border: 0; 185 | background: none; 186 | cursor: pointer; 187 | font-family: inherit; 188 | 189 | &:hover { 190 | opacity: 1; 191 | } 192 | } 193 | } 194 | } 195 | 196 | .input { 197 | border: var(--border-in-light); 198 | border-radius: 10px; 199 | padding: 10px; 200 | font-family: inherit; 201 | background-color: var(--white); 202 | color: var(--black); 203 | resize: none; 204 | min-width: 50px; 205 | } 206 | 207 | .select-with-icon { 208 | position: relative; 209 | max-width: fit-content; 210 | 211 | .select-with-icon-select { 212 | height: 100%; 213 | border: var(--border-in-light); 214 | padding: 10px 25px 10px 10px; 215 | border-radius: 10px; 216 | appearance: none; 217 | cursor: pointer; 218 | background-color: var(--white); 219 | color: var(--black); 220 | text-align: center; 221 | } 222 | 223 | .select-with-icon-icon { 224 | position: absolute; 225 | top: 50%; 226 | right: 10px; 227 | transform: translateY(-50%); 228 | pointer-events: none; 229 | } 230 | } -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/config/build.ts: -------------------------------------------------------------------------------- 1 | const COMMIT_ID: string = (() => { 2 | try { 3 | const childProcess = require("child_process"); 4 | return childProcess 5 | .execSync('git log -1 --format="%at000" --date=unix') 6 | .toString() 7 | .trim(); 8 | } catch (e) { 9 | console.error("[Build Config] No git or not from git repo."); 10 | return "unknown"; 11 | } 12 | })(); 13 | 14 | export const getBuildConfig = () => { 15 | if (typeof process === "undefined") { 16 | throw Error( 17 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 18 | ); 19 | } 20 | 21 | return { 22 | commitId: COMMIT_ID, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | } 14 | } 15 | } 16 | 17 | const ACCESS_CODES = (function getAccessCodes(): Set { 18 | const code = process.env.CODE; 19 | 20 | try { 21 | const codes = (code?.split(",") ?? []) 22 | .filter((v) => !!v) 23 | .map((v) => md5.hash(v.trim())); 24 | return new Set(codes); 25 | } catch (e) { 26 | return new Set(); 27 | } 28 | })(); 29 | 30 | export const getServerSideConfig = () => { 31 | if (typeof process === "undefined") { 32 | throw Error( 33 | "[Server Config] you are importing a nodejs-only module outside of nodejs", 34 | ); 35 | } 36 | 37 | return { 38 | apiKey: process.env.OPENAI_API_KEY, 39 | code: process.env.CODE, 40 | codes: ACCESS_CODES, 41 | needCode: ACCESS_CODES.size > 0, 42 | baseUrl: process.env.BASE_URL, 43 | proxyUrl: process.env.PROXY_URL, 44 | isVercel: !!process.env.VERCEL, 45 | hideUserApiKey: !!process.env.HIDE_USER_API_KEY, 46 | enableGPT4: !process.env.DISABLE_GPT4, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/constant.ts: -------------------------------------------------------------------------------- 1 | export const OWNER = "iszhouhuabo"; 2 | export const REPO = "ChatGPT-Next-Web"; 3 | export const REPO_PYTHON = "chatgpt-next-web-fastapi"; 4 | export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; 5 | export const REPO_PYTHON_URL = `https://github.com/${OWNER}/${REPO_PYTHON}`; 6 | export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; 7 | export const UPDATE_URL = `${REPO_URL}#keep-updated`; 8 | export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; 9 | export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; 10 | export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; 11 | 12 | export enum Path { 13 | Home = "/", 14 | Chat = "/chat", 15 | Settings = "/settings", 16 | NewChat = "/new-chat", 17 | Masks = "/masks", 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 | } 37 | 38 | export const MAX_SIDEBAR_WIDTH = 500; 39 | export const MIN_SIDEBAR_WIDTH = 230; 40 | export const NARROW_SIDEBAR_WIDTH = 100; 41 | 42 | export const ACCESS_CODE_PREFIX = "ak-"; 43 | 44 | export const LAST_INPUT_KEY = "last-input"; 45 | 46 | export const REQUEST_TIMEOUT_MS = 60000; 47 | 48 | export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; 49 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/auto.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/black-bot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/app/icons/bot.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/brain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/break.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/chat.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/app/icons/chatgpt.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/close.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/github-py.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/max.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/plugin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/prompt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/reload.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/rename.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/return.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/send-white.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/icons/three-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 { getBuildConfig } from "./config/build"; 6 | 7 | const buildConfig = getBuildConfig(); 8 | 9 | export const metadata = { 10 | title: "ChatGPT Next Web", 11 | description: "Your personal ChatGPT Chat Bot.", 12 | viewport: { 13 | width: "device-width", 14 | initialScale: 1, 15 | maximumScale: 1, 16 | }, 17 | themeColor: [ 18 | { media: "(prefers-color-scheme: light)", color: "#fafafa" }, 19 | { media: "(prefers-color-scheme: dark)", color: "#151515" }, 20 | ], 21 | appleWebApp: { 22 | title: "ChatGPT Next Web", 23 | statusBarStyle: "default", 24 | }, 25 | }; 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: { 30 | children: React.ReactNode; 31 | }) { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | {children} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 CS from "./cs"; 13 | import KO from "./ko"; 14 | import { merge } from "../utils/merge"; 15 | 16 | export type { LocaleType, RequiredLocaleType } from "./cn"; 17 | 18 | export const AllLangs = [ 19 | "en", 20 | "cn", 21 | "tw", 22 | "fr", 23 | "es", 24 | "it", 25 | "tr", 26 | "jp", 27 | "de", 28 | "vi", 29 | "ru", 30 | "cs", 31 | "ko", 32 | ] as const; 33 | export type Lang = (typeof AllLangs)[number]; 34 | 35 | export const ALL_LANG_OPTIONS: Record = { 36 | cn: "简体中文", 37 | en: "English", 38 | tw: "繁體中文", 39 | fr: "Français", 40 | es: "Español", 41 | it: "Italiano", 42 | tr: "Türkçe", 43 | jp: "日本語", 44 | de: "Deutsch", 45 | vi: "Tiếng Việt", 46 | ru: "Русский", 47 | cs: "Čeština", 48 | ko: "한국어", 49 | }; 50 | 51 | const LANG_KEY = "lang"; 52 | const DEFAULT_LANG = "en"; 53 | 54 | function getItem(key: string) { 55 | try { 56 | return localStorage.getItem(key); 57 | } catch { 58 | return null; 59 | } 60 | } 61 | 62 | function setItem(key: string, value: string) { 63 | try { 64 | localStorage.setItem(key, value); 65 | } catch {} 66 | } 67 | 68 | function getLanguage() { 69 | try { 70 | return navigator.language.toLowerCase(); 71 | } catch { 72 | console.log("[Lang] failed to detect user lang."); 73 | return DEFAULT_LANG; 74 | } 75 | } 76 | 77 | export function getLang(): Lang { 78 | const savedLang = getItem(LANG_KEY); 79 | 80 | if (AllLangs.includes((savedLang ?? "") as Lang)) { 81 | return savedLang as Lang; 82 | } 83 | 84 | const lang = getLanguage(); 85 | 86 | for (const option of AllLangs) { 87 | if (lang.includes(option)) { 88 | return option; 89 | } 90 | } 91 | 92 | return DEFAULT_LANG; 93 | } 94 | 95 | export function changeLang(lang: Lang) { 96 | setItem(LANG_KEY, lang); 97 | location.reload(); 98 | } 99 | 100 | const fallbackLang = EN; 101 | const targetLang = { 102 | en: EN, 103 | cn: CN, 104 | tw: TW, 105 | fr: FR, 106 | es: ES, 107 | it: IT, 108 | tr: TR, 109 | jp: JP, 110 | de: DE, 111 | vi: VI, 112 | ru: RU, 113 | cs: CS, 114 | ko: KO, 115 | }[getLang()] as typeof CN; 116 | 117 | // if target lang missing some fields, it will use fallback lang string 118 | merge(fallbackLang, targetLang); 119 | 120 | export default fallbackLang as typeof CN; 121 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/masks/typing.ts: -------------------------------------------------------------------------------- 1 | import { type Mask } from "../store/mask"; 2 | 3 | export type BuiltinMask = Omit & { 4 | builtin: true; 5 | }; 6 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/store/access.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { StoreKey } from "../constant"; 4 | import { getHeaders } from "../client/api"; 5 | import { BOT_HELLO } from "./chat"; 6 | import { ALL_MODELS } from "./config"; 7 | import { OPENAI_URL } from "@/app/api/common"; 8 | import nextConfig from "@/next.config.mjs"; 9 | 10 | export interface AccessControlStore { 11 | accessCode: string; 12 | token: string; 13 | 14 | needCode: boolean; 15 | hideUserApiKey: boolean; 16 | openaiUrl: string; 17 | 18 | updateToken: (_: string) => void; 19 | updateCode: (_: string) => void; 20 | enabledAccessControl: () => boolean; 21 | isAuthorized: () => boolean; 22 | fetch: () => void; 23 | } 24 | 25 | let fetchState = 0; // 0 not fetch, 1 fetching, 2 done 26 | 27 | export const useAccessStore = create()( 28 | persist( 29 | (set, get) => ({ 30 | token: "", 31 | accessCode: "", 32 | needCode: true, 33 | hideUserApiKey: false, 34 | openaiUrl: /*nextConfig?.env?.BASE_URL + */ "/api/openai/", 35 | 36 | enabledAccessControl() { 37 | get().fetch(); 38 | 39 | return get().needCode; 40 | }, 41 | updateCode(code: string) { 42 | set(() => ({ accessCode: code })); 43 | }, 44 | updateToken(token: string) { 45 | set(() => ({ token })); 46 | }, 47 | isAuthorized() { 48 | get().fetch(); 49 | 50 | // has token or has code or disabled access control 51 | return ( 52 | !!get().token || !!get().accessCode || !get().enabledAccessControl() 53 | ); 54 | }, 55 | fetch() { 56 | if (fetchState > 0) return; 57 | fetchState = 1; 58 | fetch(/*nextConfig?.env?.BASE_URL +*/ "/api/config", { 59 | method: "post", 60 | body: null, 61 | headers: { 62 | ...getHeaders(), 63 | }, 64 | }) 65 | .then((res) => res.json()) 66 | .then((res: DangerConfig) => { 67 | console.log("[Config] got config from server", res); 68 | set(() => ({ ...res })); 69 | 70 | if (!res.enableGPT4) { 71 | ALL_MODELS.forEach((model) => { 72 | if (model.name.startsWith("gpt-4")) { 73 | (model as any).available = false; 74 | } 75 | }); 76 | } 77 | 78 | if ((res as any).botHello) { 79 | BOT_HELLO.content = (res as any).botHello; 80 | } 81 | }) 82 | .catch(() => { 83 | console.error("[Config] failed to fetch config"); 84 | }) 85 | .finally(() => { 86 | fetchState = 2; 87 | }); 88 | }, 89 | }), 90 | { 91 | name: StoreKey.Access, 92 | version: 1, 93 | }, 94 | ), 95 | ); 96 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/store/config.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | import { StoreKey } from "../constant"; 4 | 5 | export enum SubmitKey { 6 | Enter = "Enter", 7 | CtrlEnter = "Ctrl + Enter", 8 | ShiftEnter = "Shift + Enter", 9 | AltEnter = "Alt + Enter", 10 | MetaEnter = "Meta + Enter", 11 | } 12 | 13 | export enum Theme { 14 | Auto = "auto", 15 | Dark = "dark", 16 | Light = "light", 17 | } 18 | 19 | export const DEFAULT_CONFIG = { 20 | submitKey: SubmitKey.CtrlEnter as SubmitKey, 21 | avatar: "1f603", 22 | fontSize: 14, 23 | theme: Theme.Auto as Theme, 24 | tightBorder: false, 25 | sendPreviewBubble: true, 26 | sidebarWidth: 300, 27 | 28 | disablePromptHint: false, 29 | 30 | dontShowMaskSplashScreen: false, // dont show splash screen when create chat 31 | 32 | modelConfig: { 33 | model: "gpt-3.5-turbo" as ModelType, 34 | temperature: 0.5, 35 | max_tokens: 2000, 36 | presence_penalty: 0, 37 | sendMemory: true, 38 | historyMessageCount: 4, 39 | compressMessageLengthThreshold: 1000, 40 | }, 41 | }; 42 | 43 | export type ChatConfig = typeof DEFAULT_CONFIG; 44 | 45 | export type ChatConfigStore = ChatConfig & { 46 | reset: () => void; 47 | update: (updater: (config: ChatConfig) => void) => void; 48 | }; 49 | 50 | export type ModelConfig = ChatConfig["modelConfig"]; 51 | 52 | const ENABLE_GPT4 = true; 53 | 54 | export const ALL_MODELS = [ 55 | { 56 | name: "gpt-4", 57 | available: ENABLE_GPT4, 58 | }, 59 | { 60 | name: "gpt-4-0314", 61 | available: ENABLE_GPT4, 62 | }, 63 | { 64 | name: "gpt-4-32k", 65 | available: ENABLE_GPT4, 66 | }, 67 | { 68 | name: "gpt-4-32k-0314", 69 | available: ENABLE_GPT4, 70 | }, 71 | { 72 | name: "gpt-4-mobile", 73 | available: ENABLE_GPT4, 74 | }, 75 | { 76 | name: "text-davinci-002-render-sha-mobile", 77 | available: true, 78 | }, 79 | { 80 | name: "gpt-3.5-turbo", 81 | available: true, 82 | }, 83 | { 84 | name: "gpt-3.5-turbo-0301", 85 | available: true, 86 | }, 87 | { 88 | name: "qwen-v1", // 通义千问 89 | available: false, 90 | }, 91 | { 92 | name: "ernie", // 文心一言 93 | available: false, 94 | }, 95 | { 96 | name: "spark", // 讯飞星火 97 | available: false, 98 | }, 99 | { 100 | name: "llama", // llama 101 | available: false, 102 | }, 103 | { 104 | name: "chatglm", // chatglm-6b 105 | available: false, 106 | }, 107 | ] as const; 108 | 109 | export type ModelType = (typeof ALL_MODELS)[number]["name"]; 110 | 111 | export function limitNumber( 112 | x: number, 113 | min: number, 114 | max: number, 115 | defaultValue: number, 116 | ) { 117 | if (typeof x !== "number" || isNaN(x)) { 118 | return defaultValue; 119 | } 120 | 121 | return Math.min(max, Math.max(min, x)); 122 | } 123 | 124 | export function limitModel(name: string) { 125 | return ALL_MODELS.some((m) => m.name === name && m.available) 126 | ? name 127 | : ALL_MODELS[4].name; 128 | } 129 | 130 | export const ModalConfigValidator = { 131 | model(x: string) { 132 | return limitModel(x) as ModelType; 133 | }, 134 | max_tokens(x: number) { 135 | return limitNumber(x, 0, 32000, 2000); 136 | }, 137 | presence_penalty(x: number) { 138 | return limitNumber(x, -2, 2, 0); 139 | }, 140 | temperature(x: number) { 141 | return limitNumber(x, 0, 1, 1); 142 | }, 143 | }; 144 | 145 | export const useAppConfig = create()( 146 | persist( 147 | (set, get) => ({ 148 | ...DEFAULT_CONFIG, 149 | 150 | reset() { 151 | set(() => ({ ...DEFAULT_CONFIG })); 152 | }, 153 | 154 | update(updater) { 155 | const config = { ...get() }; 156 | updater(config); 157 | set(() => config); 158 | }, 159 | }), 160 | { 161 | name: StoreKey.Config, 162 | version: 2, 163 | migrate(persistedState, version) { 164 | if (version === 2) return persistedState as any; 165 | 166 | const state = persistedState as ChatConfig; 167 | state.modelConfig.sendMemory = true; 168 | state.modelConfig.historyMessageCount = 4; 169 | state.modelConfig.compressMessageLengthThreshold = 1000; 170 | state.dontShowMaskSplashScreen = false; 171 | 172 | return state; 173 | }, 174 | }, 175 | ), 176 | ); 177 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./update"; 3 | export * from "./access"; 4 | export * from "./config"; 5 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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: ModelConfig; 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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/store/prompt.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 Prompt { 8 | id?: number; 9 | isUser?: boolean; 10 | title: string; 11 | content: string; 12 | } 13 | 14 | export interface PromptStore { 15 | counter: number; 16 | latestId: number; 17 | prompts: Record; 18 | 19 | add: (prompt: Prompt) => number; 20 | get: (id: number) => Prompt | undefined; 21 | remove: (id: number) => void; 22 | search: (text: string) => Prompt[]; 23 | update: (id: number, updater: (prompt: Prompt) => void) => void; 24 | 25 | getUserPrompts: () => Prompt[]; 26 | } 27 | 28 | export const SearchService = { 29 | ready: false, 30 | builtinEngine: new Fuse([], { keys: ["title"] }), 31 | userEngine: new Fuse([], { keys: ["title"] }), 32 | count: { 33 | builtin: 0, 34 | }, 35 | allPrompts: [] as Prompt[], 36 | builtinPrompts: [] as Prompt[], 37 | 38 | init(builtinPrompts: Prompt[], userPrompts: Prompt[]) { 39 | if (this.ready) { 40 | return; 41 | } 42 | this.allPrompts = userPrompts.concat(builtinPrompts); 43 | this.builtinPrompts = builtinPrompts.slice(); 44 | this.builtinEngine.setCollection(builtinPrompts); 45 | this.userEngine.setCollection(userPrompts); 46 | this.ready = true; 47 | }, 48 | 49 | remove(id: number) { 50 | this.userEngine.remove((doc) => doc.id === id); 51 | }, 52 | 53 | add(prompt: Prompt) { 54 | this.userEngine.add(prompt); 55 | }, 56 | 57 | search(text: string) { 58 | const userResults = this.userEngine.search(text); 59 | const builtinResults = this.builtinEngine.search(text); 60 | return userResults.concat(builtinResults).map((v) => v.item); 61 | }, 62 | }; 63 | 64 | export const usePromptStore = create()( 65 | persist( 66 | (set, get) => ({ 67 | counter: 0, 68 | latestId: 0, 69 | prompts: {}, 70 | 71 | add(prompt) { 72 | const prompts = get().prompts; 73 | prompt.id = get().latestId + 1; 74 | prompt.isUser = true; 75 | prompts[prompt.id] = prompt; 76 | 77 | set(() => ({ 78 | latestId: prompt.id!, 79 | prompts: prompts, 80 | })); 81 | 82 | return prompt.id!; 83 | }, 84 | 85 | get(id) { 86 | const targetPrompt = get().prompts[id]; 87 | 88 | if (!targetPrompt) { 89 | return SearchService.builtinPrompts.find((v) => v.id === id); 90 | } 91 | 92 | return targetPrompt; 93 | }, 94 | 95 | remove(id) { 96 | const prompts = get().prompts; 97 | delete prompts[id]; 98 | SearchService.remove(id); 99 | 100 | set(() => ({ 101 | prompts, 102 | counter: get().counter + 1, 103 | })); 104 | }, 105 | 106 | getUserPrompts() { 107 | const userPrompts = Object.values(get().prompts ?? {}); 108 | userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0)); 109 | return userPrompts; 110 | }, 111 | 112 | update(id: number, updater) { 113 | const prompt = get().prompts[id] ?? { 114 | title: "", 115 | content: "", 116 | id, 117 | }; 118 | 119 | SearchService.remove(id); 120 | updater(prompt); 121 | const prompts = get().prompts; 122 | prompts[id] = prompt; 123 | set(() => ({ prompts })); 124 | SearchService.add(prompt); 125 | }, 126 | 127 | search(text) { 128 | if (text.length === 0) { 129 | // return all rompts 130 | return SearchService.allPrompts.concat([...get().getUserPrompts()]); 131 | } 132 | return SearchService.search(text) as Prompt[]; 133 | }, 134 | }), 135 | { 136 | name: StoreKey.Prompt, 137 | version: 1, 138 | onRehydrateStorage(state) { 139 | const PROMPT_URL = "./prompts.json"; 140 | 141 | type PromptList = Array<[string, string]>; 142 | 143 | fetch(PROMPT_URL) 144 | .then((res) => res.json()) 145 | .then((res) => { 146 | let fetchPrompts = [res.en, res.cn]; 147 | if (getLang() === "cn") { 148 | fetchPrompts = fetchPrompts.reverse(); 149 | } 150 | const builtinPrompts = fetchPrompts.map( 151 | (promptList: PromptList) => { 152 | return promptList.map( 153 | ([title, content]) => 154 | ({ 155 | id: Math.random(), 156 | title, 157 | content, 158 | } as Prompt), 159 | ); 160 | }, 161 | ); 162 | 163 | const userPrompts = 164 | usePromptStore.getState().getUserPrompts() ?? []; 165 | 166 | const allPromptsForSearch = builtinPrompts 167 | .reduce((pre, cur) => pre.concat(cur), []) 168 | .filter((v) => !!v.title && !!v.content); 169 | SearchService.count.builtin = res.en.length + res.cn.length; 170 | SearchService.init(allPromptsForSearch, userPrompts); 171 | }); 172 | }, 173 | }, 174 | ), 175 | ); 176 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 { showToast } from "../components/ui-lib"; 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 | function queryMeta(key: string, defaultValue?: string): string { 21 | let ret: string; 22 | if (document) { 23 | const meta = document.head.querySelector( 24 | `meta[name='${key}']`, 25 | ) as HTMLMetaElement; 26 | ret = meta?.content ?? ""; 27 | } else { 28 | ret = defaultValue ?? ""; 29 | } 30 | 31 | return ret; 32 | } 33 | 34 | const ONE_MINUTE = 60 * 1000; 35 | 36 | export const useUpdateStore = create()( 37 | persist( 38 | (set, get) => ({ 39 | lastUpdate: 0, 40 | remoteVersion: "", 41 | 42 | lastUpdateUsage: 0, 43 | 44 | version: "unknown", 45 | 46 | async getLatestVersion(force = false) { 47 | set(() => ({ version: queryMeta("version") ?? "unknown" })); 48 | 49 | const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE; 50 | if (!force && !overTenMins) return; 51 | 52 | set(() => ({ 53 | lastUpdate: Date.now(), 54 | })); 55 | 56 | try { 57 | const data = await (await fetch(FETCH_COMMIT_URL)).json(); 58 | const remoteCommitTime = data[0].commit.committer.date; 59 | const remoteId = new Date(remoteCommitTime).getTime().toString(); 60 | set(() => ({ 61 | remoteVersion: remoteId, 62 | })); 63 | console.log("[Got Upstream] ", remoteId); 64 | } catch (error) { 65 | console.error("[Fetch Upstream Commit Id]", error); 66 | } 67 | }, 68 | 69 | async updateUsage(force = false) { 70 | const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; 71 | if (!overOneMinute && !force) return; 72 | 73 | set(() => ({ 74 | lastUpdateUsage: Date.now(), 75 | })); 76 | 77 | try { 78 | const usage = await api.llm.usage(); 79 | 80 | if (usage) { 81 | set(() => ({ 82 | used: usage.used, 83 | subscription: usage.total, 84 | })); 85 | } 86 | } catch (e) { 87 | showToast((e as Error).message); 88 | } 89 | }, 90 | }), 91 | { 92 | name: StoreKey.Update, 93 | version: 1, 94 | }, 95 | ), 96 | ); 97 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/typing.ts: -------------------------------------------------------------------------------- 1 | export type Updater = (updater: (value: T) => void) => void; 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/app/utils.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { showToast } from "./components/ui-lib"; 3 | import Locale from "./locales"; 4 | 5 | export function trimTopic(topic: string) { 6 | return topic.replace(/[,。!?”“"、,.!?]*$/, ""); 7 | } 8 | 9 | export async function copyToClipboard(text: string) { 10 | try { 11 | await navigator.clipboard.writeText(text); 12 | showToast(Locale.Copy.Success); 13 | } catch (error) { 14 | const textArea = document.createElement("textarea"); 15 | textArea.value = text; 16 | document.body.appendChild(textArea); 17 | textArea.focus(); 18 | textArea.select(); 19 | try { 20 | document.execCommand("copy"); 21 | showToast(Locale.Copy.Success); 22 | } catch (error) { 23 | showToast(Locale.Copy.Failed); 24 | } 25 | document.body.removeChild(textArea); 26 | } 27 | } 28 | 29 | export function downloadAs(text: string, filename: string) { 30 | const element = document.createElement("a"); 31 | element.setAttribute( 32 | "href", 33 | "data:text/plain;charset=utf-8," + encodeURIComponent(text), 34 | ); 35 | element.setAttribute("download", filename); 36 | 37 | element.style.display = "none"; 38 | document.body.appendChild(element); 39 | 40 | element.click(); 41 | 42 | document.body.removeChild(element); 43 | } 44 | 45 | export function readFromFile() { 46 | return new Promise((res, rej) => { 47 | const fileInput = document.createElement("input"); 48 | fileInput.type = "file"; 49 | fileInput.accept = "application/json"; 50 | 51 | fileInput.onchange = (event: any) => { 52 | const file = event.target.files[0]; 53 | const fileReader = new FileReader(); 54 | fileReader.onload = (e: any) => { 55 | res(e.target.result); 56 | }; 57 | fileReader.onerror = (e) => rej(e); 58 | fileReader.readAsText(file); 59 | }; 60 | 61 | fileInput.click(); 62 | }); 63 | } 64 | 65 | export function isIOS() { 66 | const userAgent = navigator.userAgent.toLowerCase(); 67 | return /iphone|ipad|ipod/.test(userAgent); 68 | } 69 | 70 | export function useWindowSize() { 71 | const [size, setSize] = useState({ 72 | width: window.innerWidth, 73 | height: window.innerHeight, 74 | }); 75 | 76 | useEffect(() => { 77 | const onResize = () => { 78 | setSize({ 79 | width: window.innerWidth, 80 | height: window.innerHeight, 81 | }); 82 | }; 83 | 84 | window.addEventListener("resize", onResize); 85 | 86 | return () => { 87 | window.removeEventListener("resize", onResize); 88 | }; 89 | }, []); 90 | 91 | return size; 92 | } 93 | 94 | export const MOBILE_MAX_WIDTH = 600; 95 | export function useMobileScreen() { 96 | const { width } = useWindowSize(); 97 | 98 | return width <= MOBILE_MAX_WIDTH; 99 | } 100 | 101 | export function isMobileScreen() { 102 | if (typeof window === "undefined") { 103 | return false; 104 | } 105 | return window.innerWidth <= MOBILE_MAX_WIDTH; 106 | } 107 | 108 | export function isFirefox() { 109 | return ( 110 | typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent) 111 | ); 112 | } 113 | 114 | export function selectOrCopy(el: HTMLElement, content: string) { 115 | const currentSelection = window.getSelection(); 116 | 117 | if (currentSelection?.type === "Range") { 118 | return false; 119 | } 120 | 121 | copyToClipboard(content); 122 | 123 | return true; 124 | } 125 | 126 | function getDomContentWidth(dom: HTMLElement) { 127 | const style = window.getComputedStyle(dom); 128 | const paddingWidth = 129 | parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); 130 | const width = dom.clientWidth - paddingWidth; 131 | return width; 132 | } 133 | 134 | function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) { 135 | let dom = document.getElementById(id); 136 | 137 | if (!dom) { 138 | dom = document.createElement("span"); 139 | dom.style.position = "absolute"; 140 | dom.style.wordBreak = "break-word"; 141 | dom.style.fontSize = "14px"; 142 | dom.style.transform = "translateY(-200vh)"; 143 | dom.style.pointerEvents = "none"; 144 | dom.style.opacity = "0"; 145 | dom.id = id; 146 | document.body.appendChild(dom); 147 | init?.(dom); 148 | } 149 | 150 | return dom!; 151 | } 152 | 153 | export function autoGrowTextArea(dom: HTMLTextAreaElement) { 154 | const measureDom = getOrCreateMeasureDom("__measure"); 155 | const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => { 156 | dom.innerText = "TEXT_FOR_MEASURE"; 157 | }); 158 | 159 | const width = getDomContentWidth(dom); 160 | measureDom.style.width = width + "px"; 161 | measureDom.innerText = dom.value !== "" ? dom.value : "1"; 162 | const endWithEmptyLine = dom.value.endsWith("\n"); 163 | const height = parseFloat(window.getComputedStyle(measureDom).height); 164 | const singleLineHeight = parseFloat( 165 | window.getComputedStyle(singleLineDom).height, 166 | ); 167 | 168 | const rows = 169 | Math.round(height / singleLineHeight) + (endWithEmptyLine ? 1 : 0); 170 | 171 | return rows; 172 | } 173 | 174 | export function getCSSVar(varName: string) { 175 | return getComputedStyle(document.body).getPropertyValue(varName).trim(); 176 | } 177 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | return ["```json", msg, "```"].join("\n"); 10 | } 11 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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. -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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. -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/cover.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/enable-actions-sync.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/enable-actions-sync.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/enable-actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/enable-actions.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/more.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/settings.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/vercel/vercel-create-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/vercel/vercel-create-1.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/vercel/vercel-create-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/vercel/vercel-create-2.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/vercel/vercel-create-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/vercel/vercel-create-3.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/vercel/vercel-env-edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/vercel/vercel-env-edit.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/docs/images/vercel/vercel-redeploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/docs/images/vercel/vercel-redeploy.jpg -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 即可重新部署。 -------------------------------------------------------------------------------- /ChatGPT-Next-Web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | async rewrites() { 5 | const ret = [ 6 | { 7 | source: "/api/proxy/:path*", 8 | destination: "https://api.openai.com/:path*", 9 | }, 10 | { 11 | source: "/google-fonts/:path*", 12 | destination: "https://fonts.googleapis.com/:path*", 13 | }, 14 | { 15 | source: "/sharegpt", 16 | destination: "https://sharegpt.com/api/conversations", 17 | }, 18 | ]; 19 | 20 | const apiUrl = process.env.API_URL; 21 | if (apiUrl) { 22 | console.log("[Next] using api url ", apiUrl); 23 | ret.push({ 24 | source: "/api/:path*", 25 | destination: `${apiUrl}/:path*`, 26 | }); 27 | } 28 | 29 | return { 30 | beforeFiles: ret, 31 | }; 32 | }, 33 | webpack(config) { 34 | config.module.rules.push({ 35 | test: /\.svg$/, 36 | use: ["@svgr/webpack"], 37 | }); 38 | 39 | return config; 40 | }, 41 | output: "standalone", 42 | env: { 43 | // 没有搞懂重写之后为什么打字机效果会丢失,只能暂时使用参数导出方式,在 api 调用处使用 44 | BASE_URL: process.env.BASE_URL?? "https://py.chatools.online", 45 | }, 46 | }; 47 | 48 | export default nextConfig; 49 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-next-web", 3 | "version": "1.9.3", 4 | "private": false, 5 | "license": "Anti 996", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "prompts": "node ./scripts/fetch-prompts.mjs", 12 | "prepare": "husky install", 13 | "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" 14 | }, 15 | "dependencies": { 16 | "@fortaine/fetch-event-source": "^3.0.6", 17 | "@hello-pangea/dnd": "^16.2.0", 18 | "@svgr/webpack": "^6.5.1", 19 | "@vercel/analytics": "^0.1.11", 20 | "emoji-picker-react": "^4.4.7", 21 | "fuse.js": "^6.6.2", 22 | "html-to-image": "^1.11.11", 23 | "mermaid": "^10.1.0", 24 | "next": "^13.4.3", 25 | "node-fetch": "^3.3.1", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-markdown": "^8.0.5", 29 | "react-router-dom": "^6.10.0", 30 | "rehype-highlight": "^6.0.0", 31 | "rehype-katex": "^6.0.2", 32 | "remark-breaks": "^3.0.2", 33 | "remark-gfm": "^3.0.1", 34 | "remark-math": "^5.1.1", 35 | "sass": "^1.59.2", 36 | "spark-md5": "^3.0.2", 37 | "use-debounce": "^9.0.3", 38 | "zustand": "^4.3.6" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^18.14.6", 42 | "@types/react": "^18.0.28", 43 | "@types/react-dom": "^18.0.11", 44 | "@types/react-katex": "^3.0.0", 45 | "@types/spark-md5": "^3.0.2", 46 | "cross-env": "^7.0.3", 47 | "eslint": "^8.36.0", 48 | "eslint-config-next": "13.2.3", 49 | "eslint-config-prettier": "^8.8.0", 50 | "eslint-plugin-prettier": "^4.2.1", 51 | "husky": "^8.0.0", 52 | "lint-staged": "^13.2.0", 53 | "prettier": "^2.8.7", 54 | "typescript": "4.9.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/favicon-16x16.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/favicon-32x32.png -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/ChatGPT-Next-Web/public/favicon.ico -------------------------------------------------------------------------------- /ChatGPT-Next-Web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | User-agent: vitals.vercel-insights.com 4 | Allow: / -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | } -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/scripts/.gitignore: -------------------------------------------------------------------------------- 1 | proxychains.conf 2 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /ChatGPT-Next-Web/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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster AS base 2 | ENV OPENAI_API_KEY="" 3 | ENV CODE="" 4 | WORKDIR /app 5 | COPY requirements.txt . 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | COPY . . 8 | CMD [ "python", "main.py" ] 9 | # CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Louye 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 预览 3 | 4 |

ChatGPT Next Web For FastAPI

5 | 6 | 一键免费部署你的私人 ChatGPT 网页应用。 7 | 8 | [演示 前端 Demo](https://zhb.chatools.online/) / [演示 后台 Demo](https://chatgpt-next-web-fastapi.vercel.app/) / [反馈 Issues](https://github.com/iszhouhuabo/chatgpt-next-web-fastapi/issues) 9 | 10 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fiszhouhuabo%2Fchatgpt-next-web-fastapi&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web-fastapi&repository-name=chatgpt-next-web-fastapi) 11 | 12 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/iszhouhuabo/chatgpt-next-web-fastapi) 13 | 14 | ![主界面](https://raw.githubusercontent.com/iszhouhuabo/ChatGPT-Next-Web/main/docs/images/cover.png) 15 | 16 |
17 | 18 | ## ChatGPT-Next-Web For Python FastApi 19 | 20 | > 站在巨人的肩膀上 - `Yidadaa` 21 | > 22 | > 如果你觉得这个项目对你有帮助,并且情况允许的话,可以帮忙 Star 一下,总之非常感谢支持~ 23 | 24 | 前端基于 `Yidadaa` 的 [ChatGPT-Next-Web](https://github.com/Yidadaa/ChatGPT-Next-Web) 25 | 二次开发而来,只做了稍微的修改(这样同步更新`Yidadaa`的代码和功能更加方便,不要重复造轮子),主要去掉了 `NodeJs` 26 | 的后台,代码文件:`ChatGPT-Next-Web` 27 | 28 | 将后端代码修改成 `Python` 呢,主要是为了降低后台工程师的开发成本;因为之后准备引入后台管理这块,`Python` 搞起来相对简单一点。 29 | 30 |
31 | 32 | ## 关于 FastAPI 33 | 34 | 不用担忧 `Python` 的效率问题,因为基于 `fastapi` ,FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API。 35 | > 它使用 Python 3.6+ 中的类型注释,以及 Python 3.7+ 中的异步语法,这使得代码易于阅读、维护和自动化测试。 36 | > FastAPI 是基于 Starlette 框架构建的,因此它继承了 Starlette 的许多特性,如 WebSocket 支持、GraphQL 支持、高性能、基于标准 37 | > Python 类型提示的自动文档生成等。 38 | > FastAPI 还提供了一个简单易用的依赖注入系统,使得开发人员可以轻松地编写可维护和可测试的代码。它还支持 OpenAPI 和 JSON 39 | > Schema 规范,可以自动生成 API 文档,并提供交互式 API 文档 UI。 40 | 41 | 据官方说,和 NodeJs/Go 的速度有的一拼,详细介绍可以查看官方: [FastAPI](https://fastapi.tiangolo.com) 42 | 43 |
44 | 45 | ## 开发计划 46 | 47 | - [x] 为每个对话设置系统 Prompt 48 | - [x] 允许用户自行编辑内置 Prompt 列表 49 | - [x] 预制角色:使用预制角色快速定制新对话 50 | - [x] 分享为图片,分享到 ShareGPT 链接 51 | 52 | - [ ] 界面文字自定义 53 | - [ ] OpenAI 密钥轮询 54 | - [ ] 用户登录、账号管理 55 | - [ ] 消息云同步 56 | 57 |
58 | 59 | ## 开始使用 60 | 61 | ### Python fastapi 后端 62 | 63 | 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 64 | 2. 点击右侧按钮开始部署: 65 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/iszhouhuabo/chatgpt-next-web-fastapi&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=chatgpt-next-web-fastapi) 66 | ,直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE; 67 | > 目前使用 Vercel 部署,SSE 流没办法正常运作,会有事件循环问题;可能是 Vercel 的配置有问题,目前正在想办法解决中... 本地开发和Docker部署不受影响 68 | 3. 部署完毕后,即可开始使用; 69 | 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 70 | 在某些区域被污染了,绑定自定义域名即可直连。 71 | 72 | ### ChatGPT-Next-Web 前端 73 | 74 | > Wait .... 75 | 76 |
77 | 78 | ## 环境变量 79 | 80 | ### `OPENAI_API_KEY` (必填项) 81 | 82 | OpanAI 密钥,你在 openai 账户页面申请的 api key。 83 | 84 | ### `CODE` (可选) 85 | 86 | 访问密码,可选,可以使用逗号隔开多个密码。 87 | 88 | ### `HIDE_USER_API_KEY` (可选) 89 | 90 | 隐藏前端自定义 OpenAI 密钥,设置成 `1` ,使用者将不能使用自己的 OpenAI 密钥 91 | 92 | ### `PROXY_URL` (可选) 93 | 94 | 代理服务器,请选用能信任的提供者;否则可能有泄漏 OpenAI 密钥的风险 95 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/api/__init__.py -------------------------------------------------------------------------------- /api/auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from constant import access_code_prefix 4 | from settings.config import settings 5 | 6 | 7 | def auth_headers(authorization: str) -> {bool, str}: 8 | authorize_code = os.getenv("CODE") 9 | 10 | if settings.code is None: 11 | return {"error": False, "message": "Not Open code model", "api_key": settings.openai_api_key} 12 | 13 | if authorization is None: 14 | return {"error": True, "message": "No Authorization header provided"} 15 | 16 | api_key_or_code = authorization.removeprefix("Bearer ") 17 | 18 | if not api_key_or_code.startswith(access_code_prefix): 19 | return {"error": False, "message": "You have key", "api_key": api_key_or_code} 20 | 21 | if api_key_or_code.removeprefix(access_code_prefix) in authorize_code.split(","): 22 | return {"error": False, "message": "Use System api key", "api_key": settings.openai_api_key} 23 | 24 | return {"error": True, "message": "Invalid Authorization header provided"} 25 | -------------------------------------------------------------------------------- /api/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/api/chat/__init__.py -------------------------------------------------------------------------------- /api/chat/chat.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import openai 4 | from fastapi import Request, HTTPException, Response 5 | 6 | from msg.chat_messages import ChatMessages 7 | from utils.all_utils import build_stream_msg 8 | 9 | 10 | async def chat_not_stream(api_key: str, 11 | chat: ChatMessages) -> Response: 12 | openai.api_key = api_key 13 | try: 14 | response = openai.ChatCompletion.create( 15 | model=chat.model, 16 | temperature=chat.temperature, 17 | presence_penalty=chat.presence_penalty, 18 | frequency_penalty=chat.frequency_penalty, 19 | stream=False, 20 | messages=chat.messages) 21 | return response 22 | except Exception as e: 23 | raise HTTPException(status_code=500, detail=e) 24 | 25 | 26 | async def chat_stream( 27 | request: Request, 28 | api_key: str, 29 | chat: ChatMessages): 30 | openai.api_key = api_key 31 | try: 32 | response = openai.ChatCompletion.create( 33 | model=chat.model, 34 | temperature=chat.temperature, 35 | presence_penalty=chat.presence_penalty, 36 | frequency_penalty=chat.frequency_penalty, 37 | stream=True, 38 | messages=chat.messages) 39 | except Exception as e: 40 | # yield build_stream_msg(e.http_body, True) if e.http_body is not None else build_stream_msg(e.user_message)\ 41 | raise HTTPException(status_code=500, detail=e) 42 | else: 43 | for trunk in response: 44 | if await request.is_disconnected(): 45 | break 46 | if trunk is None: 47 | continue 48 | if trunk['choices'][0]['finish_reason'] is not None: 49 | break 50 | else: 51 | yield json.dumps(trunk) 52 | finally: 53 | yield '[DONE]' 54 | -------------------------------------------------------------------------------- /api/chat/errors.py: -------------------------------------------------------------------------------- 1 | def handler(error: Exception): 2 | if error is None: 3 | return None 4 | # open ai error sts and msg 5 | http_status = error.http_status 6 | http_body = error.http_body if error.http_body is not None else error.user_message 7 | 8 | if http_status == 401: 9 | return "没有输入正确的密钥! \n{}".format(http_body) 10 | if http_status == 443: 11 | return "AI 忙不过来了,请稍后再试!" 12 | if "Connection aborted" in http_body or "Connection reset by peer" in http_body: 13 | return "AI 不小心下线了,请稍后再试!" 14 | if "No API key provided" in http_body: 15 | return "没有设置 API KYE!" 16 | return http_body 17 | -------------------------------------------------------------------------------- /api/chat/router.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from fastapi import APIRouter, Request, Header, HTTPException 3 | from sse_starlette import EventSourceResponse 4 | from starlette.responses import JSONResponse 5 | 6 | from api.auth import auth_headers 7 | from api.chat.chat import chat_stream, chat_not_stream 8 | from msg.chat_messages import ChatMessages 9 | from utils.all_utils import get_path, get_headers 10 | 11 | chat_router = APIRouter() 12 | 13 | 14 | # 聊天 15 | @chat_router.post("/v1/chat/completions") 16 | async def completions(request: Request, authorization: str = Header(None)): 17 | chat_message = ChatMessages(data=await request.json()) 18 | 19 | auth_rep = auth_headers(authorization) 20 | 21 | if auth_rep["error"]: 22 | raise HTTPException(status_code=401, detail=auth_rep["message"]) 23 | 24 | if chat_message.stream: 25 | return EventSourceResponse( 26 | chat_stream(request, auth_rep["api_key"], chat_message)) 27 | # return StreamingResponse(chat_stream(request, auth_rep["api_key"], chat_message), 28 | # media_type="text/event-stream") 29 | else: 30 | return JSONResponse(await chat_not_stream(auth_rep["api_key"], chat_message)) 31 | 32 | 33 | # 已经使用额度 34 | @chat_router.get("/dashboard/billing/usage") 35 | async def usage(start_date: str, end_date: str, authorization: str = Header(None)): 36 | response = requests.get( 37 | get_path() + "/dashboard/billing/usage?start_date={}&end_date={}".format(start_date, end_date), 38 | headers=get_headers(authorization)) 39 | return JSONResponse(response.json()) 40 | 41 | 42 | # 总额度 43 | @chat_router.get("/dashboard/billing/subscription") 44 | async def subscription(authorization: str = Header(None)): 45 | response = requests.get(get_path() + "/dashboard/billing/subscription", headers=get_headers(authorization)) 46 | return JSONResponse(response.json()) 47 | -------------------------------------------------------------------------------- /api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/api/config/__init__.py -------------------------------------------------------------------------------- /api/config/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from starlette.responses import JSONResponse 3 | 4 | from settings.config import settings 5 | 6 | config_router = APIRouter() 7 | 8 | 9 | # 给前端的一些配置 10 | @config_router.post("") 11 | async def subscription() -> JSONResponse: 12 | return JSONResponse({"needCode": True if settings.code else False, 13 | "hideUserApiKey": settings.hide_user_api_key, 14 | "enableGPT4": not settings.disable_gpt4}) 15 | -------------------------------------------------------------------------------- /constant.py: -------------------------------------------------------------------------------- 1 | access_code_prefix = "ak-" 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | py-chatgpt-next-web: 4 | image: zhouhuabo/py-chatgpt-next-web:latest 5 | ports: 6 | - "8000:5000" 7 | environment: 8 | - OPENAI_API_KEY=$OPENAI_API_KEY 9 | - CODE=$CODE -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import uvicorn 4 | from fastapi import FastAPI, Request 5 | from rich.console import Console 6 | from rich.table import Table 7 | from starlette.middleware.cors import CORSMiddleware 8 | from starlette.responses import JSONResponse 9 | 10 | from api.chat.router import chat_router 11 | from api.config.router import config_router 12 | from utils.all_utils import get_now 13 | 14 | # load_dotenv() 15 | 16 | app = FastAPI(default_response_class=JSONResponse, default_encoding="utf-8") 17 | 18 | # 配置允许的跨域请求来源和方法 19 | # origins = [ 20 | # "http://localhost", 21 | # "http://localhost:3000", 22 | # "http://localhost:8000", 23 | # ] 24 | 25 | app.add_middleware( 26 | CORSMiddleware, 27 | allow_origins=["*"], 28 | allow_credentials=True, 29 | allow_methods=["*"], 30 | allow_headers=["*"], 31 | ) 32 | 33 | app.include_router(chat_router, prefix="/api/openai") 34 | app.include_router(config_router, prefix="/api/config") 35 | 36 | 37 | @app.get("/") 38 | async def index(): 39 | return "请使用 Web 前端访问,不支持直接访问。前端地址:https://zhb.chatools.online" 40 | 41 | 42 | @app.middleware("http") 43 | async def log_requests(request: Request, call_next): 44 | if request.method.lower() == "options": 45 | response = await call_next(request) 46 | return response 47 | 48 | headers = dict(request.headers) 49 | client_host = request.client.host 50 | print( 51 | f"ZHB ??*_* :Time: {get_now()} | Host: {client_host} -> Request headers: \ 52 | {headers['authorization'] if 'authorization' in headers else 'None'}") 53 | response = await call_next(request) 54 | return response 55 | 56 | 57 | @app.on_event("startup") 58 | async def startup_event(): 59 | console = Console() 60 | table = Table(show_header=True, header_style="bold magenta") 61 | table.add_column("OpenAI API Key", style="dim", width=40) 62 | table.add_column("CODE", width=40) 63 | table.add_row(os.getenv("OPENAI_API_KEY"), os.getenv("CODE")) 64 | console.print("You Config Info ⬇️⬇️⬇️") 65 | console.print(table) 66 | console.print("欢迎来到 ChatGPT-Next-Web For Python FastApi 😊😊😊") 67 | console.print("Star: https://github.com/iszhouhuabo/chatgpt-next-web-fastapi") 68 | 69 | # console.print(settings) 70 | 71 | 72 | # 关闭时 73 | @app.on_event("shutdown") 74 | async def on_shutdown(): 75 | console = Console() 76 | console.print("See You Again...") 77 | 78 | 79 | if __name__ == "__main__": 80 | uvicorn.run("main:app", host="0.0.0.0", port=os.getenv("PORT") if os.getenv("PORT") is not None or "" else 8000, 81 | reload=False) 82 | -------------------------------------------------------------------------------- /msg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/msg/__init__.py -------------------------------------------------------------------------------- /msg/chat_messages.py: -------------------------------------------------------------------------------- 1 | class ChatMessages: 2 | def __init__(self, data): 3 | self.messages: [] = data["messages"] 4 | self.model: str = data["model"] 5 | self.temperature = data["temperature"] 6 | self.frequency_penalty = 0.0 7 | self.presence_penalty = data["presence_penalty"] 8 | self.stream: bool = data["stream"] if "stream" in data else False 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | anyio==3.7.0 4 | async-timeout==4.0.2 5 | attrs==23.1.0 6 | certifi==2023.5.7 7 | charset-normalizer==3.1.0 8 | click==8.1.3 9 | exceptiongroup==1.1.1 10 | fastapi==0.97.0 11 | frozenlist==1.3.3 12 | h11==0.14.0 13 | httptools==0.5.0 14 | idna==3.4 15 | markdown-it-py==3.0.0 16 | mdurl==0.1.2 17 | multidict==6.0.4 18 | openai==0.27.8 19 | pydantic==1.10.9 20 | python-dotenv==1.0.0 21 | requests==2.31.0 22 | rich==13.4.2 23 | sniffio==1.3.0 24 | sse-starlette==1.6.1 25 | starlette==0.28.0 26 | tqdm==4.65.0 27 | #typing_extensions==4.6.2 28 | urllib3==2.0.3 29 | uvicorn==0.22.0 30 | uvloop==0.17.0 31 | watchfiles==0.19.0 32 | websockets==11.0.3 33 | yarl==1.9.2 34 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/settings/__init__.py -------------------------------------------------------------------------------- /settings/config.py: -------------------------------------------------------------------------------- 1 | # 全局配置信息 2 | from functools import lru_cache 3 | from typing import Optional 4 | 5 | from pydantic import BaseSettings 6 | 7 | 8 | class Settings(BaseSettings): 9 | openai_api_key: Optional[str] 10 | code: Optional[str] 11 | proxy_url: Optional[str] 12 | 13 | base_url: str = 'https://api.openai.com' 14 | openai_org_id: Optional[str] 15 | 16 | hide_user_api_key: Optional[bool] = False 17 | disable_gpt4: Optional[bool] = False 18 | 19 | class Config: 20 | env_file = ".env" 21 | 22 | @lru_cache(maxsize=None) 23 | def get(self, name: str): 24 | return getattr(self, name) 25 | 26 | 27 | settings = Settings() 28 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iszhouhuabo/chatgpt-next-web-fastapi/cff184c468b66d65e71b1cbbf5b640ffd409d456/utils/__init__.py -------------------------------------------------------------------------------- /utils/all_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | 5 | from fastapi import HTTPException 6 | 7 | from api.auth import auth_headers 8 | 9 | 10 | def build_msg(msg): 11 | if msg is not None: 12 | return {"choices": [{"message": {"content": f"{msg}"}}]} 13 | return {"choices": [{"message": {"content": "不知道的错误,请稍后再试!"}}]} 14 | 15 | 16 | def build_stream_msg(msg, is_json: bool = False): 17 | if msg is not None: 18 | if "Rate limit reached" in msg: 19 | format_msg = "系统过载, 请稍后再试! `rate limit`" 20 | else: 21 | format_msg = f"```json \n{msg}```" if is_json else msg 22 | return json.dumps( 23 | {"choices": [{"delta": {"content": f"{format_msg}"}}]}) 24 | return json.dumps({"choices": [{"delta": {"content": "不知道的错误,请稍后再试!"}}]}) 25 | 26 | 27 | def get_path(): 28 | open_path = os.getenv("BASE_URL") 29 | return open_path if open_path else "https://api.openai.com" 30 | 31 | 32 | def get_headers(authorization: str): 33 | openai_headers = { 34 | "Content-Type": "application/json", 35 | "x-requested-with": "XMLHttpRequest", 36 | "Authorization": "", 37 | } 38 | auth_rep = auth_headers(authorization) 39 | 40 | if auth_rep["error"]: 41 | raise HTTPException(status_code=401, detail=auth_rep["message"]) 42 | 43 | openai_headers["Authorization"] = f"Bearer {auth_rep['api_key']}" 44 | 45 | return openai_headers 46 | 47 | 48 | def get_now(): 49 | timestamp = time.time() 50 | time_tuple = time.localtime(timestamp) 51 | return time.strftime('%Y-%m-%d %H:%M:%S', time_tuple) 52 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "devCommand": "uvicorn main:app --host 0.0.0.0 --port 8000", 3 | "builds": [ 4 | { 5 | "src": "main.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "main.py" 13 | } 14 | ] 15 | } --------------------------------------------------------------------------------