diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..53e4d9b24 --- /dev/null +++ b/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + [ + "next/babel", + { + "preset-env": { + "targets": { + "browsers": ["> 0.25%, not dead"] + } + } + } + ] + ] +} diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..0f4bf0e7c --- /dev/null +++ b/.env.template @@ -0,0 +1,34 @@ + +# Your openai api key. (required) +OPENAI_API_KEY=sk-xxxx + +# Access passsword, separated by comma. (optional) +CODE=your-password + +# You can start service behind a proxy +PROXY_URL=http://localhost:7890 + +# Override openai api request base url. (optional) +# Default: https://api.openai.com +# Examples: http://your-openai-proxy.com +BASE_URL= + +# Specify OpenAI organization ID.(optional) +# Default: Empty +# If you do not want users to input their own API key, set this value to 1. +OPENAI_ORG_ID= + +# (optional) +# Default: Empty +# If you do not want users to input their own API key, set this value to 1. +HIDE_USER_API_KEY= + +# (optional) +# Default: Empty +# If you do not want users to use GPT-4, set this value to 1. +DISABLE_GPT4= + +# (optional) +# Default: Empty +# If you do not want users to query balance, set this value to 1. +HIDE_BALANCE_QUERY= \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/功能建议.md b/.github/ISSUE_TEMPLATE/功能建议.md index 9ed1c845d..3fc3d0769 100644 --- a/.github/ISSUE_TEMPLATE/功能建议.md +++ b/.github/ISSUE_TEMPLATE/功能建议.md @@ -7,6 +7,10 @@ assignees: '' --- +> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 + +> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) + **这个功能与现有的问题有关吗?** 如果有关,请在此列出链接或者描述问题。 diff --git a/.github/ISSUE_TEMPLATE/反馈问题.md b/.github/ISSUE_TEMPLATE/反馈问题.md index ea56aa6fa..270263f06 100644 --- a/.github/ISSUE_TEMPLATE/反馈问题.md +++ b/.github/ISSUE_TEMPLATE/反馈问题.md @@ -7,10 +7,18 @@ assignees: '' --- +> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 + +> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) + **反馈须知** -> 请在下方中括号内输入 x 来表示你已经知晓相关内容。 + +⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。 + +请在下方中括号内输入 x 来表示你已经知晓相关内容。 - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 +- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。 **描述问题** 请在此描述你遇到了什么问题。 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..3a3cce576 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml new file mode 100644 index 000000000..b928ad6c1 --- /dev/null +++ b/.github/workflows/app.yml @@ -0,0 +1,105 @@ +name: Release App + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + create-release: + permissions: + contents: write + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.create-release.outputs.result }} + + steps: + - uses: actions/checkout@v3 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: get version + run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV + - name: create release + id: create-release + uses: actions/github-script@v6 + with: + script: | + const { data } = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + return data.id + + build-tauri: + needs: create-release + permissions: + contents: write + strategy: + fail-fast: false + matrix: + config: + - os: ubuntu-latest + arch: x86_64 + rust_target: x86_64-unknown-linux-gnu + - os: macos-latest + arch: x86_64 + rust_target: x86_64-apple-darwin + - os: macos-latest + arch: aarch64 + rust_target: aarch64-apple-darwin + - os: windows-latest + arch: x86_64 + rust_target: x86_64-pc-windows-msvc + + runs-on: ${{ matrix.config.os }} + steps: + - uses: actions/checkout@v3 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.config.rust_target }} + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.config.rust_target }} + - name: install dependencies (ubuntu only) + if: matrix.config.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + - name: install frontend dependencies + run: yarn install # change this to npm or pnpm depending on which one you use + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + releaseId: ${{ needs.create-release.outputs.release_id }} + + publish-release: + permissions: + contents: write + runs-on: ubuntu-latest + needs: [create-release, build-tauri] + + steps: + - name: publish release + id: publish-release + uses: actions/github-script@v6 + env: + release_id: ${{ needs.create-release.outputs.release_id }} + with: + script: | + github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: process.env.release_id, + draft: false, + prerelease: false + }) diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml new file mode 100644 index 000000000..560f66d34 --- /dev/null +++ b/.github/workflows/issue-translator.yml @@ -0,0 +1,15 @@ +name: Issue Translator +on: + issue_comment: + types: [created] + issues: + types: [opened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: usthe/issues-translate-action@v2.7 + with: + IS_MODIFY_TITLE: false + CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 15d324074..ebf5587d0 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -5,7 +5,7 @@ permissions: on: schedule: - - cron: "0 * * * *" # every hour + - cron: "0 0 * * *" # every day workflow_dispatch: jobs: @@ -35,6 +35,6 @@ jobs: - name: Sync check if: failure() run: | - echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。" - echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" exit 1 diff --git a/.gitignore b/.gitignore index 37f6b9029..b00b0e325 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,11 @@ yarn-error.log* next-env.d.ts dev -public/prompts.json - .vscode -.idea \ No newline at end of file +.idea + +# docker-compose env files +.env + +*.key +*.key.pub \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7ed7bc155..720a0cfe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY package.json yarn.lock ./ -RUN yarn config set registry 'https://registry.npm.taobao.org' +RUN yarn config set registry 'https://registry.npmmirror.com/' RUN yarn install FROM base AS builder @@ -41,6 +41,7 @@ COPY --from=builder /app/.next/server ./.next/server EXPOSE 3000 CMD if [ -n "$PROXY_URL" ]; then \ + export HOSTNAME="127.0.0.1"; \ protocol=$(echo $PROXY_URL | cut -d: -f1); \ host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ port=$(echo $PROXY_URL | cut -d: -f3); \ @@ -50,6 +51,8 @@ CMD if [ -n "$PROXY_URL" ]; then \ echo "remote_dns_subnet 224" >> $conf; \ echo "tcp_read_time_out 15000" >> $conf; \ echo "tcp_connect_time_out 8000" >> $conf; \ + echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ + echo "localnet ::1/128" >> $conf; \ echo "[ProxyList]" >> $conf; \ echo "$protocol $host $port" >> $conf; \ cat /etc/proxychains.conf; \ diff --git a/LICENSE b/LICENSE index 4f00efc87..542e91f4e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,75 +1,21 @@ -版权所有(c)<2023> +MIT License -反996许可证版本1.0 +Copyright (c) 2023 Zhang Yifei -在符合下列条件的情况下, -特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品” -)的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使 -用、复制,修改,衍生利用、散布,发布和再许可: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不 - 得自行修改。 -2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或 - 经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和 - 标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可 - 执行,则个人或法人实体必须遵守国际劳工标准的核心公约。 -3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同 - 意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和 - 标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该 - 等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规 - 情况的有关当局报告或投诉上述违反许可证的行为的权利。 - -该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用 -性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均 -不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。 - - -------------------------- ENGLISH ------------------------------ - - -Copyright (c) <2023> - -Anti 996 License Version 1.0 (Draft) - -Permission is hereby granted to any individual or legal entity obtaining a copy -of this licensed work (including the source code, documentation and/or related -items, hereinafter collectively referred to as the "licensed work"), free of -charge, to deal with the licensed work for any purpose, including without -limitation, the rights to use, reproduce, modify, prepare derivative works of, -publish, distribute and sublicense the licensed work, subject to the following -conditions: - -1. The individual or the legal entity must conspicuously display, without - modification, this License on each redistributed or derivative copy of the - Licensed Work. - -2. The individual or the legal entity must strictly comply with all applicable - laws, regulations, rules and standards of the jurisdiction relating to - labor and employment where the individual is physically located or where - the individual was born or naturalized; or where the legal entity is - registered or is operating (whichever is stricter). In case that the - jurisdiction has no such laws, regulations, rules and standards or its - laws, regulations, rules and standards are unenforceable, the individual - or the legal entity are required to comply with Core International Labor - Standards. - -3. The individual or the legal entity shall not induce or force its - employee(s), whether full-time or part-time, or its independent - contractor(s), in any methods, to agree in oral or written form, - to directly or indirectly restrict, weaken or relinquish his or - her rights or remedies under such laws, regulations, rules and - standards relating to labor and employment as mentioned above, - no matter whether such written or oral agreement are enforceable - under the laws of the said jurisdiction, nor shall such individual - or the legal entity limit, in any methods, the rights of its employee(s) - or independent contractor(s) from reporting or complaining to the copyright - holder or relevant authorities monitoring the compliance of the license - about its violation(s) of the said license. - -THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT -HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION -WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3b6308be9..07455d00d 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,27 @@

ChatGPT Next Web

-English / [简体中文](./README_CN.md) +English / [简体中文](./README_CN.md) / [日本語](./README_JA.md) -One-Click to deploy well-designed ChatGPT web UI on Vercel. +One-Click to get well-designed cross-platform ChatGPT web UI. -一键免费部署你的私人 ChatGPT 网页应用。 +一键免费部署你的跨平台私人 ChatGPT 应用。 -[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) +[![Web][Web-image]][web-url] +[![Windows][Windows-image]][download-url] +[![MacOS][MacOS-image]][download-url] +[![Linux][Linux-image]][download-url] -[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) +[Web App](https://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/mortiest_ricky) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) + +[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) + +[web-url]: https://chatgpt.nextweb.fun +[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases +[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge +[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows +[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple +[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) @@ -24,54 +36,63 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel. ## Features - **Deploy for free with one-click** on Vercel in under 1 minute +- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) +- Fully compatible with self-deployed llms, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI) - Privacy first, all data stored locally in the browser +- Markdown support: LaTex, mermaid, code highlight, etc. - Responsive design, dark mode and PWA - Fast first screen loading speed (~100kb), support streaming response +- New in v2: create, share and debug your chat tools with prompt templates (mask) - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) - Automatically compresses chat history to support long conversations while also saving your tokens -- One-click export all chat history with full Markdown support -- I18n supported +- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia ## Roadmap - [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) - [x] User Prompt: user can edit and save custom prompts to prompt list -- [ ] Prompt Template: create a new chat with pre-defined in-context prompts -- [ ] Share as image, share to ShareGPT -- [ ] Desktop App with tauri -- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. -- [ ] Plugins: support network search, caculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) +- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) +- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) +- [x] Desktop App with tauri +- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. +- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) -### Not in Plan +## What's New -- User login, accounts, cloud sync -- UI text customize +- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/). +- 🚀 v2.7 let's share conversations as image, or share to ShareGPT! +- 🚀 v2.8 now we have a client that runs across all platforms! ## 主要功能 - 在 1 分钟内使用 Vercel **免费一键部署** +- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) +- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等 - 精心设计的 UI,响应式设计,支持深色模式,支持 PWA - 极快的首屏加载速度(~100kb),支持流式响应 - 隐私安全,所有数据保存在用户浏览器本地 +- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 -- 一键导出聊天记录,完整的 Markdown 支持 +- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 ## 开发计划 - [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) - [x] 允许用户自行编辑内置 Prompt 列表 -- [ ] 提示词模板:使用预制上下文快速定制新对话 -- [ ] 分享为图片,分享到 ShareGPT -- [ ] 使用 tauri 打包桌面应用 -- [ ] 支持自部署的大语言模型 +- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) +- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) +- [x] 使用 tauri 打包桌面应用 +- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) -### 不会开发的功能 +## 最新动态 -- 界面文字自定义 -- 用户登录、账号管理、消息云同步 +- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 +- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com +- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 +- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 ## Get Started @@ -102,7 +123,9 @@ We recommend that you follow the steps below to re-deploy: ### Enable Automatic Updates -After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: +> If you encounter a failure of Upstream Sync execution, please manually sync fork once. + +After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: ![Automatic Updates](./docs/images/enable-actions.jpg) @@ -110,9 +133,9 @@ After forking the project, due to the limitations imposed by Github, you need to ### Manually Updating Code -If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. +If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. -You can star or watch this project or follow author to get release notifictions in time. +You can star or watch this project or follow author to get release notifications in time. ## Access Password @@ -146,6 +169,32 @@ Access passsword, separated by comma. Override openai api request base url. +### `OPENAI_ORG_ID` (optional) + +Specify OpenAI organization ID. + +### `HIDE_USER_API_KEY` (optional) + +> Default: Empty + +If you do not want users to input their own API key, set this value to 1. + +### `DISABLE_GPT4` (optional) + +> Default: Empty + +If you do not want users to use GPT-4, set this value to 1. + +### `HIDE_BALANCE_QUERY` (optional) + +> Default: Empty + +If you do not want users to query balance, set this value to 1. + +## Requirements + +NodeJS >= 18, Docker >= 20 + ## Development > [简体中文 > 如何进行二次开发](./README_CN.md#开发) @@ -156,6 +205,9 @@ Before starting development, you must create a new `.env.local` file at project ``` OPENAI_API_KEY= + +# if you are not able to access openai service, use this BASE_URL +BASE_URL=https://chatgpt1.nextweb.fun/api/proxy ``` ### Local Development @@ -178,8 +230,8 @@ yarn dev docker pull yidadaa/chatgpt-next-web docker run -d -p 3000:3000 \ - -e OPENAI_API_KEY="sk-xxxx" \ - -e CODE="your-password" \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ yidadaa/chatgpt-next-web ``` @@ -187,24 +239,44 @@ You can start service behind a proxy: ```shell docker run -d -p 3000:3000 \ - -e OPENAI_API_KEY="sk-xxxx" \ - -e CODE="your-password" \ - -e PROXY_URL="http://localhost:7890" \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + -e PROXY_URL=http://localhost:7890 \ yidadaa/chatgpt-next-web ``` +If your proxy needs password, use: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 user pass" +``` + ### Shell ```shell bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` +## Documentation + +> Please go to the [docs][./docs] directory for more documentation instructions. + +- [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md) +- [Frequent Ask Questions](./docs/faq-en.md) +- [How to add a new translation](./docs/translation.md) +- [How to use Vercel (No English)](./docs/vercel-cn.md) +- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md) + ## Screenshots ![Settings](./docs/images/settings.png) ![More](./docs/images/more.png) +## Translation + +If you want to add a new translation, read this [document](./docs/translation.md). + ## Donation [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) @@ -228,6 +300,15 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [@yankunsong](https://github.com/yankunsong) [@ypwhs](https://github.com/ypwhs) [@fxxxchao](https://github.com/fxxxchao) +[@hotic](https://github.com/hotic) +[@WingCH](https://github.com/WingCH) +[@jtung4](https://github.com/jtung4) +[@micozhu](https://github.com/micozhu) +[@jhansion](https://github.com/jhansion) +[@Sha1rholder](https://github.com/Sha1rholder) +[@AnsonHyq](https://github.com/AnsonHyq) +[@synwith](https://github.com/synwith) +[@piksonGit](https://github.com/piksonGit) ### Contributor @@ -235,4 +316,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ## LICENSE -[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN) +[MIT](https://opensource.org/license/mit/) diff --git a/README_CN.md b/README_CN.md index d2d64aa00..e593e45da 100644 --- a/README_CN.md +++ b/README_CN.md @@ -15,16 +15,6 @@ -## 主要功能 - -- 在 1 分钟内使用 Vercel **免费一键部署** -- 精心设计的 UI,响应式设计,支持深色模式 -- 极快的首屏加载速度(~100kb) -- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) -- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 -- 一键导出聊天记录,完整的 Markdown 支持 -- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 - ## 开始使用 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); @@ -44,6 +34,8 @@ ### 打开自动更新 +> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次! + 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新: ![自动更新](./docs/images/enable-actions.jpg) @@ -72,7 +64,7 @@ code1,code2,code3 ## 环境变量 -> 本项目大多数配置项都通过环境变量来设置。 +> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。 ### `OPENAI_API_KEY` (必填项) @@ -94,9 +86,23 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 > 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。 -## 开发 +### `OPENAI_ORG_ID` (可选) -> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 +指定 OpenAI 中的组织 ID。 + +### `HIDE_USER_API_KEY` (可选) + +如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 + +### `DISABLE_GPT4` (可选) + +如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。 + +### `HIDE_BALANCE_QUERY` (可选) + +如果你不想让用户查询余额,将此环境变量设置为 1 即可。 + +## 开发 点击下方按钮,开始二次开发: @@ -106,25 +112,31 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 ``` OPENAI_API_KEY= + +# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 +BASE_URL=https://chatgpt2.nextweb.fun/api/proxy ``` ### 本地开发 -1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT; -2. 执行 `yarn install && yarn dev` 即可。 +1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT; +2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署! +3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。 ## 部署 ### 容器部署 (推荐) -> 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。 +> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。 + +> ⚠️ 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。 ```shell docker pull yidadaa/chatgpt-next-web docker run -d -p 3000:3000 \ - -e OPENAI_API_KEY="sk-xxxx" \ - -e CODE="页面访问密码" \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=页面访问密码 \ yidadaa/chatgpt-next-web ``` @@ -132,13 +144,21 @@ docker run -d -p 3000:3000 \ ```shell docker run -d -p 3000:3000 \ - -e OPENAI_API_KEY="sk-xxxx" \ - -e CODE="页面访问密码" \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=页面访问密码 \ --net=host \ - -e PROXY_URL="http://127.0.0.1:7890" \ + -e PROXY_URL=http://127.0.0.1:7890 \ yidadaa/chatgpt-next-web ``` +如果你的本地代理需要账号密码,可以使用: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 user password" +``` + +如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。 + ### 本地部署 在控制台运行下方命令: @@ -147,6 +167,8 @@ docker run -d -p 3000:3000 \ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` +⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。 + ## 鸣谢 ### 捐赠者 @@ -157,8 +179,9 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) +### 相关项目 +- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型 + ## 开源协议 -> 反对 996,从我开始。 - -[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN) +[MIT](https://opensource.org/license/mit/) diff --git a/README_ES.md b/README_ES.md new file mode 100644 index 000000000..a5787a996 --- /dev/null +++ b/README_ES.md @@ -0,0 +1,173 @@ +
+预览 + +

ChatGPT Next Web

+ +Implemente su aplicación web privada ChatGPT de forma gratuita con un solo clic. + +[Demo demo](https://chat-gpt-next-web.vercel.app/) / [Problemas de comentarios](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Únete a Discord](https://discord.gg/zrhvHCr79N) / [Grupo QQ](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [Desarrolladores de consejos](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donar](#捐赠-donate-usdt) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +![主界面](./docs/images/cover.png) + +
+ +## Comenzar + +1. Prepara el tuyo [Clave API OpenAI](https://platform.openai.com/account/api-keys); +2. Haga clic en el botón de la derecha para iniciar la implementación: + [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), inicie sesión directamente con su cuenta de Github y recuerde completar la clave API y la suma en la página de variables de entorno[Contraseña de acceso a la página](#配置页面访问密码) CÓDIGO; +3. Una vez implementado, puede comenzar; +4. (Opcional)[Enlazar un nombre de dominio personalizado](https://vercel.com/docs/concepts/projects/domains/add-a-domain): El nombre de dominio DNS asignado por Vercel está contaminado en algunas regiones y puede conectarse directamente enlazando un nombre de dominio personalizado. + +## Manténgase actualizado + +Si sigue los pasos anteriores para implementar su proyecto con un solo clic, es posible que siempre diga "La actualización existe" porque Vercel creará un nuevo proyecto para usted de forma predeterminada en lugar de bifurcar el proyecto, lo que evitará que la actualización se detecte correctamente. +Le recomendamos que siga estos pasos para volver a implementar: + +- Eliminar el repositorio original; +- Utilice el botón de bifurcación en la esquina superior derecha de la página para bifurcar este proyecto; +- En Vercel, vuelva a seleccionar e implementar,[Echa un vistazo al tutorial detallado](./docs/vercel-cn.md#如何新建项目)。 + +### Activar actualizaciones automáticas + +> Si encuentra un error de ejecución de Upstream Sync, ¡Sync Fork manualmente una vez! + +Cuando bifurca el proyecto, debido a las limitaciones de Github, debe ir manualmente a la página Acciones de su proyecto bifurcado para habilitar Flujos de trabajo y habilitar Upstream Sync Action, después de habilitarlo, puede activar las actualizaciones automáticas cada hora: + +![自动更新](./docs/images/enable-actions.jpg) + +![启用自动更新](./docs/images/enable-actions-sync.jpg) + +### Actualizar el código manualmente + +Si desea que el manual se actualice inmediatamente, puede consultarlo [Documentación para Github](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) Aprenda a sincronizar un proyecto bifurcado con código ascendente. + +Puede destacar / ver este proyecto o seguir al autor para recibir notificaciones de nuevas actualizaciones de funciones. + +## Configurar la contraseña de acceso a la página + +> Después de configurar la contraseña, el usuario debe completar manualmente el código de acceso en la página de configuración para chatear normalmente, de lo contrario, se solicitará el estado no autorizado a través de un mensaje. + +> **advertir**: Asegúrese de establecer el número de dígitos de la contraseña lo suficientemente largo, preferiblemente más de 7 dígitos, de lo contrario[Será volado](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。 + +Este proyecto proporciona control de permisos limitado, agregue el nombre al nombre en la página Variables de entorno del Panel de control del proyecto Vercel `CODE` Variables de entorno con valores para contraseñas personalizadas separadas por comas: + + code1,code2,code3 + +Después de agregar o modificar la variable de entorno, por favor**Redesplegar**proyecto para poner en vigor los cambios. + +## Variable de entorno + +> La mayoría de los elementos de configuración de este proyecto se establecen a través de variables de entorno, tutorial:[Cómo modificar las variables de entorno de Vercel](./docs/vercel-cn.md)。 + +### `OPENAI_API_KEY` (Requerido) + +OpanAI key, la clave API que solicita en la página de su cuenta openai. + +### `CODE` (Opcional) + +Las contraseñas de acceso, opcionalmente, se pueden separar por comas. + +**advertir**: Si no completa este campo, cualquiera puede usar directamente su sitio web implementado, lo que puede hacer que su token se consuma rápidamente, se recomienda completar esta opción. + +### `BASE_URL` (Opcional) + +> Predeterminado: `https://api.openai.com` + +> Ejemplos: `http://your-openai-proxy.com` + +URL del proxy de interfaz OpenAI, complete esta opción si configuró manualmente el proxy de interfaz openAI. + +> Si encuentra problemas con el certificado SSL, establezca el `BASE_URL` El protocolo se establece en http. + +### `OPENAI_ORG_ID` (Opcional) + +Especifica el identificador de la organización en OpenAI. + +### `HIDE_USER_API_KEY` (Opcional) + +Si no desea que los usuarios rellenen la clave de API ellos mismos, establezca esta variable de entorno en 1. + +### `DISABLE_GPT4` (Opcional) + +Si no desea que los usuarios utilicen GPT-4, establezca esta variable de entorno en 1. + +### `HIDE_BALANCE_QUERY` (Opcional) + +Si no desea que los usuarios consulte el saldo, establezca esta variable de entorno en 1. + +## explotación + +> No se recomienda encarecidamente desarrollar o implementar localmente, debido a algunas razones técnicas, es difícil configurar el agente API de OpenAI localmente, a menos que pueda asegurarse de que puede conectarse directamente al servidor OpenAI. + +Haga clic en el botón de abajo para iniciar el desarrollo secundario: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +Antes de empezar a escribir código, debe crear uno nuevo en la raíz del proyecto `.env.local` archivo, lleno de variables de entorno: + + OPENAI_API_KEY= + +### Desarrollo local + +1. Instale nodejs 18 e hilo, pregunte a ChatGPT para obtener más detalles; +2. ejecutar `yarn install && yarn dev` Enlatar. ⚠️ Nota: Este comando es solo para desarrollo local, no para implementación. +3. Úselo si desea implementar localmente `yarn install && yarn start` comando, puede cooperar con pm2 a daemon para evitar ser asesinado, pregunte a ChatGPT para obtener más detalles. + +## desplegar + +### Implementación de contenedores (recomendado) + +> La versión de Docker debe ser 20 o posterior, de lo contrario se indicará que no se puede encontrar la imagen. + +> ⚠️ Nota: Las versiones de Docker están de 1 a 2 días por detrás de la última versión la mayor parte del tiempo, por lo que es normal que sigas diciendo "La actualización existe" después de la implementación. + +```shell +docker pull yidadaa/chatgpt-next-web + +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + yidadaa/chatgpt-next-web +``` + +También puede especificar proxy: + +```shell +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + --net=host \ + -e PROXY_URL=http://127.0.0.1:7890 \ + yidadaa/chatgpt-next-web +``` + +Si necesita especificar otras variables de entorno, agréguelas usted mismo en el comando anterior `-e 环境变量=环境变量值` para especificar. + +### Implementación local + +Ejecute el siguiente comando en la consola: + +```shell +bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) +``` + +⚠️ Nota: Si tiene problemas durante la instalación, utilice la implementación de Docker. + +## Reconocimiento + +### donante + +> Ver versión en inglés. + +### Colaboradores + +[Ver la lista de colaboradores del proyecto](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) + +## Licencia de código abierto + +[MIT](https://opensource.org/license/mit/) diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 000000000..72a0d5373 --- /dev/null +++ b/README_JA.md @@ -0,0 +1,275 @@ +
+icon + +

ChatGPT Next Web

+ +[English](./README.md) / [简体中文](./README_CN.md) / 日本語 + +ワンクリックで、クロスプラットフォーム ChatGPT ウェブ UI が表示されます。 + +[![Web][Web-image]][web-url] +[![Windows][Windows-image]][download-url] +[![MacOS][MacOS-image]][download-url] +[![Linux][Linux-image]][download-url] + +[Web App](https://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discord](https://discord.gg/YCkeafCafC) / [コーヒーをおごる](https://www.buymeacoffee.com/yidadaa) / [QQ グループ](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [開発者への報酬](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) + +[web-url]: https://chatgpt.nextweb.fun +[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases +[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge +[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows +[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple +[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +![cover](./docs/images/cover.png) + +
+ +## 特徴 + +- Vercel で 1 分以内に**ワンクリックで無料デプロイ**。 +- コンパクトなクライアント (~5MB) on Linux/Windows/MacOS、[今すぐダウンロード](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) +- [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) または [LocalAI](https://github.com/go-skynet/LocalAI) との使用をお勧めします +- プライバシー第一、すべてのデータはブラウザにローカルに保存されます +- マークダウンのサポート: LaTex、マーメイド、コードハイライトなど +- レスポンシブデザイン、ダークモード、PWA +- 最初の画面読み込み速度が速い(~100kb)、ストリーミングレスポンスをサポート +- v2 の新機能:プロンプトテンプレート(マスク)でチャットツールを作成、共有、デバッグ +- [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) と [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) による素晴らしいプロンプト +- トークンを保存しながら、長い会話をサポートするために自動的にチャット履歴を圧縮します +- 国際化: English、简体中文、繁体中文、日本語、Français、Español、Italiano、Türkçe、Deutsch、Tiếng Việt、Русский、Čeština、한국어 + +## ロードマップ + +- [x] システムプロンプト: ユーザー定義のプロンプトをシステムプロンプトとして固定 [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138) +- [x] ユーザープロンプト: ユーザはカスタムプロンプトを編集し、プロンプトリストに保存することができます。 +- [x] プロンプトテンプレート: 事前に定義されたインコンテキストプロンプトで新しいチャットを作成 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) +- [x] イメージとして共有、ShareGPT への共有 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) +- [x] tauri を使ったデスクトップアプリ +- [x] セルフホストモデル: [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) と完全に互換性があり、[LocalAI](https://github.com/go-skynet/LocalAI) のサーバーデプロイも可能です: llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly など +- [ ] プラグイン: ネットワーク検索、計算機、その他のAPIなどをサポート [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) + +## 新機能 + +- 🚀 v2.0 がリリースされ、プロンプト・テンプレートが作成できるようになりました!こちらをお読みください: [ChatGPT プロンプトエンジニアリング Tips: ゼロ、一発、数発プロンプト](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/)。 +- 💡 このプロジェクトをいつでもどこでも簡単に使いたいですか?このデスクトッププラグインをお試しください: https://github.com/mushan0x0/AI0x0.com +- 🚀 v2.7 では、会話を画像として共有したり、ShareGPT に共有することができます! +- 🚀 v2.8 全てのプラットフォームで動作するクライアントができました! + +## 始める + +> [簡体字中国語 > 始め方](./README_CN.md#开始使用) + +1. [OpenAI API Key](https://platform.openai.com/account/api-keys) を取得する; +2. クリック + [![Vercel でデプロイ](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)をクリックします。`CODE` はあなたのページのパスワードであることを忘れないでください; +3. お楽しみください :) + +## FAQ + +[簡体字中国語 > よくある質問](./docs/faq-cn.md) + +[English > FAQ](./docs/faq-en.md) + +## 更新を継続する + +> [簡体字中国語 > コードを最新の状態に保つ方法](./README_CN.md#保持更新) + +上記の手順に沿ってワンクリックで自分のプロジェクトをデプロイした場合、"Updates Available" が常に表示される問題に遭遇するかもしれません。これは、Vercel がこのプロジェクトをフォークする代わりに、デフォルトで新しいプロジェクトを作成するため、アップデートを正しく検出できないためです。 + +以下の手順で再デプロイすることをお勧めします: + +- 元のリポジトリを削除してください; +- ページの右上にあるフォークボタンを使って、このプロジェクトをフォークする; +- Vercel を選択し、再度デプロイする。[詳しいチュートリアルを参照](./docs/vercel-cn.md)。 + +### 自動アップデートを有効にする + +> Upstream Sync の実行に失敗した場合は、手動で一度フォークしてください。 + +プロジェクトをフォークした後、GitHub の制限により、フォークしたプロジェクトの Actions ページで Workflows と Upstream Sync Action を手動で有効にする必要があります。有効にすると、1 時間ごとに自動更新がスケジュールされます: + +![Automatic Updates](./docs/images/enable-actions.jpg) + +![Enable Automatic Updates](./docs/images/enable-actions-sync.jpg) + +### 手動でコードを更新する + +すぐに更新したい場合は、[GitHub ドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) をチェックして、フォークしたプロジェクトを上流のコードと同期させる方法を学んでください。 + +このプロジェクトにスターをつけたり、ウォッチしたり、作者をフォローすることで、リリースの通知を受け取ることができます。 + +## アクセスパスワード + +> [簡体字中国語 > アクセスパスワードを増やす方法](./README_CN.md#配置页面访问密码) + +このプロジェクトではアクセス制御を制限しています。vercel の環境変数のページに `CODE` という環境変数を追加してください。その値は次のようにカンマで区切られたパスワードでなければなりません: + +``` +code1,code2,code3 +``` + +この環境変数を追加または変更した後は、変更を有効にするためにプロジェクトを再デプロイしてください。 + +## 環境変数 + +> [簡体字中国語 > API キー、アクセスパスワード、インターフェイスプロキシ設定方法](./README_CN.md#环境变量) + +### `OPENAI_API_KEY` (必須) + +OpenAI の api キー。 + +### `CODE` (オプション) + +カンマで区切られたアクセスパスワード。 + +### `BASE_URL` (オプション) + +> デフォルト: `https://api.openai.com` + +> 例: `http://your-openai-proxy.com` + +OpenAI api のリクエストベースの url をオーバーライドします。 + +### `OPENAI_ORG_ID` (オプション) + +OpenAI の組織 ID を指定します。 + +### `HIDE_USER_API_KEY` (オプション) + +> デフォルト: 空 + +ユーザーに自分の API キーを入力させたくない場合は、この値を 1 に設定する。 + +### `DISABLE_GPT4` (オプション) + +> デフォルト: 空 + +ユーザーに GPT-4 を使用させたくない場合は、この値を 1 に設定する。 + +### `HIDE_BALANCE_QUERY` (オプション) + +> デフォルト: 空 + +ユーザーに残高を照会させたくない場合は、この値を 1 に設定する。 + +## 必要条件 + +NodeJS >= 18、Docker >= 20 + +## Development + +> [簡体字中国語 > 二次開発の進め方](./README_CN.md#开发) + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +開発を始める前に、プロジェクトのルートに新しい `.env.local` ファイルを作成し、そこに api キーを置く必要があります: + +``` +OPENAI_API_KEY= + +# OpenAI サービスにアクセスできない場合は、この BASE_URL を使用してください +BASE_URL=https://chatgpt1.nextweb.fun/api/proxy +``` + +### ローカルデプロイ + +```shell +# 1. nodejs と yarn をまずインストールする +# 2. `.env.local` にローカルの env vars を設定する +# 3. 実行 +yarn install +yarn dev +``` + +## デプロイ + +> [簡体字中国語 > プライベートサーバーへのデプロイ方法](./README_CN.md#部署) + +### Docker (推奨) + +```shell +docker pull yidadaa/chatgpt-next-web + +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + yidadaa/chatgpt-next-web +``` + +プロキシの後ろでサービスを開始することができる: + +```shell +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + -e PROXY_URL=http://localhost:7890 \ + yidadaa/chatgpt-next-web +``` + +プロキシにパスワードが必要な場合: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 user pass" +``` + +### シェル + +```shell +bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) +``` + +## スクリーンショット + +![Settings](./docs/images/settings.png) + +![More](./docs/images/more.png) + +## 翻訳 + +新しい翻訳を追加したい場合は、この[ドキュメント](./docs/translation.md)をお読みください。 + +## 寄付 + +[コーヒーをおごる](https://www.buymeacoffee.com/yidadaa) + +## スペシャルサンクス + +### スポンサー + +> 寄付金額が 100 元以上のユーザーのみリストアップしています + +[@mushan0x0](https://github.com/mushan0x0) +[@ClarenceDan](https://github.com/ClarenceDan) +[@zhangjia](https://github.com/zhangjia) +[@hoochanlon](https://github.com/hoochanlon) +[@relativequantum](https://github.com/relativequantum) +[@desenmeng](https://github.com/desenmeng) +[@webees](https://github.com/webees) +[@chazzhou](https://github.com/chazzhou) +[@hauy](https://github.com/hauy) +[@Corwin006](https://github.com/Corwin006) +[@yankunsong](https://github.com/yankunsong) +[@ypwhs](https://github.com/ypwhs) +[@fxxxchao](https://github.com/fxxxchao) +[@hotic](https://github.com/hotic) +[@WingCH](https://github.com/WingCH) +[@jtung4](https://github.com/jtung4) +[@micozhu](https://github.com/micozhu) +[@jhansion](https://github.com/jhansion) +[@Sha1rholder](https://github.com/Sha1rholder) +[@AnsonHyq](https://github.com/AnsonHyq) +[@synwith](https://github.com/synwith) +[@piksonGit](https://github.com/piksonGit) + +### コントリビューター + +[コントリビューター達](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) + +## ライセンス + +[MIT](https://opensource.org/license/mit/) diff --git a/README_KO.md b/README_KO.md new file mode 100644 index 000000000..519dd9d9b --- /dev/null +++ b/README_KO.md @@ -0,0 +1,187 @@ +
+프리뷰 + +

ChatGPT Next Web

+ +개인 ChatGPT 웹 애플리케이션을 한 번의 클릭으로 무료로 배포하세요. + +[데모 Demo](https://chat-gpt-next-web.vercel.app/) / [피드백 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discord 참여](https://discord.gg/zrhvHCr79N) / [QQ 그룹](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [개발자에게 기부](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [기부 Donate](#기부-donate-usdt) + +[![Vercel로 배포하기](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) + +[![Gitpod에서 열기](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +![메인 화면](./docs/images/cover.png) + +
+ +## 사용 시작 + +1. [OpenAI API Key](https://platform.openai.com/account/api-keys)를 준비합니다. +2. 오른쪽 버튼을 클릭하여 배포를 시작하십시오: + [![Vercel로 배포하기](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web). Github 계정으로 바로 로그인하십시오. API Key와 [페이지 접근 비밀번호](#페이지-접근-비밀번호-설정) CODE를 환경 변수 페이지에 입력하십시오. +3. 배포가 완료되면 사용을 시작하십시오. +4. (선택 사항) [사용자 정의 도메인 바인딩](https://vercel.com/docs/concepts/projects/domains/add-a-domain) : Vercel에서 할당한 도메인 DNS가 일부 지역에서 오염되어 있습니다. 사용자 정의 도메인을 바인딩하면 직접 연결할 수 있습니다. + +## 업데이트 유지 + +위의 단계대로 프로젝트를 배포한 경우 "업데이트가 있습니다"라는 메시지가 항상 표시될 수 있습니다. 이는 Vercel이 기본적으로 새 프로젝트를 생성하고이 프로젝트를 포크하지 않기 때문입니다. 이 문제는 업데이트를 올바르게 감지할 수 없습니다. +아래 단계를 따라 다시 배포하십시오: + +- 기존 저장소를 삭제합니다. +- 페이지 오른쪽 상단의 포크 버튼을 사용하여 이 프로젝트를 포크합니다. +- Vercel에서 다시 선택하여 배포하십시오. [자세한 튜토리얼 보기](./docs/vercel-cn.md#새-프로젝트-만드는-방법). + +### 자동 업데이트 활성화 + +> Upstream Sync 오류가 발생한 경우 수동으로 Sync Fork를 한 번 실행하십시오! + +프로젝트를 포크한 후 GitHub의 제한으로 인해 포크한 프로젝트의 동작 페이지에서 워크플로우를 수동으로 활성화해야 합니다. Upstream Sync Action을 활성화하면 매시간마다 자동 업데이트가 활성화됩니다: + +![자동 업데이트](./docs/images/enable-actions.jpg) + +![자동 업데이트 활성화](./docs/images/enable-actions-sync.jpg) + +### 수동으로 코드 업데이트 + +수동으로 즉시 업데이트하려면 [GitHub 문서](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)에서 포크된 프로젝트를 어떻게 원본 코드와 동기화하는지 확인하십시오. + +이 프로젝트에 별표/감시를 부여하거나 작성자를 팔로우하여 새 기능 업데이트 알림을 받을 수 있습니다. + +## 페이지 접근 비밀번호 설정 + +> 비밀번호가 설정된 후, 사용자는 설정 페이지에서 접근 코드를 수동으로 입력하여 정상적으로 채팅할 수 있습니다. 그렇지 않으면 메시지를 통해 권한이 없는 상태가 표시됩니다. + +> **경고** : 비밀번호의 길이를 충분히 길게 설정하십시오. 최소 7 자리 이상이 좋습니다. 그렇지 않으면 [해킹될 수 있습니다](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518). + +이 프로젝트는 제한된 권한 제어 기능을 제공합니다. Vercel 프로젝트 컨트롤 패널의 환경 변수 페이지에서 `CODE`라는 환경 변수를 추가하십시오. 값은 쉼표로 구분된 사용자 정의 비밀번호로 설정됩니다. (아래 예시의 경우 `code1` `code2` `code3` 3개의 비밀번호가 생성됩니다.) + +``` +code1,code2,code3 +``` + +이 환경 변수를 추가하거나 수정한 후에는 프로젝트를 다시 배포하여 변경 사항을 적용해야 합니다. + +## 환경 변수 +> 이 프로젝트에서 대부분의 설정 요소들은 환경 변수를 통해 설정됩니다. [Vercel 환경변수 수정 방법.](./docs/vercel-ko.md)。 + +## OPENAI_API_KEY (필수 항목) + +OpenAI 키로, openai 계정 페이지에서 신청한 api key입니다. + +## CODE (선택 가능) + +접근 비밀번호로, 선택적입니다. 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다. + +**경고** : 이 항목을 입력하지 않으면, 누구나 여러분이 배포한 웹사이트를 직접 사용할 수 있게 됩니다. 이로 인해 토큰이 빠르게 소진될 수 있으므로, 이 항목을 반드시 입력하는 것이 좋습니다. + +## BASE_URL (선택 가능) + +> 기본값: `https://api.openai.com` + +> 예시: `http://your-openai-proxy.com` + +OpenAI 인터페이스 프록시 URL입니다. 만약, 수동으로 openai 인터페이스 proxy를 설정했다면, 이 항목을 입력하셔야 합니다. + +**참고**: SSL 인증서 문제가 발생한 경우, BASE_URL의 프로토콜을 http로 설정하세요. + +## OPENAI_ORG_ID (선택 가능) + +OpenAI 내의 조직 ID를 지정합니다. + +## HIDE_USER_API_KEY (선택 가능) + +사용자가 API Key를 직접 입력하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요. + +## DISABLE_GPT4 (선택 가능) + +사용자가 GPT-4를 사용하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요. + +## HIDE_BALANCE_QUERY (선택 가능) + +사용자가 잔액을 조회하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요. + +## 개발 + +아래 버튼을 클릭하여 개발을 시작하세요: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) + +코드 작성을 전, 프로젝트 루트 디렉토리에 `.env.local` 파일을 새로 만들고 해당 파일에 환경 변수를 입력해야 합니다: + +``` +OPENAI_API_KEY=<여기에 여러분의 api 키를 입력하세요> + +#중국 사용자들은 이 프로젝트에 포함된 프록시를 사용하여 개발할 수 있습니다. 또는 다른 프록시 주소를 자유롭게 선택할 수 있습니다. +BASE_URL=https://chatgpt1.nextweb.fun/api/proxy +``` + + +### 로컬 환경에서의 개발 + +1. nodejs 18과 yarn을 설치하세요. 자세한 사항은 ChatGPT에 문의하십시오. +2. `yarn install && yarn dev` 명령을 실행하세요. ⚠️ 주의: 이 명령은 로컬 개발 전용입니다. 배포용으로 사용하지 마십시오! +3. 로컬에서 배포하고 싶다면, `yarn install && yarn build && yarn start` 명령을 사용하세요. pm2와 함께 사용하여 프로세스를 보호하고, 강제 종료되지 않도록 할 수 있습니다. 자세한 내용은 ChatGPT에 문의하세요. + +## 배포 + +### 컨테이너 배포 (추천) + +> Docker 버전은 20 이상이어야 합니다. 그렇지 않으면 이미지를 찾을 수 없다는 메시지가 표시됩니다. + +> ⚠️ 주의: docker 버전은 대부분의 경우 최신 버전보다 1~2일 뒤처집니다. 따라서 배포 후 "업데이트 가능" 알림이 지속적으로 나타날 수 있으며, 이는 정상적인 현상입니다. + +```shell +docker pull yidadaa/chatgpt-next-web + +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=페이지 접근 비밀번호 \ + yidadaa/chatgpt-next-web +``` + +프록시를 지정하려면 다음을 사용하세요: + +```shell +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=페이지 접근 비밀번호 \ + --net=host \ + -e PROXY_URL=http://127.0.0.1:7890 \ + yidadaa/chatgpt-next-web +``` + +로컬 프록시에 사용자 이름과 비밀번호가 필요한 경우, 아래와 같이 사용하세요: + +```shell +-e PROXY_URL="http://127.0.0.1:7890 사용자이름 비밀번호" +``` + +다른 환경 변수를 지정해야 하는 경우, 위의 명령에 `-e 환경변수=환경변수값`을 추가하여 지정하세요. + +### 로컬 배포 + +콘솔에서 아래의 명령을 실행하세요: + +```shell +bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) +``` + +⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요. + +## 감사의 말 + +### 기부자 + +> 영문 버전 참조. + +### 기여자 + +[프로젝트 기여자 목록 보기](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) + +### 관련 프로젝트 +- [one-api](https://github.com/songquanpeng/one-api): 통합 대형 모델 할당 관리 플랫폼, 주요 대형 언어 모델 모두 지원 + +## 오픈소스 라이센스 + +[MIT](https://opensource.org/license/mit/) diff --git a/app/api/auth.ts b/app/api/auth.ts new file mode 100644 index 000000000..e0453b2b4 --- /dev/null +++ b/app/api/auth.ts @@ -0,0 +1,65 @@ +import { NextRequest } from "next/server"; +import { getServerSideConfig } from "../config/server"; +import md5 from "spark-md5"; +import { ACCESS_CODE_PREFIX } from "../constant"; + +function getIP(req: NextRequest) { + let ip = req.ip ?? req.headers.get("x-real-ip"); + const forwardedFor = req.headers.get("x-forwarded-for"); + + if (!ip && forwardedFor) { + ip = forwardedFor.split(",").at(0) ?? ""; + } + + return ip; +} + +function parseApiKey(bearToken: string) { + const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX); + + return { + accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length), + apiKey: isOpenAiKey ? token : "", + }; +} + +export function auth(req: NextRequest) { + const authToken = req.headers.get("Authorization") ?? ""; + + // check if it is openai api key or user token + const { accessCode, apiKey: token } = parseApiKey(authToken); + + const hashedCode = md5.hash(accessCode ?? "").trim(); + + const serverConfig = getServerSideConfig(); + console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]); + console.log("[Auth] got access code:", accessCode); + console.log("[Auth] hashed access code:", hashedCode); + console.log("[User IP] ", getIP(req)); + console.log("[Time] ", new Date().toLocaleString()); + + if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { + return { + error: true, + msg: !accessCode ? "empty access code" : "wrong access code", + }; + } + + // if user does not provide an api key, inject system api key + if (!token) { + const apiKey = serverConfig.apiKey; + if (apiKey) { + console.log("[Auth] use system api key"); + req.headers.set("Authorization", `Bearer ${apiKey}`); + } else { + console.log("[Auth] admin did not provide an api key"); + } + } else { + console.log("[Auth] use user api key"); + } + + return { + error: false, + }; +} diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts deleted file mode 100644 index 41f135495..000000000 --- a/app/api/chat-stream/route.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createParser } from "eventsource-parser"; -import { NextRequest } from "next/server"; -import { requestOpenai } from "../common"; - -async function createStream(req: NextRequest) { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - const res = await requestOpenai(req); - - const contentType = res.headers.get("Content-Type") ?? ""; - if (!contentType.includes("stream")) { - const content = await ( - await res.text() - ).replace(/provided:.*. You/, "provided: ***. You"); - console.log("[Stream] error ", content); - return "```json\n" + content + "```"; - } - - const stream = new ReadableStream({ - async start(controller) { - function onParse(event: any) { - if (event.type === "event") { - const data = event.data; - // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream - if (data === "[DONE]") { - controller.close(); - return; - } - try { - const json = JSON.parse(data); - const text = json.choices[0].delta.content; - const queue = encoder.encode(text); - controller.enqueue(queue); - } catch (e) { - controller.error(e); - } - } - } - - const parser = createParser(onParse); - for await (const chunk of res.body as any) { - parser.feed(decoder.decode(chunk, { stream: true })); - } - }, - }); - return stream; -} - -export async function POST(req: NextRequest) { - try { - const stream = await createStream(req); - return new Response(stream); - } catch (error) { - console.error("[Chat Stream]", error); - return new Response( - ["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""), - ); - } -} - -export const config = { - runtime: "edge", -}; diff --git a/app/api/common.ts b/app/api/common.ts index 53ab18ed6..cd2936ee3 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,13 +1,18 @@ -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; -const OPENAI_URL = "api.openai.com"; +export const OPENAI_URL = "api.openai.com"; const DEFAULT_PROTOCOL = "https"; -const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL; -const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; +const PROTOCOL = process.env.PROTOCOL || DEFAULT_PROTOCOL; +const BASE_URL = process.env.BASE_URL || OPENAI_URL; +const DISABLE_GPT4 = !!process.env.DISABLE_GPT4; export async function requestOpenai(req: NextRequest) { - const apiKey = req.headers.get("token"); - const openaiPath = req.headers.get("path"); + const controller = new AbortController(); + const authValue = req.headers.get("Authorization") ?? ""; + const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( + "/api/openai/", + "", + ); let baseUrl = BASE_URL; @@ -15,15 +20,79 @@ export async function requestOpenai(req: NextRequest) { baseUrl = `${PROTOCOL}://${baseUrl}`; } + if (baseUrl.endsWith('/')) { + baseUrl = baseUrl.slice(0, -1); + } + console.log("[Proxy] ", openaiPath); console.log("[Base Url]", baseUrl); - return fetch(`${baseUrl}/${openaiPath}`, { + if (process.env.OPENAI_ORG_ID) { + console.log("[Org ID]", process.env.OPENAI_ORG_ID); + } + + const timeoutId = setTimeout(() => { + controller.abort(); + }, 10 * 60 * 1000); + + const fetchUrl = `${baseUrl}/${openaiPath}`; + const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, + "Cache-Control": "no-store", + Authorization: authValue, + ...(process.env.OPENAI_ORG_ID && { + "OpenAI-Organization": process.env.OPENAI_ORG_ID, + }), }, method: req.method, body: req.body, - }); + // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse gpt4 request + if (DISABLE_GPT4 && req.body) { + try { + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody); + + if ((jsonBody?.model ?? "").includes("gpt-4")) { + return NextResponse.json( + { + error: true, + message: "you are not allowed to use gpt-4 model", + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error("[OpenAI] gpt4 filter", e); + } + } + + try { + const res = await fetch(fetchUrl, fetchOptions); + + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } } diff --git a/app/api/config/route.ts b/app/api/config/route.ts index e04e22a0c..7749e6e9e 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { getServerSideConfig } from "../../config/server"; @@ -8,14 +8,20 @@ const serverConfig = getServerSideConfig(); // 警告!不要在这里写入任何敏感信息! const DANGER_CONFIG = { needCode: serverConfig.needCode, + hideUserApiKey: serverConfig.hideUserApiKey, + disableGPT4: serverConfig.disableGPT4, + hideBalanceQuery: serverConfig.hideBalanceQuery, }; declare global { type DangerConfig = typeof DANGER_CONFIG; } -export async function POST(req: NextRequest) { - return NextResponse.json({ - needCode: serverConfig.needCode, - }); +async function handle() { + return NextResponse.json(DANGER_CONFIG); } + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts new file mode 100644 index 000000000..9df005a31 --- /dev/null +++ b/app/api/openai/[...path]/route.ts @@ -0,0 +1,77 @@ +import { type OpenAIListModelResponse } from "@/app/client/platforms/openai"; +import { getServerSideConfig } from "@/app/config/server"; +import { OpenaiPath } from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "../../auth"; +import { requestOpenai } from "../../common"; + +const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); + +function getModels(remoteModelRes: OpenAIListModelResponse) { + const config = getServerSideConfig(); + + if (config.disableGPT4) { + remoteModelRes.data = remoteModelRes.data.filter( + (m) => !m.id.startsWith("gpt-4"), + ); + } + + return remoteModelRes; +} + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[OpenAI Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const subpath = params.path.join("/"); + + if (!ALLOWD_PATH.has(subpath)) { + console.log("[OpenAI Route] forbidden path ", subpath); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + subpath, + }, + { + status: 403, + }, + ); + } + + const authResult = auth(req); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await requestOpenai(req); + + // list models + if (subpath === OpenaiPath.ListModelPath && response.status === 200) { + const resJson = (await response.json()) as OpenAIListModelResponse; + const availableModels = getModels(resJson); + return NextResponse.json(availableModels, { + status: response.status, + }); + } + + return response; + } catch (e) { + console.error("[OpenAI] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts deleted file mode 100644 index 0ac94bdd5..000000000 --- a/app/api/openai/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { requestOpenai } from "../common"; - -async function makeRequest(req: NextRequest) { - try { - const api = await requestOpenai(req); - const res = new NextResponse(api.body); - res.headers.set("Content-Type", "application/json"); - res.headers.set("Cache-Control", "no-cache"); - return res; - } catch (e) { - console.error("[OpenAI] ", req.body, e); - return NextResponse.json( - { - error: true, - msg: JSON.stringify(e), - }, - { - status: 500, - } - ); - } -} - -export async function POST(req: NextRequest) { - return makeRequest(req); -} - -export async function GET(req: NextRequest) { - return makeRequest(req); -} - -export const config = { - runtime: "edge", -}; diff --git a/app/api/openai/typing.ts b/app/api/openai/typing.ts deleted file mode 100644 index b936530c3..000000000 --- a/app/api/openai/typing.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { - CreateChatCompletionRequest, - CreateChatCompletionResponse, -} from "openai"; - -export type ChatRequest = CreateChatCompletionRequest; -export type ChatResponse = CreateChatCompletionResponse; diff --git a/app/client/api.ts b/app/client/api.ts new file mode 100644 index 000000000..b04dd88b8 --- /dev/null +++ b/app/client/api.ts @@ -0,0 +1,151 @@ +import { getClientConfig } from "../config/client"; +import { ACCESS_CODE_PREFIX } from "../constant"; +import { ChatMessage, ModelType, useAccessStore } from "../store"; +import { ChatGPTApi } from "./platforms/openai"; + +export const ROLES = ["system", "user", "assistant"] as const; +export type MessageRole = (typeof ROLES)[number]; + +export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; +export type ChatModel = ModelType; + +export interface RequestMessage { + role: MessageRole; + content: string; +} + +export interface LLMConfig { + model: string; + temperature?: number; + top_p?: number; + stream?: boolean; + presence_penalty?: number; + frequency_penalty?: number; +} + +export interface ChatOptions { + messages: RequestMessage[]; + config: LLMConfig; + + onUpdate?: (message: string, chunk: string) => void; + onFinish: (message: string) => void; + onError?: (err: Error) => void; + onController?: (controller: AbortController) => void; +} + +export interface LLMUsage { + used: number; + total: number; +} + +export interface LLMModel { + name: string; + available: boolean; +} + +export abstract class LLMApi { + abstract chat(options: ChatOptions): Promise; + abstract usage(): Promise; + abstract models(): Promise; +} + +type ProviderName = "openai" | "azure" | "claude" | "palm"; + +interface Model { + name: string; + provider: ProviderName; + ctxlen: number; +} + +interface ChatProvider { + name: ProviderName; + apiConfig: { + baseUrl: string; + apiKey: string; + summaryModel: Model; + }; + models: Model[]; + + chat: () => void; + usage: () => void; +} + +export class ClientApi { + public llm: LLMApi; + + constructor() { + this.llm = new ChatGPTApi(); + } + + config() {} + + prompts() {} + + masks() {} + + async share(messages: ChatMessage[], avatarUrl: string | null = null) { + const msgs = messages + .map((m) => ({ + from: m.role === "user" ? "human" : "gpt", + value: m.content, + })) + .concat([ + { + from: "human", + value: + "Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web", + }, + ]); + // 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用 + // Please do not modify this message + + console.log("[Share]", messages, msgs); + const clientConfig = getClientConfig(); + const proxyUrl = "/sharegpt"; + const rawUrl = "https://sharegpt.com/api/conversations"; + const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl; + const res = await fetch(shareUrl, { + body: JSON.stringify({ + avatarUrl, + items: msgs, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const resJson = await res.json(); + console.log("[Share]", resJson); + if (resJson.id) { + return `https://shareg.pt/${resJson.id}`; + } + } +} + +export const api = new ClientApi(); + +export function getHeaders() { + const accessStore = useAccessStore.getState(); + let headers: Record = { + "Content-Type": "application/json", + "x-requested-with": "XMLHttpRequest", + }; + + const makeBearer = (token: string) => `Bearer ${token.trim()}`; + const validString = (x: string) => x && x.length > 0; + + // use user's api key first + if (validString(accessStore.token)) { + headers.Authorization = makeBearer(accessStore.token); + } else if ( + accessStore.enabledAccessControl() && + validString(accessStore.accessCode) + ) { + headers.Authorization = makeBearer( + ACCESS_CODE_PREFIX + accessStore.accessCode, + ); + } + + return headers; +} diff --git a/app/client/controller.ts b/app/client/controller.ts new file mode 100644 index 000000000..a2e00173d --- /dev/null +++ b/app/client/controller.ts @@ -0,0 +1,37 @@ +// To store message streaming controller +export const ChatControllerPool = { + controllers: {} as Record, + + addController( + sessionId: string, + messageId: string, + controller: AbortController, + ) { + const key = this.key(sessionId, messageId); + this.controllers[key] = controller; + return key; + }, + + stop(sessionId: string, messageId: string) { + const key = this.key(sessionId, messageId); + const controller = this.controllers[key]; + controller?.abort(); + }, + + stopAll() { + Object.values(this.controllers).forEach((v) => v.abort()); + }, + + hasPending() { + return Object.values(this.controllers).length > 0; + }, + + remove(sessionId: string, messageId: string) { + const key = this.key(sessionId, messageId); + delete this.controllers[key]; + }, + + key(sessionId: string, messageIndex: string) { + return `${sessionId},${messageIndex}`; + }, +}; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts new file mode 100644 index 000000000..fd4eb59ce --- /dev/null +++ b/app/client/platforms/openai.ts @@ -0,0 +1,281 @@ +import { + DEFAULT_API_HOST, + DEFAULT_MODELS, + OpenaiPath, + REQUEST_TIMEOUT_MS, +} from "@/app/constant"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; + +import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; +import Locale from "../../locales"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; +import { getClientConfig } from "@/app/config/client"; + +export interface OpenAIListModelResponse { + object: string; + data: Array<{ + id: string; + object: string; + root: string; + }>; +} + +export class ChatGPTApi implements LLMApi { + private disableListModels = true; + + path(path: string): string { + let openaiUrl = useAccessStore.getState().openaiUrl; + const apiPath = "/api/openai"; + + if (openaiUrl.length === 0) { + const isApp = !!getClientConfig()?.isApp; + openaiUrl = isApp ? DEFAULT_API_HOST : apiPath; + } + if (openaiUrl.endsWith("/")) { + openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); + } + if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { + openaiUrl = "https://" + openaiUrl; + } + return [openaiUrl, path].join("/"); + } + + extractMessage(res: any) { + return res.choices?.at(0)?.message?.content ?? ""; + } + + async chat(options: ChatOptions) { + const messages = options.messages.map((v) => ({ + role: v.role, + content: v.content, + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const requestPayload = { + messages, + stream: options.config.stream, + model: modelConfig.model, + temperature: modelConfig.temperature, + presence_penalty: modelConfig.presence_penalty, + frequency_penalty: modelConfig.frequency_penalty, + top_p: modelConfig.top_p, + }; + + console.log("[Request] openai payload: ", requestPayload); + + const shouldStream = !!options.config.stream; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = this.path(OpenaiPath.ChatPath); + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), + }; + + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + + if (shouldStream) { + let responseText = ""; + let finished = false; + + const finish = () => { + if (!finished) { + options.onFinish(responseText); + finished = true; + } + }; + + controller.signal.onabort = finish; + + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[OpenAI] request response content type: ", + contentType, + ); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text); + const delta = json.choices[0].delta.content; + if (delta) { + responseText += delta; + options.onUpdate?.(responseText, delta); + } + } catch (e) { + console.error("[Request] parse error", text, msg); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + } catch (e) { + console.log("[Request] failed to make a chat request", e); + options.onError?.(e as Error); + } + } + async usage() { + const formatDate = (d: Date) => + `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d + .getDate() + .toString() + .padStart(2, "0")}`; + const ONE_DAY = 1 * 24 * 60 * 60 * 1000; + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startDate = formatDate(startOfMonth); + const endDate = formatDate(new Date(Date.now() + ONE_DAY)); + + const [used, subs] = await Promise.all([ + fetch( + this.path( + `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`, + ), + { + method: "GET", + headers: getHeaders(), + }, + ), + fetch(this.path(OpenaiPath.SubsPath), { + method: "GET", + headers: getHeaders(), + }), + ]); + + if (used.status === 401) { + throw new Error(Locale.Error.Unauthorized); + } + + if (!used.ok || !subs.ok) { + throw new Error("Failed to query usage from openai"); + } + + const response = (await used.json()) as { + total_usage?: number; + error?: { + type: string; + message: string; + }; + }; + + const total = (await subs.json()) as { + hard_limit_usd?: number; + }; + + if (response.error && response.error.type) { + throw Error(response.error.message); + } + + if (response.total_usage) { + response.total_usage = Math.round(response.total_usage) / 100; + } + + if (total.hard_limit_usd) { + total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100; + } + + return { + used: response.total_usage, + total: total.hard_limit_usd, + } as LLMUsage; + } + + async models(): Promise { + if (this.disableListModels) { + return DEFAULT_MODELS.slice(); + } + + const res = await fetch(this.path(OpenaiPath.ListModelPath), { + method: "GET", + headers: { + ...getHeaders(), + }, + }); + + const resJson = (await res.json()) as OpenAIListModelResponse; + const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); + console.log("[Models]", chatModels); + + if (!chatModels) { + return []; + } + + return chatModels.map((m) => ({ + name: m.id, + available: true, + })); + } +} +export { OpenaiPath }; diff --git a/app/command.ts b/app/command.ts new file mode 100644 index 000000000..e515e5f0b --- /dev/null +++ b/app/command.ts @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import Locale from "./locales"; + +type Command = (param: string) => void; +interface Commands { + fill?: Command; + submit?: Command; + mask?: Command; + code?: Command; + settings?: Command; +} + +export function useCommand(commands: Commands = {}) { + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + let shouldUpdate = false; + searchParams.forEach((param, name) => { + const commandName = name as keyof Commands; + if (typeof commands[commandName] === "function") { + commands[commandName]!(param); + searchParams.delete(name); + shouldUpdate = true; + } + }); + + if (shouldUpdate) { + setSearchParams(searchParams); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams, commands]); +} + +interface ChatCommands { + new?: Command; + newm?: Command; + next?: Command; + prev?: Command; + clear?: Command; + del?: Command; +} + +export const ChatCommandPrefix = ":"; + +export function useChatCommand(commands: ChatCommands = {}) { + function extract(userInput: string) { + return ( + userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput + ) as keyof ChatCommands; + } + + function search(userInput: string) { + const input = extract(userInput); + const desc = Locale.Chat.Commands; + return Object.keys(commands) + .filter((c) => c.startsWith(input)) + .map((c) => ({ + title: desc[c as keyof ChatCommands], + content: ChatCommandPrefix + c, + })); + } + + function match(userInput: string) { + const command = extract(userInput); + const matched = typeof commands[command] === "function"; + + return { + matched, + invoke: () => matched && commands[command]!(userInput), + }; + } + + return { match, search }; +} diff --git a/app/components/auth.module.scss b/app/components/auth.module.scss new file mode 100644 index 000000000..6630c0613 --- /dev/null +++ b/app/components/auth.module.scss @@ -0,0 +1,36 @@ +.auth-page { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + flex-direction: column; + + .auth-logo { + transform: scale(1.4); + } + + .auth-title { + font-size: 24px; + font-weight: bold; + line-height: 2; + } + + .auth-tips { + font-size: 14px; + } + + .auth-input { + margin: 3vh 0; + } + + .auth-actions { + display: flex; + justify-content: center; + flex-direction: column; + + button:not(:last-child) { + margin-bottom: 10px; + } + } +} diff --git a/app/components/auth.tsx b/app/components/auth.tsx new file mode 100644 index 000000000..1ca83dcd3 --- /dev/null +++ b/app/components/auth.tsx @@ -0,0 +1,55 @@ +import styles from "./auth.module.scss"; +import { IconButton } from "./button"; + +import { useNavigate } from "react-router-dom"; +import { Path } from "../constant"; +import { useAccessStore } from "../store"; +import Locale from "../locales"; + +import BotIcon from "../icons/bot.svg"; +import { useEffect } from "react"; +import { getClientConfig } from "../config/client"; + +export function AuthPage() { + const navigate = useNavigate(); + const access = useAccessStore(); + + const goHome = () => navigate(Path.Home); + + useEffect(() => { + if (getClientConfig()?.isApp) { + navigate(Path.Settings); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ +
+ +
{Locale.Auth.Title}
+
{Locale.Auth.Tips}
+ + { + access.updateCode(e.currentTarget.value); + }} + /> + +
+ + +
+
+ ); +} diff --git a/app/components/button.module.scss b/app/components/button.module.scss index 3a3393e7b..e332df2d2 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -18,6 +18,35 @@ cursor: not-allowed; opacity: 0.5; } + + &.primary { + background-color: var(--primary); + color: white; + + path { + fill: white !important; + } + } + + &.danger { + color: rgba($color: red, $alpha: 0.8); + border-color: rgba($color: red, $alpha: 0.5); + background-color: rgba($color: red, $alpha: 0.05); + + &:hover { + border-color: red; + background-color: rgba($color: red, $alpha: 0.1); + } + + path { + fill: red !important; + } + } + + &:hover, + &:focus { + border-color: var(--primary); + } } .shadow { @@ -28,10 +57,6 @@ border: var(--border-in-light); } -.icon-button:hover { - border-color: var(--primary); -} - .icon-button-icon { width: 16px; height: 16px; @@ -47,9 +72,12 @@ } .icon-button-text { - margin-left: 5px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + &:not(:first-child) { + margin-left: 5px; + } } diff --git a/app/components/button.tsx b/app/components/button.tsx index 1675a4b7d..7a5633924 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -2,16 +2,20 @@ import * as React from "react"; import styles from "./button.module.scss"; +export type ButtonType = "primary" | "danger" | null; + export function IconButton(props: { onClick?: () => void; - icon: JSX.Element; + icon?: JSX.Element; + type?: ButtonType; text?: string; bordered?: boolean; shadow?: boolean; - noDark?: boolean; className?: string; title?: string; disabled?: boolean; + tabIndex?: number; + autoFocus?: boolean; }) { return (