From f3b508c08831544b5a17bf8f1d40fabbd1b62b21 Mon Sep 17 00:00:00 2001 From: Zhang Yichi <66503962+ZhangYichi-ZYc@users.noreply.github.com> Date: Thu, 3 Aug 2023 23:52:18 +0800 Subject: [PATCH 001/202] Update Model Pricing.md OpenAI has updated their model prices, reducing the input of GPT-3.5 to $0.0015/1000 tokens --- docs/faq-cn.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq-cn.md b/docs/faq-cn.md index e4aa1e774a3..f9463eb953c 100644 --- a/docs/faq-cn.md +++ b/docs/faq-cn.md @@ -212,7 +212,8 @@ OpenAI 网站计费说明:https://openai.com/pricing#language-models OpenAI 根据 token 数收费,1000 个 token 通常可代表 750 个英文单词,或 500 个汉字。输入(Prompt)和输出(Completion)分别统计费用。 |模型|用户输入(Prompt)计费|模型输出(Completion)计费|每次交互最大 token 数| |----|----|----|----| -|gpt-3.5|$0.002 / 1 千 tokens|$0.002 / 1 千 tokens|4096| +|gpt-3.5-turbo|$0.0015 / 1 千 tokens|$0.002 / 1 千 tokens|4096| +|gpt-3.5-turbo-16K|$0.003 / 1 千 tokens|$0.004 / 1 千 tokens|16384| |gpt-4|$0.03 / 1 千 tokens|$0.06 / 1 千 tokens|8192| |gpt-4-32K|$0.06 / 1 千 tokens|$0.12 / 1 千 tokens|32768| From d1096582a50887363554ad7f60821209326b6bbb Mon Sep 17 00:00:00 2001 From: 7lsu Date: Mon, 7 Aug 2023 17:44:32 +0800 Subject: [PATCH 002/202] font family display enhance --- app/components/home.tsx | 3 +-- app/styles/globals.scss | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index b3cec893ecf..c6829c2dc18 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -104,8 +104,7 @@ const loadAsyncGoogleFont = () => { getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl; linkEl.rel = "stylesheet"; linkEl.href = - googleFontUrl + - "/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"; + googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap"; document.head.appendChild(linkEl); }; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 0417087e9d2..6542ca6ecf6 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -89,7 +89,7 @@ html { height: var(--full-height); - font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons", + font-family: "Noto Sans", "SF Pro SC", "SF Pro Text", "SF Pro Icons", "PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif; } From 769c2f9f49b1fd0d0e8e30b3bf579805c6259b7b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 8 Aug 2023 21:22:41 +0800 Subject: [PATCH 003/202] feat: close #2583 do not summarize with gpt-4 --- app/store/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index f06c59481da..ef68f7d9e52 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -553,7 +553,7 @@ export const useChatStore = create()( date: "", }), ), - config: { ...modelConfig, stream: true }, + config: { ...modelConfig, stream: true, model: "gpt-3.5-turbo" }, onUpdate(message) { session.memoryPrompt = message; }, From 4ab9141429ba170308443284bd06c84dac027788 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 8 Aug 2023 21:24:45 +0800 Subject: [PATCH 004/202] fix: #2564 should not clear message when error --- app/store/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index ef68f7d9e52..a6176589918 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -332,7 +332,7 @@ export const useChatStore = create()( }, onError(error) { const isAborted = error.message.includes("aborted"); - botMessage.content = + botMessage.content += "\n\n" + prettyObject({ error: true, From b14c5cd89c760ac81b555c0b4eb061c34cae6978 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 8 Aug 2023 21:36:37 +0800 Subject: [PATCH 005/202] fix: #2485 one-time-use body --- app/api/common.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/common.ts b/app/api/common.ts index 3146b6bd99d..e5afb4d89f4 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -43,6 +43,8 @@ export async function requestOpenai(req: NextRequest) { }, 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, From 153e7ac7e4fc2ceb6ec14137812f6f26e862fd9a Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Wed, 9 Aug 2023 01:22:48 +0900 Subject: [PATCH 006/202] Fix typo in README.md notifictions -> notifications --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0f298fb992..55e88d90e9a 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ After forking the project, due to the limitations imposed by GitHub, you need to 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 From 67c8ec6d7e9bf0857d607660a24dc126862065ad Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 9 Aug 2023 15:27:08 +0800 Subject: [PATCH 007/202] chore: change ACCESS_CODE_PREFIX to nk- --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index 0a94eed4e5a..8b28af323bb 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -41,7 +41,7 @@ export const MAX_SIDEBAR_WIDTH = 500; export const MIN_SIDEBAR_WIDTH = 230; export const NARROW_SIDEBAR_WIDTH = 100; -export const ACCESS_CODE_PREFIX = "ak-"; +export const ACCESS_CODE_PREFIX = "nk-"; export const LAST_INPUT_KEY = "last-input"; From 9834a67cbd5bd0fda85173b0c1a466791521e037 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 9 Aug 2023 15:37:13 +0800 Subject: [PATCH 008/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a8a66642a16..2ec2c1a843d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.2" + "version": "2.9.3" }, "tauri": { "allowlist": { From 0b7de6f7b2fc0043631607dd880e810605b312a9 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 10 Aug 2023 10:47:06 +0800 Subject: [PATCH 009/202] fix: #2594 trim the / --- app/api/common.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/common.ts b/app/api/common.ts index e5afb4d89f4..cd2936ee39f 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -20,6 +20,10 @@ 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); From 8ee506104692c6a8a3ee325b05e01bfaace6f5ca Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 11 Aug 2023 19:34:21 +0900 Subject: [PATCH 010/202] Add Japanese README --- README.md | 2 +- README_JA.md | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 README_JA.md diff --git a/README.md b/README.md index 55e88d90e9a..1662e8c7ea2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

ChatGPT Next Web

-English / [简体中文](./README_CN.md) +English / [简体中文](./README_CN.md) / [日本語](./README_JA.md) One-Click to get well-designed cross-platform ChatGPT web UI. diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 00000000000..6018a1b010c --- /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/) From 836a00e104bb423382991c0a305e6146718af8bf Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 11 Aug 2023 19:49:36 +0900 Subject: [PATCH 011/202] Add cloudflare-pages-ja.md --- docs/cloudflare-pages-ja.md | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/cloudflare-pages-ja.md diff --git a/docs/cloudflare-pages-ja.md b/docs/cloudflare-pages-ja.md new file mode 100644 index 00000000000..6409a9344c4 --- /dev/null +++ b/docs/cloudflare-pages-ja.md @@ -0,0 +1,38 @@ +# Cloudflare Pages 導入ガイド + +## 新規プロジェクトの作成方法 +GitHub でこのプロジェクトをフォークし、dash.cloudflare.com にログインして Pages にアクセスします。 + +1. "Create a project" をクリックする。 +2. "Connect to Git" を選択する。 +3. Cloudflare Pages を GitHub アカウントに接続します。 +4. フォークしたプロジェクトを選択します。 +5. "Begin setup" をクリックする。 +6. "Project name" と "Production branch" はデフォルト値を使用するか、必要に応じて変更してください。 +7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。 +8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください: + ``` + npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify + ``` +9. "Build output directory" はデフォルト値を使用し、変更しない。 +10. "Root Directory" を変更しない。 +11. "Environment variables" は、">" をクリックし、"Add variable" をクリックします。そして以下の情報を入力します: + - `NODE_VERSION=20.1` + - `NEXT_TELEMETRY_DISABLE=1` + - `OPENAI_API_KEY=your_own_API_key` + - `YARN_VERSION=1.22.19` + - `PHP_VERSION=7.4` + + 必要に応じて、以下の項目を入力してください: + + - `CODE= Optional, access passwords, multiple passwords can be separated by commas` + - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` + - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` + - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` + +12. "Save and Deploy" をクリックする。 +13. 互換性フラグを記入する必要があるため、"Cancel deployment" をクリックする。 +14. "Build settings" の "Functions" から "Compatibility flags" を見つける。 +15. "Configure Production compatibility flag" と "Configure Preview compatibility flag" の両方に "nodejs_compat "を記入する。 +16. "Deployments" に移動し、"Retry deployment" をクリックします。 +17. お楽しみください。 From 887eaef1aa27e13614ceb39c92eaf63e3fb67d5d Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 11 Aug 2023 19:58:54 +0900 Subject: [PATCH 012/202] Add vercel-ja.md --- docs/vercel-ja.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/vercel-ja.md diff --git a/docs/vercel-ja.md b/docs/vercel-ja.md new file mode 100644 index 00000000000..dfdd034c5d4 --- /dev/null +++ b/docs/vercel-ja.md @@ -0,0 +1,48 @@ +# Vercel 使用説明書 + +## 新規プロジェクトの作成方法 + +このプロジェクトを GitHub からフォークし、Vercel で新しい Vercel プロジェクトを作成して再デプロイする必要がある場合は、以下の手順に従ってください。 + +![vercel-create-1](./images/vercel/vercel-create-1.jpg) + +1. Vercel コンソールのホームページにアクセスします; +2. 新規追加をクリックする; +3. プロジェクトを選択します。 + +![vercel-create-2](./images/vercel/vercel-create-2.jpg) + +1. Git リポジトリのインポートで、chatgpt-next-web を検索します; +2 .新しいフォークプロジェクトを選択し、インポートをクリックします。 + +![vercel-create-3](./images/vercel/vercel-create-3.jpg) + +1. Project Settings ページで、Environment Variables をクリックして環境変数を設定する; +2. OPENAI_API_KEY と CODE という名前の環境変数を追加します; +3. 環境変数に対応する値を入力します; +4. Add をクリックして、環境変数の追加を確認する; +5. OPENAI_API_KEY を必ず追加してください; +6. Deploy をクリックして作成し、デプロイが完了するまで約 5 分間辛抱強く待つ。 + +## カスタムドメイン名の追加方法 + +\[TODO] + +## 環境変数の変更方法 + +![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) + +1. 内部 Vercel プロジェクトコンソールに移動し、上部の設定ボタンをクリックします; +2. 左側の Environment Variables をクリックします; +3. 既存のエントリーの右側のボタンをクリックします; +4. 編集を選択して編集し、保存する。 + +⚠️️ 注意: [プロジェクトの再デプロイ](#再実装の方法)環境変数を変更するたびに、変更を有効にするために必要です! + +## 再実装の方法 + +![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) + +1. Vercelプロジェクトの内部コンソールに移動し、一番上のDeploymentsボタンをクリックします; +2. リストの一番上の項目の右のボタンを選択します; +3. 再デプロイをクリックして再デプロイします。 From 99220d72da5c05d35c7b9250814b8d230492ad31 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 11 Aug 2023 20:28:18 +0900 Subject: [PATCH 013/202] Add faq-ja.md --- docs/faq-ja.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/faq-ja.md diff --git a/docs/faq-ja.md b/docs/faq-ja.md new file mode 100644 index 00000000000..8d50ffab7f0 --- /dev/null +++ b/docs/faq-ja.md @@ -0,0 +1,191 @@ +# よくある質問 + +## 早く助けを求めるには? + +1. ChatGPT / Bing / Baidu / Google などに尋ねてください。 +2. オンラインの友達に聞く。背景情報と問題の詳細な説明を提供してください。質の高い質問ほど、有益な回答を得られる可能性が高くなります。 + +# デプロイメントに関する質問 + +## なぜ Docker のデプロイバージョンは常に更新を要求するのか + +Docker のバージョンは安定版と同等であり、最新の Docker は常に最新のリリースバージョンと一致しています。現在、私たちのリリース頻度は1~2日に1回なので、Dockerのバージョンは常に最新のコミットから1~2日遅れており、これは予想されることです。 + +## Vercel での展開方法 + +1. GitHub アカウントを登録し、このプロジェクトをフォークする。 +2. Vercel を登録し(携帯電話認証が必要、中国の番号でも可)、GitHub アカウントを接続する。 +3. Vercel で新規プロジェクトを作成し、GitHub でフォークしたプロジェクトを選択し、必要な環境変数を入力し、デプロイを開始する。デプロイ後、Vercel が提供するドメインからプロジェクトにアクセスできます。(中国本土ではプロキシが必要) + +- 中国で直接アクセスする必要がある場合: DNS プロバイダーで、cname.vercel-dns.com を指すドメイン名の CNAME レコードを追加します。その後、Vercel でドメインアクセスを設定してください。 + +## Vercel 環境変数の変更方法 + +- Vercel のコンソールページに入ります; +- chatgpt-next-web プロジェクトを選択してください; +- ページ上部の設定オプションをクリックしてください; +- サイドバーで環境変数オプションを見つけます; +- 必要に応じて対応する値を変更してください。 + +## 環境変数 CODE とは何ですか?設定する必要がありますか? + +カスタムアクセスパスワードです: + +1. 設定しないで、環境変数を削除する。この時、誰でもあなたのプロジェクトにアクセスすることができます。 +2. プロジェクトをデプロイするときに、環境変数 CODE を設定する(カンマ区切りで複数のパスワードをサポート)。アクセスパスワードを設定した後、ユーザーはそれを使用するために設定ページでアクセスパスワードを入力する必要があります。[関連手順](https://github.com/Yidadaa/ChatGPT-Next-Web#access-password) + +## なぜ私がデプロイしたバージョンにはストリーミングレスポンスがないのでしょうか? + +> 関連する議論: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386) + +nginx のリバースプロキシを使っている場合、設定ファイルに以下のコードを追加する必要があります: + +``` +# キャッシュなし、ストリーミング出力をサポート +proxy_cache off; # キャッシュをオフにする +proxy_buffering off; # プロキシバッファリングをオフにする +chunked_transfer_encoding on; # チャンク転送エンコーディングをオンにする +tcp_nopush on; # TCP NOPUSH オプションをオンにし、Nagleアルゴリズムを無効にする +tcp_nodelay on; # TCP NODELAY オプションをオンにし、遅延ACKアルゴリズムを無効にする +keepalive_timeout 300; # keep-alive のタイムアウトを 65 秒に設定する +``` + +netlify でデプロイしている場合、この問題はまだ解決待ちです。 + +## デプロイしましたが、アクセスできません。 + +以下の問題を確認し、トラブルシューティングを行ってください: + +- サービスは開始されていますか? +- ポートは正しくマッピングされていますか? +- ファイアウォールのポートは開いていますか? +- サーバーへのルートは問題ありませんか? +- ドメイン名は正しく解決されていますか? + +## "Error: Loading CSS chunk xxx failed..." と表示されることがあります。 + +Next.js では、最初のホワイトスクリーンの時間を短縮するために、デフォルトでチャンキングを有効にしています。技術的な詳細はこちらをご覧ください: + +- https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading +- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4 +- https://github.com/vercel/next.js/issues/38507 +- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4 + +ただし、Next.js は古いブラウザとの互換性に制限があるため、このエラーが発生することがあります。 + +ビルド時にチャンキングを無効にすることができます。 + +Vercel プラットフォームの場合は、環境変数に `DISABLE_CHUNK=1` を追加して再デプロイします。 +セルフデプロイのプロジェクトでは、ビルド時に `DISABLE_CHUNK=1 yarn build` を使用することができます。 +Docker ユーザーの場合、ビルドはパッケージング時にすでに完了しているため、この機能を無効にすることは現在サポートされていません。 + +この機能を無効にすると、ユーザーの最初の訪問時にすべてのリソースがロードされることに注意してください。その結果、ユーザーのネットワーク接続が悪い場合、ホワイト・スクリーンの時間が長くなり、ユーザーエクスペリエンスに影響を与える可能性があります。この点を考慮の上、ご判断ください。 + +# 使用法に関する質問 + +## なぜいつも "An error occurred, please try again later" と表示されるのですか? + +様々な原因が考えられますので、以下の項目を順番にチェックしてみてください: + +- まず、コードのバージョンが最新版かどうかを確認し、最新版にアップデートしてから再試行してください; +- api キーが正しく設定されているか確認してください。環境変数名は大文字とアンダースコアでなければなりません; +- api キーが使用可能かどうか確認する; +- 上記のステップを踏んでも問題が解決しない場合は、issue エリアに新しい issue を投稿し、vercel のランタイムログまたは docker のランタイムログを添付してください。 + +## ChatGPT の返信が文字化けするのはなぜですか? + +設定画面-機種設定の中に `temperature` という項目があります。この値が 1 より大きい場合、返信が文字化けすることがあります。1 以内に調整してください。 + +## 設定ページでアクセスパスワードを入力してください」と表示される。 + +プロジェクトでは環境変数 CODE でアクセスパスワードを設定しています。初めて使うときは、設定ページでアクセスコードを入力する必要があります。 + +## 使用すると、"You exceeded your current quota, ..." と表示される。 + +API KEY に問題があります。残高不足です。 + +## プロキシとは何ですか? + +OpenAI の IP 制限により、中国をはじめとする一部の国や地域では、OpenAI API に直接接続することができず、プロキシを経由する必要があります。プロキシサーバ(フォワードプロキシ)を利用するか、事前に設定された OpenAI API リバースプロキシを利用します。 + +- フォワードプロキシの例: VPN ラダー。docker デプロイの場合は、環境変数 HTTP_PROXY にプロキシアドレス (http://address:port) を設定します。 +- リバースプロキシの例: 他人のプロキシアドレスを使うか、Cloudflare を通じて無料で設定できる。プロジェクトの環境変数 BASE_URL にプロキシアドレスを設定してください。 + +## 中国のサーバーにデプロイできますか? + +可能ですが、対処すべき問題があります: + +- GitHub や OpenAI などのウェブサイトに接続するにはプロキシが必要です; +- GitHub や OpenAI のようなウェブサイトに接続するにはプロキシが必要です; +- 中国の政策により、海外のウェブサイト/ChatGPT 関連アプリケーションへのプロキシアクセスが制限されており、ブロックされる可能性があります。 + +# ネットワークサービス関連の質問 + +## クラウドフレアとは何ですか? + +Cloudflare(CF)は、CDN、ドメイン管理、静的ページホスティング、エッジコンピューティング機能展開などを提供するネットワークサービスプロバイダーです。一般的な使用例: メインの購入やホスティング(解決、ダイナミックドメインなど)、サーバーへの CDN の適用(ブロックされないように IP を隠すことができる)、ウェブサイト(CF Pages)の展開。CF はほとんどのサービスを無料で提供しています。 + +## Vercel とは? + +Vercel はグローバルなクラウドプラットフォームで、開発者がモダンなウェブアプリケーションをより迅速に構築、デプロイできるように設計されています。このプロジェクトや多くのウェブアプリケーションは、ワンクリックで Vercel 上に無料でデプロイできます。コードを理解する必要も、Linux を理解する必要も、サーバーを持つ必要も、お金を払う必要も、OpenAI API プロキシを設定する必要もありません。欠点は、中国の制限なしにアクセスするためにドメイン名をバインドする必要があることだ。 + +## ドメイン名の取得方法 + +1. Namesilo(アリペイ対応)や Cloudflare(海外プロバイダー)、Wanwang(中国国内プロバイダー)などのドメインプロバイダーに登録する。 +2. 無料ドメインプロバイダー: eu.org(セカンドレベルドメイン)など。 +3. 無料セカンドレベルドメインを友人に頼む。 + +## サーバーの取得方法 + +- 海外サーバープロバイダーの例 Amazon Web Services、Google Cloud、Vultr、Bandwagon、Hostdare など。 + 海外サーバーの注意点 サーバー回線は中国でのアクセス速度に影響するため、CN2 GIA、CN2 回線を推奨。もしサーバーが中国でアクセスしにくい場合(深刻なパケットロスなど)、CDN(Cloudflare のようなプロバイダーのもの)を使ってみるとよいでしょう。 +- 国内のサーバープロバイダー アリババクラウド、テンセントなど + 国内サーバーの注意点 ドメイン名の解決にはファイリングが必要。国内サーバーの帯域幅は比較的高い。海外のウェブサイト(GitHub、OpenAI など)へのアクセスにはプロキシが必要。 + +# OpenAI 関連の質問 + +## OpenAI のアカウントを登録するには? + +chat.openai.com にアクセスして登録してください。以下のものが必要です: + +- 優れた VPN (OpenAI はサポートされている地域のネイティブ IP アドレスしか許可しません) +- サポートされているメール (例: Gmail や会社/学校のメール。Outlook や QQ のメールは不可) +- SMS 認証を受ける方法(SMS-activate ウェブサイトなど) + +## OpenAI API を有効にするには?API 残高の確認方法は? + +公式ウェブサイト(VPN が必要): https://platform.openai.com/account/usage +VPN なしで残高を確認するためにプロキシを設定しているユーザーもいます。API キーの漏洩を避けるため、信頼できる情報源であることを確認してください。 + +## OpenAI の新規アカウントに API 残高がないのはなぜですか? + +(4月6日更新) 新規登録アカウントは通常 24 時間以内に API 残高が表示されます。現在、新規アカウントには 5 ドルの残高が与えられています。 + +## OpenAI API へのチャージ方法を教えてください。 + +OpenAI では、指定された地域のクレジットカードのみご利用いただけます(中国のクレジットカードはご利用いただけません)。お住まいの地域のクレジットカードに対応していない場合は、以下の方法があります: + +1. Depay バーチャルクレジットカード +2. 海外のクレジットカードを申し込む +3. オンラインでトップアップしてくれる人を探す + +## GPT-4 API にアクセスするには? + +(4月6日更新) GPT-4 API へのアクセスには別途申請が必要です。以下のアドレスにアクセスし、ウェイティングリストに参加するための情報を入力してください(OpenAI の組織 ID をご用意ください): https://openai.com/waitlist/gpt-4-api +その後、メールの更新をお待ちください。 + +## Azure OpenAI インターフェースの使い方 + +次を参照: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371) + +## トークンの消費が速いのはなぜですか? + +> 関連する議論: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518) + +- GPT-4 にアクセスし、GPT-4 の API を定期的に使用している場合、GPT-4 の価格は GPT-3.5 の約 15 倍であるため、請求額が急激に増加します; +- GPT-3.5 を使用しており、頻繁に使用していないにもかかわらず、請求額が急速に増加している場合は、以下の手順で直ちにトラブルシューティングを行ってください: + - OpenAI のウェブサイトで API キーの消費記録を確認してください。トークンが 1 時間ごとに消費され、毎回数万トークンが消費される場合は、キーが流出している可能性があります。すぐに削除して再生成してください。**適当なサイトで残高を確認しないでください。** + - パスワードが 5 文字以下など短い場合、ブルートフォースによるコストは非常に低くなります。誰かが大量のパスワードの組み合わせを試したかどうかを確認するために、docker のログを検索することを推奨する。キーワード:アクセスコードの取得 +- これら 2 つの方法を実行することで、トークンが急速に消費された原因を突き止めることができます: + - OpenAI の消費記録に異常があるが、Docker ログに問題がない場合、API キーが流出したことを意味します; + - Docker ログにアクセスコード取得のブルートフォース試行回数が多い場合は、パスワードがクラックされています。 From c94713475f4ae19fe07e51f0204f59dc9b171578 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 13 Aug 2023 02:47:07 +0800 Subject: [PATCH 014/202] Update globals.scss --- app/styles/globals.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 6542ca6ecf6..beb30d7827b 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -105,6 +105,7 @@ body { align-items: center; user-select: none; touch-action: pan-x pan-y; + overflow: hidden; @media only screen and (max-width: 600px) { background-color: var(--second); From a496bc5a6387a8c25364dec7b78df96058639643 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 14 Aug 2023 10:57:24 +0800 Subject: [PATCH 015/202] fix: #2614 better rtl detecting algo --- app/components/markdown.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 0c6a2d4377d..e2a156a0bdd 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -38,12 +38,6 @@ export function Mermaid(props: { code: string }) { if (!svg) return; const text = new XMLSerializer().serializeToString(svg); const blob = new Blob([text], { type: "image/svg+xml" }); - console.log(blob); - // const url = URL.createObjectURL(blob); - // const win = window.open(url); - // if (win) { - // win.onload = () => URL.revokeObjectURL(url); - // } showImageModal(URL.createObjectURL(blob)); } @@ -152,7 +146,7 @@ export function Markdown( className="markdown-body" style={{ fontSize: `${props.fontSize ?? 14}px`, - direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr", + direction: /^[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr", }} ref={mdRef} onContextMenu={props.onContextMenu} From 808e4b38a3f25f7d60b801808b9f08b44a0c72b6 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 14 Aug 2023 11:10:02 +0800 Subject: [PATCH 016/202] fixup --- app/components/markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index e2a156a0bdd..4a84969c511 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -146,11 +146,11 @@ export function Markdown( className="markdown-body" style={{ fontSize: `${props.fontSize ?? 14}px`, - direction: /^[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr", }} ref={mdRef} onContextMenu={props.onContextMenu} onDoubleClickCapture={props.onDoubleClickCapture} + dir="auto" > {props.loading ? ( From 20a508e2d6e16252e44f6a9cbb07dd5c195b6fc3 Mon Sep 17 00:00:00 2001 From: imldy Date: Sat, 15 Jul 2023 02:13:39 +0800 Subject: [PATCH 017/202] feat: add autoGenerateTitle option (cherry picked from commit 656ab94a9c4edfee820616b8cfc39f5ee9952a3a) --- app/components/settings.tsx | 16 ++++++++++++++++ app/locales/cn.ts | 5 +++++ app/locales/en.ts | 5 +++++ app/store/chat.ts | 2 ++ app/store/config.ts | 7 ++++++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index c438f68c563..1e6ef7139ba 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -529,6 +529,22 @@ export function Settings() { > + + + updateConfig( + (config) => + (config.enableAutoGenerateTitle = e.currentTarget.checked), + ) + } + > + + ()( }, summarizeSession() { + const config = useAppConfig.getState(); const session = get().currentSession(); // remove error messages if any @@ -487,6 +488,7 @@ export const useChatStore = create()( // should summarize topic after chating more than 50 words const SUMMARIZE_MIN_LEN = 50; if ( + config.enableAutoGenerateTitle && session.topic === DEFAULT_TOPIC && countMessages(messages) >= SUMMARIZE_MIN_LEN ) { diff --git a/app/store/config.ts b/app/store/config.ts index d963d39ddfc..7070ea05e32 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -27,6 +27,7 @@ export const DEFAULT_CONFIG = { theme: Theme.Auto as Theme, tightBorder: !!getClientConfig()?.isApp, sendPreviewBubble: true, + enableAutoGenerateTitle: true, sidebarWidth: 300, disablePromptHint: false, @@ -147,7 +148,7 @@ export const useAppConfig = create()( }), { name: StoreKey.Config, - version: 3.6, + version: 3.7, migrate(persistedState, version) { const state = persistedState as ChatConfig; @@ -170,6 +171,10 @@ export const useAppConfig = create()( state.modelConfig.enableInjectSystemPrompts = true; } + if (version < 3.7) { + state.enableAutoGenerateTitle = true; + } + return state as any; }, }, From 803b66ae9d871300bb077a0b89ddf050d5240936 Mon Sep 17 00:00:00 2001 From: imldy Date: Mon, 14 Aug 2023 20:47:02 +0800 Subject: [PATCH 018/202] chore: Concise description --- app/locales/cn.ts | 3 +-- app/locales/en.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 0636a116af3..3929e09e7ca 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -172,8 +172,7 @@ const cn = { }, AutoGenerateTitle: { Title: "自动生成标题", - SubTitle: - "根据对话内容生成合适的标题(需标题为默认标题,并且内容长度大于设定的最小长度)", + SubTitle: "根据对话内容生成合适的标题", }, Mask: { Splash: { diff --git a/app/locales/en.ts b/app/locales/en.ts index 75cd02ed506..d37149c9298 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -174,8 +174,7 @@ const en: LocaleType = { }, AutoGenerateTitle: { Title: "Auto Generate Title", - SubTitle: - "Generate a suitable title based on the conversation content (requires default title and content length greater than the set minimum length)", + SubTitle: "Generate a suitable title based on the conversation content", }, Mask: { Splash: { From ae8226907ff03100cafd45ba5d648d2a62f77fef Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 14 Aug 2023 21:36:29 +0800 Subject: [PATCH 019/202] feat: close #2621 use better default api url --- app/client/platforms/openai.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 9dc92e9aede..fd4eb59ce77 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -13,6 +13,7 @@ import { fetchEventSource, } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; +import { getClientConfig } from "@/app/config/client"; export interface OpenAIListModelResponse { object: string; @@ -28,13 +29,16 @@ export class ChatGPTApi implements LLMApi { path(path: string): string { let openaiUrl = useAccessStore.getState().openaiUrl; + const apiPath = "/api/openai"; + if (openaiUrl.length === 0) { - openaiUrl = DEFAULT_API_HOST; + 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("/api/openai")) { + if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { openaiUrl = "https://" + openaiUrl; } return [openaiUrl, path].join("/"); From e8e01aa60d559fb7654b0f5e9521aa637e3d0b22 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 14 Aug 2023 21:55:18 +0800 Subject: [PATCH 020/202] feat: close #2618 use correct html lang attr --- app/components/home.tsx | 14 +++++++++++++- app/layout.tsx | 2 +- app/locales/index.ts | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index c6829c2dc18..745298d560e 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -15,7 +15,7 @@ import dynamic from "next/dynamic"; import { Path, SlotID } from "../constant"; import { ErrorBoundary } from "./error"; -import { getLang } from "../locales"; +import { getISOLang, getLang } from "../locales"; import { HashRouter as Router, @@ -86,6 +86,17 @@ export function useSwitchTheme() { }, [config.theme]); } +function useHtmlLang() { + useEffect(() => { + const lang = getISOLang(); + const htmlLang = document.documentElement.lang; + + if (lang !== htmlLang) { + document.documentElement.lang = lang; + } + }, []); +} + const useHasHydrated = () => { const [hasHydrated, setHasHydrated] = useState(false); @@ -168,6 +179,7 @@ export function useLoadData() { export function Home() { useSwitchTheme(); useLoadData(); + useHtmlLang(); useEffect(() => { console.log("[Config] got config from build time", getClientConfig()); diff --git a/app/layout.tsx b/app/layout.tsx index 883a268d368..5e0762653a8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,7 +3,7 @@ import "./styles/globals.scss"; import "./styles/markdown.scss"; import "./styles/highlight.scss"; import { getClientConfig } from "./config/client"; -import { type Metadata } from 'next'; +import { type Metadata } from "next"; export const metadata: Metadata = { title: "ChatGPT Next Web", diff --git a/app/locales/index.ts b/app/locales/index.ts index 7ece458383a..528600bec81 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -116,3 +116,13 @@ export function changeLang(lang: Lang) { setItem(LANG_KEY, lang); location.reload(); } + +export function getISOLang() { + const isoLangString: Record = { + cn: "zh-Hans", + tw: "zh-Hant", + }; + + const lang = getLang(); + return isoLangString[lang] ?? lang; +} From db5c7aba788c5f0a1a347f7d68baa5f0b1c5f516 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 14 Aug 2023 22:11:38 +0800 Subject: [PATCH 021/202] fix: #2615 scrollbar jitter under certain message counts --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a99f72f1527..f661d0a47b7 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -940,7 +940,7 @@ function _Chat() { const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE; const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE; - if (isTouchTopEdge) { + if (isTouchTopEdge && !isTouchBottomEdge) { setMsgRenderIndex(prevPageMsgIndex); } else if (isTouchBottomEdge) { setMsgRenderIndex(nextPageMsgIndex); From b380421fd56da4d9faa5646afa4468c2a954ddc7 Mon Sep 17 00:00:00 2001 From: wangwentong Date: Tue, 15 Aug 2023 13:32:34 +0800 Subject: [PATCH 022/202] support json export --- app/components/exporter.tsx | 61 +++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index ab6fad29ea1..604b8823def 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -127,7 +127,7 @@ export function MessageExporter() { ]; const { currentStep, setCurrentStepIndex, currentStepIndex } = useSteps(steps); - const formats = ["text", "image"] as const; + const formats = ["text", "image", "json"] as const; type ExportFormat = (typeof formats)[number]; const [exportConfig, setExportConfig] = useState({ @@ -157,7 +157,21 @@ export function MessageExporter() { session.mask.context, selection, ]); - + function preview() { + if (exportConfig.format === "text") { + return ( + + ); + } else if (exportConfig.format === "json") { + return ( + + ); + } else { + return ( + + ); + } + } return ( <> {currentStep.value === "preview" && ( -
- {exportConfig.format === "text" ? ( - - ) : ( - - )} -
+
{preview()}
)} ); @@ -545,12 +550,44 @@ export function MarkdownPreviewer(props: { const download = () => { downloadAs(mdText, `${props.topic}.md`); }; + return ( + <> + +
+
{mdText}
+
+ + ); +} + +export function JsonPreviewer(props: { + messages: ChatMessage[]; + topic: string; +}) { + const msgs = props.messages.map((m) => ({ + role: m.role, + content: m.content, + })); + const mdText = "\n" + JSON.stringify(msgs, null, 2) + "\n"; + + const copy = () => { + copyToClipboard(JSON.stringify(msgs, null, 2)); + }; + const download = () => { + downloadAs(JSON.stringify(msgs, null, 2), `${props.topic}.json`); + }; return ( <>
From 840277f5846ab13eaec0f3848ebd86d3a4ade410 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 15 Aug 2023 22:42:55 +0800 Subject: [PATCH 023/202] fix: #2566 click avatar to edit context messages --- app/components/chat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index f661d0a47b7..6562085856e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1123,9 +1123,9 @@ function _Chat() { 10, ); chatStore.updateCurrentSession((session) => { - const m = session.messages.find( - (m) => m.id === message.id, - ); + const m = session.mask.context + .concat(session.messages) + .find((m) => m.id === message.id); if (m) { m.content = newMessage; } From ed62c871567e9c5781f742932b0e0521833cded0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 15 Aug 2023 22:50:42 +0800 Subject: [PATCH 024/202] feat: close #2638 hide auth page and use better unauth tips --- app/components/auth.tsx | 9 +++++++++ app/locales/cn.ts | 8 ++++++-- app/locales/en.ts | 8 ++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index de0df454283..1ca83dcd314 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -7,6 +7,8 @@ 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(); @@ -14,6 +16,13 @@ export function AuthPage() { const goHome = () => navigate(Path.Home); + useEffect(() => { + if (getClientConfig()?.isApp) { + navigate(Path.Settings); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 3929e09e7ca..19e804b3a35 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,10 +1,14 @@ +import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; +const isApp = !!getClientConfig()?.isApp; + const cn = { WIP: "该功能仍在开发中……", Error: { - Unauthorized: - "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。", + Unauthorized: isApp + ? "检测到无效 API Key,请前往[设置](/#/settings)页检查 API Key 是否配置正确。" + : "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。", }, Auth: { Title: "需要密码", diff --git a/app/locales/en.ts b/app/locales/en.ts index d37149c9298..64cdc38bb41 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -1,12 +1,16 @@ +import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; import { LocaleType } from "./index"; // if you are adding a new translation, please use PartialLocaleType instead of LocaleType + +const isApp = !!getClientConfig()?.isApp; const en: LocaleType = { WIP: "Coming Soon...", Error: { - Unauthorized: - "Unauthorized access, please enter access code in [auth](/#/auth) page.", + Unauthorized: isApp + ? "Invalid API Key, please check it in [Settings](/#/settings) page." + : "Unauthorized access, please enter access code in [auth](/#/auth) page, or enter your OpenAI API Key.", }, Auth: { Title: "Need Access Code", From 5a7ec38ecdb516da0c7f3908ccebc03093f3e5bc Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 16 Aug 2023 11:22:51 +0800 Subject: [PATCH 025/202] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1662e8c7ea2..e1f42e1a099 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. [![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) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa) +[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) From 1d7286c16192a91cc58480733d6448919d8b79bc Mon Sep 17 00:00:00 2001 From: Phil Huang Date: Thu, 17 Aug 2023 23:50:38 +0800 Subject: [PATCH 026/202] Improve the text in tw.ts Adoption of text that is more closely aligned with the usage of zh-tw --- app/locales/tw.ts | 68 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 45c3caa02aa..ad1ee0bb676 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -20,7 +20,7 @@ const tw: PartialLocaleType = { Retry: "重試", Delete: "刪除", }, - Rename: "重命名對話", + Rename: "重新命名對話", Typing: "正在輸入…", Input: (submitKey: string) => { var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`; @@ -31,8 +31,8 @@ const tw: PartialLocaleType = { }, Send: "發送", Config: { - Reset: "重置默认", - SaveAs: "另存为面具", + Reset: "重置預設", + SaveAs: "另存新檔", }, }, Export: { @@ -62,7 +62,7 @@ const tw: PartialLocaleType = { Lang: { Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` - All: "所有语言", + All: "所有語言", }, Avatar: "大頭貼", FontSize: { @@ -70,7 +70,7 @@ const tw: PartialLocaleType = { SubTitle: "聊天內容的字型大小", }, InjectSystemPrompts: { - Title: "注入系統提示", + Title: "匯入系統提示", SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示", }, Update: { @@ -86,12 +86,12 @@ const tw: PartialLocaleType = { TightBorder: "緊湊邊框", SendPreviewBubble: { Title: "預覽氣泡", - SubTitle: "在预览气泡中预览 Markdown 内容", + SubTitle: "在預覽氣泡中預覽 Markdown 内容", }, Mask: { Splash: { - Title: "面具启动页", - SubTitle: "新建聊天时,展示面具启动页", + Title: "面具啟動頁面", + SubTitle: "新增聊天時,呈現面具啟動頁面", }, }, Prompt: { @@ -109,7 +109,7 @@ const tw: PartialLocaleType = { Search: "搜尋提示詞", }, EditModal: { - Title: "编辑提示词", + Title: "編輯提示詞", }, }, HistoryCount: { @@ -179,53 +179,53 @@ const tw: PartialLocaleType = { Edit: "前置上下文和歷史記憶", Add: "新增一條", }, - Plugin: { Name: "插件" }, + Plugin: { Name: "外掛" }, Mask: { Name: "面具", Page: { - Title: "预设角色面具", - SubTitle: (count: number) => `${count} 个预设角色定义`, - Search: "搜索角色面具", - Create: "新建", + Title: "預設角色面具", + SubTitle: (count: number) => `${count} 個預設角色定義`, + Search: "搜尋角色面具", + Create: "新增", }, Item: { - Info: (count: number) => `包含 ${count} 条预设对话`, - Chat: "对话", + Info: (count: number) => `包含 ${count} 條預設對話`, + Chat: "對話", View: "查看", - Edit: "编辑", + Edit: "編輯", Delete: "删除", - DeleteConfirm: "确认删除?", + DeleteConfirm: "確認删除?", }, EditModal: { Title: (readonly: boolean) => - `编辑预设面具 ${readonly ? "(只读)" : ""}`, - Download: "下载预设", - Clone: "克隆预设", + `編輯預設面具 ${readonly ? "(只读)" : ""}`, + Download: "下載預設", + Clone: "克隆預設", }, Config: { - Avatar: "角色头像", - Name: "角色名称", + Avatar: "角色頭像", + Name: "角色名稱", }, }, NewChat: { Return: "返回", - Skip: "跳过", - Title: "挑选一个面具", - SubTitle: "现在开始,与面具背后的灵魂思维碰撞", - More: "搜索更多", - NotShow: "不再展示", - ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", + Skip: "跳過", + Title: "挑選一個面具", + SubTitle: "現在開始,與面具背後的靈魂思維碰撞", + More: "搜尋更多", + NotShow: "不再呈現", + ConfirmNoShow: "確認禁用?禁用後可以随時在設定中重新啟用。", }, UI: { - Confirm: "确认", + Confirm: "確認", Cancel: "取消", - Close: "关闭", - Create: "新建", - Edit: "编辑", + Close: "關閉", + Create: "新增", + Edit: "編輯", }, Exporter: { Model: "模型", - Messages: "消息", + Messages: "訊息", Topic: "主題", Time: "時間", }, From 35b0bd76f82b16ed4f598193e68bb7a3279a70da Mon Sep 17 00:00:00 2001 From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:12:27 +0300 Subject: [PATCH 027/202] Update markdown.tsx --- app/components/markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 4a84969c511..e7a35b8023b 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -115,6 +115,7 @@ function _MarkDownContent(props: { content: string }) { ]} components={{ pre: PreCode, + p: (pProps) =>

, a: (aProps) => { const href = aProps.href || ""; const isInternal = /^\/#/i.test(href); @@ -150,7 +151,6 @@ export function Markdown( ref={mdRef} onContextMenu={props.onContextMenu} onDoubleClickCapture={props.onDoubleClickCapture} - dir="auto" > {props.loading ? ( From e78b15b9f010d366ed47b1019e5110e2cc286a04 Mon Sep 17 00:00:00 2001 From: Clarence Dan Date: Fri, 18 Aug 2023 17:12:02 +0800 Subject: [PATCH 028/202] Update chat.module.scss Specify styles for iOS devices. --- app/components/chat.module.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index d407d28e4f1..77b6ae1a1f1 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -349,6 +349,14 @@ padding: 7px; } } + /* Specific styles for iOS devices */ + @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) { + @supports (-webkit-touch-callout: none) { + .chat-message-edit { + top: -10%; + } + } + } } .chat-message-status { From 16685ddb6c49099eeee718c5abe96c256427eb90 Mon Sep 17 00:00:00 2001 From: Clarence Dan Date: Fri, 18 Aug 2023 17:21:58 +0800 Subject: [PATCH 029/202] Update chat.module.scss --- app/components/chat.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 77b6ae1a1f1..16790ccb1db 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -353,7 +353,7 @@ @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) { @supports (-webkit-touch-callout: none) { .chat-message-edit { - top: -10%; + top: -8%; } } } From aa3f96f89cfdf92cc23f338bc6e11c54bc1c4bae Mon Sep 17 00:00:00 2001 From: Clarence Dan <48417261+ClarenceDan@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:18:16 +0800 Subject: [PATCH 030/202] Update globals.scss --- app/styles/globals.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/globals.scss b/app/styles/globals.scss index beb30d7827b..def28680c1a 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -349,7 +349,7 @@ pre { justify-content: center; border: var(--border-in-light); box-shadow: var(--card-shadow); - border-radius: 10px; + border-radius: 11px; } .one-line { From 50eb7a5f98b094e955b5268a7154ce4e1f436c45 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 18 Aug 2023 18:47:54 +0800 Subject: [PATCH 031/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2ec2c1a843d..b09715cb312 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.3" + "version": "2.9.4" }, "tauri": { "allowlist": { From f84572443fc53394d2e7372d0856ae72039c60e9 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 21 Aug 2023 16:00:19 +0800 Subject: [PATCH 032/202] Update faq-cn.md --- docs/faq-cn.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/faq-cn.md b/docs/faq-cn.md index f9463eb953c..bf79ef7d991 100644 --- a/docs/faq-cn.md +++ b/docs/faq-cn.md @@ -101,7 +101,7 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒 项目通过环境变量 CODE 设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。 -## 使用时提示"You exceeded your current quota, ..." +## 使用时提示 "You exceeded your current quota, ..." API KEY 有问题。余额不足。 @@ -122,6 +122,9 @@ API KEY 有问题。余额不足。 注意,关闭此特性后,用户会在第一次访问网站时加载所有资源,如果用户网络状况较差,可能会引起较长时间的白屏,从而影响用户使用体验,所以自行考虑。 +## 使用时遇到 "NotFoundError: Failed to execute 'removeChild' on 'Node': The node...." +请关闭浏览器自身的自动翻译功能,并关闭所有自动翻译插件。 + # 网络服务相关问题 ## Cloudflare 是什么? From a4040fc1ee1665851b0c6fe005bacdc778d51c92 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 21 Aug 2023 18:30:44 +0800 Subject: [PATCH 033/202] Update README_CN.md --- README_CN.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README_CN.md b/README_CN.md index 16d3ec19bbc..568cd229a4c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -179,6 +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): 一站式大模型额度管理平台,支持市面上所有主流大语言模型 + ## 开源协议 [MIT](https://opensource.org/license/mit/) From e1142216eca8c91701457a2a85cbe45d1e7c3ec9 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 21 Aug 2023 18:33:45 +0800 Subject: [PATCH 034/202] fix: #2672 should use correct resend index --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6562085856e..9a9488dd2cd 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -802,7 +802,7 @@ function _Chat() { (m) => m.id === message.id, ); - if (resendingIndex <= 0 || resendingIndex >= session.messages.length) { + if (resendingIndex < 0 || resendingIndex >= session.messages.length) { console.error("[Chat] failed to find resending message", message); return; } From 4952e41132a9f5b917ae15013bbdf4170876fc90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:56:27 +0000 Subject: [PATCH 035/202] chore(deps): bump next from 13.4.9 to 13.4.19 Bumps [next](https://github.com/vercel/next.js) from 13.4.9 to 13.4.19. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v13.4.9...v13.4.19) --- updated-dependencies: - dependency-name: next dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 124 +++++++++++++++++++++++++-------------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index cbb51546bcd..8ddc36c1bc2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "html-to-image": "^1.11.11", "mermaid": "^10.2.3", "nanoid": "^4.0.2", - "next": "^13.4.9", + "next": "^13.4.19", "node-fetch": "^3.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 9c7688bc5f2..6d58d787d01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1131,10 +1131,10 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@next/env@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" - integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== +"@next/env@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" + integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== "@next/eslint-plugin-next@13.2.3": version "13.2.3" @@ -1143,50 +1143,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677" - integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ== - -"@next/swc-darwin-x64@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8" - integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A== - -"@next/swc-linux-arm64-gnu@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1" - integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg== - -"@next/swc-linux-arm64-musl@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0" - integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw== - -"@next/swc-linux-x64-gnu@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a" - integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg== - -"@next/swc-linux-x64-musl@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a" - integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A== - -"@next/swc-win32-arm64-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a" - integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA== - -"@next/swc-win32-ia32-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190" - integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA== - -"@next/swc-win32-x64-msvc@13.4.9": - version "13.4.9" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b" - integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw== +"@next/swc-darwin-arm64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" + integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== + +"@next/swc-darwin-x64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" + integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== + +"@next/swc-linux-arm64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" + integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== + +"@next/swc-linux-arm64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" + integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== + +"@next/swc-linux-x64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" + integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== + +"@next/swc-linux-x64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" + integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== + +"@next/swc-win32-arm64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" + integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== + +"@next/swc-win32-ia32-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" + integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== + +"@next/swc-win32-x64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" + integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -4654,12 +4654,12 @@ neo-async@^2.6.2: resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next@^13.4.9: - version "13.4.9" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba" - integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA== +next@^13.4.19: + version "13.4.19" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" + integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== dependencies: - "@next/env" "13.4.9" + "@next/env" "13.4.19" "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" @@ -4668,15 +4668,15 @@ next@^13.4.9: watchpack "2.4.0" zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.4.9" - "@next/swc-darwin-x64" "13.4.9" - "@next/swc-linux-arm64-gnu" "13.4.9" - "@next/swc-linux-arm64-musl" "13.4.9" - "@next/swc-linux-x64-gnu" "13.4.9" - "@next/swc-linux-x64-musl" "13.4.9" - "@next/swc-win32-arm64-msvc" "13.4.9" - "@next/swc-win32-ia32-msvc" "13.4.9" - "@next/swc-win32-x64-msvc" "13.4.9" + "@next/swc-darwin-arm64" "13.4.19" + "@next/swc-darwin-x64" "13.4.19" + "@next/swc-linux-arm64-gnu" "13.4.19" + "@next/swc-linux-arm64-musl" "13.4.19" + "@next/swc-linux-x64-gnu" "13.4.19" + "@next/swc-linux-x64-musl" "13.4.19" + "@next/swc-win32-arm64-msvc" "13.4.19" + "@next/swc-win32-ia32-msvc" "13.4.19" + "@next/swc-win32-x64-msvc" "13.4.19" node-domexception@^1.0.0: version "1.0.0" From a1c7f86ff36d3e3f1289a6d09479f42fe371db1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:57:07 +0000 Subject: [PATCH 036/202] chore(deps-dev): bump eslint-config-next from 13.2.3 to 13.4.19 Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 13.2.3 to 13.4.19. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v13.4.19/packages/eslint-config-next) --- updated-dependencies: - dependency-name: eslint-config-next dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 106 ++++++++++++++++++++++++--------------------------- 2 files changed, 51 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index cbb51546bcd..b2ba6d23801 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/spark-md5": "^3.0.2", "cross-env": "^7.0.3", "eslint": "^8.44.0", - "eslint-config-next": "13.2.3", + "eslint-config-next": "13.4.19", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "husky": "^8.0.0", diff --git a/yarn.lock b/yarn.lock index 9c7688bc5f2..01584be7362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1136,10 +1136,10 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== -"@next/eslint-plugin-next@13.2.3": - version "13.2.3" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.3.tgz#5af8ddeac6dbe028c812a0e59c41952c004d95d5" - integrity sha512-QmMPItnU7VeojI1KnuwL9SLFWEwmaNHNlnOGpoTwdLoSiP9sc8KYiAHWEc4/44L+cAdCxcZYvn7frcRNP5l84Q== +"@next/eslint-plugin-next@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz#93d130c37b47fd120f6d111aee36a60611148df1" + integrity sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ== dependencies: glob "7.1.7" @@ -1548,49 +1548,50 @@ resolved "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@typescript-eslint/parser@^5.42.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.0.tgz#f675bf2cd1a838949fd0de5683834417b757e4fa" - integrity sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.4.0.tgz#47e7c6e22ff1248e8675d95f488890484de67600" + integrity sha512-I1Ah1irl033uxjxO9Xql7+biL3YD7w9IU8zF+xlzD/YxY6a4b7DYA08PXUUCbm2sEljwJF6ERFy2kTGAGcNilg== dependencies: - "@typescript-eslint/scope-manager" "5.57.0" - "@typescript-eslint/types" "5.57.0" - "@typescript-eslint/typescript-estree" "5.57.0" + "@typescript-eslint/scope-manager" "6.4.0" + "@typescript-eslint/types" "6.4.0" + "@typescript-eslint/typescript-estree" "6.4.0" + "@typescript-eslint/visitor-keys" "6.4.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.57.0.tgz#79ccd3fa7bde0758059172d44239e871e087ea36" - integrity sha512-NANBNOQvllPlizl9LatX8+MHi7bx7WGIWYjPHDmQe5Si/0YEYfxSljJpoTyTWFTgRy3X8gLYSE4xQ2U+aCozSw== +"@typescript-eslint/scope-manager@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz#3048e4262ba3eafa4e2e69b08912d9037ec646ae" + integrity sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig== dependencies: - "@typescript-eslint/types" "5.57.0" - "@typescript-eslint/visitor-keys" "5.57.0" + "@typescript-eslint/types" "6.4.0" + "@typescript-eslint/visitor-keys" "6.4.0" -"@typescript-eslint/types@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.57.0.tgz#727bfa2b64c73a4376264379cf1f447998eaa132" - integrity sha512-mxsod+aZRSyLT+jiqHw1KK6xrANm19/+VFALVFP5qa/aiJnlP38qpyaTd0fEKhWvQk6YeNZ5LGwI1pDpBRBhtQ== +"@typescript-eslint/types@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.4.0.tgz#5b109a59a805f0d8d375895e42d9e5f0037f66ee" + integrity sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg== -"@typescript-eslint/typescript-estree@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.0.tgz#ebcd0ee3e1d6230e888d88cddf654252d41e2e40" - integrity sha512-LTzQ23TV82KpO8HPnWuxM2V7ieXW8O142I7hQTxWIHDcCEIjtkat6H96PFkYBQqGFLW/G/eVVOB9Z8rcvdY/Vw== +"@typescript-eslint/typescript-estree@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz#3c58d20632db93fec3d6ab902acbedf593d37276" + integrity sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA== dependencies: - "@typescript-eslint/types" "5.57.0" - "@typescript-eslint/visitor-keys" "5.57.0" + "@typescript-eslint/types" "6.4.0" + "@typescript-eslint/visitor-keys" "6.4.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/visitor-keys@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.0.tgz#e2b2f4174aff1d15eef887ce3d019ecc2d7a8ac1" - integrity sha512-ery2g3k0hv5BLiKpPuwYt9KBkAp2ugT6VvyShXdLOkax895EC55sP0Tx5L0fZaQueiK3fBLvHVvEl3jFS5ia+g== +"@typescript-eslint/visitor-keys@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz#96a426cdb1add28274abd7a34aefe27f8b7d51ef" + integrity sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA== dependencies: - "@typescript-eslint/types" "5.57.0" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "6.4.0" + eslint-visitor-keys "^3.4.1" "@vercel/analytics@^0.1.11": version "0.1.11" @@ -2872,20 +2873,20 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -eslint-config-next@13.2.3: - version "13.2.3" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.3.tgz#8a952bfd856f492684a30dd5fcdc8979c97c1cc2" - integrity sha512-kPulHiQEHGei9hIaaNGygHRc0UzlWM+3euOmYbxNkd2Nbhci5rrCDeMBMPSV8xgUssphDGmwDHWbk4VZz3rlZQ== +eslint-config-next@13.4.19: + version "13.4.19" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.4.19.tgz#f46be9d4bd9e52755f846338456132217081d7f8" + integrity sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g== dependencies: - "@next/eslint-plugin-next" "13.2.3" + "@next/eslint-plugin-next" "13.4.19" "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" eslint-plugin-import "^2.26.0" eslint-plugin-jsx-a11y "^6.5.1" eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" eslint-config-prettier@^8.8.0: version "8.8.0" @@ -2971,7 +2972,7 @@ eslint-plugin-prettier@^4.2.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^4.5.0: +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version "4.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== @@ -5326,7 +5327,7 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.7: +semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -5682,6 +5683,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-api-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.2.tgz#7c094f753b6705ee4faee25c3c684ade52d66d99" + integrity sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ== + ts-dedent@^2.2.0: version "2.2.0" resolved "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" @@ -5697,23 +5703,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" From 30c656cedae8461b6baeaa7d82250dd1b2cfb73a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:57:23 +0000 Subject: [PATCH 037/202] chore(deps): bump mermaid from 10.2.3 to 10.3.1 Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.2.3 to 10.3.1. - [Release notes](https://github.com/mermaid-js/mermaid/releases) - [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md) - [Commits](https://github.com/mermaid-js/mermaid/compare/v10.2.3...v10.3.1) --- updated-dependencies: - dependency-name: mermaid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 80 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index cbb51546bcd..32150b84c56 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "emoji-picker-react": "^4.4.7", "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", - "mermaid": "^10.2.3", + "mermaid": "^10.3.1", "nanoid": "^4.0.2", "next": "^13.4.9", "node-fetch": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index 9c7688bc5f2..11d42ad6a36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1000,10 +1000,10 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@braintree/sanitize-url@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" - integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== +"@braintree/sanitize-url@^6.0.1": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" + integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" @@ -1415,6 +1415,23 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/d3-scale-chromatic@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" + integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== + +"@types/d3-scale@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" + integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-time@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== + "@types/debug@^4.0.0": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -2290,6 +2307,13 @@ cytoscape@^3.23.0: heap "^0.2.6" lodash "^4.17.21" +"d3-array@1 - 2": + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: version "3.2.3" resolved "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.3.tgz#39f1f4954e4a09ff69ac597c2d61906b04e84740" @@ -2406,6 +2430,11 @@ d3-hierarchy@3: dependencies: d3-color "1 - 3" +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + "d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: version "3.1.0" resolved "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" @@ -2426,6 +2455,14 @@ d3-random@3: resolved "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== +d3-sankey@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d" + integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ== + dependencies: + d3-array "1 - 2" + d3-shape "^1.2.0" + d3-scale-chromatic@3: version "3.0.0" resolved "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" @@ -2457,6 +2494,13 @@ d3-shape@3: dependencies: d3-path "^3.1.0" +d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + "d3-time-format@2 - 4", d3-time-format@4: version "4.1.0" resolved "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" @@ -2683,10 +2727,10 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -dompurify@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.3.tgz#4b115d15a091ddc96f232bcef668550a2f6f1430" - integrity sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ== +dompurify@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.5.tgz#eb3d9cfa10037b6e73f32c586682c4b2ab01fbed" + integrity sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A== domutils@^2.8.0: version "2.8.0" @@ -3640,6 +3684,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5: resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -4266,19 +4315,22 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.2.3: - version "10.2.3" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.2.3.tgz#789d3b582c5da8c69aa4a7c0e2b826562c8c8b12" - integrity sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw== +mermaid@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.3.1.tgz#2f3c7e9f6bd7a8da2bef71cce2a542c8eba2a62e" + integrity sha512-hkenh7WkuRWPcob3oJtrN3W+yzrrIYuWF1OIfk/d0xGE8UWlvDhfexaHmDwwe8DKQgqMLI8DWEPwGprxkumjuw== dependencies: - "@braintree/sanitize-url" "^6.0.2" + "@braintree/sanitize-url" "^6.0.1" + "@types/d3-scale" "^4.0.3" + "@types/d3-scale-chromatic" "^3.0.0" cytoscape "^3.23.0" cytoscape-cose-bilkent "^4.1.0" cytoscape-fcose "^2.1.0" d3 "^7.4.0" + d3-sankey "^0.12.3" dagre-d3-es "7.0.10" dayjs "^1.11.7" - dompurify "3.0.3" + dompurify "^3.0.5" elkjs "^0.8.2" khroma "^2.0.0" lodash-es "^4.17.21" From 3499dfb285b0638e7948cae0142c62cc8c772272 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:57:49 +0000 Subject: [PATCH 038/202] chore(deps-dev): bump prettier from 2.8.8 to 3.0.2 Bumps [prettier](https://github.com/prettier/prettier) from 2.8.8 to 3.0.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.8.8...3.0.2) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cbb51546bcd..1d9c4bdc467 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "eslint-plugin-prettier": "^4.2.1", "husky": "^8.0.0", "lint-staged": "^13.2.2", - "prettier": "^2.8.8", + "prettier": "^3.0.2", "typescript": "4.9.5", "webpack": "^5.88.1" }, diff --git a/yarn.lock b/yarn.lock index 9c7688bc5f2..cb6d3736c46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4947,10 +4947,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.8.8: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b" + integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ== prop-types@^15.0.0, prop-types@^15.8.1: version "15.8.1" From 4d3fdbdd80777c516d11cfedb03f2e2776476d2a Mon Sep 17 00:00:00 2001 From: reece00 <37351410+reece00@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:35:51 +0800 Subject: [PATCH 039/202] The mobile terminal ishitbottom does not perform -10 --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 9a9488dd2cd..dfda4055b76 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -935,7 +935,7 @@ function _Chat() { const isTouchTopEdge = e.scrollTop <= edgeThreshold; const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold; - const isHitBottom = bottomHeight >= e.scrollHeight - 10; + const isHitBottom = bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10); const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE; const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE; From 1debde30462d0cc933978a71e39d77705ad4a1b3 Mon Sep 17 00:00:00 2001 From: pengoosedev <73521518+pengoosedev@users.noreply.github.com> Date: Wed, 23 Aug 2023 00:08:08 +0900 Subject: [PATCH 040/202] docs: Add README_KO --- README_KO.md | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 README_KO.md diff --git a/README_KO.md b/README_KO.md new file mode 100644 index 00000000000..86fba5115e5 --- /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/) From bc0f18409863c0f7654050b49d79a2666bcb99c5 Mon Sep 17 00:00:00 2001 From: pengoosedev <73521518+pengoosedev@users.noreply.github.com> Date: Wed, 23 Aug 2023 01:08:31 +0900 Subject: [PATCH 041/202] docs: Add cloudflare-pages-ko --- docs/cloudflare-pages-ko.md | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/cloudflare-pages-ko.md diff --git a/docs/cloudflare-pages-ko.md b/docs/cloudflare-pages-ko.md new file mode 100644 index 00000000000..68a96232ffc --- /dev/null +++ b/docs/cloudflare-pages-ko.md @@ -0,0 +1,39 @@ +## Cloudflare 페이지 배포 가이드 + +## 새 프로젝트를 만드는 방법 +이 프로젝트를 Github에서 포크한 다음 dash.cloudflare.com에 로그인하고 페이지로 이동합니다. + +1. "프로젝트 만들기"를 클릭합니다. +2. "Git에 연결"을 선택합니다. +3. Cloudflare 페이지를 GitHub 계정과 연결합니다. +4. 포크한 프로젝트를 선택합니다. +5. "설정 시작"을 클릭합니다. +6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다. +7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다. +8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요: + `` + npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental- minify + ``` +9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오. +10. "루트 디렉토리"는 수정하지 마십시오. +11. "환경 변수"의 경우 ">"를 클릭한 다음 "변수 추가"를 클릭합니다. 다음에 따라 정보를 입력합니다: + + - node_version=20.1`. + - next_telemetry_disable=1`. + - `OPENAI_API_KEY=자신의 API 키` + - ``yarn_version=1.22.19`` + - ``php_version=7.4``. + + 실제 필요에 따라 다음 옵션을 선택적으로 입력합니다: + + - `CODE= 선택적으로 액세스 비밀번호를 입력하며 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다`. + - `OPENAI_ORG_ID= 선택 사항, OpenAI에서 조직 ID 지정` + - `HIDE_USER_API_KEY=1 선택 사항, 사용자가 API 키를 입력하지 못하도록 합니다. + - `DISABLE_GPT4=1 옵션, 사용자가 GPT-4를 사용하지 못하도록 설정` 12. + +12. "저장 후 배포"를 클릭합니다. +13. 호환성 플래그를 입력해야 하므로 "배포 취소"를 클릭합니다. +14. "빌드 설정", "기능"으로 이동하여 "호환성 플래그"를 찾습니다. +"프로덕션 호환성 플래그 구성" 및 "프리뷰 호환성 플래그 구성"에서 "nodejs_compat"를 입력합니다. +16. "배포"로 이동하여 "배포 다시 시도"를 클릭합니다. +17. 즐기세요! \ No newline at end of file From b146cd889fd3d17220a6d8200c8625a4a076496c Mon Sep 17 00:00:00 2001 From: pengoosedev <73521518+pengoosedev@users.noreply.github.com> Date: Wed, 23 Aug 2023 01:28:54 +0900 Subject: [PATCH 042/202] Add faq-ko --- docs/faq-ko.md | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/faq-ko.md diff --git a/docs/faq-ko.md b/docs/faq-ko.md new file mode 100644 index 00000000000..9eb6bbbb259 --- /dev/null +++ b/docs/faq-ko.md @@ -0,0 +1,230 @@ +# 자주 묻는 질문 + +## 어떻게 빠르게 도움을 받을 수 있나요? + +1. ChatGPT / Bing / Baidu / Google 등에 질문합니다. +2. 인터넷 사용자에게 질문합니다. 문제의 배경 정보와 자세한 문제 설명을 제공하세요. 질 좋은 질문은 유용한 답변을 쉽게 받을 수 있습니다. + +# 배포 관련 질문 + +각종 배포 방법에 대한 자세한 튜토리얼 참조: [링크](https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b) + +## 왜 Docker 배포 버전이 계속 업데이트 알림을 주나요? + +Docker 버전은 사실상 안정된 버전과 같습니다. latest Docker는 항상 latest release version과 일치합니다. 현재 우리의 발행 빈도는 하루 또는 이틀에 한 번이므로 Docker 버전은 항상 최신 커밋보다 하루나 이틀 뒤처집니다. 이것은 예상된 것입니다. + +## Vercel에서 어떻게 배포하나요? + +1. Github 계정을 등록하고, 이 프로젝트를 포크합니다. +2. Vercel을 등록합니다(휴대폰 인증 필요, 중국 번호 사용 가능), Github 계정을 연결합니다. +3. Vercel에서 새 프로젝트를 생성하고, Github에서 포크한 프로젝트를 선택합니다. 환경 변수를 필요에 따라 입력한 후 배포를 시작합니다. 배포 후에는 VPN이 있는 환경에서 Vercel이 제공하는 도메인으로 프로젝트에 접근할 수 있습니다. +4. 중국에서 방화벽 없이 접근하려면: 도메인 관리 사이트에서 도메인의 CNAME 레코드를 추가하고, cname.vercel-dns.com을 가리키게 합니다. 그런 다음 Vercel에서 도메인 접근을 설정합니다. + +## Vercel 환경 변수를 어떻게 수정하나요? + +- Vercel의 제어판 페이지로 이동합니다. +- chatgpt next web 프로젝트를 선택합니다. +- 페이지 상단의 Settings 옵션을 클릭합니다. +- 사이드바의 Environment Variables 옵션을 찾습니다. +- 해당 값을 수정합니다. + +## 환경 변수 CODE는 무엇이며, 반드시 설정해야 하나요? + +이것은 당신이 사용자 정의한 접근 비밀번호입니다. 다음 중 하나를 선택할 수 있습니다: + +1. 설정하지 않습니다. 해당 환경 변수를 삭제합니다. 주의: 이 경우 누구나 프로젝트에 접근할 수 있습니다. +2. 프로젝트를 배포할 때 환경 변수 CODE를 설정합니다(여러 비밀번호는 쉼표로 구분). 접근 비밀번호를 설정하면 사용자는 설정 페이지에서 접근 비밀번호를 입력해야만 사용할 수 있습니다. [관련 설명 참조](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81) + +## 왜 내 배포 버전에 스트리밍 응답이 없나요? + +> 관련 토론: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386) + +nginx 리버스 프록시를 사용하는 경우, 설정 파일에 다음 코드를 추가해야 합니다: + +```nginx +# 캐시하지 않고, 스트리밍 출력 지원 +proxy_cache off; # 캐시 비활성화 +proxy_buffering off; # 프록시 버퍼링 비활성화 +chunked_transfer_encoding on; # 청크 전송 인코딩 활성화 +tcp_nopush on; # TCP NOPUSH 옵션 활성화, Nagle 알고리즘 금지 +tcp_nodelay on; # TCP NODELAY 옵션 활성화, 지연 ACK 알고리즘 금지 +keepalive_timeout 300; # keep-alive 타임아웃을 65초로 설정 +``` + +netlify에서 배포하는 경우, 이 문제는 아직 해결되지 않았습니다. 기다려 주십시오. + +## 배포했지만 액세스할 수 없는 경우. + +다음의 사항들을 확인해보세요: + +- 서비스가 배포 중인가요? +- 포트가 올바르게 매핑되었나요? +- 방화벽에서 포트가 열렸나요? +- 서버 경로가 유효한가요? +- 도메인 이름이 올바른가요? + +## 프록시란 무엇이며 어떻게 사용하나요? + +중국 및 일부 국가에서는 OpenAI의 IP 제한으로 인해 OpenAI API에 직접 연결할 수 없으며 프록시를 거쳐야 합니다. 프록시 서버(정방향 프록시)를 사용하거나 OpenAI API에 대해 설정된 역방향 프록시를 사용할 수 있습니다. + +- 정방향 프록시 예: 사이언티픽 인터넷 래더. 도커 배포의 경우 환경 변수 HTTP_PROXY를 프록시 주소(예: 10.10.10.10:8002)로 설정합니다. +- 역방향 프록시 예: 다른 사람이 구축한 프록시 주소를 사용하거나 Cloudflare를 통해 무료로 설정할 수 있습니다. 프로젝트 환경 변수 BASE_URL을 프록시 주소로 설정합니다. + +## 국내 서버를 배포할 수 있나요? + +예. 하지만 해결해야 할 문제가 있습니다: + +- github 및 openAI와 같은 사이트에 연결하려면 프록시가 필요합니다; +- 도메인 이름 확인을 설정하려면 국내 서버를 신청해야 합니다; +- 국내 정책에 따라 프록시가 엑스트라넷/ChatGPT 관련 애플리케이션에 액세스하지 못하도록 제한되어 차단될 수 있습니다. + +## 도커 배포 후 네트워크 오류가 발생하는 이유는 무엇인가요? + +https://github.com/Yidadaa/ChatGPT-Next-Web/issues/1569 에서 토론을 참조하세요. + +## 사용 관련 문제 + +## "문제가 발생했습니다, 나중에 다시 시도하세요"라는 메시지가 계속 뜨는 이유는 무엇인가요? + +여러 가지 이유가 있을 수 있으니 순서대로 확인해 주세요: + +- 코드 버전이 최신 버전인지 확인하고, 최신 버전으로 업데이트한 후 다시 시도해 주세요; +- API 키가 올바르게 설정되었는지 확인해주세요. 환경 변수 이름은 모두 대문자이며 밑줄이 있어야 합니다; +- API 키가 사용 가능한지 확인해 주세요; +- 위 단계를 수행한 후에도 문제를 확인할 수 없는 경우, 이슈 영역에 신규 이슈를 제출하고 버셀의 런타임 로그 또는 도커 런타임 로그를 첨부해 주시기 바랍니다. + +## ChatGPT 응답이 왜곡되는 이유는 무엇인가요? + +설정 - 모델 설정 섹션에 '온도'에 대한 값이 있는데, 이 값이 1보다 크면 응답이 왜곡될 수 있으니 1 이내로 다시 설정해 주세요. + +## "권한이 없는 상태입니다, 설정 페이지에서 액세스 비밀번호를 입력하세요"? + +프로젝트에서 환경 변수 CODE에 접근 비밀번호를 설정했습니다. 처음 사용할 때는 설정 페이지에서 액세스 코드를 입력해야 합니다. + +## 사용 시 "현재 할당량을 초과했습니다, ..."라는 메시지가 표시됩니다. + +API 키에 문제가 있습니다. 잔액이 부족합니다. + +## "오류: CSS 청크 xxx를 로드하지 못했습니다..."와 함께 사용. + +첫 번째 화이트 스크린 시간을 줄이기 위해 청크 컴파일이 기본적으로 활성화되어 있으며, 기술 원칙은 아래를 참조하세요: + +- https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading +- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4 +- https://github.com/vercel/next.js/issues/38507 +- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4 + +그러나 NextJS는 호환성이 좋지 않아 구형 브라우저에서 이 오류가 발생할 수 있으므로 컴파일 시 청크 컴파일을 비활성화할 수 있습니다. + +버셀 플랫폼의 경우 환경 변수에 `DISABLE_CHUNK=1`을 추가하고 다시 배포합니다; +자체 컴파일 및 배포한 프로젝트의 경우, 빌드 시 `DISABLE_CHUNK=1 yarn build`를 사용하여 빌드합니다; +Docker 사용자의 경우, Docker가 프로젝트를 패키징할 때 이미 빌드하기 때문에 이 기능을 해제하는 것은 지원되지 않습니다. + +이 기능을 끄면 사용자가 웹사이트를 처음 방문할 때 모든 리소스를 로드하므로 인터넷 연결 상태가 좋지 않은 경우 흰색 화면이 길게 표시되어 사용자 경험에 영향을 줄 수 있으므로 사용자가 직접 고려하시기 바랍니다. + +"## NotFoundError: '노드': 노드....에서 'removeChild'를 실행하지 못했습니다." 오류가 발생했습니다. +브라우저의 자체 자동 번역 기능을 비활성화하고 모든 자동 번역 플러그인을 닫아주세요. + +## 웹 서비스 관련 문제 + +## 클라우드플레어란 무엇인가요? + +Cloudflare(CF)는 CDN, 도메인 관리, 정적 페이지 호스팅, 엣지 컴퓨팅 기능 배포 등을 제공하는 웹 서비스 제공업체입니다. 일반적인 용도: 도메인 구매 및/또는 호스팅(리졸브, 동적 도메인 등), 서버에 CDN 설치(벽에서 IP를 숨기는 기능), 웹사이트 배포(CF 페이지). CF는 이러한 서비스 대부분을 무료로 제공합니다. + +## Vercel이란 무엇인가요? + +Vercel은 개발자가 최신 웹 애플리케이션을 더 빠르게 빌드하고 배포할 수 있도록 설계된 글로벌 클라우드 플랫폼입니다. 이 프로젝트와 많은 웹 애플리케이션을 클릭 한 번으로 Vercel에 무료로 배포할 수 있습니다. 코드, 리눅스, 서버, 수수료가 필요 없고 OpenAI API 프록시를 설정할 필요도 없습니다. 단점은 중국에서 장벽 없이 액세스하려면 도메인 이름을 바인딩해야 한다는 것입니다. + +## 도메인 네임은 어떻게 얻나요? + +1) 도메인 네임 공급업체로 이동하여 해외에서는 Namesilo(알리페이 지원), 클라우드플레어 등, 중국에서는 월드와이드웹과 같은 도메인 네임을 등록합니다. 2) 무료 도메인 네임 공급업체: 예: eBay; +2. 무료 도메인 네임 제공업체: eu.org(두 번째 레벨 도메인 네임) 등..; +3. 친구에게 무료 2단계 도메인 네임을 요청합니다. + +## 서버를 얻는 방법 + +- 외국 서버 제공업체의 예: 아마존 클라우드, 구글 클라우드, 벌터, 밴드왜건, 호스트데어 등; + 해외 서버 문제: 서버 라인은 해당 국가의 액세스 속도에 영향을 미치므로 CN2 GIA 및 CN2 라인 서버를 권장합니다. 국내 서버의 접속에 문제가 있는 경우(심각한 패킷 손실 등) CDN(Cloudflare 및 기타 제공 업체)을 설정해 볼 수 있습니다. +- 국내 서버 제공업체: 알리윈, 텐센트 등; + 국내 서버 문제: 도메인 이름 확인을 신청해야 하며, 국내 서버 대역폭이 더 비싸고, 해외 사이트(Github, openAI 등)에 액세스하려면 프록시가 필요합니다. + +## 서버는 언제 신청해야 하나요? + +중국 본토에서 운영되는 웹사이트는 규제 요건에 따라 신고해야 합니다. 실제로 서버가 중국에 있고 도메인 네임 레졸루션이 있는 경우 서버 제공업체가 규제 신고 요건을 시행하며, 그렇지 않으면 서비스가 종료됩니다. 일반적인 규칙은 다음과 같습니다: +|서버 위치|도메인 네임 공급자|파일링 필요 여부| +|---|---|---| +|국내|국내|예 +|국내|외국|예 +|외국|외국인|아니요 +|외국|국내|일반적으로 아니요| + +서버 공급자를 전환한 후 파일링을 전환해야 합니다. + +## OpenAI 관련 질문 + +## OpenAI 계정은 어떻게 가입하나요? + +chat.openai.com으로 이동하여 등록하세요. 다음이 필요합니다: + +- 유효한 래더(OpenAI는 지역별 기본 IP 주소를 지원합니다) +- 지원되는 이메일 주소(예: Outlook이나 qq가 아닌 Gmail 또는 회사/학교 이메일) +- SMS 인증을 받을 수 있는 방법(예: SMS 활성화 웹사이트) + +## OpenAI API는 어떻게 열 수 있나요? API 잔액은 어떻게 확인하나요? + +공식 웹사이트 주소(래더 필요): https://platform.openai.com/account/usage +일부 사용자는 래더 없이 잔액 조회 에이전트를 구축한 경우가 있으니, 해당 사용자에게 요청해 주시기 바랍니다. API 키 유출을 방지하기 위해 신뢰할 수 있는 소스인지 확인하시기 바랍니다. + +## 새로 등록한 OpenAI 계정에 API 잔액이 없는 이유는 무엇인가요? + +(4월 6일 업데이트) 새로 등록된 계정은 일반적으로 24시간 후에 API 잔액이 표시됩니다. 현재 새로 등록된 계정에는 $5의 잔액이 표시됩니다. + +## OpenAI API를 충전하려면 어떻게 해야 하나요? + +OpenAI는 특정 지역의 신용카드만 사용할 수 있습니다(중국 신용카드는 사용할 수 없음). 충전 방법의 몇 가지 예는 다음과 같습니다: + +1. 가상 신용카드로 결제하기 +2. 해외 신용카드 신청 +3. 온라인에서 신용카드를 충전할 사람 찾기 + +## GPT-4 API 액세스는 어떻게 사용하나요? + +- GPT-4 API 액세스는 별도의 신청이 필요합니다. 다음 주소로 이동하여 정보를 입력하여 신청 대기열 대기자 명단에 들어가세요(OpenAI 조직 ID를 준비하세요): https://openai.com/waitlist/gpt-4-api. + 그런 다음 이메일 메시지를 기다립니다. +- ChatGPT Plus를 사용하도록 설정했다고 해서 GPT-4 권한이 있는 것은 아니며, 서로 관련이 없습니다. + +## Azure OpenAI 인터페이스 사용 방법 + +참조: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371) + +## 내 토큰이 왜 이렇게 빨리 소모되나요? + +> 관련 토론: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518) + +- GPT 4에 액세스 권한이 있고 매일 GPT 4 API를 사용하는 경우, GPT 4 가격이 GPT 3.5의 약 15배이기 때문에 청구 금액이 급격히 증가합니다; +- GPT 3.5를 자주 사용하지 않는데도 요금이 급격하게 증가하는 경우 아래 단계를 따라 확인하시기 바랍니다: + - 오픈아이 공식 웹사이트로 이동하여 API 키 소비 기록을 확인하고, 매 시간마다 토큰이 소비되고 매번 수만 개의 토큰이 소비된다면 키가 유출된 것이므로 즉시 삭제하고 재생성하시기 바랍니다. 즉시 키를 삭제하고 다시 생성하시기 바랍니다. 지저분한 웹사이트에서 잔액을 확인하지 마세요. ** + - 비밀번호 설정이 5자리 이내의 문자와 같이 매우 짧으면 블라스팅 비용이 매우 낮습니다. 도커의 로그 기록을 검색하여 누군가 많은 수의 비밀번호 조합을 시도했는지 확인하는 것이 좋습니다. 키워드: 액세스 코드를 얻었습니다. +- 이 두 가지 방법을 사용하면 토큰이 소비되는 이유를 빠르게 찾을 수 있습니다: + - 오픈아이 소비 기록은 비정상적이지만 도커 로그는 정상이라면 API 키가 유출되고 있다는 뜻입니다; + - 도커 로그에서 액세스 코드 버스트 레코드가 많이 발견되면 비밀번호가 버스트된 것입니다. + + +## API의 가격은 어떻게 청구되나요? + +OpenAI의 청구 지침은 https://openai.com/pricing#language-models 에서 확인할 수 있습니다. +OpenAI는 토큰 수에 따라 요금을 청구하며, 일반적으로 1000토큰은 영어 단어 750개 또는 중국어 문자 500개를 나타냅니다. 입력(프롬프트)과 출력(완료)은 별도로 청구됩니다. + +|모델|사용자 입력(프롬프트) 청구 |모델 출력(완료) 청구 |인터랙션당 최대 토큰 수 | +|----|----|----|----| +|GPT-3.5-TURBO|$0.0015 / 1천 토큰|$0.002 / 1천 토큰|4096| +|GPT-3.5-TURBO-16K|$0.003 / 1천 토큰|$0.004 / 1천 토큰|16384| |GPT-4|$0.004 / 1천 토큰|16384 +|GPT-3.5-TURBO-16K|$0.003 / 1천 토큰|$0.004 / 1천 토큰|16384| |GPT-4|$0.03 / 1천 토큰|$0.06 / 1천 토큰|8192 +|GPT-4-32K|$0.06 / 1천 토큰|$0.12 / 1천 토큰|32768| + +## gpt-3.5-터보와 gpt3.5-터보-0301(또는 gpt3.5-터보-mmdd) 모델의 차이점은 무엇인가요? + +공식 문서 설명: https://platform.openai.com/docs/models/gpt-3-5 + +- GPT-3.5-TURBO는 최신 모델이며 지속적으로 업데이트될 예정입니다. +- gpt-3.5-turbo-0301은 3월 1일에 고정된 모델의 스냅샷으로, 변경되지 않으며 3개월 후에 새로운 스냅샷으로 대체될 예정입니다. \ No newline at end of file From d3de07ecf3b1439f42f452c08d114184da71d2b9 Mon Sep 17 00:00:00 2001 From: pengoosedev <73521518+pengoosedev@users.noreply.github.com> Date: Wed, 23 Aug 2023 01:33:46 +0900 Subject: [PATCH 043/202] docs: Add vercel-ko --- docs/vercel-ko.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/vercel-ko.md diff --git a/docs/vercel-ko.md b/docs/vercel-ko.md new file mode 100644 index 00000000000..725a827dc9a --- /dev/null +++ b/docs/vercel-ko.md @@ -0,0 +1,39 @@ +# Vercel 사용 방법 + +## 새 프로젝트 생성 방법 +이 프로젝트를 Github에서 포크한 후, 다시 배포하려면 Vercel에서 새로운 Vercel 프로젝트를 생성해야 하며, 다음 단계를 따라야 합니다. + +![vercel-create-1](./images/vercel/vercel-create-1.jpg) +1. Vercel 콘솔 홈 페이지로 이동합니다; +2. 새로 추가를 클릭합니다; +3. 프로젝트를 선택합니다. + +![vercel-create-2](./images/vercel/vercel-create-2.jpg) +1. Git 리포지토리 가져오기에서 chatgpt-next-web을 검색합니다. 2. 새 포크를 선택합니다; +2. 새로 포크된 프로젝트를 선택하고 가져오기를 클릭합니다. + +![vercel-create-3](./images/vercel/vercel-create-3.jpg) +1. 프로젝트 구성 페이지에서 환경 변수 설정을 클릭하여 환경 변수 설정을 시작합니다; +2. OPENAI_API_KEY, CODE ([Access Code](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)). 환경 변수를 설정합니다; +3. 환경 변수의 값을 입력합니다; +4. 추가를 클릭하여 환경 변수 추가를 확인합니다; +5. OPENAI_API_KEY를 추가해야 하며, 그렇지 않으면 작동하지 않습니다; +6. 배포를 클릭하여 도메인 이름 생성을 완료하고 배포가 완료될 때까지 약 5분간 기다립니다. + +## 사용자 정의 도메인 네임 추가 방법 +[TODO] + +## 환경 변수 변경 방법 +![vercel-env-edit](./images/vercel/vercel-env-edit.jpg) +1. 버셀 프로젝트의 내부 콘솔로 이동하여 상단의 설정 버튼을 클릭합니다; +2. 왼쪽의 환경 변수를 클릭합니다; +3. 기존 항목 오른쪽에 있는 버튼을 클릭합니다; +4. 편집을 선택하여 수정하고 저장합니다. + +⚠️️ 참고: 환경 변수를 변경할 때마다 [프로젝트를 재배포](#如何重新部署)해야 변경 사항을 적용할 수 있습니다! + +## 재배포 방법 +![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) +1. 버셀 내부 프로젝트 콘솔로 이동하여 상단의 배포 버튼을 클릭합니다; +2. 목록에서 맨 위 항목 오른쪽에 있는 버튼을 선택합니다; +3. 재배포를 클릭하여 재배포합니다. \ No newline at end of file From 0113d4499b310b1e507ffb9738dd85585bd88336 Mon Sep 17 00:00:00 2001 From: B0zal Date: Wed, 23 Aug 2023 21:14:43 +0700 Subject: [PATCH 044/202] [Feature] Better JSON Exporter #2692 [+] A view looks better [+] auto minify json when click a copy in markdown and download Co-Authored-By: wangwentong-lunaon <39506652+wangwentong-lunaon@users.noreply.github.com> Co-Authored-By: Yifei Zhang Co-Authored-By: B0zal <48602426+kfear1337@users.noreply.github.com> --- app/components/exporter.tsx | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 604b8823def..2e3cd84aa75 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -565,21 +565,32 @@ export function MarkdownPreviewer(props: { ); } +// modified by BackTrackZ now it's looks better + export function JsonPreviewer(props: { messages: ChatMessage[]; topic: string; }) { - const msgs = props.messages.map((m) => ({ - role: m.role, - content: m.content, - })); - const mdText = "\n" + JSON.stringify(msgs, null, 2) + "\n"; + const msgs = { + messages: [ + { + role: "system", + content: "You are an assistant that " + props.topic, + }, + ...props.messages.map((m) => ({ + role: m.role, + content: m.content, + })), + ], + }; + const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```"; + const minifiedJson = JSON.stringify(msgs); const copy = () => { - copyToClipboard(JSON.stringify(msgs, null, 2)); + copyToClipboard(minifiedJson); }; const download = () => { - downloadAs(JSON.stringify(msgs, null, 2), `${props.topic}.json`); + downloadAs(JSON.stringify(msgs), `${props.topic}.json`); }; return ( @@ -587,12 +598,12 @@ export function JsonPreviewer(props: { -
-
{mdText}
+
+
); -} +} \ No newline at end of file From 8cac51abbefcb6e939f1190d50eb4b966680542a Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 24 Aug 2023 10:54:28 +0800 Subject: [PATCH 045/202] chore: #2699 Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e1f42e1a099..25801e6430c 100644 --- a/README.md +++ b/README.md @@ -230,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 ``` @@ -239,9 +239,9 @@ 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 ``` From d8b6ebf6cbcfcad7865f51e4a75e912a9aa87d8f Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 24 Aug 2023 11:09:17 +0800 Subject: [PATCH 046/202] fix: #2699 remove double quotes in readme --- README_CN.md | 10 +++++----- README_ES.md | 10 +++++----- README_JA.md | 10 +++++----- README_KO.md | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README_CN.md b/README_CN.md index 568cd229a4c..1111540e992 100644 --- a/README_CN.md +++ b/README_CN.md @@ -135,8 +135,8 @@ BASE_URL=https://chatgpt1.nextweb.fun/api/proxy 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 ``` @@ -144,10 +144,10 @@ 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 ``` diff --git a/README_ES.md b/README_ES.md index 34e9678f9ad..a5787a996d0 100644 --- a/README_ES.md +++ b/README_ES.md @@ -130,8 +130,8 @@ Antes de empezar a escribir código, debe crear uno nuevo en la raíz del proyec 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=your-password \ yidadaa/chatgpt-next-web ``` @@ -139,10 +139,10 @@ También puede especificar proxy: ```shell docker run -d -p 3000:3000 \ - -e OPENAI_API_KEY="sk-xxxx" \ - -e CODE="页面访问密码" \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ --net=host \ - -e PROXY_URL="http://127.0.0.1:7890" \ + -e PROXY_URL=http://127.0.0.1:7890 \ yidadaa/chatgpt-next-web ``` diff --git a/README_JA.md b/README_JA.md index 6018a1b010c..72a0d5373f6 100644 --- a/README_JA.md +++ b/README_JA.md @@ -196,8 +196,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 ``` @@ -205,9 +205,9 @@ docker run -d -p 3000:3000 \ ```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 ``` diff --git a/README_KO.md b/README_KO.md index 86fba5115e5..519dd9d9bb5 100644 --- a/README_KO.md +++ b/README_KO.md @@ -135,8 +135,8 @@ BASE_URL=https://chatgpt1.nextweb.fun/api/proxy 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 ``` @@ -144,10 +144,10 @@ 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 ``` From 19dd71eb051a3c301c37ab21538147a88915ee8b Mon Sep 17 00:00:00 2001 From: huni Date: Thu, 24 Aug 2023 22:04:42 +0800 Subject: [PATCH 047/202] setup shell support for debian --- scripts/setup.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 751a9ac17c2..43af2b186fa 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -10,7 +10,7 @@ case "$(uname -s)" in exit 1 fi else - if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")$ ]]; then + if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")|(ID=\"debian\")$ ]]; then echo "Unsupported Linux distribution." exit 1 fi @@ -32,6 +32,9 @@ if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v y if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=ubuntu" ]]; then sudo apt-get update sudo apt-get -y install nodejs git yarn + elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=debian" ]]; then + sudo apt-get update + sudo apt-get -y install nodejs git yarn elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=centos" ]]; then sudo yum -y install epel-release sudo yum -y install nodejs git yarn From 925d28495af7b4d9f722da59fe97115c6898acb7 Mon Sep 17 00:00:00 2001 From: huni Date: Thu, 24 Aug 2023 23:24:26 +0800 Subject: [PATCH 048/202] fix bug of ! near check of system. --- scripts/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 43af2b186fa..73ed61b1326 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -10,7 +10,7 @@ case "$(uname -s)" in exit 1 fi else - if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")|(ID=\"debian\")$ ]]; then + if [[ !"$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")|(ID=\"debian\")$ ]]; then echo "Unsupported Linux distribution." exit 1 fi From 507b7fee56b8c63ffee6bbbe684f4acedaf27640 Mon Sep 17 00:00:00 2001 From: B0zal Date: Fri, 25 Aug 2023 06:31:33 +0700 Subject: [PATCH 049/202] [+] Language indonesia (my country) --- app/locales/id.ts | 349 +++++++++++++++++++++++++++++++++++++++++++ app/locales/index.ts | 3 + 2 files changed, 352 insertions(+) create mode 100644 app/locales/id.ts diff --git a/app/locales/id.ts b/app/locales/id.ts new file mode 100644 index 00000000000..f73d94ef9c8 --- /dev/null +++ b/app/locales/id.ts @@ -0,0 +1,349 @@ +import { SubmitKey } from "../store/config"; +import { PartialLocaleType } from "./index"; + +const id: PartialLocaleType = { + WIP: "Coming Soon...", + Error: { + Unauthorized: + "Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.", + }, + Auth: { + Title: "Diperlukan Kode Akses", + Tips: "Masukkan kode akses di bawah", + Input: "Kode Akses", + Confirm: "Konfirmasi", + Later: "Nanti", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} pesan`, + }, + Chat: { + SubTitle: (count: number) => `${count} pesan`, + Actions: { + ChatList: "Buka Daftar Chat", + CompressedHistory: "Ekspor Riwayat Terkompresi", + Export: "Ekspor Semua Pesan sebagai Markdown", + Copy: "Salin", + Stop: "Berhenti", + Retry: "Coba Lagi", + Pin: "Pin", + PinToastContent: "2 pesan telah ditandai", + PinToastAction: "Lihat", + Delete: "Hapus", + Edit: "Edit", + }, + Commands: { + new: "Mulai Chat Baru", + newm: "Mulai Chat Baru dengan Masks", + next: "Chat Selanjutnya", + prev: "Chat Sebelumnya", + clear: "Bersihkan Percakapan", + del: "Hapus Chat", + }, + InputActions: { + Stop: "Berhenti", + ToBottom: "Ke Bagian Bawah", + Theme: { + auto: "Otomatis", + light: "Tema Terang", + dark: "Tema Gelap", + }, + Prompt: "Prompts", + Masks: "Masks", + Clear: "Bersihkan Percakapan", + Settings: "Pengaturan", + }, + Rename: "Ubah Nama Chat", + Typing: "Mengetik...", + Input: (submitKey: string) => { + var inputHints = `${submitKey} untuk mengirim`; + if (submitKey === String(SubmitKey.Enter)) { + inputHints += ", Shift + Enter untuk membalut"; + } + return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"; + }, + Send: "Kirim", + Config: { + Reset: "Reset ke Default", + SaveAs: "Simpan sebagai Masks", + }, + }, + Export: { + Title: "Ekspor Pesan", + Copy: "Salin Semua", + Download: "Unduh", + MessageFromYou: "Pesan dari Anda", + MessageFromChatGPT: "Pesan dari ChatGPT", + Share: "Bagikan ke ShareGPT", + Format: { + Title: "Format Ekspor", + SubTitle: "Markdown atau Gambar PNG", + }, + IncludeContext: { + Title: "Sertakan Konteks", + SubTitle: "Apakah akan menyertakan masks", + }, + Steps: { + Select: "Pilih", + Preview: "Pratinjau", + }, + }, + Select: { + Search: "Cari", + All: "Pilih Semua", + Latest: "Pilih Terbaru", + Clear: "Bersihkan", + }, + Memory: { + Title: "Prompt Memori", + EmptyContent: "Belum ada yang tersedia.", + Send: "Kirim Memori", + Copy: "Salin Memori", + Reset: "Reset", + ResetConfirm: + "Jika Anda mereset, riwayat obrolan saat ini dan memori historis akan dihapus. Apakah Anda yakin ingin melakukan reset?", + }, + Home: { + NewChat: "Obrolan Baru", + DeleteChat: "Anda yakin ingin menghapus percakapan yang dipilih?", + DeleteToast: "Percakapan telah dihapus", + Revert: "Kembali", + }, + Settings: { + Title: "Pengaturan", + SubTitle: "Semua Pengaturan", + Danger: { + Reset: { + Title: "Setel Ulang Semua Pengaturan", + SubTitle: "Mengembalikan semua pengaturan ke nilai default", + Action: "Setel Ulang", + Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?", + }, + Clear: { + Title: "Hapus Semua Data", + SubTitle: "Menghapus semua pesan dan pengaturan", + Action: "Hapus", + Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?", + }, + }, + Lang: { + Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "Semua Bahasa", + }, + Avatar: "Avatar", + FontSize: { + Title: "Ukuran Font", + SubTitle: "Ubah ukuran font konten chat", + }, + InjectSystemPrompts: { + Title: "Suntikkan Petunjuk Sistem", + SubTitle: + "Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan", + }, + InputTemplate: { + Title: "Template Input", + SubTitle: "Pesan baru akan diisi menggunakan template ini", + }, + + Update: { + Version: (x: string) => `Version: ${x}`, + IsLatest: "Versi terbaru", + CheckUpdate: "Periksa Pembaruan", + IsChecking: "Memeriksa pembaruan...", + FoundUpdate: (x: string) => `Versi terbaru ditemukan: ${x}`, + GoToUpdate: "Perbarui Sekarang", + }, + AutoGenerateTitle: { + Title: "Hasilkan Judul Otomatis", + SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan", + }, + SendKey: "Kirim", + Theme: "Tema", + TightBorder: "Batas Ketat", + SendPreviewBubble: { + Title: "Pratinjau Obrolan", + SubTitle: "Pratinjau Obrolan dengan markdown", + }, + Mask: { + Splash: { + Title: "Layar Pembuka Masks", + SubTitle: + "Tampilkan layar pembuka masks sebelum memulai percakapan baru", + }, + Builtin: { + Title: "Sembunyikan Masks Bawaan", + SubTitle: "Sembunyikan Masks bawaan dari daftar masks", + }, + }, + Prompt: { + Disable: { + Title: "Nonaktifkan Otomatisasi", + SubTitle: "Aktifkan/Matikan otomatisasi", + }, + List: "Daftar Prompt", + ListCount: (builtin: number, custom: number) => + `${builtin} bawaan, ${custom} penggunaan khusus`, + Edit: "Edit", + Modal: { + Title: "Daftar Prompt", + Add: "Tambahkan", + Search: "Cari Prompt", + }, + EditModal: { + Title: "Edit Prompt", + }, + }, + HistoryCount: { + Title: "Jumlah Pesan Riwayat", + SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan", + }, + CompressThreshold: { + Title: "Batas Kompresi Riwayat", + SubTitle: + "Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", + }, + Token: { + Title: "Kunci API", + SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses", + Placeholder: "Kunci API OpenAI", + }, + Usage: { + Title: "Saldo Akun", + SubTitle(used: any, total: any) { + return `Digunakan bulan ini: ${used}, total langganan: ${total}`; + }, + IsChecking: "Memeriksa...", + Check: "Periksa", + NoAccess: "Masukkan kunci API untuk memeriksa saldo", + }, + AccessCode: { + Title: "Kode Akses", + SubTitle: "Kontrol akses diaktifkan", + Placeholder: "Diperlukan kode akses", + }, + Endpoint: { + Title: "Endpoint", + SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom", + }, + Model: "Model", + Temperature: { + Title: "Suhu", + SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya", + }, + TopP: { + Title: "Top P", + SubTitle: "Tidak mengubah nilai dengan suhu", + }, + MaxTokens: { + Title: "Token Maksimum", + SubTitle: "Panjang maksimum token input dan output", + }, + PresencePenalty: { + Title: "Penalti Kehadiran", + SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul", + }, + FrequencyPenalty: { + Title: "Penalti Frekuensi", + SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama", + }, + }, + Store: { + DefaultTopic: "Percakapan Baru", + BotHello: "Halo! Bagaimana saya bisa membantu Anda hari ini?", + Error: "Terjadi kesalahan, silakan coba lagi nanti.", + Prompt: { + History: (content: string) => + "Ini adalah ringkasan singkat dari riwayat percakapan: " + content, + Topic: + "Buat judul berisi empat hingga lima kata untuk percakapan kita yang tidak akan disertakan dalam ringkasan percakapan, seperti instruksi, format, kutipan, tanda baca awal, tanda kutip pendahuluan, atau karakter tambahan. Silakan coba dengan kutipan berakhir.", + Summarize: + "Buat ringkasan percakapan dalam 200 kata yang akan digunakan sebagai promp di masa depan.", + }, + }, + Copy: { + Success: "Berhasil disalin ke clipboard", + Failed: "Gagal menyalin, berikan izin untuk memberikan izin", + }, + Context: { + Toast: (x: any) => `Dengan ${x} promp kontekstual`, + Edit: "Pengaturan Obrolan Saat Ini", + Add: "Tambahkan Promp", + Clear: "Bersihkan Konteks", + Revert: "Kembali ke Posisi Sebelumnya", + }, + Plugin: { + Name: "Plugin", + }, + Mask: { + Name: "Masks", + Page: { + Title: "Template Promp", + SubTitle: (count: number) => `${count} template prompt`, + Search: "Cari template", + Create: "Buat", + }, + Item: { + Info: (count: number) => `${count} prompt`, + Chat: "Obrolan", + View: "Lihat", + Edit: "Edit", + Delete: "Hapus", + DeleteConfirm: "Anda yakin ingin menghapus?", + }, + EditModal: { + Title: (readonly: boolean) => + `Edit Template Prompt ${readonly ? "(hanya baca)" : ""}`, + Download: "Unduh", + Clone: "Duplikat", + }, + Config: { + Avatar: "Avatar Bot", + Name: "Nama Bot", + Sync: { + Title: "Gunakan Konfigurasi Global", + SubTitle: "Gunakan konfigurasi global dalam percakapan ini", + Confirm: + "Pastikan untuk mengganti konfigurasi kustom dengan konfigurasi global?", + }, + HideContext: { + Title: "Sembunyikan Prompt Konteks", + SubTitle: "Tidak menampilkan prompt konteks dalam obrolan", + }, + Share: { + Title: "Bagikan Masks Ini", + SubTitle: "Buat tautan untuk masks ini", + Action: "Salin Tautan", + }, + }, + }, + NewChat: { + Return: "Kembali", + Skip: "Lewati", + Title: "Pilih Masks", + SubTitle: "Berkonversasilah dengan diri Anda di balik masks", + More: "Lebih Lanjut", + NotShow: "Jangan Tampilkan Sekarang", + ConfirmNoShow: + "Pastikan untuk menonaktifkannya? Anda dapat mengaktifkannya nanti melalui pengaturan.", + }, + + UI: { + Confirm: "Konfirmasi", + Cancel: "Batal", + Close: "Tutup", + Create: "Buat", + Edit: "Edit", + }, + Exporter: { + Model: "Model", + Messages: "Pesan", + Topic: "Topik", + Time: "Waktu", + }, + URLCommand: { + Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ", + Settings: "Pengaturan terdeteksi dari url, konfirmasi untuk diterapkan ?", + }, +}; + +export default id; diff --git a/app/locales/index.ts b/app/locales/index.ts index 528600bec81..79e314facdd 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -1,6 +1,7 @@ import cn from "./cn"; import en from "./en"; import tw from "./tw"; +import id from "./id"; import fr from "./fr"; import es from "./es"; import it from "./it"; @@ -25,6 +26,7 @@ const ALL_LANGS = { tw, jp, ko, + id, fr, es, it, @@ -48,6 +50,7 @@ export const ALL_LANG_OPTIONS: Record = { tw: "繁體中文", jp: "日本語", ko: "한국어", + id: "Indonesia", fr: "Français", es: "Español", it: "Italiano", From f65b0128e7cea3f4440f4f55ded8a3e2290bf7a6 Mon Sep 17 00:00:00 2001 From: B0zal Date: Fri, 25 Aug 2023 12:39:24 +0700 Subject: [PATCH 050/202] Issue #2702 should be fixed now kiw kiw where the ChatGPTicon.src glitches and breaks when exporting the image for the second time without refreshing the page. --- app/components/exporter.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 2e3cd84aa75..ba82c2851c8 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -383,7 +383,7 @@ export function PreviewActions(props: { function ExportAvatar(props: { avatar: string }) { if (props.avatar === DEFAULT_MASK_AVATAR) { return ( - ; + return ; } export function ImagePreviewer(props: { @@ -422,6 +422,7 @@ export function ImagePreviewer(props: { ]) .then(() => { showToast(Locale.Copy.Success); + refreshPreview(); }); } catch (e) { console.error("[Copy Image] ", e); @@ -447,11 +448,19 @@ export function ImagePreviewer(props: { link.download = `${props.topic}.png`; link.href = blob; link.click(); + refreshPreview(); } }) .catch((e) => console.log("[Export Image] ", e)); }; + const refreshPreview = () => { + const dom = previewRef.current; + if (dom) { + dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching + } + }; + return (
Date: Fri, 25 Aug 2023 17:09:39 +0700 Subject: [PATCH 051/202] [+] FineTuned Sysmessage Depends for local language this for JSON Exporter --- app/components/exporter.tsx | 6 +++--- app/locales/ar.ts | 3 +++ app/locales/bn.ts | 4 ++-- app/locales/cn.ts | 5 ++++- app/locales/cs.ts | 4 ++-- app/locales/de.ts | 3 +++ app/locales/en.ts | 3 +++ app/locales/es.ts | 3 +++ app/locales/fr.ts | 3 +++ app/locales/id.ts | 3 +++ app/locales/it.ts | 3 +++ app/locales/jp.ts | 1 + app/locales/ko.ts | 3 +++ app/locales/ru.ts | 3 +++ app/locales/tr.ts | 3 +++ app/locales/tw.ts | 1 + app/locales/vi.ts | 3 +++ 17 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 2e3cd84aa75..df794adce7a 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -575,7 +575,7 @@ export function JsonPreviewer(props: { messages: [ { role: "system", - content: "You are an assistant that " + props.topic, + content: `${Locale.FineTuned.Sysmessage} ${props.topic}`, }, ...props.messages.map((m) => ({ role: m.role, @@ -602,8 +602,8 @@ export function JsonPreviewer(props: { messages={props.messages} />
- +
); -} \ No newline at end of file +} diff --git a/app/locales/ar.ts b/app/locales/ar.ts index a86b1648a22..520cb26356e 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -233,6 +233,9 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Plugin: { Name: "المكوّن الإضافي", }, + FineTuned: { + Sysmessage: "أنت مساعد ي", + }, Mask: { Name: "الأقنعة", Page: { diff --git a/app/locales/bn.ts b/app/locales/bn.ts index 9eda157f54e..d9559d7190c 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -268,8 +268,8 @@ const bn: PartialLocaleType = { Clear: "সঙ্গতি পরিস্কার করুন", Revert: "পূর্ববর্তী অবস্থানে ফিরে যান", }, - Plugin: { - Name: "প্লাগইন", + FineTuned: { + Sysmessage: "আপনি একটি সহকারী যা", }, Mask: { Name: "মাস্ক", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 19e804b3a35..3f4324a6f79 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -288,7 +288,10 @@ const cn = { Revert: "恢复上下文", }, Plugin: { - Name: "插件", + Name: "你是一个助手 ", + }, + FineTuned: { + Sysmessage: "你是一个助手", }, Mask: { Name: "面具", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index b8a3b974c34..d1d5d635704 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -185,8 +185,8 @@ const cs: PartialLocaleType = { Edit: "Kontextové a paměťové pokyny", Add: "Přidat pokyn", }, - Plugin: { - Name: "Plugin", + FineTuned: { + Sysmessage: "Jste asistent, který", }, Mask: { Name: "Maska", diff --git a/app/locales/de.ts b/app/locales/de.ts index 59b1fc927a1..e0bdc52b749 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -192,6 +192,9 @@ const de: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Du bist ein Assistent, der", + }, Mask: { Name: "Mask", Page: { diff --git a/app/locales/en.ts b/app/locales/en.ts index 64cdc38bb41..981357274d8 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -295,6 +295,9 @@ const en: LocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "You are an assistant that", + }, Mask: { Name: "Mask", Page: { diff --git a/app/locales/es.ts b/app/locales/es.ts index 6145eccc824..a6ae154f44f 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -190,6 +190,9 @@ const es: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Eres un asistente que", + }, Mask: { Name: "Mask", Page: { diff --git a/app/locales/fr.ts b/app/locales/fr.ts index a98d4a43220..f5200f2719c 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -244,6 +244,9 @@ const fr: PartialLocaleType = { Plugin: { Name: "Extension", }, + FineTuned: { + Sysmessage: "Eres un asistente que", + }, Mask: { Name: "Masque", Page: { diff --git a/app/locales/id.ts b/app/locales/id.ts index f73d94ef9c8..c3a2a5f8807 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -274,6 +274,9 @@ const id: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Anda adalah asisten yang", + }, Mask: { Name: "Masks", Page: { diff --git a/app/locales/it.ts b/app/locales/it.ts index 6a2eabf408b..bf20747b108 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -191,6 +191,9 @@ const it: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Sei un assistente che", + }, Mask: { Name: "Mask", Page: { diff --git a/app/locales/jp.ts b/app/locales/jp.ts index e27a4c6d907..c3e00fa0964 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -204,6 +204,7 @@ const jp: PartialLocaleType = { Add: "追加", }, Plugin: { Name: "プラグイン" }, + FineTuned: { Sysmessage: "あなたはアシスタントです" }, Mask: { Name: "キャラクタープリセット", Page: { diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 194e44769c8..717ce30b2f8 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -186,6 +186,9 @@ const ko: PartialLocaleType = { Plugin: { Name: "플러그인", }, + FineTuned: { + Sysmessage: "당신은 어시스턴트입니다", + }, Mask: { Name: "마스크", Page: { diff --git a/app/locales/ru.ts b/app/locales/ru.ts index 313acf54444..bf98b4eb865 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -191,6 +191,9 @@ const ru: PartialLocaleType = { Plugin: { Name: "Плагин", }, + FineTuned: { + Sysmessage: "Вы - ассистент, который", + }, Mask: { Name: "Маска", Page: { diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 46fdd6285e7..06996d83dac 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -191,6 +191,9 @@ const tr: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Sen bir asistansın", + }, Mask: { Name: "Mask", Page: { diff --git a/app/locales/tw.ts b/app/locales/tw.ts index ad1ee0bb676..15f6648e659 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -180,6 +180,7 @@ const tw: PartialLocaleType = { Add: "新增一條", }, Plugin: { Name: "外掛" }, + FineTuned: { Sysmessage: "你是一個助手" }, Mask: { Name: "面具", Page: { diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 2117734b01e..8f53a3dc1ee 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -186,6 +186,9 @@ const vi: PartialLocaleType = { Plugin: { Name: "Plugin", }, + FineTuned: { + Sysmessage: "Bạn là một trợ lý", + }, Mask: { Name: "Mẫu", Page: { From 63c93a42b5fa5973edf102a6ece29aa294538375 Mon Sep 17 00:00:00 2001 From: B0zal Date: Fri, 25 Aug 2023 18:15:50 +0700 Subject: [PATCH 052/202] [+] Fixed language missing for finetuned --- app/locales/bn.ts | 3 +++ app/locales/cn.ts | 2 +- app/locales/cs.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/locales/bn.ts b/app/locales/bn.ts index d9559d7190c..2d2266b3f43 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -268,6 +268,9 @@ const bn: PartialLocaleType = { Clear: "সঙ্গতি পরিস্কার করুন", Revert: "পূর্ববর্তী অবস্থানে ফিরে যান", }, + Plugin: { + Name: "প্লাগইন", + }, FineTuned: { Sysmessage: "আপনি একটি সহকারী যা", }, diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 3f4324a6f79..7c0473bb318 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -288,7 +288,7 @@ const cn = { Revert: "恢复上下文", }, Plugin: { - Name: "你是一个助手 ", + Name: "插件", }, FineTuned: { Sysmessage: "你是一个助手", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index d1d5d635704..57aa803e42b 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -185,6 +185,9 @@ const cs: PartialLocaleType = { Edit: "Kontextové a paměťové pokyny", Add: "Přidat pokyn", }, + Plugin: { + Name: "Plugin", + }, FineTuned: { Sysmessage: "Jste asistent, který", }, From 5e23ad2db1628d096eaf0f38b610011ed5c8fd65 Mon Sep 17 00:00:00 2001 From: B0zal Date: Fri, 25 Aug 2023 18:43:37 +0700 Subject: [PATCH 053/202] Security Update Potentially unsafe external link --- app/components/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index b0b778fc5ab..634639f1d40 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -174,7 +174,7 @@ export function SideBar(props: { className?: string }) {
From 22a6819f7b81b654fbb1632f425b42411f94cc98 Mon Sep 17 00:00:00 2001 From: B0zal Date: Fri, 25 Aug 2023 18:48:38 +0700 Subject: [PATCH 054/202] Security Update [+] Protect Prototype --- app/utils/merge.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/utils/merge.ts b/app/utils/merge.ts index 758d6df84fd..fd7a4da98ca 100644 --- a/app/utils/merge.ts +++ b/app/utils/merge.ts @@ -1,9 +1,13 @@ export function merge(target: any, source: any) { Object.keys(source).forEach(function (key) { - if (source[key] && typeof source[key] === "object") { + if ( + source.hasOwnProperty(key) && // Check if the property is not inherited + source[key] && + typeof source[key] === "object" || key === "__proto__" || key === "constructor" + ) { merge((target[key] = target[key] || {}), source[key]); return; } target[key] = source[key]; }); -} +} \ No newline at end of file From 0ae57589a9cabc1a87dfb5fd056057133aae9b7d Mon Sep 17 00:00:00 2001 From: B0zal Date: Sun, 27 Aug 2023 06:09:55 +0700 Subject: [PATCH 055/202] This branch only for scanner --- .github/workflows/codeql.yml | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..b75ec37b67b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +# Modified by backtrackz +# Mark TRAP cache skipped because it only a few lines +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '18 8 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + id: codeql + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" + codeql-path: /opt/hostedtoolcache/CodeQL/2.14.1/x64/codeql/codeql + upload-trap-cache: true + upload-trap-cache-exclude: '.*' + + - name: Mark TRAP cache skipped + if: steps.codeql.outputs.upload_trap_cache_skipped == 'true' + run: echo "::set-output name=trap_cache_skipped::true" + + - name: Start Analysis + if: steps.codeql.outputs.upload_trap_cache_skipped == 'false' + run: echo "Starting analysis..." \ No newline at end of file From 71d06647d07e22b3a1d114ef07bc4fe5373fc19d Mon Sep 17 00:00:00 2001 From: B0zal Date: Sun, 27 Aug 2023 09:15:05 +0700 Subject: [PATCH 056/202] Delete codeql.yml it won't work if someone try, must have licence advance security --- .github/workflows/codeql.yml | 55 ------------------------------------ 1 file changed, 55 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index b75ec37b67b..00000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Modified by backtrackz -# Mark TRAP cache skipped because it only a few lines -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '18 8 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - id: codeql - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" - codeql-path: /opt/hostedtoolcache/CodeQL/2.14.1/x64/codeql/codeql - upload-trap-cache: true - upload-trap-cache-exclude: '.*' - - - name: Mark TRAP cache skipped - if: steps.codeql.outputs.upload_trap_cache_skipped == 'true' - run: echo "::set-output name=trap_cache_skipped::true" - - - name: Start Analysis - if: steps.codeql.outputs.upload_trap_cache_skipped == 'false' - run: echo "Starting analysis..." \ No newline at end of file From 3bd76b9156627116b8bbcf038e08e35d84438447 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 28 Aug 2023 00:02:52 +0800 Subject: [PATCH 057/202] feat: close #2580 only use 3.5 to summarize when not using custom models --- app/constant.ts | 2 ++ app/store/chat.ts | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 8b28af323bb..ba0b22c7f78 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -63,6 +63,8 @@ Knowledge cutoff: 2021-09 Current model: {{model}} Current time: {{time}}`; +export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; + export const DEFAULT_MODELS = [ { name: "gpt-4", diff --git a/app/store/chat.ts b/app/store/chat.ts index bcdd99569db..20603fe48b0 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -11,6 +11,7 @@ import { DEFAULT_INPUT_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE, StoreKey, + SUMMARIZE_MODEL, } from "../constant"; import { api, RequestMessage } from "../client/api"; import { ChatControllerPool } from "../client/controller"; @@ -80,6 +81,11 @@ function createEmptySession(): ChatSession { }; } +function getSummarizeModel(currentModel: string) { + // if it is using gpt-* models, force to use 3.5 to summarize + return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel; +} + interface ChatStore { sessions: ChatSession[]; currentSessionIndex: number; @@ -501,7 +507,7 @@ export const useChatStore = create()( api.llm.chat({ messages: topicMessages, config: { - model: "gpt-3.5-turbo", + model: getSummarizeModel(session.mask.modelConfig.model), }, onFinish(message) { get().updateCurrentSession( @@ -555,7 +561,11 @@ export const useChatStore = create()( date: "", }), ), - config: { ...modelConfig, stream: true, model: "gpt-3.5-turbo" }, + config: { + ...modelConfig, + stream: true, + model: getSummarizeModel(session.mask.modelConfig.model), + }, onUpdate(message) { session.memoryPrompt = message; }, From 49046125235d11f85ee0dc81f2424f2cde91f1eb Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 28 Aug 2023 00:50:53 +0800 Subject: [PATCH 058/202] feat: close #2430 add a simple user maual --- README.md | 10 ++++ docs/user-manual-cn.md | 101 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 docs/user-manual-cn.md diff --git a/README.md b/README.md index 1662e8c7ea2..5e76e9a7ae6 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,16 @@ If your proxy needs password, use: 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) diff --git a/docs/user-manual-cn.md b/docs/user-manual-cn.md new file mode 100644 index 00000000000..883bbc23e33 --- /dev/null +++ b/docs/user-manual-cn.md @@ -0,0 +1,101 @@ +# 用户手册 User Manual + +> No english version yet, please read this doc with ChatGPT or other translation tools. + +本文档用于解释 ChatGPT Next Web 的部分功能介绍和设计原则。 + +## 面具 (Mask) + +### 什么是面具?它和提示词的区别是什么? + +面具 = 多个预设提示词 + 模型设置 + 对话设置。 + +其中预设提示词(Contextual Prompts)一般用于 In-Context Learning,用于让 ChatGPT 生成更加符合要求的输出,也可以增加系统约束或者输入有限的额外知识。 + +模型设置则顾名思义,使用此面具创建的对话都会默认使用对应的模型参数。 + +对话设置是与对话体验相关的一系列设置,我们会在下方的章节中依次介绍。 + +### 如何添加一个预设面具? + +目前仅能够通过编辑源代码的方式添加预设面具,请根据需要编辑 [mask](../app/masks/) 目录下对应语言的文件即可。 + +编辑步骤如下: + +1. 在 ChatGPT Next Web 中配置好一个面具; +2. 使用面具编辑页面的下载按钮,将面具保存为 JSON 格式; +3. 让 ChatGPT 帮你将 json 文件格式化为对应的 ts 代码; +4. 放入对应的 .ts 文件。 + +后续会增加使用旁加载的方式加载面具。 + +## 对话 (Chat) + +### 对话框上方的按钮的作用 + +在默认状态下,将鼠标移动到按钮上,即可查看按钮的文字说明,我们依次介绍: + +- 对话设置:当前对话的设置,它与全局设置的关系,请查看下一小节的说明; +- 颜色主题:点击即可在自动、暗黑、浅色之间轮换; +- 快捷指令:项目内置的快捷填充预设提示词,也可以在对话框中输入 / 进行搜索; +- 所有面具:进入面具页面; +- 清除聊天:插入一个清除标记,标记上方的聊天将不会发给 GPT,效果相当于清除了当前对话,当然,你也可以再次点击该按钮,可取消清除; +- 模型设置:更改当前对话的模型,注意,此按钮只会修改当前对话的模型,并不会修改全局默认模型。 + +### 对话内设置与全局设置的关系 + +目前有两处设置入口: + +1. 页面左下角的设置按钮,进入后是全局设置页; +2. 对话框上方的设置按钮,进入后是对话设置页。 + +在新建对话后,该对话的设置默认与全局设置保持同步,修改全局设置,则新建对话的对话内设置也会被同步修改。 + +一旦用户手动更改过对话内设置,则对话内设置将与全局设置断开同步,此时更改全局设置,将不会对该对话生效。 + +如果想恢复两者的同步关系,可以将“对话内设置 -> 使用全局设置”选项勾选。 + +### 对话内设置项的含义 + +点开对话框上方的按钮,进入对话内设置,内容从上到下依次为: + +- 预设提示词列表:可以增加、删除、排序预设提示词 +- 角色头像:顾名思义 +- 角色名称:顾名思义 +- 隐藏预设对话:隐藏后,预设提示词不会出现在聊天界面 +- 使用全局设置:用于表示当前对话是否使用全局对话设置 +- 模型设置选项:剩余的选项与全局设置选项含义一致,见下一小节 + +### 全局设置项的含义 + +- model / temperature / top_p / max_tokens / presence_penalty / frequency_penalty 均为 ChatGPT 的设置参数,详情请查阅 OpenAI 官方文档,再次不再赘述; +- 注入系统级提示信息、用户输入预处理:详情请看 [https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/2144) +- 附带历史消息数:用户每次输入消息并发送时,所携带的最近 n 条消息数量; +- 历史消息长度压缩阈值:当已经产生的聊天字数达到该数值以后,则自动触发历史摘要功能; +- 历史摘要:是否启用历史摘要功能。 + +### 什么是历史摘要? + +历史摘要功能,也是历史消息压缩功能,是保证长对话场景下保持历史记忆的关键,合理使用该功能可以在不丢失历史话题信息的情况下,节省所使用的 token。 + +由于 ChatGPT API 的长度限制,我们以 3.5 模型为例,它只能接受小于 4096 tokens 的对话消息,一旦超出这个数值,就会报错。 + +同时为了让 ChatGPT 理解我们对话的上下文,往往会携带多条历史消息来提供上下文信息,而当对话进行一段时间之后,很容易就会触发长度限制。 + +为了解决此问题,我们增加了历史记录压缩功能,假设阈值为 1000 字符,那么每次用户产生的聊天记录超过 1000 字符时,都会将没有被总结过的消息,发送给 ChatGPT,让其产生一个 100 字所有的摘要。 + +这样,历史信息就从 1000 字压缩到了 100 字,这是一种有损压缩,但已能满足大多数使用场景。 + +### 什么时候应该关闭历史摘要? + +历史摘要可能会影响 ChatGPT 的对话质量,所以如果对话场景是翻译、信息提取等一次性对话场景,请直接关闭历史摘要功能,并将历史消息数设置为 0。 + +### 当用户发送一条消息时,有哪些信息被发送出去了? + +当用户在对话框输入了一条消息后,发送给 ChatGPT 的消息,包含以下几个部分: + +1. 系统级提示词:用于尽可能贴近 ChatGPT 官方 WebUI 的使用体验,可在设置中关闭此信息; +2. 历史摘要:作为长期记忆,提供长久但模糊的上下文信息; +3. 预设提示词:当前对话内设置的预设提示词,用于 In-Context Learning 或者注入系统级限制; +4. 最近 n 条对话记录:作为短期记忆,提供短暂但精确的上下文信息; +5. 用户当前输入的消息。 From 4a182517daeda046604fafc98c14df26d6eafd97 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 28 Aug 2023 01:04:24 +0800 Subject: [PATCH 059/202] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d54e47cd56..07455d00d82 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. - 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 -- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어 +- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia ## Roadmap From b0c32159e744448b9fbdfa63b1f46bed5050f9a2 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 28 Aug 2023 01:15:46 +0800 Subject: [PATCH 060/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b09715cb312..2256d5b34d9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.4" + "version": "2.9.5" }, "tauri": { "allowlist": { From 05aa5206693377ab7bbea70f85a0f12a542145cf Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 28 Aug 2023 11:06:12 +0800 Subject: [PATCH 061/202] Revert "chore(deps): bump next from 13.4.9 to 13.4.19" --- package.json | 2 +- yarn.lock | 124 +++++++++++++++++++++++++-------------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 26a77c80148..6610083bd0d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "html-to-image": "^1.11.11", "mermaid": "^10.3.1", "nanoid": "^4.0.2", - "next": "^13.4.19", + "next": "^13.4.9", "node-fetch": "^3.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index a0e4c965408..cbce2ef174a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1131,10 +1131,10 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@next/env@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" - integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== +"@next/env@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e" + integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw== "@next/eslint-plugin-next@13.4.19": version "13.4.19" @@ -1143,50 +1143,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" - integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== - -"@next/swc-darwin-x64@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" - integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== - -"@next/swc-linux-arm64-gnu@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" - integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== - -"@next/swc-linux-arm64-musl@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" - integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== - -"@next/swc-linux-x64-gnu@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" - integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== - -"@next/swc-linux-x64-musl@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" - integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== - -"@next/swc-win32-arm64-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" - integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== - -"@next/swc-win32-ia32-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" - integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== - -"@next/swc-win32-x64-msvc@13.4.19": - version "13.4.19" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" - integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== +"@next/swc-darwin-arm64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677" + integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ== + +"@next/swc-darwin-x64@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8" + integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A== + +"@next/swc-linux-arm64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1" + integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg== + +"@next/swc-linux-arm64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0" + integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw== + +"@next/swc-linux-x64-gnu@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a" + integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg== + +"@next/swc-linux-x64-musl@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a" + integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A== + +"@next/swc-win32-arm64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a" + integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA== + +"@next/swc-win32-ia32-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190" + integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA== + +"@next/swc-win32-x64-msvc@13.4.9": + version "13.4.9" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b" + integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -4707,12 +4707,12 @@ neo-async@^2.6.2: resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next@^13.4.19: - version "13.4.19" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" - integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== +next@^13.4.9: + version "13.4.9" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba" + integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA== dependencies: - "@next/env" "13.4.19" + "@next/env" "13.4.9" "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" @@ -4721,15 +4721,15 @@ next@^13.4.19: watchpack "2.4.0" zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.4.19" - "@next/swc-darwin-x64" "13.4.19" - "@next/swc-linux-arm64-gnu" "13.4.19" - "@next/swc-linux-arm64-musl" "13.4.19" - "@next/swc-linux-x64-gnu" "13.4.19" - "@next/swc-linux-x64-musl" "13.4.19" - "@next/swc-win32-arm64-msvc" "13.4.19" - "@next/swc-win32-ia32-msvc" "13.4.19" - "@next/swc-win32-x64-msvc" "13.4.19" + "@next/swc-darwin-arm64" "13.4.9" + "@next/swc-darwin-x64" "13.4.9" + "@next/swc-linux-arm64-gnu" "13.4.9" + "@next/swc-linux-arm64-musl" "13.4.9" + "@next/swc-linux-x64-gnu" "13.4.9" + "@next/swc-linux-x64-musl" "13.4.9" + "@next/swc-win32-arm64-msvc" "13.4.9" + "@next/swc-win32-ia32-msvc" "13.4.9" + "@next/swc-win32-x64-msvc" "13.4.9" node-domexception@^1.0.0: version "1.0.0" From 885f2a32260b93adfbf58818913ba25ddac28d94 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 5 Sep 2023 01:54:28 +0800 Subject: [PATCH 062/202] feat: close #2752 auto re-fill unfinished input --- app/components/chat.tsx | 21 ++++++++++++++++++++- app/constant.ts | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index dfda4055b76..6fb497303eb 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -80,6 +80,7 @@ import { MAX_RENDER_MSG_COUNT, Path, REQUEST_TIMEOUT_MS, + UNFINISHED_INPUT, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -935,7 +936,8 @@ function _Chat() { const isTouchTopEdge = e.scrollTop <= edgeThreshold; const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold; - const isHitBottom = bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10); + const isHitBottom = + bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10); const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE; const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE; @@ -1013,6 +1015,23 @@ function _Chat() { // edit / insert message modal const [isEditingMessage, setIsEditingMessage] = useState(false); + // remember unfinished input + useEffect(() => { + // try to load from local storage + const key = UNFINISHED_INPUT(session.id); + const mayBeUnfinishedInput = localStorage.getItem(key); + if (mayBeUnfinishedInput && userInput.length === 0) { + setUserInput(mayBeUnfinishedInput); + localStorage.removeItem(key); + } + + const dom = inputRef.current; + return () => { + localStorage.setItem(key, dom?.value ?? ""); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
diff --git a/app/constant.ts b/app/constant.ts index ba0b22c7f78..2141820ce9d 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -44,6 +44,7 @@ export const NARROW_SIDEBAR_WIDTH = 100; export const ACCESS_CODE_PREFIX = "nk-"; export const LAST_INPUT_KEY = "last-input"; +export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; export const REQUEST_TIMEOUT_MS = 60000; From 2c077aca5a4a345a2544fcab36160047aa51eac2 Mon Sep 17 00:00:00 2001 From: Ricky Robinett Date: Wed, 6 Sep 2023 15:36:12 -0400 Subject: [PATCH 063/202] fix cloudflare deployment instructions --- docs/cloudflare-pages-en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cloudflare-pages-en.md b/docs/cloudflare-pages-en.md index ee8ff6a6b3a..2279ff232a4 100644 --- a/docs/cloudflare-pages-en.md +++ b/docs/cloudflare-pages-en.md @@ -12,7 +12,7 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: ``` - npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify + npx @cloudflare/next-on-pages --experimental-minify ``` 9. For "Build output directory", use the default value and do not modify it. 10. Do not modify "Root Directory". @@ -35,4 +35,4 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 14. Go to "Build settings", "Functions", and find "Compatibility flags". 15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag". 16. Go to "Deployments" and click "Retry deployment". -17. Enjoy. \ No newline at end of file +17. Enjoy. From f7a6fa987322d800bd058f7ffa2641361c53b12d Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 7 Sep 2023 17:43:17 +0800 Subject: [PATCH 064/202] Update README_CN.md --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index 1111540e992..e593e45da9c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -114,7 +114,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 OPENAI_API_KEY= # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 -BASE_URL=https://chatgpt1.nextweb.fun/api/proxy +BASE_URL=https://chatgpt2.nextweb.fun/api/proxy ``` ### 本地开发 From 505c8cde81e2db83f9bd92fa05237a09dd3f645c Mon Sep 17 00:00:00 2001 From: shoito <37051+shoito@users.noreply.github.com> Date: Sat, 9 Sep 2023 16:10:24 +0900 Subject: [PATCH 065/202] improve japanese translations --- app/locales/jp.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/locales/jp.ts b/app/locales/jp.ts index c3e00fa0964..b63e8ba3a56 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -19,7 +19,11 @@ const jp: PartialLocaleType = { Copy: "コピー", Stop: "停止", Retry: "リトライ", + Pin: "ピン", + PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました", + PinToastAction: "表示", Delete: "削除", + Edit: "編集", }, Rename: "チャットの名前を変更", Typing: "入力中…", @@ -33,7 +37,7 @@ const jp: PartialLocaleType = { Send: "送信", Config: { Reset: "リセット", - SaveAs: "另存为面具", + SaveAs: "保存", }, }, Export: { From 38f6956e71a3d582b24e67ee93d263fcc7367725 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 11 Sep 2023 00:20:23 +0800 Subject: [PATCH 066/202] feat: close #2754 add import/export to file --- app/components/error.tsx | 7 +- app/components/mask.tsx | 6 +- app/components/settings.tsx | 104 +++++++-------- app/locales/cn.ts | 11 ++ app/store/access.ts | 149 +++++++++------------ app/store/chat.ts | 135 ++++++++++--------- app/store/config.ts | 165 +++++++++++------------ app/store/mask.ts | 164 +++++++++++------------ app/store/prompt.ts | 260 +++++++++++++++++------------------- app/store/sync.ts | 156 +++++++++++++--------- app/store/update.ts | 140 +++++++++---------- app/utils/clone.ts | 3 + app/utils/store.ts | 55 ++++++++ app/utils/sync.ts | 162 ++++++++++++++++++++++ 14 files changed, 862 insertions(+), 655 deletions(-) create mode 100644 app/utils/clone.ts create mode 100644 app/utils/store.ts create mode 100644 app/utils/sync.ts diff --git a/app/components/error.tsx b/app/components/error.tsx index b38341e22ff..914740f9686 100644 --- a/app/components/error.tsx +++ b/app/components/error.tsx @@ -4,8 +4,8 @@ import GithubIcon from "../icons/github.svg"; import ResetIcon from "../icons/reload.svg"; import { ISSUE_URL } from "../constant"; import Locale from "../locales"; -import { downloadAs } from "../utils"; import { showConfirm } from "./ui-lib"; +import { useSyncStore } from "../store/sync"; interface IErrorBoundaryState { hasError: boolean; @@ -26,10 +26,7 @@ export class ErrorBoundary extends React.Component { clearAndSaveData() { try { - downloadAs( - JSON.stringify(localStorage), - "chatgpt-next-web-snapshot.json", - ); + useSyncStore.getState().export(); } finally { localStorage.clear(); location.reload(); diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 3d8ce3a26f7..1ee1c239a74 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -410,7 +410,7 @@ export function MaskPage() { const closeMaskModal = () => setEditingMaskId(undefined); const downloadAll = () => { - downloadAs(JSON.stringify(masks), FileName.Masks); + downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks); }; const importFromFile = () => { @@ -452,11 +452,13 @@ export function MaskPage() { icon={} bordered onClick={downloadAll} + text={Locale.UI.Export} />
} + text={Locale.UI.Import} bordered onClick={() => importFromFile()} /> @@ -604,7 +606,7 @@ export function MaskPage() { - maskStore.update(editingMaskId!, updater) + maskStore.updateMask(editingMaskId!, updater) } readonly={editingMask.builtin} /> diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 1e6ef7139ba..19c54515f1b 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -10,6 +10,9 @@ import ClearIcon from "../icons/clear.svg"; import LoadingIcon from "../icons/three-dots.svg"; import EditIcon from "../icons/edit.svg"; import EyeIcon from "../icons/eye.svg"; +import DownloadIcon from "../icons/download.svg"; +import UploadIcon from "../icons/upload.svg"; + import { Input, List, @@ -49,6 +52,7 @@ import { Avatar, AvatarPicker } from "./emoji"; import { getClientConfig } from "../config/client"; import { useSyncStore } from "../store/sync"; import { nanoid } from "nanoid"; +import { useMaskStore } from "../store/mask"; function EditPromptModal(props: { id: string; onClose: () => void }) { const promptStore = usePromptStore(); @@ -75,7 +79,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) { readOnly={!prompt.isUser} className={styles["edit-prompt-title"]} onInput={(e) => - promptStore.update( + promptStore.updatePrompt( props.id, (prompt) => (prompt.title = e.currentTarget.value), ) @@ -87,7 +91,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) { className={styles["edit-prompt-content"]} rows={10} onInput={(e) => - promptStore.update( + promptStore.updatePrompt( props.id, (prompt) => (prompt.content = e.currentTarget.value), ) @@ -127,14 +131,15 @@ function UserPromptModal(props: { onClose?: () => void }) { actions={[ - promptStore.add({ + onClick={() => { + const promptId = promptStore.add({ id: nanoid(), createdAt: Date.now(), title: "Empty Prompt", content: "Empty Prompt Content", - }) - } + }); + setEditingPromptId(promptId); + }} icon={} bordered text={Locale.Settings.Prompt.Modal.Add} @@ -244,19 +249,31 @@ function DangerItems() { function SyncItems() { const syncStore = useSyncStore(); const webdav = syncStore.webDavConfig; + const chatStore = useChatStore(); + const promptStore = usePromptStore(); + const maskStore = useMaskStore(); + + const stateOverview = useMemo(() => { + const sessions = chatStore.sessions; + const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0); - // not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332 - return null; + return { + chat: sessions.length, + message: messageCount, + prompt: Object.keys(promptStore.prompts).length, + mask: Object.keys(maskStore.masks).length, + }; + }, [chatStore.sessions, maskStore.masks, promptStore.prompts]); return ( } - text="同步" + text={Locale.UI.Sync} onClick={() => { syncStore.check().then(console.log); }} @@ -264,50 +281,25 @@ function SyncItems() { - - - { - syncStore.update( - (config) => (config.server = e.currentTarget.value), - ); - }} - /> - - - - { - syncStore.update( - (config) => (config.username = e.currentTarget.value), - ); - }} - /> - - - - { - syncStore.update( - (config) => (config.password = e.currentTarget.value), - ); - }} - /> +
+ } + text={Locale.UI.Export} + onClick={() => { + syncStore.export(); + }} + /> + } + text={Locale.UI.Import} + onClick={() => { + syncStore.import(); + }} + /> +
); @@ -562,6 +554,8 @@ export function Settings() { + + - - { + return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`; + }, + ImportFailed: "导入失败", + }, Mask: { Splash: { Title: "面具启动页", @@ -355,6 +363,9 @@ const cn = { Close: "关闭", Create: "新建", Edit: "编辑", + Export: "导出", + Import: "导入", + Sync: "同步", }, Exporter: { Model: "模型", diff --git a/app/store/access.ts b/app/store/access.ts index b602116317a..9eaa81e5ea3 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,28 +1,7 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant"; import { getHeaders } from "../client/api"; -import { BOT_HELLO } from "./chat"; import { getClientConfig } from "../config/client"; - -export interface AccessControlStore { - accessCode: string; - token: string; - - needCode: boolean; - hideUserApiKey: boolean; - hideBalanceQuery: boolean; - disableGPT4: boolean; - - openaiUrl: string; - - updateToken: (_: string) => void; - updateCode: (_: string) => void; - updateOpenAiUrl: (_: string) => void; - enabledAccessControl: () => boolean; - isAuthorized: () => boolean; - fetch: () => void; -} +import { createPersistStore } from "../utils/store"; let fetchState = 0; // 0 not fetch, 1 fetching, 2 done @@ -30,72 +9,74 @@ const DEFAULT_OPENAI_URL = getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; console.log("[API] default openai url", DEFAULT_OPENAI_URL); -export const useAccessStore = create()( - persist( - (set, get) => ({ - token: "", - accessCode: "", - needCode: true, - hideUserApiKey: false, - hideBalanceQuery: false, - disableGPT4: false, +const DEFAULT_ACCESS_STATE = { + token: "", + accessCode: "", + needCode: true, + hideUserApiKey: false, + hideBalanceQuery: false, + disableGPT4: false, - openaiUrl: DEFAULT_OPENAI_URL, + openaiUrl: DEFAULT_OPENAI_URL, +}; - enabledAccessControl() { - get().fetch(); +export const useAccessStore = createPersistStore( + { ...DEFAULT_ACCESS_STATE }, - return get().needCode; - }, - updateCode(code: string) { - set(() => ({ accessCode: code?.trim() })); - }, - updateToken(token: string) { - set(() => ({ token: token?.trim() })); - }, - updateOpenAiUrl(url: string) { - set(() => ({ openaiUrl: url?.trim() })); - }, - isAuthorized() { - get().fetch(); + (set, get) => ({ + enabledAccessControl() { + this.fetch(); - // has token or has code or disabled access control - return ( - !!get().token || !!get().accessCode || !get().enabledAccessControl() - ); - }, - fetch() { - if (fetchState > 0 || getClientConfig()?.buildMode === "export") return; - fetchState = 1; - fetch("/api/config", { - method: "post", - body: null, - headers: { - ...getHeaders(), - }, - }) - .then((res) => res.json()) - .then((res: DangerConfig) => { - console.log("[Config] got config from server", res); - set(() => ({ ...res })); + return get().needCode; + }, + updateCode(code: string) { + set(() => ({ accessCode: code?.trim() })); + }, + updateToken(token: string) { + set(() => ({ token: token?.trim() })); + }, + updateOpenAiUrl(url: string) { + set(() => ({ openaiUrl: url?.trim() })); + }, + isAuthorized() { + this.fetch(); + + // has token or has code or disabled access control + return ( + !!get().token || !!get().accessCode || !this.enabledAccessControl() + ); + }, + fetch() { + if (fetchState > 0 || getClientConfig()?.buildMode === "export") return; + fetchState = 1; + fetch("/api/config", { + method: "post", + body: null, + headers: { + ...getHeaders(), + }, + }) + .then((res) => res.json()) + .then((res: DangerConfig) => { + console.log("[Config] got config from server", res); + set(() => ({ ...res })); - if (res.disableGPT4) { - DEFAULT_MODELS.forEach( - (m: any) => (m.available = !m.name.startsWith("gpt-4")), - ); - } - }) - .catch(() => { - console.error("[Config] failed to fetch config"); - }) - .finally(() => { - fetchState = 2; - }); - }, - }), - { - name: StoreKey.Access, - version: 1, + if (res.disableGPT4) { + DEFAULT_MODELS.forEach( + (m: any) => (m.available = !m.name.startsWith("gpt-4")), + ); + } + }) + .catch(() => { + console.error("[Config] failed to fetch config"); + }) + .finally(() => { + fetchState = 2; + }); }, - ), + }), + { + name: StoreKey.Access, + version: 1, + }, ); diff --git a/app/store/chat.ts b/app/store/chat.ts index 20603fe48b0..9b603902050 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -18,6 +18,7 @@ import { ChatControllerPool } from "../client/controller"; import { prettyObject } from "../utils/format"; import { estimateTokenLength } from "../utils/token"; import { nanoid } from "nanoid"; +import { createPersistStore } from "../utils/store"; export type ChatMessage = RequestMessage & { date: string; @@ -140,12 +141,22 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { return output; } -export const useChatStore = create()( - persist( - (set, get) => ({ - sessions: [createEmptySession()], - currentSessionIndex: 0, +const DEFAULT_CHAT_STATE = { + sessions: [createEmptySession()], + currentSessionIndex: 0, +}; +export const useChatStore = createPersistStore( + DEFAULT_CHAT_STATE, + (set, _get) => { + function get() { + return { + ..._get(), + ...methods, + }; + } + + const methods = { clearSessions() { set(() => ({ sessions: [createEmptySession()], @@ -184,7 +195,7 @@ export const useChatStore = create()( }); }, - newSession(mask) { + newSession(mask: Mask) { const session = createEmptySession(); if (mask) { @@ -207,14 +218,14 @@ export const useChatStore = create()( })); }, - nextSession(delta) { + nextSession(delta: number) { const n = get().sessions.length; const limit = (x: number) => (x + n) % n; const i = get().currentSessionIndex; get().selectSession(limit(i + delta)); }, - deleteSession(index) { + deleteSession(index: number) { const deletingLastSession = get().sessions.length === 1; const deletedSession = get().sessions.at(index); @@ -271,7 +282,7 @@ export const useChatStore = create()( return session; }, - onNewMessage(message) { + onNewMessage(message: ChatMessage) { get().updateCurrentSession((session) => { session.messages = session.messages.concat(); session.lastUpdate = Date.now(); @@ -280,7 +291,7 @@ export const useChatStore = create()( get().summarizeSession(); }, - async onUserInput(content) { + async onUserInput(content: string) { const session = get().currentSession(); const modelConfig = session.mask.modelConfig; @@ -580,14 +591,14 @@ export const useChatStore = create()( } }, - updateStat(message) { + updateStat(message: ChatMessage) { get().updateCurrentSession((session) => { session.stat.charCount += message.content.length; // TODO: should update chat count and word count }); }, - updateCurrentSession(updater) { + updateCurrentSession(updater: (session: ChatSession) => void) { const sessions = get().sessions; const index = get().currentSessionIndex; updater(sessions[index]); @@ -598,56 +609,60 @@ export const useChatStore = create()( localStorage.clear(); location.reload(); }, - }), - { - name: StoreKey.Chat, - version: 3.1, - migrate(persistedState, version) { - const state = persistedState as any; - const newState = JSON.parse(JSON.stringify(state)) as ChatStore; - - if (version < 2) { - newState.sessions = []; - - const oldSessions = state.sessions; - for (const oldSession of oldSessions) { - const newSession = createEmptySession(); - newSession.topic = oldSession.topic; - newSession.messages = [...oldSession.messages]; - newSession.mask.modelConfig.sendMemory = true; - newSession.mask.modelConfig.historyMessageCount = 4; - newSession.mask.modelConfig.compressMessageLengthThreshold = 1000; - newState.sessions.push(newSession); - } - } - - if (version < 3) { - // migrate id to nanoid - newState.sessions.forEach((s) => { - s.id = nanoid(); - s.messages.forEach((m) => (m.id = nanoid())); - }); + }; + + return methods; + }, + { + name: StoreKey.Chat, + version: 3.1, + migrate(persistedState, version) { + const state = persistedState as any; + const newState = JSON.parse( + JSON.stringify(state), + ) as typeof DEFAULT_CHAT_STATE; + + if (version < 2) { + newState.sessions = []; + + const oldSessions = state.sessions; + for (const oldSession of oldSessions) { + const newSession = createEmptySession(); + newSession.topic = oldSession.topic; + newSession.messages = [...oldSession.messages]; + newSession.mask.modelConfig.sendMemory = true; + newSession.mask.modelConfig.historyMessageCount = 4; + newSession.mask.modelConfig.compressMessageLengthThreshold = 1000; + newState.sessions.push(newSession); } + } - // Enable `enableInjectSystemPrompts` attribute for old sessions. - // Resolve issue of old sessions not automatically enabling. - if (version < 3.1) { - newState.sessions.forEach((s) => { - if ( - // Exclude those already set by user - !s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts") - ) { - // Because users may have changed this configuration, - // the user's current configuration is used instead of the default - const config = useAppConfig.getState(); - s.mask.modelConfig.enableInjectSystemPrompts = - config.modelConfig.enableInjectSystemPrompts; - } - }); - } + if (version < 3) { + // migrate id to nanoid + newState.sessions.forEach((s) => { + s.id = nanoid(); + s.messages.forEach((m) => (m.id = nanoid())); + }); + } + + // Enable `enableInjectSystemPrompts` attribute for old sessions. + // Resolve issue of old sessions not automatically enabling. + if (version < 3.1) { + newState.sessions.forEach((s) => { + if ( + // Exclude those already set by user + !s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts") + ) { + // Because users may have changed this configuration, + // the user's current configuration is used instead of the default + const config = useAppConfig.getState(); + s.mask.modelConfig.enableInjectSystemPrompts = + config.modelConfig.enableInjectSystemPrompts; + } + }); + } - return newState; - }, + return newState as any; }, - ), + }, ); diff --git a/app/store/config.ts b/app/store/config.ts index 7070ea05e32..5fa136a06fd 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -3,6 +3,7 @@ import { persist } from "zustand/middleware"; import { LLMModel } from "../client/api"; import { getClientConfig } from "../config/client"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant"; +import { createPersistStore } from "../utils/store"; export type ModelType = (typeof DEFAULT_MODELS)[number]["name"]; @@ -21,6 +22,8 @@ export enum Theme { } export const DEFAULT_CONFIG = { + lastUpdate: Date.now(), // timestamp, to merge state + submitKey: SubmitKey.CtrlEnter as SubmitKey, avatar: "1f603", fontSize: 14, @@ -55,13 +58,6 @@ export const DEFAULT_CONFIG = { export type ChatConfig = typeof DEFAULT_CONFIG; -export type ChatConfigStore = ChatConfig & { - reset: () => void; - update: (updater: (config: ChatConfig) => void) => void; - mergeModels: (newModels: LLMModel[]) => void; - allModels: () => LLMModel[]; -}; - export type ModelConfig = ChatConfig["modelConfig"]; export function limitNumber( @@ -98,85 +94,80 @@ export const ModalConfigValidator = { }, }; -export const useAppConfig = create()( - persist( - (set, get) => ({ - ...DEFAULT_CONFIG, - - reset() { - set(() => ({ ...DEFAULT_CONFIG })); - }, - - update(updater) { - const config = { ...get() }; - updater(config); - set(() => config); - }, - - mergeModels(newModels) { - if (!newModels || newModels.length === 0) { - return; - } - - const oldModels = get().models; - const modelMap: Record = {}; - - for (const model of oldModels) { - model.available = false; - modelMap[model.name] = model; - } - - for (const model of newModels) { - model.available = true; - modelMap[model.name] = model; - } - - set(() => ({ - models: Object.values(modelMap), - })); - }, - - allModels() { - const customModels = get() - .customModels.split(",") - .filter((v) => !!v && v.length > 0) - .map((m) => ({ name: m, available: true })); - - const models = get().models.concat(customModels); - return models; - }, - }), - { - name: StoreKey.Config, - version: 3.7, - migrate(persistedState, version) { - const state = persistedState as ChatConfig; - - if (version < 3.4) { - state.modelConfig.sendMemory = true; - state.modelConfig.historyMessageCount = 4; - state.modelConfig.compressMessageLengthThreshold = 1000; - state.modelConfig.frequency_penalty = 0; - state.modelConfig.top_p = 1; - state.modelConfig.template = DEFAULT_INPUT_TEMPLATE; - state.dontShowMaskSplashScreen = false; - state.hideBuiltinMasks = false; - } - - if (version < 3.5) { - state.customModels = "claude,claude-100k"; - } - - if (version < 3.6) { - state.modelConfig.enableInjectSystemPrompts = true; - } - - if (version < 3.7) { - state.enableAutoGenerateTitle = true; - } - - return state as any; - }, +export const useAppConfig = createPersistStore( + { ...DEFAULT_CONFIG }, + (set, get) => ({ + reset() { + set(() => ({ ...DEFAULT_CONFIG })); + }, + + mergeModels(newModels: LLMModel[]) { + if (!newModels || newModels.length === 0) { + return; + } + + const oldModels = get().models; + const modelMap: Record = {}; + + for (const model of oldModels) { + model.available = false; + modelMap[model.name] = model; + } + + for (const model of newModels) { + model.available = true; + modelMap[model.name] = model; + } + + set(() => ({ + models: Object.values(modelMap), + })); }, - ), + + allModels() { + const customModels = get() + .customModels.split(",") + .filter((v) => !!v && v.length > 0) + .map((m) => ({ name: m, available: true })); + + const models = get().models.concat(customModels); + return models; + }, + }), + { + name: StoreKey.Config, + version: 3.8, + migrate(persistedState, version) { + const state = persistedState as ChatConfig; + + if (version < 3.4) { + state.modelConfig.sendMemory = true; + state.modelConfig.historyMessageCount = 4; + state.modelConfig.compressMessageLengthThreshold = 1000; + state.modelConfig.frequency_penalty = 0; + state.modelConfig.top_p = 1; + state.modelConfig.template = DEFAULT_INPUT_TEMPLATE; + state.dontShowMaskSplashScreen = false; + state.hideBuiltinMasks = false; + } + + if (version < 3.5) { + state.customModels = "claude,claude-100k"; + } + + if (version < 3.6) { + state.modelConfig.enableInjectSystemPrompts = true; + } + + if (version < 3.7) { + state.enableAutoGenerateTitle = true; + } + + if (version < 3.8) { + state.lastUpdate = Date.now(); + } + + return state as any; + }, + }, ); diff --git a/app/store/mask.ts b/app/store/mask.ts index 02132b77d92..82c41fece86 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -1,11 +1,10 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import { BUILTIN_MASKS } from "../masks"; import { getLang, Lang } from "../locales"; import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { ModelConfig, useAppConfig } from "./config"; import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; +import { createPersistStore } from "../utils/store"; export type Mask = { id: string; @@ -25,14 +24,6 @@ export const DEFAULT_MASK_STATE = { }; export type MaskState = typeof DEFAULT_MASK_STATE; -type MaskStore = MaskState & { - create: (mask?: Partial) => Mask; - update: (id: string, updater: (mask: Mask) => void) => void; - delete: (id: string) => void; - search: (text: string) => Mask[]; - get: (id?: string) => Mask | null; - getAll: () => Mask[]; -}; export const DEFAULT_MASK_AVATAR = "gpt-bot"; export const createEmptyMask = () => @@ -46,89 +37,92 @@ export const createEmptyMask = () => lang: getLang(), builtin: false, createdAt: Date.now(), - } as Mask); + }) as Mask; + +export const useMaskStore = createPersistStore( + { ...DEFAULT_MASK_STATE }, -export const useMaskStore = create()( - persist( - (set, get) => ({ - ...DEFAULT_MASK_STATE, + (set, get) => ({ + ...DEFAULT_MASK_STATE, - create(mask) { - const masks = get().masks; - const id = nanoid(); - masks[id] = { - ...createEmptyMask(), - ...mask, - id, - builtin: false, - }; + create(mask?: Partial) { + const masks = get().masks; + const id = nanoid(); + masks[id] = { + ...createEmptyMask(), + ...mask, + id, + builtin: false, + }; - set(() => ({ masks })); + set(() => ({ masks })); + get().markUpdate(); - return masks[id]; - }, - update(id, updater) { - const masks = get().masks; - const mask = masks[id]; - if (!mask) return; - const updateMask = { ...mask }; - updater(updateMask); - masks[id] = updateMask; - set(() => ({ masks })); - }, - delete(id) { - const masks = get().masks; - delete masks[id]; - set(() => ({ masks })); - }, + return masks[id]; + }, + updateMask(id: string, updater: (mask: Mask) => void) { + const masks = get().masks; + const mask = masks[id]; + if (!mask) return; + const updateMask = { ...mask }; + updater(updateMask); + masks[id] = updateMask; + set(() => ({ masks })); + get().markUpdate(); + }, + delete(id: string) { + const masks = get().masks; + delete masks[id]; + set(() => ({ masks })); + get().markUpdate(); + }, - get(id) { - return get().masks[id ?? 1145141919810]; - }, - getAll() { - const userMasks = Object.values(get().masks).sort( - (a, b) => b.createdAt - a.createdAt, - ); - const config = useAppConfig.getState(); - if (config.hideBuiltinMasks) return userMasks; - const buildinMasks = BUILTIN_MASKS.map( - (m) => - ({ - ...m, - modelConfig: { - ...config.modelConfig, - ...m.modelConfig, - }, - } as Mask), - ); - return userMasks.concat(buildinMasks); - }, - search(text) { - return Object.values(get().masks); - }, - }), - { - name: StoreKey.Mask, - version: 3.1, + get(id?: string) { + return get().masks[id ?? 1145141919810]; + }, + getAll() { + const userMasks = Object.values(get().masks).sort( + (a, b) => b.createdAt - a.createdAt, + ); + const config = useAppConfig.getState(); + if (config.hideBuiltinMasks) return userMasks; + const buildinMasks = BUILTIN_MASKS.map( + (m) => + ({ + ...m, + modelConfig: { + ...config.modelConfig, + ...m.modelConfig, + }, + }) as Mask, + ); + return userMasks.concat(buildinMasks); + }, + search(text: string) { + return Object.values(get().masks); + }, + }), + { + name: StoreKey.Mask, + version: 3.1, - migrate(state, version) { - const newState = JSON.parse(JSON.stringify(state)) as MaskState; + migrate(state, version) { + const newState = JSON.parse(JSON.stringify(state)) as MaskState; - // migrate mask id to nanoid - if (version < 3) { - Object.values(newState.masks).forEach((m) => (m.id = nanoid())); - } + // migrate mask id to nanoid + if (version < 3) { + Object.values(newState.masks).forEach((m) => (m.id = nanoid())); + } - if (version < 3.1) { - const updatedMasks: Record = {}; - Object.values(newState.masks).forEach((m) => { - updatedMasks[m.id] = m; - }); - newState.masks = updatedMasks; - } + if (version < 3.1) { + const updatedMasks: Record = {}; + Object.values(newState.masks).forEach((m) => { + updatedMasks[m.id] = m; + }); + newState.masks = updatedMasks; + } - return newState as any; - }, + return newState as any; }, - ), + }, ); diff --git a/app/store/prompt.ts b/app/store/prompt.ts index e743f914c8c..c6cff1a6532 100644 --- a/app/store/prompt.ts +++ b/app/store/prompt.ts @@ -1,9 +1,8 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import Fuse from "fuse.js"; import { getLang } from "../locales"; import { StoreKey } from "../constant"; import { nanoid } from "nanoid"; +import { createPersistStore } from "../utils/store"; export interface Prompt { id: string; @@ -13,19 +12,6 @@ export interface Prompt { createdAt: number; } -export interface PromptStore { - counter: number; - prompts: Record; - - add: (prompt: Prompt) => string; - get: (id: string) => Prompt | undefined; - remove: (id: string) => void; - search: (text: string) => Prompt[]; - update: (id: string, updater: (prompt: Prompt) => void) => void; - - getUserPrompts: () => Prompt[]; -} - export const SearchService = { ready: false, builtinEngine: new Fuse([], { keys: ["title"] }), @@ -62,130 +48,136 @@ export const SearchService = { }, }; -export const usePromptStore = create()( - persist( - (set, get) => ({ - counter: 0, - latestId: 0, - prompts: {}, - - add(prompt) { - const prompts = get().prompts; - prompt.id = nanoid(); - prompt.isUser = true; - prompt.createdAt = Date.now(); - prompts[prompt.id] = prompt; - - set(() => ({ - latestId: prompt.id!, - prompts: prompts, - })); - - return prompt.id!; - }, - - get(id) { - const targetPrompt = get().prompts[id]; - - if (!targetPrompt) { - return SearchService.builtinPrompts.find((v) => v.id === id); - } +export const usePromptStore = createPersistStore( + { + counter: 0, + prompts: {} as Record, + }, - return targetPrompt; - }, - - remove(id) { - const prompts = get().prompts; - delete prompts[id]; - SearchService.remove(id); - - set(() => ({ - prompts, - counter: get().counter + 1, - })); - }, - - getUserPrompts() { - const userPrompts = Object.values(get().prompts ?? {}); - userPrompts.sort((a, b) => - b.id && a.id ? b.createdAt - a.createdAt : 0, - ); - return userPrompts; - }, - - update(id, updater) { - const prompt = get().prompts[id] ?? { - title: "", - content: "", - id, - }; - - SearchService.remove(id); - updater(prompt); - const prompts = get().prompts; - prompts[id] = prompt; - set(() => ({ prompts })); - SearchService.add(prompt); - }, - - search(text) { - if (text.length === 0) { - // return all rompts - return get().getUserPrompts().concat(SearchService.builtinPrompts); - } - return SearchService.search(text) as Prompt[]; - }, - }), - { - name: StoreKey.Prompt, - version: 3, - - migrate(state, version) { - const newState = JSON.parse(JSON.stringify(state)) as PromptStore; - - if (version < 3) { - Object.values(newState.prompts).forEach((p) => (p.id = nanoid())); + (set, get) => ({ + add(prompt: Prompt) { + const prompts = get().prompts; + prompt.id = nanoid(); + prompt.isUser = true; + prompt.createdAt = Date.now(); + prompts[prompt.id] = prompt; + + set(() => ({ + prompts: prompts, + })); + + return prompt.id!; + }, + + get(id: string) { + const targetPrompt = get().prompts[id]; + + if (!targetPrompt) { + return SearchService.builtinPrompts.find((v) => v.id === id); + } + + return targetPrompt; + }, + + remove(id: string) { + const prompts = get().prompts; + delete prompts[id]; + + Object.entries(prompts).some(([key, prompt]) => { + if (prompt.id === id) { + delete prompts[key]; + return true; } + return false; + }); - return newState; - }, - - onRehydrateStorage(state) { - const PROMPT_URL = "./prompts.json"; - - type PromptList = Array<[string, string]>; - - fetch(PROMPT_URL) - .then((res) => res.json()) - .then((res) => { - let fetchPrompts = [res.en, res.cn]; - if (getLang() === "cn") { - fetchPrompts = fetchPrompts.reverse(); - } - const builtinPrompts = fetchPrompts.map( - (promptList: PromptList) => { - return promptList.map( - ([title, content]) => - ({ - id: nanoid(), - title, - content, - createdAt: Date.now(), - } as Prompt), - ); - }, - ); + SearchService.remove(id); + + set(() => ({ + prompts, + counter: get().counter + 1, + })); + }, + + getUserPrompts() { + const userPrompts = Object.values(get().prompts ?? {}); + userPrompts.sort((a, b) => + b.id && a.id ? b.createdAt - a.createdAt : 0, + ); + return userPrompts; + }, + + updatePrompt(id: string, updater: (prompt: Prompt) => void) { + const prompt = get().prompts[id] ?? { + title: "", + content: "", + id, + }; + + SearchService.remove(id); + updater(prompt); + const prompts = get().prompts; + prompts[id] = prompt; + set(() => ({ prompts })); + SearchService.add(prompt); + }, + + search(text: string) { + if (text.length === 0) { + // return all rompts + return this.getUserPrompts().concat(SearchService.builtinPrompts); + } + return SearchService.search(text) as Prompt[]; + }, + }), + { + name: StoreKey.Prompt, + version: 3, + + migrate(state, version) { + const newState = JSON.parse(JSON.stringify(state)) as { + prompts: Record; + }; - const userPrompts = - usePromptStore.getState().getUserPrompts() ?? []; + if (version < 3) { + Object.values(newState.prompts).forEach((p) => (p.id = nanoid())); + } + + return newState as any; + }, - const allPromptsForSearch = builtinPrompts - .reduce((pre, cur) => pre.concat(cur), []) - .filter((v) => !!v.title && !!v.content); - SearchService.count.builtin = res.en.length + res.cn.length; - SearchService.init(allPromptsForSearch, userPrompts); + onRehydrateStorage(state) { + const PROMPT_URL = "./prompts.json"; + + type PromptList = Array<[string, string]>; + + fetch(PROMPT_URL) + .then((res) => res.json()) + .then((res) => { + let fetchPrompts = [res.en, res.cn]; + if (getLang() === "cn") { + fetchPrompts = fetchPrompts.reverse(); + } + const builtinPrompts = fetchPrompts.map((promptList: PromptList) => { + return promptList.map( + ([title, content]) => + ({ + id: nanoid(), + title, + content, + createdAt: Date.now(), + }) as Prompt, + ); }); - }, + + const userPrompts = usePromptStore.getState().getUserPrompts() ?? []; + + const allPromptsForSearch = builtinPrompts + .reduce((pre, cur) => pre.concat(cur), []) + .filter((v) => !!v.title && !!v.content); + SearchService.count.builtin = res.en.length + res.cn.length; + SearchService.init(allPromptsForSearch, userPrompts); + }); }, - ), + }, ); diff --git a/app/store/sync.ts b/app/store/sync.ts index 1a111f75a47..fc6028098d5 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -1,7 +1,15 @@ import { Updater } from "../typing"; -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import { StoreKey } from "../constant"; +import { createPersistStore } from "../utils/store"; +import { + AppState, + getLocalAppState, + mergeAppState, + setLocalAppState, +} from "../utils/sync"; +import { downloadAs, readFromFile } from "../utils"; +import { showToast } from "../components/ui-lib"; +import Locale from "../locales"; export interface WebDavConfig { server: string; @@ -20,68 +28,86 @@ export interface SyncStore { headers: () => { Authorization: string }; } -const FILE = { - root: "/chatgpt-next-web/", -}; - -export const useSyncStore = create()( - persist( - (set, get) => ({ - webDavConfig: { - server: "", - username: "", - password: "", - }, - - lastSyncTime: 0, - - update(updater) { - const config = { ...get().webDavConfig }; - updater(config); - set({ webDavConfig: config }); - }, - - async check() { - try { - const res = await fetch(this.path(""), { - method: "PROFIND", - headers: this.headers(), - }); - console.log(res); - return res.status === 207; - } catch (e) { - console.error("[Sync] ", e); - return false; - } - }, - - path(path: string) { - let url = get().webDavConfig.server; - - if (!url.endsWith("/")) { - url += "/"; - } - - if (path.startsWith("/")) { - path = path.slice(1); - } - - return url + path; - }, - - headers() { - const auth = btoa( - [get().webDavConfig.username, get().webDavConfig.password].join(":"), - ); - - return { - Authorization: `Basic ${auth}`, - }; - }, - }), - { - name: StoreKey.Sync, - version: 1, +export const useSyncStore = createPersistStore( + { + webDavConfig: { + server: "", + username: "", + password: "", }, - ), + + lastSyncTime: 0, + }, + (set, get) => ({ + webDavConfig: { + server: "", + username: "", + password: "", + }, + + lastSyncTime: 0, + + export() { + const state = getLocalAppState(); + const fileName = `Backup-${new Date().toLocaleString()}.json`; + downloadAs(JSON.stringify(state), fileName); + }, + + async import() { + const rawContent = await readFromFile(); + + try { + const remoteState = JSON.parse(rawContent) as AppState; + const localState = getLocalAppState(); + mergeAppState(localState, remoteState); + setLocalAppState(localState); + location.reload(); + } catch (e) { + console.error("[Import]", e); + showToast(Locale.Settings.Sync.ImportFailed); + } + }, + + async check() { + try { + const res = await fetch(this.path(""), { + method: "PROFIND", + headers: this.headers(), + }); + console.log(res); + return res.status === 207; + } catch (e) { + console.error("[Sync] ", e); + return false; + } + }, + + path(path: string) { + let url = get().webDavConfig.server; + + if (!url.endsWith("/")) { + url += "/"; + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + return url + path; + }, + + headers() { + const auth = btoa( + [get().webDavConfig.username, get().webDavConfig.password].join(":"), + ); + + return { + Authorization: `Basic ${auth}`, + }; + }, + }), + { + name: StoreKey.Sync, + version: 1, + }, ); diff --git a/app/store/update.ts b/app/store/update.ts index dd4d3c7246e..42b86586c62 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -1,24 +1,7 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; import { api } from "../client/api"; import { getClientConfig } from "../config/client"; - -export interface UpdateStore { - versionType: "date" | "tag"; - lastUpdate: number; - version: string; - remoteVersion: string; - - used?: number; - subscription?: number; - lastUpdateUsage: number; - - getLatestVersion: (force?: boolean) => Promise; - updateUsage: (force?: boolean) => Promise; - - formatVersion: (version: string) => string; -} +import { createPersistStore } from "../utils/store"; const ONE_MINUTE = 60 * 1000; @@ -35,7 +18,9 @@ function formatVersionDate(t: string) { ].join(""); } -async function getVersion(type: "date" | "tag") { +type VersionType = "date" | "tag"; + +async function getVersion(type: VersionType) { if (type === "date") { const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as { commit: { @@ -55,75 +40,76 @@ async function getVersion(type: "date" | "tag") { } } -export const useUpdateStore = create()( - persist( - (set, get) => ({ - versionType: "tag", - lastUpdate: 0, - version: "unknown", - remoteVersion: "", - - lastUpdateUsage: 0, +export const useUpdateStore = createPersistStore( + { + versionType: "tag" as VersionType, + lastUpdate: 0, + version: "unknown", + remoteVersion: "", + used: 0, + subscription: 0, + + lastUpdateUsage: 0, + }, + (set, get) => ({ + formatVersion(version: string) { + if (get().versionType === "date") { + version = formatVersionDate(version); + } + return version; + }, - formatVersion(version: string) { - if (get().versionType === "date") { - version = formatVersionDate(version); - } - return version; - }, + async getLatestVersion(force = false) { + const versionType = get().versionType; + let version = + versionType === "date" + ? getClientConfig()?.commitDate + : getClientConfig()?.version; - async getLatestVersion(force = false) { - const versionType = get().versionType; - let version = - versionType === "date" - ? getClientConfig()?.commitDate - : getClientConfig()?.version; + set(() => ({ version })); - set(() => ({ version })); + const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE; + if (!force && !shouldCheck) return; - const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE; - if (!force && !shouldCheck) return; + set(() => ({ + lastUpdate: Date.now(), + })); + try { + const remoteId = await getVersion(versionType); set(() => ({ - lastUpdate: Date.now(), + remoteVersion: remoteId, })); + console.log("[Got Upstream] ", remoteId); + } catch (error) { + console.error("[Fetch Upstream Commit Id]", error); + } + }, - try { - const remoteId = await getVersion(versionType); - set(() => ({ - remoteVersion: remoteId, - })); - console.log("[Got Upstream] ", remoteId); - } catch (error) { - console.error("[Fetch Upstream Commit Id]", error); - } - }, + async updateUsage(force = false) { + const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; + if (!overOneMinute && !force) return; - async updateUsage(force = false) { - const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; - if (!overOneMinute && !force) return; + set(() => ({ + lastUpdateUsage: Date.now(), + })); - set(() => ({ - lastUpdateUsage: Date.now(), - })); + try { + const usage = await api.llm.usage(); - try { - const usage = await api.llm.usage(); - - if (usage) { - set(() => ({ - used: usage.used, - subscription: usage.total, - })); - } - } catch (e) { - console.error((e as Error).message); + if (usage) { + set(() => ({ + used: usage.used, + subscription: usage.total, + })); } - }, - }), - { - name: StoreKey.Update, - version: 1, + } catch (e) { + console.error((e as Error).message); + } }, - ), + }), + { + name: StoreKey.Update, + version: 1, + }, ); diff --git a/app/utils/clone.ts b/app/utils/clone.ts new file mode 100644 index 00000000000..2958b6b9c35 --- /dev/null +++ b/app/utils/clone.ts @@ -0,0 +1,3 @@ +export function deepClone(obj: T) { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/app/utils/store.ts b/app/utils/store.ts new file mode 100644 index 00000000000..cd151dc4925 --- /dev/null +++ b/app/utils/store.ts @@ -0,0 +1,55 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { Updater } from "../typing"; +import { deepClone } from "./clone"; + +type SecondParam = T extends ( + _f: infer _F, + _s: infer S, + ...args: infer _U +) => any + ? S + : never; + +type MakeUpdater = { + lastUpdateTime: number; + + markUpdate: () => void; + update: Updater; +}; + +type SetStoreState = ( + partial: T | Partial | ((state: T) => T | Partial), + replace?: boolean | undefined, +) => void; + +export function createPersistStore( + defaultState: T, + methods: ( + set: SetStoreState>, + get: () => T & MakeUpdater, + ) => M, + persistOptions: SecondParam>>, +) { + return create>()( + persist((set, get) => { + return { + ...defaultState, + ...methods(set as any, get), + + lastUpdateTime: 0, + markUpdate() { + set({ lastUpdateTime: Date.now() } as Partial< + T & M & MakeUpdater + >); + }, + update(updater) { + const state = deepClone(get()); + updater(state); + get().markUpdate(); + set(state); + }, + }; + }, persistOptions), + ); +} diff --git a/app/utils/sync.ts b/app/utils/sync.ts new file mode 100644 index 00000000000..ab1f1f44918 --- /dev/null +++ b/app/utils/sync.ts @@ -0,0 +1,162 @@ +import { + ChatSession, + useAccessStore, + useAppConfig, + useChatStore, +} from "../store"; +import { useMaskStore } from "../store/mask"; +import { usePromptStore } from "../store/prompt"; +import { StoreKey } from "../constant"; +import { merge } from "./merge"; + +type NonFunctionKeys = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K; +}[keyof T]; +type NonFunctionFields = Pick>; + +export function getNonFunctionFileds(obj: T) { + const ret: any = {}; + + Object.entries(obj).map(([k, v]) => { + if (typeof v !== "function") { + ret[k] = v; + } + }); + + return ret as NonFunctionFields; +} + +export type GetStoreState = T extends { getState: () => infer U } + ? NonFunctionFields + : never; + +const LocalStateSetters = { + [StoreKey.Chat]: useChatStore.setState, + [StoreKey.Access]: useAccessStore.setState, + [StoreKey.Config]: useAppConfig.setState, + [StoreKey.Mask]: useMaskStore.setState, + [StoreKey.Prompt]: usePromptStore.setState, +} as const; + +const LocalStateGetters = { + [StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()), + [StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()), + [StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()), + [StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()), + [StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()), +} as const; + +export type AppState = { + [k in keyof typeof LocalStateGetters]: ReturnType< + (typeof LocalStateGetters)[k] + >; +}; + +type Merger = ( + localState: U, + remoteState: U, +) => U; + +type StateMerger = { + [K in keyof AppState]: Merger; +}; + +// we merge remote state to local state +const MergeStates: StateMerger = { + [StoreKey.Chat]: (localState, remoteState) => { + // merge sessions + const localSessions: Record = {}; + localState.sessions.forEach((s) => (localSessions[s.id] = s)); + + remoteState.sessions.forEach((remoteSession) => { + const localSession = localSessions[remoteSession.id]; + if (!localSession) { + // if remote session is new, just merge it + localState.sessions.push(remoteSession); + } else { + // if both have the same session id, merge the messages + const localMessageIds = new Set(localSession.messages.map((v) => v.id)); + remoteSession.messages.forEach((m) => { + if (!localMessageIds.has(m.id)) { + localSession.messages.push(m); + } + }); + + // sort local messages with date field in asc order + localSession.messages.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + } + }); + + // sort local sessions with date field in desc order + localState.sessions.sort( + (a, b) => + new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(), + ); + + return localState; + }, + [StoreKey.Prompt]: (localState, remoteState) => { + localState.prompts = { + ...remoteState.prompts, + ...localState.prompts, + }; + return localState; + }, + [StoreKey.Mask]: (localState, remoteState) => { + localState.masks = { + ...remoteState.masks, + ...localState.masks, + }; + return localState; + }, + [StoreKey.Config]: mergeWithUpdate, + [StoreKey.Access]: mergeWithUpdate, +}; + +export function getLocalAppState() { + const appState = Object.fromEntries( + Object.entries(LocalStateGetters).map(([key, getter]) => { + return [key, getter()]; + }), + ) as AppState; + + return appState; +} + +export function setLocalAppState(appState: AppState) { + Object.entries(LocalStateSetters).forEach(([key, setter]) => { + setter(appState[key as keyof AppState]); + }); +} + +export function mergeAppState(localState: AppState, remoteState: AppState) { + Object.keys(localState).forEach((k: string) => { + const key = k as T; + const localStoreState = localState[key]; + const remoteStoreState = remoteState[key]; + MergeStates[key](localStoreState, remoteStoreState); + }); + + return localState; +} + +/** + * Merge state with `lastUpdateTime`, older state will be override + */ +export function mergeWithUpdate( + localState: T, + remoteState: T, +) { + const localUpdateTime = localState.lastUpdateTime ?? 0; + const remoteUpdateTime = localState.lastUpdateTime ?? 1; + + if (localUpdateTime < remoteUpdateTime) { + merge(remoteState, localState); + return { ...remoteState }; + } else { + merge(localState, remoteState); + return { ...localState }; + } +} From 5dced2808802fb015e0c5e6e70fbdb9d794bd183 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 11 Sep 2023 00:22:14 +0800 Subject: [PATCH 067/202] fixup: add en locales --- app/locales/en.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/locales/en.ts b/app/locales/en.ts index 981357274d8..e3129578728 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -180,6 +180,14 @@ const en: LocaleType = { Title: "Auto Generate Title", SubTitle: "Generate a suitable title based on the conversation content", }, + Sync: { + LastUpdate: "Last Update", + LocalState: "Local Data", + Overview: (overview: any) => { + return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`; + }, + ImportFailed: "Failed to import from file", + }, Mask: { Splash: { Title: "Mask Splash Screen", @@ -355,6 +363,9 @@ const en: LocaleType = { Close: "Close", Create: "Create", Edit: "Edit", + Export: "Export", + Import: "Import", + Sync: "Sync", }, Exporter: { Model: "Model", From c73a91a0f5d90a3a4b341feba3aff30c7aaed4b9 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 11 Sep 2023 00:24:05 +0800 Subject: [PATCH 068/202] fixup: fix type errors --- app/store/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 9b603902050..269cc4a33c9 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -195,7 +195,7 @@ export const useChatStore = createPersistStore( }); }, - newSession(mask: Mask) { + newSession(mask?: Mask) { const session = createEmptySession(); if (mask) { From 415e9dc9131594adec4af5510cd7379fa46a258e Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 11 Sep 2023 00:34:51 +0800 Subject: [PATCH 069/202] fixup: minor sync fixup --- app/components/settings.tsx | 3 ++- app/store/config.ts | 2 -- app/store/mask.ts | 2 -- app/store/sync.ts | 20 +------------------- 4 files changed, 3 insertions(+), 24 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 19c54515f1b..4106c97042a 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -22,6 +22,7 @@ import { Popover, Select, showConfirm, + showToast, } from "./ui-lib"; import { ModelConfigList } from "./model-config"; @@ -275,7 +276,7 @@ function SyncItems() { icon={} text={Locale.UI.Sync} onClick={() => { - syncStore.check().then(console.log); + showToast(Locale.WIP); }} /> diff --git a/app/store/config.ts b/app/store/config.ts index 5fa136a06fd..b0131954296 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -1,5 +1,3 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; import { LLMModel } from "../client/api"; import { getClientConfig } from "../config/client"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant"; diff --git a/app/store/mask.ts b/app/store/mask.ts index 82c41fece86..dfd4089b757 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -43,8 +43,6 @@ export const useMaskStore = createPersistStore( { ...DEFAULT_MASK_STATE }, (set, get) => ({ - ...DEFAULT_MASK_STATE, - create(mask?: Partial) { const masks = get().masks; const id = nanoid(); diff --git a/app/store/sync.ts b/app/store/sync.ts index fc6028098d5..466a98cf5c3 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -17,17 +17,6 @@ export interface WebDavConfig { password: string; } -export interface SyncStore { - webDavConfig: WebDavConfig; - lastSyncTime: number; - - update: Updater; - check: () => Promise; - - path: (path: string) => string; - headers: () => { Authorization: string }; -} - export const useSyncStore = createPersistStore( { webDavConfig: { @@ -39,18 +28,11 @@ export const useSyncStore = createPersistStore( lastSyncTime: 0, }, (set, get) => ({ - webDavConfig: { - server: "", - username: "", - password: "", - }, - - lastSyncTime: 0, - export() { const state = getLocalAppState(); const fileName = `Backup-${new Date().toLocaleString()}.json`; downloadAs(JSON.stringify(state), fileName); + set({ lastSyncTime: Date.now() }); }, async import() { From 57158890c3640efb5254a7b4e66aad7d534ea5fc Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 11 Sep 2023 00:39:56 +0800 Subject: [PATCH 070/202] fixup --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 4106c97042a..9de603bb30e 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -270,7 +270,7 @@ function SyncItems() { } From 605dd72354764ea2f07753130cbafe1d55b50d97 Mon Sep 17 00:00:00 2001 From: B0zal Date: Mon, 11 Sep 2023 08:49:08 +0700 Subject: [PATCH 071/202] [+] CodeQL Report Fix log injection vulnerability in useSyncStore Severity : High Sanitize the 'res' object before logging it in the 'check' method of useSyncStore to prevent log injection attacks. The 'res' object is now sanitized by extracting only the necessary properties ('status', 'statusText', and 'headers') and logging the sanitized object instead. This ensures that only safe and expected data is logged, mitigating the risk of log injection vulnerabilities. --- app/store/sync.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/store/sync.ts b/app/store/sync.ts index 466a98cf5c3..502cf71cb12 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -56,7 +56,12 @@ export const useSyncStore = createPersistStore( method: "PROFIND", headers: this.headers(), }); - console.log(res); + const sanitizedRes = { + status: res.status, + statusText: res.statusText, + headers: res.headers, + }; + console.log(sanitizedRes); return res.status === 207; } catch (e) { console.error("[Sync] ", e); From e36abc3ac6c68a8f804af3d61ed8ce78e6d0af12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:31:50 +0000 Subject: [PATCH 072/202] chore(deps): bump emoji-picker-react from 4.4.8 to 4.5.1 Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.4.8 to 4.5.1. - [Release notes](https://github.com/ealush/emoji-picker-react/releases) - [Commits](https://github.com/ealush/emoji-picker-react/commits) --- updated-dependencies: - dependency-name: emoji-picker-react dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6610083bd0d..584640e0655 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@hello-pangea/dnd": "^16.3.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", - "emoji-picker-react": "^4.4.7", + "emoji-picker-react": "^4.5.1", "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", "mermaid": "^10.3.1", diff --git a/yarn.lock b/yarn.lock index cbce2ef174a..fc36251b806 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2762,10 +2762,10 @@ elkjs@^0.8.2: resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== -emoji-picker-react@^4.4.7: - version "4.4.8" - resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.4.8.tgz#cd18e942720d0d01e3d488a008f5e79aa315ec87" - integrity sha512-5bbj0PCvpjB64PZj31wZ35EoebF2mKoHqEEx9u2ZLghx7sGoD1MgyDhse851rqROypjhmK9IUY15QBa7mCLP0g== +emoji-picker-react@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195" + integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw== dependencies: clsx "^1.2.1" From 8469f448b50ad9e2db96ecc003a9bb7cdc8573da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:32:00 +0000 Subject: [PATCH 073/202] chore(deps): bump react-router-dom from 6.14.1 to 6.15.0 Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.14.1 to 6.15.0. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.15.0/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 6610083bd0d..46c3c16efec 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.7", - "react-router-dom": "^6.14.1", + "react-router-dom": "^6.15.0", "rehype-highlight": "^6.0.0", "rehype-katex": "^6.0.3", "remark-breaks": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index cbce2ef174a..2fa343ed325 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,10 +1221,10 @@ tiny-glob "^0.2.9" tslib "^2.4.0" -"@remix-run/router@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" - integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ== +"@remix-run/router@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc" + integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg== "@rushstack/eslint-patch@^1.1.3": version "1.2.0" @@ -5092,20 +5092,20 @@ react-redux@^8.1.1: react-is "^18.0.0" use-sync-external-store "^1.0.0" -react-router-dom@^6.14.1: - version "6.14.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" - integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw== +react-router-dom@^6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40" + integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ== dependencies: - "@remix-run/router" "1.7.1" - react-router "6.14.1" + "@remix-run/router" "1.8.0" + react-router "6.15.0" -react-router@6.14.1: - version "6.14.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810" - integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g== +react-router@6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8" + integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg== dependencies: - "@remix-run/router" "1.7.1" + "@remix-run/router" "1.8.0" react@^18.2.0: version "18.2.0" From 1bbf310c460f809f3344b29c71e38c4244bb9cdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:32:25 +0000 Subject: [PATCH 074/202] chore(deps-dev): bump typescript from 4.9.5 to 5.2.2 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.2.2. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.2.2) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6610083bd0d..3a04b7495ac 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "husky": "^8.0.0", "lint-staged": "^13.2.2", "prettier": "^3.0.2", - "typescript": "4.9.5", + "typescript": "5.2.2", "webpack": "^5.88.1" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index cbce2ef174a..bf69bb6f8e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5786,10 +5786,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== unbox-primitive@^1.0.2: version "1.0.2" From ff60ffca3e5483f8f474af1fa1d1d15fded4889a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:32:34 +0000 Subject: [PATCH 075/202] chore(deps-dev): bump eslint from 8.44.0 to 8.49.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.49.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.49.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 88 +++++++++++++++++++++++++--------------------------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 6610083bd0d..bba4d18aed5 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@types/react-katex": "^3.0.0", "@types/spark-md5": "^3.0.2", "cross-env": "^7.0.3", - "eslint": "^8.44.0", + "eslint": "^8.49.0", "eslint-config-next": "13.4.19", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/yarn.lock b/yarn.lock index cbce2ef174a..b8a546882f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1012,15 +1012,15 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724" - integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ== +"@eslint-community/regexpp@^4.6.1": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005" + integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg== -"@eslint/eslintrc@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" - integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1032,10 +1032,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.44.0": - version "8.44.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" - integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== +"@eslint/js@8.49.0": + version "8.49.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333" + integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w== "@fortaine/fetch-event-source@^3.0.6": version "3.0.6" @@ -1055,10 +1055,10 @@ redux "^4.2.1" use-memo-one "^1.1.3" -"@humanwhocodes/config-array@^0.11.10": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== +"@humanwhocodes/config-array@^0.11.11": + version "0.11.11" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" + integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1779,7 +1779,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3050,40 +3050,40 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" - integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" - integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.44.0: - version "8.44.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" - integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== +eslint@^8.49.0: + version "8.49.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42" + integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.1.0" - "@eslint/js" "8.44.0" - "@humanwhocodes/config-array" "^0.11.10" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "8.49.0" + "@humanwhocodes/config-array" "^0.11.11" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.1" - espree "^9.6.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3093,7 +3093,6 @@ eslint@^8.44.0: globals "^13.19.0" graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" @@ -3105,13 +3104,12 @@ eslint@^8.44.0: natural-compare "^1.4.0" optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" - integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" acorn-jsx "^5.3.2" @@ -3635,7 +3633,7 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5588,7 +5586,7 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== From b5e89d444055ee92b806855499f505f8b12bfa52 Mon Sep 17 00:00:00 2001 From: B0zal Date: Tue, 12 Sep 2023 06:56:55 +0700 Subject: [PATCH 076/202] [+] Updated Auth Page - Made changes to the Auth Page to reset the input field for the access code when the "Later" button is clicked. This ensures that only expected user is logged by entering access code or entering their OpenAI API Key, mitigating the risk of small bug issue --- app/components/auth.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 1ca83dcd314..9a5b0c655e6 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -15,6 +15,7 @@ export function AuthPage() { const access = useAccessStore(); const goHome = () => navigate(Path.Home); + const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string useEffect(() => { if (getClientConfig()?.isApp) { @@ -48,7 +49,10 @@ export function AuthPage() { type="primary" onClick={goHome} /> - + { + resetAccessCode(); + goHome(); + }} />
); From 6f83fbd21278c90cd978108abe54291c38ec10d7 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:51:02 +0800 Subject: [PATCH 077/202] feat: add webdav support --- 3 | 119 ++++++++++++++ app/api/cors/[...path]/route.ts | 44 +++++ app/components/settings.tsx | 279 +++++++++++++++++++++++++++----- app/constant.ts | 10 +- app/icons/cloud-fail.svg | 1 + app/icons/cloud-success.svg | 1 + app/icons/config.svg | 1 + app/icons/connection.svg | 1 + app/locales/cn.ts | 31 +++- app/locales/en.ts | 32 +++- app/store/sync.ts | 87 +++++----- app/utils/cloud/index.ts | 33 ++++ app/utils/cloud/upstash.ts | 39 +++++ app/utils/cloud/webdav.ts | 78 +++++++++ app/utils/cors.ts | 50 ++++++ next.config.mjs | 43 +++-- 16 files changed, 748 insertions(+), 101 deletions(-) create mode 100644 3 create mode 100644 app/api/cors/[...path]/route.ts create mode 100644 app/icons/cloud-fail.svg create mode 100644 app/icons/cloud-success.svg create mode 100644 app/icons/config.svg create mode 100644 app/icons/connection.svg create mode 100644 app/utils/cloud/index.ts create mode 100644 app/utils/cloud/upstash.ts create mode 100644 app/utils/cloud/webdav.ts create mode 100644 app/utils/cors.ts diff --git a/3 b/3 new file mode 100644 index 00000000000..371bd01ac26 --- /dev/null +++ b/3 @@ -0,0 +1,119 @@ +export const OWNER = "Yidadaa"; +export const REPO = "ChatGPT-Next-Web"; +export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; +export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; +export const UPDATE_URL = `${REPO_URL}#keep-updated`; +export const RELEASE_URL = `${REPO_URL}/releases`; +export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; +export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; +export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; +export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; + +export enum Path { + Home = "/", + Chat = "/chat", + Settings = "/settings", + NewChat = "/new-chat", + Masks = "/masks", + Auth = "/auth", +} + +export enum SlotID { + AppBody = "app-body", +} + +export enum FileName { + Masks = "masks.json", + Prompts = "prompts.json", +} + +export enum StoreKey { + Chat = "chat-next-web-store", + Access = "access-control", + Config = "app-config", + Mask = "mask-store", + Prompt = "prompt-store", + Update = "chat-update", + Sync = "sync", +} + +export const MAX_SIDEBAR_WIDTH = 500; +export const MIN_SIDEBAR_WIDTH = 230; +export const NARROW_SIDEBAR_WIDTH = 100; + +export const ACCESS_CODE_PREFIX = "nk-"; + +export const LAST_INPUT_KEY = "last-input"; +export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; + +export const STORAGE_KEY = "chatgpt-next-web"; + +export const REQUEST_TIMEOUT_MS = 60000; + +export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; + +export const OpenaiPath = { + ChatPath: "v1/chat/completions", + UsagePath: "dashboard/billing/usage", + SubsPath: "dashboard/billing/subscription", + ListModelPath: "v1/models", +}; + +export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang +export const DEFAULT_SYSTEM_TEMPLATE = ` +You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2021-09 +Current model: {{model}} +Current time: {{time}}`; + +export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; + +export const DEFAULT_MODELS = [ + { + name: "gpt-4", + available: true, + }, + { + name: "gpt-4-0314", + available: true, + }, + { + name: "gpt-4-0613", + available: true, + }, + { + name: "gpt-4-32k", + available: true, + }, + { + name: "gpt-4-32k-0314", + available: true, + }, + { + name: "gpt-4-32k-0613", + available: true, + }, + { + name: "gpt-3.5-turbo", + available: true, + }, + { + name: "gpt-3.5-turbo-0301", + available: true, + }, + { + name: "gpt-3.5-turbo-0613", + available: true, + }, + { + name: "gpt-3.5-turbo-16k", + available: true, + }, + { + name: "gpt-3.5-turbo-16k-0613", + available: true, + }, +] as const; + +export const CHAT_PAGE_SIZE = 15; +export const MAX_RENDER_MSG_COUNT = 45; diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts new file mode 100644 index 00000000000..c461d250b1d --- /dev/null +++ b/app/api/cors/[...path]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const [protocol, ...subpath] = params.path; + const targetUrl = `${protocol}://${subpath.join("/")}`; + + const method = req.headers.get("method") ?? undefined; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + console.log("[Any Proxy]", targetUrl); + + const fetchResult = fetch(targetUrl, fetchOptions); + + return fetchResult; +} + +export const GET = handle; +export const POST = handle; +export const PUT = handle; + +// nextjs dose not support those https methods, sucks +export const PROFIND = handle; +export const MKCOL = handle; + +export const runtime = "edge"; diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9de603bb30e..8e43e1d1aec 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -12,6 +12,12 @@ import EditIcon from "../icons/edit.svg"; import EyeIcon from "../icons/eye.svg"; import DownloadIcon from "../icons/download.svg"; import UploadIcon from "../icons/upload.svg"; +import ConfigIcon from "../icons/config.svg"; +import ConfirmIcon from "../icons/confirm.svg"; + +import ConnectionIcon from "../icons/connection.svg"; +import CloudSuccessIcon from "../icons/cloud-success.svg"; +import CloudFailIcon from "../icons/cloud-fail.svg"; import { Input, @@ -54,6 +60,7 @@ import { getClientConfig } from "../config/client"; import { useSyncStore } from "../store/sync"; import { nanoid } from "nanoid"; import { useMaskStore } from "../store/mask"; +import { ProviderType } from "../utils/cloud"; function EditPromptModal(props: { id: string; onClose: () => void }) { const promptStore = usePromptStore(); @@ -247,12 +254,183 @@ function DangerItems() { ); } +function CheckButton() { + const syncStore = useSyncStore(); + + const couldCheck = useMemo(() => { + return syncStore.coundSync(); + }, [syncStore]); + + const [checkState, setCheckState] = useState< + "none" | "checking" | "success" | "failed" + >("none"); + + async function check() { + setCheckState("checking"); + const valid = await syncStore.check(); + setCheckState(valid ? "success" : "failed"); + } + + if (!couldCheck) return null; + + return ( + + ) : checkState === "checking" ? ( + + ) : checkState === "success" ? ( + + ) : checkState === "failed" ? ( + + ) : ( + + ) + } + > + ); +} + +function SyncConfigModal(props: { onClose?: () => void }) { + const syncStore = useSyncStore(); + + return ( +
+ props.onClose?.()} + actions={[ + , + } + bordered + text={Locale.UI.Confirm} + />, + ]} + > + + + + + + + { + syncStore.update( + (config) => (config.useProxy = e.currentTarget.checked), + ); + }} + > + + {syncStore.useProxy ? ( + + { + syncStore.update( + (config) => (config.proxyUrl = e.currentTarget.value), + ); + }} + > + + ) : null} + + + {syncStore.provider === ProviderType.WebDAV && ( + <> + + + { + syncStore.update( + (config) => + (config.webdav.endpoint = e.currentTarget.value), + ); + }} + > + + + + { + syncStore.update( + (config) => + (config.webdav.username = e.currentTarget.value), + ); + }} + > + + + { + syncStore.update( + (config) => + (config.webdav.password = e.currentTarget.value), + ); + }} + > + + + + )} + + {syncStore.provider === ProviderType.UpStash && ( + + + + )} + +
+ ); +} + function SyncItems() { const syncStore = useSyncStore(); - const webdav = syncStore.webDavConfig; const chatStore = useChatStore(); const promptStore = usePromptStore(); const maskStore = useMaskStore(); + const couldSync = useMemo(() => { + return syncStore.coundSync(); + }, [syncStore]); + + const [showSyncConfigModal, setShowSyncConfigModal] = useState(false); const stateOverview = useMemo(() => { const sessions = chatStore.sessions; @@ -267,42 +445,71 @@ function SyncItems() { }, [chatStore.sessions, maskStore.masks, promptStore.prompts]); return ( - - - } - text={Locale.UI.Sync} - onClick={() => { - showToast(Locale.WIP); - }} - /> - + <> + + +
+ } + text={Locale.UI.Config} + onClick={() => { + setShowSyncConfigModal(true); + }} + /> + {couldSync && ( + } + text={Locale.UI.Sync} + onClick={async () => { + try { + await syncStore.sync(); + showToast(Locale.Settings.Sync.Success); + } catch (e) { + showToast(Locale.Settings.Sync.Fail); + console.error("[Sync]", e); + } + }} + /> + )} +
+
- -
- } - text={Locale.UI.Export} - onClick={() => { - syncStore.export(); - }} - /> - } - text={Locale.UI.Import} - onClick={() => { - syncStore.import(); - }} - /> -
-
-
+ +
+ } + text={Locale.UI.Export} + onClick={() => { + syncStore.export(); + }} + /> + } + text={Locale.UI.Import} + onClick={() => { + syncStore.import(); + }} + /> +
+
+
+ + {showSyncConfigModal && ( + setShowSyncConfigModal(false)} /> + )} + ); } diff --git a/app/constant.ts b/app/constant.ts index 2141820ce9d..f76eb3a9794 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -7,7 +7,9 @@ export const RELEASE_URL = `${REPO_URL}/releases`; export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; + +export const DEFAULT_CORS_HOST = "https://chatgpt2.nextweb.fun"; +export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export enum Path { Home = "/", @@ -18,6 +20,10 @@ export enum Path { Auth = "/auth", } +export enum ApiPath { + Cors = "/api/cors", +} + export enum SlotID { AppBody = "app-body", } @@ -46,6 +52,8 @@ export const ACCESS_CODE_PREFIX = "nk-"; export const LAST_INPUT_KEY = "last-input"; export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; +export const STORAGE_KEY = "chatgpt-next-web"; + export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; diff --git a/app/icons/cloud-fail.svg b/app/icons/cloud-fail.svg new file mode 100644 index 00000000000..6e6a35fe5f2 --- /dev/null +++ b/app/icons/cloud-fail.svg @@ -0,0 +1 @@ + diff --git a/app/icons/cloud-success.svg b/app/icons/cloud-success.svg new file mode 100644 index 00000000000..8c5f3d6fd14 --- /dev/null +++ b/app/icons/cloud-success.svg @@ -0,0 +1 @@ + diff --git a/app/icons/config.svg b/app/icons/config.svg new file mode 100644 index 00000000000..7e1d23a272c --- /dev/null +++ b/app/icons/config.svg @@ -0,0 +1 @@ + diff --git a/app/icons/connection.svg b/app/icons/connection.svg new file mode 100644 index 00000000000..03687302031 --- /dev/null +++ b/app/icons/connection.svg @@ -0,0 +1 @@ + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index a1753417aa7..1b8850f4507 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -179,7 +179,35 @@ const cn = { SubTitle: "根据对话内容生成合适的标题", }, Sync: { - LastUpdate: "上次同步", + CloudState: "云端数据", + NotSyncYet: "还没有进行过同步", + Success: "同步成功", + Fail: "同步失败", + + Config: { + Modal: { + Title: "配置云同步", + }, + SyncType: { + Title: "同步类型", + SubTitle: "选择喜爱的同步服务器", + }, + Proxy: { + Title: "启用代理", + SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制", + }, + ProxyUrl: { + Title: "代理地址", + SubTitle: "仅适用于本项目自带的跨域代理", + }, + + WebDav: { + Endpoint: "WebDAV 地址", + UserName: "用户名", + Password: "密码", + }, + }, + LocalState: "本地数据", Overview: (overview: any) => { return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`; @@ -366,6 +394,7 @@ const cn = { Export: "导出", Import: "导入", Sync: "同步", + Config: "配置", }, Exporter: { Model: "模型", diff --git a/app/locales/en.ts b/app/locales/en.ts index e3129578728..ebbf1a37669 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -181,7 +181,36 @@ const en: LocaleType = { SubTitle: "Generate a suitable title based on the conversation content", }, Sync: { - LastUpdate: "Last Update", + CloudState: "Last Update", + NotSyncYet: "Not sync yet", + Success: "Sync Success", + Fail: "Sync Fail", + + Config: { + Modal: { + Title: "Config Sync", + }, + SyncType: { + Title: "Sync Type", + SubTitle: "Choose your favorite sync service", + }, + Proxy: { + Title: "Enable CORS Proxy", + SubTitle: "Enable a proxy to avoid cross-origin restrictions", + }, + ProxyUrl: { + Title: "Proxy Endpoint", + SubTitle: + "Only applicable to the built-in CORS proxy for this project", + }, + + WebDav: { + Endpoint: "WebDAV Endpoint", + UserName: "User Name", + Password: "Password", + }, + }, + LocalState: "Local Data", Overview: (overview: any) => { return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`; @@ -366,6 +395,7 @@ const en: LocaleType = { Export: "Export", Import: "Import", Sync: "Sync", + Config: "Config", }, Exporter: { Model: "Model", diff --git a/app/store/sync.ts b/app/store/sync.ts index 502cf71cb12..29b6a82c235 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -1,15 +1,18 @@ import { Updater } from "../typing"; -import { StoreKey } from "../constant"; +import { ApiPath, StoreKey } from "../constant"; import { createPersistStore } from "../utils/store"; import { AppState, getLocalAppState, + GetStoreState, mergeAppState, setLocalAppState, } from "../utils/sync"; import { downloadAs, readFromFile } from "../utils"; import { showToast } from "../components/ui-lib"; import Locale from "../locales"; +import { createSyncClient, ProviderType } from "../utils/cloud"; +import { corsPath } from "../utils/cors"; export interface WebDavConfig { server: string; @@ -17,22 +20,43 @@ export interface WebDavConfig { password: string; } +export type SyncStore = GetStoreState; + export const useSyncStore = createPersistStore( { - webDavConfig: { - server: "", + provider: ProviderType.WebDAV, + useProxy: true, + proxyUrl: corsPath(ApiPath.Cors), + + webdav: { + endpoint: "", username: "", password: "", }, + upstash: { + endpoint: "", + username: "", + apiKey: "", + }, + lastSyncTime: 0, + lastProvider: "", }, (set, get) => ({ + coundSync() { + const config = get()[get().provider]; + return Object.values(config).every((c) => c.toString().length > 0); + }, + + markSyncTime() { + set({ lastSyncTime: Date.now(), lastProvider: get().provider }); + }, + export() { const state = getLocalAppState(); const fileName = `Backup-${new Date().toLocaleString()}.json`; downloadAs(JSON.stringify(state), fileName); - set({ lastSyncTime: Date.now() }); }, async import() { @@ -50,47 +74,36 @@ export const useSyncStore = createPersistStore( } }, - async check() { - try { - const res = await fetch(this.path(""), { - method: "PROFIND", - headers: this.headers(), - }); - const sanitizedRes = { - status: res.status, - statusText: res.statusText, - headers: res.headers, - }; - console.log(sanitizedRes); - return res.status === 207; - } catch (e) { - console.error("[Sync] ", e); - return false; - } + getClient() { + const provider = get().provider; + const client = createSyncClient(provider, get()); + return client; }, - path(path: string) { - let url = get().webDavConfig.server; + async sync() { + const localState = getLocalAppState(); + const provider = get().provider; + const config = get()[provider]; + const client = this.getClient(); - if (!url.endsWith("/")) { - url += "/"; + try { + const remoteState = JSON.parse( + await client.get(config.username), + ) as AppState; + mergeAppState(localState, remoteState); + setLocalAppState(localState); + } catch (e) { + console.log("[Sync] failed to get remoate state", e); } - if (path.startsWith("/")) { - path = path.slice(1); - } + await client.set(config.username, JSON.stringify(localState)); - return url + path; + this.markSyncTime(); }, - headers() { - const auth = btoa( - [get().webDavConfig.username, get().webDavConfig.password].join(":"), - ); - - return { - Authorization: `Basic ${auth}`, - }; + async check() { + const client = this.getClient(); + return await client.check(); }, }), { diff --git a/app/utils/cloud/index.ts b/app/utils/cloud/index.ts new file mode 100644 index 00000000000..63908249e85 --- /dev/null +++ b/app/utils/cloud/index.ts @@ -0,0 +1,33 @@ +import { createWebDavClient } from "./webdav"; +import { createUpstashClient } from "./upstash"; + +export enum ProviderType { + WebDAV = "webdav", + UpStash = "upstash", +} + +export const SyncClients = { + [ProviderType.UpStash]: createUpstashClient, + [ProviderType.WebDAV]: createWebDavClient, +} as const; + +type SyncClientConfig = { + [K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends ( + _: infer C, + ) => any + ? C + : never; +}; + +export type SyncClient = { + get: (key: string) => Promise; + set: (key: string, value: string) => Promise; + check: () => Promise; +}; + +export function createSyncClient( + provider: T, + config: SyncClientConfig[T], +): SyncClient { + return SyncClients[provider](config as any) as any; +} diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts new file mode 100644 index 00000000000..6f9b30f6b5e --- /dev/null +++ b/app/utils/cloud/upstash.ts @@ -0,0 +1,39 @@ +import { SyncStore } from "@/app/store/sync"; + +export type UpstashConfig = SyncStore["upstash"]; +export type UpStashClient = ReturnType; + +export function createUpstashClient(config: UpstashConfig) { + return { + async check() { + return true; + }, + + async get() { + throw Error("[Sync] not implemented"); + }, + + async set() { + throw Error("[Sync] not implemented"); + }, + + headers() { + return { + Authorization: `Basic ${config.apiKey}`, + }; + }, + path(path: string) { + let url = config.endpoint; + + if (!url.endsWith("/")) { + url += "/"; + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + return url + path; + }, + }; +} diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts new file mode 100644 index 00000000000..5386b4d1958 --- /dev/null +++ b/app/utils/cloud/webdav.ts @@ -0,0 +1,78 @@ +import { STORAGE_KEY } from "@/app/constant"; +import { SyncStore } from "@/app/store/sync"; +import { corsFetch } from "../cors"; + +export type WebDAVConfig = SyncStore["webdav"]; +export type WebDavClient = ReturnType; + +export function createWebDavClient(store: SyncStore) { + const folder = STORAGE_KEY; + const fileName = `${folder}/backup.json`; + const config = store.webdav; + const proxyUrl = + store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; + + return { + async check() { + try { + const res = await corsFetch(this.path(folder), { + method: "MKCOL", + headers: this.headers(), + proxyUrl, + }); + + console.log("[WebDav] check", res.status, res.statusText); + + return [201, 200, 404].includes(res.status); + } catch (e) { + console.error("[WebDav] failed to check", e); + } + + return false; + }, + + async get(key: string) { + const res = await corsFetch(this.path(fileName), { + method: "GET", + headers: this.headers(), + proxyUrl, + }); + + console.log("[WebDav] get key = ", key, res.status, res.statusText); + + return await res.text(); + }, + + async set(key: string, value: string) { + const res = await corsFetch(this.path(fileName), { + method: "PUT", + headers: this.headers(), + body: value, + proxyUrl, + }); + + console.log("[WebDav] set key = ", key, res.status, res.statusText); + }, + + headers() { + const auth = btoa(config.username + ":" + config.password); + + return { + authorization: `Basic ${auth}`, + }; + }, + path(path: string) { + let url = config.endpoint; + + if (!url.endsWith("/")) { + url += "/"; + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + return url + path; + }, + }; +} diff --git a/app/utils/cors.ts b/app/utils/cors.ts new file mode 100644 index 00000000000..773f152aafa --- /dev/null +++ b/app/utils/cors.ts @@ -0,0 +1,50 @@ +import { getClientConfig } from "../config/client"; +import { ApiPath, DEFAULT_CORS_HOST } from "../constant"; + +export function corsPath(path: string) { + const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : ""; + + if (!path.startsWith("/")) { + path = "/" + path; + } + + if (!path.endsWith("/")) { + path += "/"; + } + + return `${baseUrl}${path}`; +} + +export function corsFetch( + url: string, + options: RequestInit & { + proxyUrl?: string; + }, +) { + if (!url.startsWith("http")) { + throw Error("[CORS Fetch] url must starts with http/https"); + } + + let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors); + if (!proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + url = url.replace("://", "/"); + + const corsOptions = { + ...options, + method: "POST", + headers: options.method + ? { + ...options.headers, + method: options.method, + } + : options.headers, + }; + + const corsUrl = proxyUrl + url; + console.info("[CORS] target = ", corsUrl); + + return fetch(corsUrl, corsOptions); +} diff --git a/next.config.mjs b/next.config.mjs index c8f17de8c01..4faa63e5450 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -35,27 +35,29 @@ const nextConfig = { }, }; +const CorsHeaders = [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + { + key: "Access-Control-Allow-Methods", + value: "*", + }, + { + key: "Access-Control-Allow-Headers", + value: "*", + }, + { + key: "Access-Control-Max-Age", + value: "86400", + }, +]; + if (mode !== "export") { nextConfig.headers = async () => { return [ { source: "/api/:path*", - headers: [ - { key: "Access-Control-Allow-Credentials", value: "true" }, - { key: "Access-Control-Allow-Origin", value: "*" }, - { - key: "Access-Control-Allow-Methods", - value: "*", - }, - { - key: "Access-Control-Allow-Headers", - value: "*", - }, - { - key: "Access-Control-Max-Age", - value: "86400", - }, - ], + headers: CorsHeaders, }, ]; }; @@ -76,15 +78,6 @@ if (mode !== "export") { }, ]; - const apiUrl = process.env.API_URL; - if (apiUrl) { - console.log("[Next] using api url ", apiUrl); - ret.push({ - source: "/api/:path*", - destination: `${apiUrl}/:path*`, - }); - } - return { beforeFiles: ret, }; From 859cf6930fc3cbe5b1eeb52d8c481a6cd95d63c0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:51:57 +0800 Subject: [PATCH 078/202] fixup --- 3 | 119 -------------------------------------------------------------- 1 file changed, 119 deletions(-) delete mode 100644 3 diff --git a/3 b/3 deleted file mode 100644 index 371bd01ac26..00000000000 --- a/3 +++ /dev/null @@ -1,119 +0,0 @@ -export const OWNER = "Yidadaa"; -export const REPO = "ChatGPT-Next-Web"; -export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; -export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; -export const UPDATE_URL = `${REPO_URL}#keep-updated`; -export const RELEASE_URL = `${REPO_URL}/releases`; -export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; -export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; -export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; - -export enum Path { - Home = "/", - Chat = "/chat", - Settings = "/settings", - NewChat = "/new-chat", - Masks = "/masks", - Auth = "/auth", -} - -export enum SlotID { - AppBody = "app-body", -} - -export enum FileName { - Masks = "masks.json", - Prompts = "prompts.json", -} - -export enum StoreKey { - Chat = "chat-next-web-store", - Access = "access-control", - Config = "app-config", - Mask = "mask-store", - Prompt = "prompt-store", - Update = "chat-update", - Sync = "sync", -} - -export const MAX_SIDEBAR_WIDTH = 500; -export const MIN_SIDEBAR_WIDTH = 230; -export const NARROW_SIDEBAR_WIDTH = 100; - -export const ACCESS_CODE_PREFIX = "nk-"; - -export const LAST_INPUT_KEY = "last-input"; -export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; - -export const STORAGE_KEY = "chatgpt-next-web"; - -export const REQUEST_TIMEOUT_MS = 60000; - -export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; - -export const OpenaiPath = { - ChatPath: "v1/chat/completions", - UsagePath: "dashboard/billing/usage", - SubsPath: "dashboard/billing/subscription", - ListModelPath: "v1/models", -}; - -export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang -export const DEFAULT_SYSTEM_TEMPLATE = ` -You are ChatGPT, a large language model trained by OpenAI. -Knowledge cutoff: 2021-09 -Current model: {{model}} -Current time: {{time}}`; - -export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; - -export const DEFAULT_MODELS = [ - { - name: "gpt-4", - available: true, - }, - { - name: "gpt-4-0314", - available: true, - }, - { - name: "gpt-4-0613", - available: true, - }, - { - name: "gpt-4-32k", - available: true, - }, - { - name: "gpt-4-32k-0314", - available: true, - }, - { - name: "gpt-4-32k-0613", - available: true, - }, - { - name: "gpt-3.5-turbo", - available: true, - }, - { - name: "gpt-3.5-turbo-0301", - available: true, - }, - { - name: "gpt-3.5-turbo-0613", - available: true, - }, - { - name: "gpt-3.5-turbo-16k", - available: true, - }, - { - name: "gpt-3.5-turbo-16k-0613", - available: true, - }, -] as const; - -export const CHAT_PAGE_SIZE = 15; -export const MAX_RENDER_MSG_COUNT = 45; From dc555b2206ae84ce2598774398f49d967357d37d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:52:28 +0800 Subject: [PATCH 079/202] fixup --- app/api/cors/[...path]/route.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts index c461d250b1d..90404cf892a 100644 --- a/app/api/cors/[...path]/route.ts +++ b/app/api/cors/[...path]/route.ts @@ -33,12 +33,6 @@ async function handle( return fetchResult; } -export const GET = handle; export const POST = handle; -export const PUT = handle; - -// nextjs dose not support those https methods, sucks -export const PROFIND = handle; -export const MKCOL = handle; export const runtime = "edge"; From b589f48aa99e1bc3b5544b4fc81cab27385c699e Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 13 Sep 2023 03:01:28 +0800 Subject: [PATCH 080/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2256d5b34d9..d8b677bf6a0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.5" + "version": "2.9.6" }, "tauri": { "allowlist": { From 368701610f039241eeb0fda27db28803b607527e Mon Sep 17 00:00:00 2001 From: yhua1998 <101091026+yhua1998@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:57:30 +0800 Subject: [PATCH 081/202] fix: Width changes abruptly when dragging the sidebar (jumps) In useRef is non-responsive, we can't get the modified config.sidebarWidth dynamic modification value in handleMouseUp --- app/components/sidebar.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 634639f1d40..c42138efc63 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useCallback } from "react"; import styles from "./home.module.scss"; @@ -53,7 +53,7 @@ function useHotKey() { } function useDragSideBar() { - const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); + const limit = useCallback((x: number) => Math.min(MAX_SIDEBAR_WIDTH, x)); const config = useAppConfig(); const startX = useRef(0); @@ -71,14 +71,16 @@ function useDragSideBar() { }); const handleMouseUp = useRef(() => { - startDragWidth.current = config.sidebarWidth ?? 300; + // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth + // startDragWidth.current = config.sidebarWidth ?? 300; window.removeEventListener("mousemove", handleMouseMove.current); window.removeEventListener("mouseup", handleMouseUp.current); }); const onDragMouseDown = (e: MouseEvent) => { startX.current = e.clientX; - + // Remembers the initial width each time the mouse is pressed + startDragWidth.current = config.sidebarWidth window.addEventListener("mousemove", handleMouseMove.current); window.addEventListener("mouseup", handleMouseUp.current); }; From 28103c901dba611a531117fca2589cbc73f43b55 Mon Sep 17 00:00:00 2001 From: yhua1998 <101091026+yhua1998@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:24:21 +0800 Subject: [PATCH 082/202] Refactor: sidebar drag --- app/components/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index c42138efc63..db13bc9b42e 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -53,7 +53,7 @@ function useHotKey() { } function useDragSideBar() { - const limit = useCallback((x: number) => Math.min(MAX_SIDEBAR_WIDTH, x)); + const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); const config = useAppConfig(); const startX = useRef(0); From 48e6087b1be1562c50de3b4aa648445df5510539 Mon Sep 17 00:00:00 2001 From: yhua1998 <385362330@qq.com> Date: Wed, 13 Sep 2023 16:55:31 +0800 Subject: [PATCH 083/202] fix: The width of the sidebar changes abruptly by dragging it multiple times over and over again (bouncing) --- app/components/sidebar.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index db13bc9b42e..4519c4be941 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -67,7 +67,13 @@ function useDragSideBar() { lastUpdateTime.current = Date.now(); const d = e.clientX - startX.current; const nextWidth = limit(startDragWidth.current + d); - config.update((config) => (config.sidebarWidth = nextWidth)); + config.update((config) => { + if (nextWidth < MIN_SIDEBAR_WIDTH) { + config.sidebarWidth = NARROW_SIDEBAR_WIDTH; + } else { + config.sidebarWidth = nextWidth; + } + }); }); const handleMouseUp = useRef(() => { @@ -80,7 +86,7 @@ function useDragSideBar() { const onDragMouseDown = (e: MouseEvent) => { startX.current = e.clientX; // Remembers the initial width each time the mouse is pressed - startDragWidth.current = config.sidebarWidth + startDragWidth.current = config.sidebarWidth; window.addEventListener("mousemove", handleMouseMove.current); window.addEventListener("mouseup", handleMouseUp.current); }; From 4f3261b262209cf439b5220e02ed319314efb2ac Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 13 Sep 2023 20:05:58 +0700 Subject: [PATCH 084/202] [+] Some improvements to the Indonesian language. --- app/locales/id.ts | 225 ++++++++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 95 deletions(-) diff --git a/app/locales/id.ts b/app/locales/id.ts index c3a2a5f8807..6ed0e721042 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -60,7 +60,9 @@ const id: PartialLocaleType = { if (submitKey === String(SubmitKey.Enter)) { inputHints += ", Shift + Enter untuk membalut"; } - return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"; + return ( + inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah" + ); }, Send: "Kirim", Config: { @@ -114,36 +116,37 @@ const id: PartialLocaleType = { SubTitle: "Semua Pengaturan", Danger: { Reset: { - Title: "Setel Ulang Semua Pengaturan", - SubTitle: "Mengembalikan semua pengaturan ke nilai default", - Action: "Setel Ulang", - Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?", + Title: "Reset Semua Pengaturan", + SubTitle: "Reset semua item pengaturan ke nilai default", + Action: "Reset", + Confirm: "Konfirmasi untuk mereset semua pengaturan ke nilai default?", }, Clear: { Title: "Hapus Semua Data", - SubTitle: "Menghapus semua pesan dan pengaturan", + SubTitle: "Semua data yang tersimpan secara lokal akan dihapus", Action: "Hapus", - Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?", + Confirm: + "Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?", }, }, Lang: { - Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` - All: "Semua Bahasa", - }, - Avatar: "Avatar", - FontSize: { - Title: "Ukuran Font", - SubTitle: "Ubah ukuran font konten chat", - }, - InjectSystemPrompts: { - Title: "Suntikkan Petunjuk Sistem", - SubTitle: - "Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan", - }, - InputTemplate: { - Title: "Template Input", - SubTitle: "Pesan baru akan diisi menggunakan template ini", - }, + Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` + All: "Semua Bahasa", + }, + Avatar: "Avatar", + FontSize: { + Title: "Ukuran Font", + SubTitle: "Ubah ukuran font konten chat", + }, + InjectSystemPrompts: { + Title: "Suntikkan Petunjuk Sistem", + SubTitle: + "Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan", + }, + InputTemplate: { + Title: "Template Input", + SubTitle: "Pesan baru akan diisi menggunakan template ini", + }, Update: { Version: (x: string) => `Version: ${x}`, @@ -154,9 +157,39 @@ const id: PartialLocaleType = { GoToUpdate: "Perbarui Sekarang", }, AutoGenerateTitle: { - Title: "Hasilkan Judul Otomatis", - SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan", + Title: "Hasilkan Judul Otomatis", + SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan", + }, + Sync: { + CloudState: "Pembaruan Terakhir", + NotSyncYet: "Belum disinkronkan", + Success: "Sinkronisasi Berhasil", + Fail: "Sinkronisasi Gagal", + + Config: { + Modal: { + Title: "Konfigurasi Sinkronisasi", + }, + SyncType: { + Title: "Tipe Sinkronisasi", + SubTitle: "Pilih layanan sinkronisasi favorit Anda", + }, + Proxy: { + Title: "Aktifkan Proxy CORS", + SubTitle: "Aktifkan Proxy untuk menghindari pembatasan lintas sumber", + }, + ProxyUrl: { + Title: "Titik Akhir Proxy", + SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini", + }, + + WebDav: { + Endpoint: "Titik Akhir WebDAV", + UserName: "Nama Pengguna", + Password: "Kata Sandi", + }, }, + }, SendKey: "Kirim", Theme: "Tema", TightBorder: "Batas Ketat", @@ -176,76 +209,77 @@ const id: PartialLocaleType = { }, }, Prompt: { - Disable: { - Title: "Nonaktifkan Otomatisasi", - SubTitle: "Aktifkan/Matikan otomatisasi", - }, - List: "Daftar Prompt", - ListCount: (builtin: number, custom: number) => - `${builtin} bawaan, ${custom} penggunaan khusus`, - Edit: "Edit", - Modal: { - Title: "Daftar Prompt", - Add: "Tambahkan", - Search: "Cari Prompt", - }, - EditModal: { - Title: "Edit Prompt", - }, - }, - HistoryCount: { - Title: "Jumlah Pesan Riwayat", - SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan", - }, - CompressThreshold: { - Title: "Batas Kompresi Riwayat", - SubTitle: - "Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", - }, - Token: { - Title: "Kunci API", - SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses", - Placeholder: "Kunci API OpenAI", - }, - Usage: { - Title: "Saldo Akun", - SubTitle(used: any, total: any) { - return `Digunakan bulan ini: ${used}, total langganan: ${total}`; - }, - IsChecking: "Memeriksa...", - Check: "Periksa", - NoAccess: "Masukkan kunci API untuk memeriksa saldo", + Disable: { + Title: "Nonaktifkan Otomatisasi", + SubTitle: "Aktifkan/Matikan otomatisasi", }, - AccessCode: { - Title: "Kode Akses", - SubTitle: "Kontrol akses diaktifkan", - Placeholder: "Diperlukan kode akses", - }, - Endpoint: { - Title: "Endpoint", - SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom", - }, - Model: "Model", - Temperature: { - Title: "Suhu", - SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya", - }, - TopP: { - Title: "Top P", - SubTitle: "Tidak mengubah nilai dengan suhu", + List: "Daftar Prompt", + ListCount: (builtin: number, custom: number) => + `${builtin} bawaan, ${custom} penggunaan khusus`, + Edit: "Edit", + Modal: { + Title: "Daftar Prompt", + Add: "Tambahkan", + Search: "Cari Prompt", }, - MaxTokens: { - Title: "Token Maksimum", - SubTitle: "Panjang maksimum token input dan output", + EditModal: { + Title: "Edit Prompt", }, - PresencePenalty: { - Title: "Penalti Kehadiran", - SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul", + }, + HistoryCount: { + Title: "Jumlah Pesan Riwayat", + SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan", + }, + CompressThreshold: { + Title: "Batas Kompresi Riwayat", + SubTitle: + "Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", + }, + Token: { + Title: "Kunci API", + SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses", + Placeholder: "Kunci API OpenAI", + }, + Usage: { + Title: "Saldo Akun", + SubTitle(used: any, total: any) { + return `Digunakan bulan ini: ${used}, total langganan: ${total}`; }, - FrequencyPenalty: { - Title: "Penalti Frekuensi", - SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama", - }, + IsChecking: "Memeriksa...", + Check: "Periksa", + NoAccess: "Masukkan kunci API untuk memeriksa saldo", + }, + AccessCode: { + Title: "Kode Akses", + SubTitle: "Kontrol akses diaktifkan", + Placeholder: "Diperlukan kode akses", + }, + Endpoint: { + Title: "Endpoint", + SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom", + }, + Model: "Model", + Temperature: { + Title: "Suhu", + SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya", + }, + TopP: { + Title: "Top P", + SubTitle: "Tidak mengubah nilai dengan suhu", + }, + MaxTokens: { + Title: "Token Maksimum", + SubTitle: "Panjang maksimum token input dan output", + }, + PresencePenalty: { + Title: "Penalti Kehadiran", + SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul", + }, + FrequencyPenalty: { + Title: "Penalti Frekuensi", + SubTitle: + "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama", + }, }, Store: { DefaultTopic: "Percakapan Baru", @@ -261,8 +295,9 @@ const id: PartialLocaleType = { }, }, Copy: { - Success: "Berhasil disalin ke clipboard", - Failed: "Gagal menyalin, berikan izin untuk memberikan izin", + Success: "Tersalin ke clipboard", + Failed: + "Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)", }, Context: { Toast: (x: any) => `Dengan ${x} promp kontekstual`, @@ -341,7 +376,7 @@ const id: PartialLocaleType = { Model: "Model", Messages: "Pesan", Topic: "Topik", - Time: "Waktu", + Time: "Tanggal & Waktu", }, URLCommand: { Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ", From 5a7bdcfe59a812611b8bf865408ce86f22103b64 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 13 Sep 2023 20:17:30 +0700 Subject: [PATCH 085/202] [+] Some improvements to the Indonesian language. --- app/locales/id.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/locales/id.ts b/app/locales/id.ts index 6ed0e721042..5068500d1a2 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -116,10 +116,11 @@ const id: PartialLocaleType = { SubTitle: "Semua Pengaturan", Danger: { Reset: { - Title: "Reset Semua Pengaturan", - SubTitle: "Reset semua item pengaturan ke nilai default", - Action: "Reset", - Confirm: "Konfirmasi untuk mereset semua pengaturan ke nilai default?", + Title: "Setel Ulang Semua Pengaturan", + SubTitle: "Mengembalikan semua pengaturan ke nilai default", + Action: "Setel Ulang", + Confirm: + "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?", }, Clear: { Title: "Hapus Semua Data", @@ -176,15 +177,16 @@ const id: PartialLocaleType = { }, Proxy: { Title: "Aktifkan Proxy CORS", - SubTitle: "Aktifkan Proxy untuk menghindari pembatasan lintas sumber", + SubTitle: + "Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber", }, ProxyUrl: { - Title: "Titik Akhir Proxy", + Title: "Lokasi Titik Akhir Proxy CORS", SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini", }, WebDav: { - Endpoint: "Titik Akhir WebDAV", + Endpoint: "Lokasi Titik Akhir WebDAV", UserName: "Nama Pengguna", Password: "Kata Sandi", }, From 261bf0b29800485a61aa8194bb1da8254361b9d3 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 13 Sep 2023 20:49:22 +0700 Subject: [PATCH 086/202] Changes "Nama" -> "User" --- app/locales/id.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locales/id.ts b/app/locales/id.ts index 5068500d1a2..244c5ade19a 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -187,7 +187,7 @@ const id: PartialLocaleType = { WebDav: { Endpoint: "Lokasi Titik Akhir WebDAV", - UserName: "Nama Pengguna", + UserName: "User Pengguna", Password: "Kata Sandi", }, }, From 6535986484abe66c8f989c811e4e815d2c8e0728 Mon Sep 17 00:00:00 2001 From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Date: Fri, 15 Sep 2023 06:06:34 +0300 Subject: [PATCH 087/202] Update markdown.tsx --- app/components/markdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index e7a35b8023b..1a1fbf41677 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -151,6 +151,7 @@ export function Markdown( ref={mdRef} onContextMenu={props.onContextMenu} onDoubleClickCapture={props.onDoubleClickCapture} + dir="auto" > {props.loading ? ( From bd69116df2ccaf70e6948514f381cc8375fd507e Mon Sep 17 00:00:00 2001 From: Amor Zara <132665015+a6z6@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:21:42 +0800 Subject: [PATCH 088/202] Update route.ts Correct typo error and make warning more specific. --- app/api/config/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 7749e6e9e28..0bfc955bfb8 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -4,7 +4,7 @@ import { getServerSideConfig } from "../../config/server"; const serverConfig = getServerSideConfig(); -// Danger! Don not write any secret value here! +// Danger! Do not hard code any secret value here! // 警告!不要在这里写入任何敏感信息! const DANGER_CONFIG = { needCode: serverConfig.needCode, From 2c92f75c861c07d8d787cca4b81c7445ba72ce27 Mon Sep 17 00:00:00 2001 From: Amor Zara <132665015+a6z6@users.noreply.github.com> Date: Sun, 17 Sep 2023 17:32:42 +0800 Subject: [PATCH 089/202] Update .env.template --- .env.template | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 0f4bf0e7c00..1ff575f116a 100644 --- a/.env.template +++ b/.env.template @@ -15,7 +15,6 @@ 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) @@ -31,4 +30,4 @@ 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 +HIDE_BALANCE_QUERY= From c900459f73602a2589bdc6ca4ca2b93e2cd8c508 Mon Sep 17 00:00:00 2001 From: Gerhard Tan Date: Mon, 18 Sep 2023 09:37:19 +0800 Subject: [PATCH 090/202] Encode google font url --- app/components/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 745298d560e..285ca0f5852 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -115,7 +115,7 @@ const loadAsyncGoogleFont = () => { getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl; linkEl.rel = "stylesheet"; linkEl.href = - googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap"; + googleFontUrl + "/css2?family=" + encodeURIComponent("Noto Sans:wght@300;400;700;900") + "&display=swap"; document.head.appendChild(linkEl); }; From d713d016000e09f245fc9496bd9864293aaa95c0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 01:47:15 +0800 Subject: [PATCH 091/202] feat: close #2848 click drag icon to toggle sidebar width --- app/components/sidebar.tsx | 73 ++++++++++++++++++++++++-------------- app/constant.ts | 1 + app/store/config.ts | 9 +++-- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 4519c4be941..3ca1678963e 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -17,6 +17,7 @@ import Locale from "../locales"; import { useAppConfig, useChatStore } from "../store"; import { + DEFAULT_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, NARROW_SIDEBAR_WIDTH, @@ -57,39 +58,57 @@ function useDragSideBar() { const config = useAppConfig(); const startX = useRef(0); - const startDragWidth = useRef(config.sidebarWidth ?? 300); + const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH); const lastUpdateTime = useRef(Date.now()); - const handleMouseMove = useRef((e: MouseEvent) => { - if (Date.now() < lastUpdateTime.current + 50) { - return; - } - lastUpdateTime.current = Date.now(); - const d = e.clientX - startX.current; - const nextWidth = limit(startDragWidth.current + d); + const toggleSideBar = () => { config.update((config) => { - if (nextWidth < MIN_SIDEBAR_WIDTH) { - config.sidebarWidth = NARROW_SIDEBAR_WIDTH; + if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) { + config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH; } else { - config.sidebarWidth = nextWidth; + config.sidebarWidth = NARROW_SIDEBAR_WIDTH; } }); - }); - - const handleMouseUp = useRef(() => { - // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth - // startDragWidth.current = config.sidebarWidth ?? 300; - window.removeEventListener("mousemove", handleMouseMove.current); - window.removeEventListener("mouseup", handleMouseUp.current); - }); + }; - const onDragMouseDown = (e: MouseEvent) => { - startX.current = e.clientX; + const onDragStart = (e: MouseEvent) => { // Remembers the initial width each time the mouse is pressed + startX.current = e.clientX; startDragWidth.current = config.sidebarWidth; - window.addEventListener("mousemove", handleMouseMove.current); - window.addEventListener("mouseup", handleMouseUp.current); + const dragStartTime = Date.now(); + + const handleDragMove = (e: MouseEvent) => { + if (Date.now() < lastUpdateTime.current + 20) { + return; + } + lastUpdateTime.current = Date.now(); + const d = e.clientX - startX.current; + const nextWidth = limit(startDragWidth.current + d); + config.update((config) => { + if (nextWidth < MIN_SIDEBAR_WIDTH) { + config.sidebarWidth = NARROW_SIDEBAR_WIDTH; + } else { + config.sidebarWidth = nextWidth; + } + }); + }; + + const handleDragEnd = () => { + // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth + window.removeEventListener("pointermove", handleDragMove); + window.removeEventListener("pointerup", handleDragEnd); + + // if user click the drag icon, should toggle the sidebar + const shouldFireClick = Date.now() - dragStartTime < 300; + if (shouldFireClick) { + toggleSideBar(); + } + }; + + window.addEventListener("pointermove", handleDragMove); + window.addEventListener("pointerup", handleDragEnd); }; + const isMobileScreen = useMobileScreen(); const shouldNarrow = !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH; @@ -97,13 +116,13 @@ function useDragSideBar() { useEffect(() => { const barWidth = shouldNarrow ? NARROW_SIDEBAR_WIDTH - : limit(config.sidebarWidth ?? 300); + : limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH); const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`; document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); }, [config.sidebarWidth, isMobileScreen, shouldNarrow]); return { - onDragMouseDown, + onDragStart, shouldNarrow, }; } @@ -112,7 +131,7 @@ export function SideBar(props: { className?: string }) { const chatStore = useChatStore(); // drag side bar - const { onDragMouseDown, shouldNarrow } = useDragSideBar(); + const { onDragStart, shouldNarrow } = useDragSideBar(); const navigate = useNavigate(); const config = useAppConfig(); @@ -206,7 +225,7 @@ export function SideBar(props: { className?: string }) {
onDragMouseDown(e as any)} + onPointerDown={(e) => onDragStart(e as any)} >
diff --git a/app/constant.ts b/app/constant.ts index f76eb3a9794..c6cba3ef0a8 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -43,6 +43,7 @@ export enum StoreKey { Sync = "sync", } +export const DEFAULT_SIDEBAR_WIDTH = 300; export const MAX_SIDEBAR_WIDTH = 500; export const MIN_SIDEBAR_WIDTH = 230; export const NARROW_SIDEBAR_WIDTH = 100; diff --git a/app/store/config.ts b/app/store/config.ts index b0131954296..956e5f3eb81 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -1,6 +1,11 @@ import { LLMModel } from "../client/api"; import { getClientConfig } from "../config/client"; -import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant"; +import { + DEFAULT_INPUT_TEMPLATE, + DEFAULT_MODELS, + DEFAULT_SIDEBAR_WIDTH, + StoreKey, +} from "../constant"; import { createPersistStore } from "../utils/store"; export type ModelType = (typeof DEFAULT_MODELS)[number]["name"]; @@ -29,7 +34,7 @@ export const DEFAULT_CONFIG = { tightBorder: !!getClientConfig()?.isApp, sendPreviewBubble: true, enableAutoGenerateTitle: true, - sidebarWidth: 300, + sidebarWidth: DEFAULT_SIDEBAR_WIDTH, disablePromptHint: false, From 61ca60c550295c75e3e3feb8061455d298c27501 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 01:58:52 +0800 Subject: [PATCH 092/202] fix: #2817 min-height for landscape orientation on mobile phone --- app/components/home.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 77f1c8538eb..b836d2bec93 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -6,7 +6,7 @@ color: var(--black); background-color: var(--white); min-width: 600px; - min-height: 480px; + min-height: 370px; max-width: 1200px; display: flex; From adb860b4646c0c7548a059c5a8e8b3349ebdeca8 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 02:12:43 +0800 Subject: [PATCH 093/202] fix: #2820 try to fix 520 error code --- app/api/cors/[...path]/route.ts | 9 +++++++-- app/components/settings.tsx | 2 +- app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/utils/cloud/webdav.ts | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts index 90404cf892a..1f70d663082 100644 --- a/app/api/cors/[...path]/route.ts +++ b/app/api/cors/[...path]/route.ts @@ -26,13 +26,18 @@ async function handle( duplex: "half", }; - console.log("[Any Proxy]", targetUrl); + const fetchResult = await fetch(targetUrl, fetchOptions); - const fetchResult = fetch(targetUrl, fetchOptions); + console.log("[Any Proxy]", targetUrl, { + status: fetchResult.status, + statusText: fetchResult.statusText, + }); return fetchResult; } export const POST = handle; +export const GET = handle; +export const OPTIONS = handle; export const runtime = "edge"; diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 8e43e1d1aec..fb1d688f01c 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -275,7 +275,7 @@ function CheckButton() { return ( Date: Tue, 19 Sep 2023 02:21:31 +0800 Subject: [PATCH 094/202] fixup --- app/api/cors/[...path]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts index 1f70d663082..0217b12b08f 100644 --- a/app/api/cors/[...path]/route.ts +++ b/app/api/cors/[...path]/route.ts @@ -40,4 +40,4 @@ export const POST = handle; export const GET = handle; export const OPTIONS = handle; -export const runtime = "edge"; +export const runtime = "nodejs"; From 83fed429971fcc758ada9af12d52a2936b537456 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 03:18:34 +0800 Subject: [PATCH 095/202] feat: add upstash redis cloud sync --- app/components/settings.tsx | 39 ++++++++++++++++++- app/locales/cn.ts | 6 +++ app/locales/en.ts | 6 +++ app/store/sync.ts | 4 +- app/utils/cloud/upstash.ts | 74 ++++++++++++++++++++++++++++++++++--- app/utils/cloud/webdav.ts | 2 - app/utils/format.ts | 15 ++++++++ app/utils/sync.ts | 3 ++ 8 files changed, 137 insertions(+), 12 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index fb1d688f01c..8ed6b77383c 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -50,7 +50,7 @@ import Locale, { } from "../locales"; import { copyToClipboard } from "../utils"; import Link from "next/link"; -import { Path, RELEASE_URL, UPDATE_URL } from "../constant"; +import { Path, RELEASE_URL, STORAGE_KEY, UPDATE_URL } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { ErrorBoundary } from "./error"; import { InputRange } from "./input-range"; @@ -413,7 +413,42 @@ function SyncConfigModal(props: { onClose?: () => void }) { {syncStore.provider === ProviderType.UpStash && ( - + + { + syncStore.update( + (config) => + (config.upstash.endpoint = e.currentTarget.value), + ); + }} + > + + + + { + syncStore.update( + (config) => + (config.upstash.username = e.currentTarget.value), + ); + }} + > + + + { + syncStore.update( + (config) => (config.upstash.apiKey = e.currentTarget.value), + ); + }} + > + )} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index ac4a1777f5d..b2afc753457 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -207,6 +207,12 @@ const cn = { UserName: "用户名", Password: "密码", }, + + UpStash: { + Endpoint: "UpStash Redis REST Url", + UserName: "备份名称", + Password: "UpStash Redis REST Token", + }, }, LocalState: "本地数据", diff --git a/app/locales/en.ts b/app/locales/en.ts index 3f3fa7ce920..697d09d1f4e 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -210,6 +210,12 @@ const en: LocaleType = { UserName: "User Name", Password: "Password", }, + + UpStash: { + Endpoint: "UpStash Redis REST Url", + UserName: "Backup Name", + Password: "UpStash Redis REST Token", + }, }, LocalState: "Local Data", diff --git a/app/store/sync.ts b/app/store/sync.ts index 29b6a82c235..ff9f650c083 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -1,5 +1,5 @@ import { Updater } from "../typing"; -import { ApiPath, StoreKey } from "../constant"; +import { ApiPath, STORAGE_KEY, StoreKey } from "../constant"; import { createPersistStore } from "../utils/store"; import { AppState, @@ -36,7 +36,7 @@ export const useSyncStore = createPersistStore( upstash: { endpoint: "", - username: "", + username: STORAGE_KEY, apiKey: "", }, diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index 6f9b30f6b5e..5f5b9fc7925 100644 --- a/app/utils/cloud/upstash.ts +++ b/app/utils/cloud/upstash.ts @@ -1,25 +1,87 @@ +import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; +import { corsFetch } from "../cors"; +import { chunks } from "../format"; export type UpstashConfig = SyncStore["upstash"]; export type UpStashClient = ReturnType; -export function createUpstashClient(config: UpstashConfig) { +export function createUpstashClient(store: SyncStore) { + const config = store.upstash; + const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username; + const chunkCountKey = `${storeKey}-chunk-count`; + const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`; + + const proxyUrl = + store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; + return { async check() { - return true; + try { + const res = await corsFetch(this.path(`get/${storeKey}`), { + method: "GET", + headers: this.headers(), + proxyUrl, + }); + console.log("[Upstash] check", res.status, res.statusText); + return [200].includes(res.status); + } catch (e) { + console.error("[Upstash] failed to check", e); + } + return false; + }, + + async redisGet(key: string) { + const res = await corsFetch(this.path(`get/${key}`), { + method: "GET", + headers: this.headers(), + proxyUrl, + }); + + console.log("[Upstash] get key = ", key, res.status, res.statusText); + const resJson = (await res.json()) as { result: string }; + + return resJson.result; + }, + + async redisSet(key: string, value: string) { + const res = await corsFetch(this.path(`set/${key}`), { + method: "POST", + headers: this.headers(), + body: value, + proxyUrl, + }); + + console.log("[Upstash] set key = ", key, res.status, res.statusText); }, async get() { - throw Error("[Sync] not implemented"); + const chunkCount = Number(await this.redisGet(chunkCountKey)); + if (!Number.isInteger(chunkCount)) return; + + const chunks = await Promise.all( + new Array(chunkCount) + .fill(0) + .map((_, i) => this.redisGet(chunkIndexKey(i))), + ); + console.log("[Upstash] get full chunks", chunks); + return chunks.join(""); }, - async set() { - throw Error("[Sync] not implemented"); + async set(_: string, value: string) { + // upstash limit the max request size which is 1Mb for “Free” and “Pay as you go” + // so we need to split the data to chunks + let index = 0; + for await (const chunk of chunks(value)) { + await this.redisSet(chunkIndexKey(index), chunk); + index += 1; + } + await this.redisSet(chunkCountKey, index.toString()); }, headers() { return { - Authorization: `Basic ${config.apiKey}`, + Authorization: `Bearer ${config.apiKey}`, }; }, path(path: string) { diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts index 6c96c9062a4..c87fdd71e1e 100644 --- a/app/utils/cloud/webdav.ts +++ b/app/utils/cloud/webdav.ts @@ -20,9 +20,7 @@ export function createWebDavClient(store: SyncStore) { headers: this.headers(), proxyUrl, }); - console.log("[WebDav] check", res.status, res.statusText); - return [201, 200, 404, 401].includes(res.status); } catch (e) { console.error("[WebDav] failed to check", e); diff --git a/app/utils/format.ts b/app/utils/format.ts index 450d66696d9..2e8a382b95a 100644 --- a/app/utils/format.ts +++ b/app/utils/format.ts @@ -11,3 +11,18 @@ export function prettyObject(msg: any) { } return ["```json", msg, "```"].join("\n"); } + +export function* chunks(s: string, maxBytes = 1000 * 1000) { + const decoder = new TextDecoder("utf-8"); + let buf = new TextEncoder().encode(s); + while (buf.length) { + let i = buf.lastIndexOf(32, maxBytes + 1); + // If no space found, try forward search + if (i < 0) i = buf.indexOf(32, maxBytes); + // If there's no space at all, take all + if (i < 0) i = buf.length; + // This is a safe cut-off point; never half-way a multi-byte + yield decoder.decode(buf.slice(0, i)); + buf = buf.slice(i + 1); // Skip space (if any) + } +} diff --git a/app/utils/sync.ts b/app/utils/sync.ts index ab1f1f44918..1acfc1289de 100644 --- a/app/utils/sync.ts +++ b/app/utils/sync.ts @@ -69,6 +69,9 @@ const MergeStates: StateMerger = { localState.sessions.forEach((s) => (localSessions[s.id] = s)); remoteState.sessions.forEach((remoteSession) => { + // skip empty chats + if (remoteSession.messages.length === 0) return; + const localSession = localSessions[remoteSession.id]; if (!localSession) { // if remote session is new, just merge it From f1e7db6a88611a62a6ef6446c768ab16bd943173 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 03:33:17 +0800 Subject: [PATCH 096/202] feat: auto fill upstash backup name --- app/store/sync.ts | 52 +++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/app/store/sync.ts b/app/store/sync.ts index ff9f650c083..c194162fcb0 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -22,27 +22,29 @@ export interface WebDavConfig { export type SyncStore = GetStoreState; -export const useSyncStore = createPersistStore( - { - provider: ProviderType.WebDAV, - useProxy: true, - proxyUrl: corsPath(ApiPath.Cors), - - webdav: { - endpoint: "", - username: "", - password: "", - }, - - upstash: { - endpoint: "", - username: STORAGE_KEY, - apiKey: "", - }, +const DEFAULT_SYNC_STATE = { + provider: ProviderType.WebDAV, + useProxy: true, + proxyUrl: corsPath(ApiPath.Cors), + + webdav: { + endpoint: "", + username: "", + password: "", + }, - lastSyncTime: 0, - lastProvider: "", + upstash: { + endpoint: "", + username: STORAGE_KEY, + apiKey: "", }, + + lastSyncTime: 0, + lastProvider: "", +}; + +export const useSyncStore = createPersistStore( + DEFAULT_SYNC_STATE, (set, get) => ({ coundSync() { const config = get()[get().provider]; @@ -108,6 +110,16 @@ export const useSyncStore = createPersistStore( }), { name: StoreKey.Sync, - version: 1, + version: 1.1, + + migrate(persistedState, version) { + const newState = persistedState as typeof DEFAULT_SYNC_STATE; + + if (version < 1.1) { + newState.upstash.username = STORAGE_KEY; + } + + return newState as any; + }, }, ); From 37b49400db0358c9eeb1d63c8843b405097c869e Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 19 Sep 2023 11:03:03 +0800 Subject: [PATCH 097/202] Update constant.ts --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index c6cba3ef0a8..9e23ed510e4 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_CORS_HOST = "https://chatgpt2.nextweb.fun"; +export const DEFAULT_CORS_HOST = "https://nb.nextweb.fun"; export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export enum Path { From b050417ab1caf4d2c5da99039f57c84712559648 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 19 Sep 2023 11:03:22 +0800 Subject: [PATCH 098/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d8b677bf6a0..77b02a3bae8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.6" + "version": "2.9.7" }, "tauri": { "allowlist": { From 175b4e7f92abba782bdd2561c5e479bc315d6d9f Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 19 Sep 2023 11:04:10 +0800 Subject: [PATCH 099/202] Update README_CN.md --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index e593e45da9c..b31e8b367ee 100644 --- a/README_CN.md +++ b/README_CN.md @@ -114,7 +114,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 OPENAI_API_KEY= # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 -BASE_URL=https://chatgpt2.nextweb.fun/api/proxy +BASE_URL=https://nb.nextweb.fun/api/proxy ``` ### 本地开发 From eae7d6260f6d0968a59a07576bd86937b12a673a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 19 Sep 2023 23:26:52 +0800 Subject: [PATCH 100/202] fix: should not tight border in desktop app --- app/components/home.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 285ca0f5852..07d5e88b373 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -115,7 +115,10 @@ const loadAsyncGoogleFont = () => { getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl; linkEl.rel = "stylesheet"; linkEl.href = - googleFontUrl + "/css2?family=" + encodeURIComponent("Noto Sans:wght@300;400;700;900") + "&display=swap"; + googleFontUrl + + "/css2?family=" + + encodeURIComponent("Noto Sans:wght@300;400;700;900") + + "&display=swap"; document.head.appendChild(linkEl); }; @@ -125,6 +128,8 @@ function Screen() { const isHome = location.pathname === Path.Home; const isAuth = location.pathname === Path.Auth; const isMobileScreen = useMobileScreen(); + const shouldTightBorder = + config.tightBorder && !isMobileScreen && !getClientConfig()?.isApp; useEffect(() => { loadAsyncGoogleFont(); @@ -134,11 +139,9 @@ function Screen() {
{isAuth ? ( From 26e50cefea3afe79ef98cef951ef404d077aaf3d Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 20 Sep 2023 02:09:14 +0800 Subject: [PATCH 101/202] Update chat.tsx --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6fb497303eb..4e6aedecc60 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -937,7 +937,7 @@ function _Chat() { const isTouchTopEdge = e.scrollTop <= edgeThreshold; const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold; const isHitBottom = - bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10); + bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10); const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE; const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE; From 23eb7732d7011ce9476ab6309c92509e094fca81 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 20 Sep 2023 17:47:35 +0800 Subject: [PATCH 102/202] feat: support more http status check for webdav --- app/utils/cloud/webdav.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts index c87fdd71e1e..3a1553c1035 100644 --- a/app/utils/cloud/webdav.ts +++ b/app/utils/cloud/webdav.ts @@ -21,7 +21,7 @@ export function createWebDavClient(store: SyncStore) { proxyUrl, }); console.log("[WebDav] check", res.status, res.statusText); - return [201, 200, 404, 401].includes(res.status); + return [201, 200, 404, 301, 302, 307, 308].includes(res.status); } catch (e) { console.error("[WebDav] failed to check", e); } From 70b0580fb7e51110d2b6624268e35621db6b647d Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 26 Sep 2023 04:59:19 +0700 Subject: [PATCH 103/202] UI Page [sidebar] [+] fix(sidebar.tsx): update onClick function to conditionally navigate to different paths based on config.dontShowMaskSplashScreen value Ref : [Feature] Make the mask selection more streamlined Yidadaa#2895 --- app/components/sidebar.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 3ca1678963e..6212d05d982 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -160,7 +160,13 @@ export function SideBar(props: { className?: string }) { icon={} text={shouldNarrow ? undefined : Locale.Mask.Name} className={styles["sidebar-bar-button"]} - onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })} + onClick={() => { + if (config.dontShowMaskSplashScreen !== true) { + navigate(Path.NewChat, { state: { fromHome: true } }); + } else { + navigate(Path.Masks, { state: { fromHome: true } }); + } + }} shadow /> Date: Tue, 26 Sep 2023 20:45:09 +0300 Subject: [PATCH 104/202] Update chat.tsx --- app/components/chat.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 4e6aedecc60..005ee46e95c 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1155,7 +1155,13 @@ function _Chat() { {isUser ? ( ) : ( - + <> + {["system", "context"].includes(message.role) ? ( + + ) : ( + + )} + )}
From 398e229c7760c71419d0d851323b253e3edea79b Mon Sep 17 00:00:00 2001 From: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:49:17 +0300 Subject: [PATCH 105/202] Update chat.tsx --- app/components/chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 005ee46e95c..cca096eb874 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1156,7 +1156,7 @@ function _Chat() { ) : ( <> - {["system", "context"].includes(message.role) ? ( + {["system"].includes(message.role) ? ( ) : ( From 3bfcdf9c41c9a8311cb8c4b6a8a9bc5f05ce322d Mon Sep 17 00:00:00 2001 From: Eric R Date: Thu, 28 Sep 2023 06:10:22 -0400 Subject: [PATCH 106/202] [ADDED] MacOS detect --- app/utils.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/utils.ts b/app/utils.ts index 37c17dd760d..67d008b9356 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -173,3 +173,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) { export function getCSSVar(varName: string) { return getComputedStyle(document.body).getPropertyValue(varName).trim(); } + +/** + * Detects if the Operation system is MacOS + */ +export function isMacOS(): boolean { + if (window !== 'undefined') { + let userAgent = window?.navigator?.userAgent + if (userAgent.indexOf('Mac') != -1) return true + } + + return false +} From f3d5fc7a845aeba3dc120b46196dc3978c77faef Mon Sep 17 00:00:00 2001 From: Eric R Date: Thu, 28 Sep 2023 06:11:48 -0400 Subject: [PATCH 107/202] [FIXED] now the default key should be CMD on MacOS --- app/store/config.ts | 3 ++- app/utils.ts | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/store/config.ts b/app/store/config.ts index 956e5f3eb81..53df36ec2a3 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -1,4 +1,5 @@ import { LLMModel } from "../client/api"; +import { isMacOS } from "../utils"; import { getClientConfig } from "../config/client"; import { DEFAULT_INPUT_TEMPLATE, @@ -27,7 +28,7 @@ export enum Theme { export const DEFAULT_CONFIG = { lastUpdate: Date.now(), // timestamp, to merge state - submitKey: SubmitKey.CtrlEnter as SubmitKey, + submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter, avatar: "1f603", fontSize: 14, theme: Theme.Auto as Theme, diff --git a/app/utils.ts b/app/utils.ts index 67d008b9356..3db8e7c41b9 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -178,10 +178,10 @@ export function getCSSVar(varName: string) { * Detects if the Operation system is MacOS */ export function isMacOS(): boolean { - if (window !== 'undefined') { - let userAgent = window?.navigator?.userAgent - if (userAgent.indexOf('Mac') != -1) return true - } + if (typeof window !== "undefined") { + let userAgent = window?.navigator?.userAgent; + if (userAgent.indexOf("Mac") != -1) return true; + } - return false + return false; } From f1ca03e3788d89295f889eb67da270adbee6137f Mon Sep 17 00:00:00 2001 From: EricGit Date: Thu, 28 Sep 2023 13:21:17 -0400 Subject: [PATCH 108/202] [FIXED] now it should detects all macintosh --- app/utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 3db8e7c41b9..1b76285cc07 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -175,13 +175,13 @@ export function getCSSVar(varName: string) { } /** - * Detects if the Operation system is MacOS + * Detects Macintosh */ export function isMacOS(): boolean { if (typeof window !== "undefined") { - let userAgent = window?.navigator?.userAgent; - if (userAgent.indexOf("Mac") != -1) return true; + let userAgent = window.navigator.userAgent.toLocaleLowerCase(); + const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent) + return !!macintosh } - - return false; + return false } From 4a599e986f1c56c3e3543e61b9403bdd01593c37 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sat, 30 Sep 2023 22:16:34 +0700 Subject: [PATCH 109/202] UI Page [Auth Page] [+] feat(auth.tsx): add support for resetting access token in resetAccessCode function [+] fix(auth.tsx): fix formatting issue in resetAccessCode function [+] feat(locales): add support for sub tips in Auth component for multiple languages : - Add sub tips in Arabic locale (ar.ts) - Add sub tips in Bengali locale (bn.ts) - Add sub tips in Chinese locale (cn.ts) - Add sub tips in English locale (en.ts) - Add sub tips in Indonesian locale (id.ts) --- app/components/auth.tsx | 23 ++++++++++++++++++----- app/locales/ar.ts | 1 + app/locales/bn.ts | 1 + app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/locales/id.ts | 8 ++++++-- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 9a5b0c655e6..4e5ab8dc6a3 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -15,7 +15,7 @@ export function AuthPage() { const access = useAccessStore(); const goHome = () => navigate(Path.Home); - const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string + const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string useEffect(() => { if (getClientConfig()?.isApp) { @@ -42,6 +42,16 @@ export function AuthPage() { access.updateCode(e.currentTarget.value); }} /> +
{Locale.Auth.SubTips}
+ { + access.updateToken(e.currentTarget.value); + }} + />
- { - resetAccessCode(); - goHome(); - }} /> + { + resetAccessCode(); + goHome(); + }} + />
); diff --git a/app/locales/ar.ts b/app/locales/ar.ts index 520cb26356e..d5844acd695 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -10,6 +10,7 @@ const ar: PartialLocaleType = { Auth: { Title: "تحتاج إلى رمز الوصول", Tips: "يرجى إدخال رمز الوصول أدناه", + SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك", Input: "رمز الوصول", Confirm: "تأكيد", Later: "لاحقًا", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index 2d2266b3f43..2db132cecc2 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -10,6 +10,7 @@ const bn: PartialLocaleType = { Auth: { Title: "একটি অ্যাক্সেস কোড প্রয়োজন", Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন", + SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন", Input: "অ্যাক্সেস কোড", Confirm: "নিশ্চিত করুন", Later: "পরে", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index b2afc753457..8eeef9e013c 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -13,6 +13,7 @@ const cn = { Auth: { Title: "需要密码", Tips: "管理员开启了密码验证,请在下方填入访问码", + SubTips: "或者输入你的 OpenAI API 密钥", Input: "在此处填写访问码", Confirm: "确认", Later: "稍后再说", diff --git a/app/locales/en.ts b/app/locales/en.ts index 697d09d1f4e..458d53dd568 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -15,6 +15,7 @@ const en: LocaleType = { Auth: { Title: "Need Access Code", Tips: "Please enter access code below", + SubTips: "Or enter your OpenAI API Key", Input: "access code", Confirm: "Confirm", Later: "Later", diff --git a/app/locales/id.ts b/app/locales/id.ts index 244c5ade19a..4b61f6434dc 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -1,15 +1,19 @@ +import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; import { PartialLocaleType } from "./index"; +const isApp = !!getClientConfig()?.isApp; const id: PartialLocaleType = { WIP: "Coming Soon...", Error: { - Unauthorized: - "Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.", + Unauthorized: isApp + ? "Kunci API tidak valid, silakan periksa di halaman [Pengaturan](/#/settings)." + : "Akses tidak diizinkan, silakan masukkan kode akses di halaman [autentikasi](/#/auth), atau masukkan kunci API OpenAI Anda.", }, Auth: { Title: "Diperlukan Kode Akses", Tips: "Masukkan kode akses di bawah", + SubTips: "Atau masukkan kunci API OpenAI Anda", Input: "Kode Akses", Confirm: "Konfirmasi", Later: "Nanti", From bc00be9065a9ea1af8ab38925400f2bfd8dc10b3 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sat, 30 Sep 2023 22:33:18 +0700 Subject: [PATCH 110/202] Feat & Fix UI Page [Auth Page] [+] feat(auth.tsx): add goChat function to navigate to chat page [+] fix(auth.tsx): change onClick event from goHome to goChat --- app/components/auth.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 4e5ab8dc6a3..8e2b964ab3d 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -15,6 +15,7 @@ export function AuthPage() { const access = useAccessStore(); const goHome = () => navigate(Path.Home); + const goChat = () => navigate(Path.Chat); const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string useEffect(() => { @@ -57,7 +58,7 @@ export function AuthPage() { Date: Sat, 30 Sep 2023 22:59:24 +0700 Subject: [PATCH 111/202] Refactor Locale Indonesia [+] refactor(id.ts): remove unused import and isApp variable [+] fix(id.ts): update Unauthorized error message --- app/locales/id.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/locales/id.ts b/app/locales/id.ts index 4b61f6434dc..1036e66b64e 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -1,15 +1,11 @@ -import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; import { PartialLocaleType } from "./index"; -const isApp = !!getClientConfig()?.isApp; const id: PartialLocaleType = { WIP: "Coming Soon...", Error: { - Unauthorized: isApp - ? "Kunci API tidak valid, silakan periksa di halaman [Pengaturan](/#/settings)." - : "Akses tidak diizinkan, silakan masukkan kode akses di halaman [autentikasi](/#/auth), atau masukkan kunci API OpenAI Anda.", - }, + Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).", + }, Auth: { Title: "Diperlukan Kode Akses", Tips: "Masukkan kode akses di bawah", From 04b638aa064e346f72a6fcef295f151379c1be17 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sun, 1 Oct 2023 00:23:19 +0700 Subject: [PATCH 112/202] Fix & Refactor UI Page [Auth Page] [+] fix(auth.tsx): fix conditional rendering of token input field [+] refactor(auth.tsx): improve code readability by using conditional rendering for token input field --- app/components/auth.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 8e2b964ab3d..b82d0e894c7 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -43,16 +43,20 @@ export function AuthPage() { access.updateCode(e.currentTarget.value); }} /> -
{Locale.Auth.SubTips}
- { - access.updateToken(e.currentTarget.value); - }} - /> + {!access.hideUserApiKey ? ( + <> +
{Locale.Auth.SubTips}
+ { + access.updateToken(e.currentTarget.value); + }} + /> + + ) : null}
Date: Tue, 3 Oct 2023 08:08:11 +0700 Subject: [PATCH 113/202] Client App [Notification] [+] feat(global.d.ts): add support for window.__TAURI__.notification methods [+] feat(update.ts): add notification for new version availability [+] fix(Cargo.toml): add tauri feature "notification-all" to enable notifications [+] fix(tauri.conf.json): enable all notification features in tauri configuration --- app/global.d.ts | 5 +++++ app/store/update.ts | 29 +++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 6 ++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/global.d.ts b/app/global.d.ts index 524ce77dbc6..dc1d5265375 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -13,5 +13,10 @@ declare module "*.svg"; declare interface Window { __TAURI__?: { writeText(text: string): Promise; + notification:{ + requestPermission(): Promise; + isPermissionGranted(): Promise; + sendNotification(options: string | Options): void; + }; }; } diff --git a/app/store/update.ts b/app/store/update.ts index 42b86586c62..facb5732163 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -2,8 +2,10 @@ import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; import { api } from "../client/api"; import { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; +import ChatGptIcon from "../icons/chatgpt.png"; const ONE_MINUTE = 60 * 1000; +const isApp = !!getClientConfig()?.isApp; function formatVersionDate(t: string) { const d = new Date(+t); @@ -80,6 +82,33 @@ export const useUpdateStore = createPersistStore( set(() => ({ remoteVersion: remoteId, })); + if (window.__TAURI__?.notification && isApp) { + // Check if notification permission is granted + await window.__TAURI__?.notification.isPermissionGranted().then((granted) => { + if (!granted) { + // Send a notification without waiting for permission (because we don't neeed a permisison once client is already click "check") + window.__TAURI__?.notification.sendNotification({ + title: "ChatGPT Next Web", + body: `A new version (${remoteId}) is available.`, + icon: `${ChatGptIcon.src}`, + sound: "Default" + }); + } else { + // Request permission to show notifications + window.__TAURI__?.notification.requestPermission().then((permission) => { + if (permission === 'granted') { + // Show a notification using Tauri + window.__TAURI__?.notification.sendNotification({ + title: "ChatGPT Next Web", + body: `A new version (${remoteId}) is available.`, + icon: `${ChatGptIcon.src}`, + sound: "Default" + }); + } + }); + } + }); + } console.log("[Got Upstream] ", remoteId); } catch (error) { console.error("[Fetch Upstream Commit Id]", error); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ac5d04e836e..fee1c860fb9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] } +tauri = { version = "1.3.0", features = ["notification-all", "fs-all", "clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } [features] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 77b02a3bae8..147fc9944f6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -44,6 +44,12 @@ "startDragging": true, "unmaximize": true, "unminimize": true + }, + "fs": { + "all": true + }, + "notification": { + "all": true } }, "bundle": { From d2ad01a9ffd8fd0645013aca862c691af3c2f01f Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 3 Oct 2023 08:49:03 +0700 Subject: [PATCH 114/202] Client App Fix Issue [Bug] 'export' button does not work #2884 [+] fix(exporter.tsx): add async keyword to download function [+] feat(exporter.tsx): add support for saving image file using window.__TAURI__ API [+] feat(global.d.ts): add types for window.__TAURI__ API methods [+] feat(locales): add translations for download success and failure messages [+] feat(sync.ts): add support for generating backup file name with date and time [+] fix(utils.ts): add async keyword to downloadAs function and add support for saving file using window.__TAURI__ API --- app/components/exporter.tsx | 58 ++++++++++++++++++++++++++++--------- app/global.d.ts | 7 +++++ app/locales/cn.ts | 4 +++ app/locales/en.ts | 4 +++ app/locales/id.ts | 4 +++ app/store/sync.ts | 8 ++++- app/utils.ts | 43 ++++++++++++++++++++++----- 7 files changed, 106 insertions(+), 22 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 5b3e8a9a180..0a885d87463 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -433,25 +433,55 @@ export function ImagePreviewer(props: { const isMobile = useMobileScreen(); - const download = () => { + const download = async () => { showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; - toPng(dom) - .then((blob) => { - if (!blob) return; - - if (isMobile || getClientConfig()?.isApp) { - showImageModal(blob); + + const isApp = getClientConfig()?.isApp; + + try { + const blob = await toPng(dom); + if (!blob) return; + + if (isMobile || (isApp && window.__TAURI__)) { + if (isApp && window.__TAURI__) { + const result = await window.__TAURI__.dialog.save({ + defaultPath: `${props.topic}.png`, + filters: [ + { + name: "PNG Files", + extensions: ["png"], + }, + { + name: "All Files", + extensions: ["*"], + }, + ], + }); + + if (result !== null) { + const response = await fetch(blob); + const buffer = await response.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + await window.__TAURI__.fs.writeBinaryFile(result, uint8Array); + showToast(Locale.Download.Success); + } else { + showToast(Locale.Download.Failed); + } } else { - const link = document.createElement("a"); - link.download = `${props.topic}.png`; - link.href = blob; - link.click(); - refreshPreview(); + showImageModal(blob); } - }) - .catch((e) => console.log("[Export Image] ", e)); + } else { + const link = document.createElement("a"); + link.download = `${props.topic}.png`; + link.href = blob; + link.click(); + refreshPreview(); + } + } catch (error) { + showToast(Locale.Download.Failed); + } }; const refreshPreview = () => { diff --git a/app/global.d.ts b/app/global.d.ts index dc1d5265375..e0a2c3f0686 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -13,6 +13,13 @@ declare module "*.svg"; declare interface Window { __TAURI__?: { writeText(text: string): Promise; + invoke(command: string, payload?: Record): Promise; + dialog: { + save(options?: Record): Promise; + }; + fs: { + writeBinaryFile(path: string, data: Uint8Array): Promise; + }; notification:{ requestPermission(): Promise; isPermissionGranted(): Promise; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index b2afc753457..746f3580bd0 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -323,6 +323,10 @@ const cn = { Success: "已写入剪切板", Failed: "复制失败,请赋予剪切板权限", }, + Download: { + Success: "内容已下载到您的目录。", + Failed: "下载失败。", + }, Context: { Toast: (x: any) => `包含 ${x} 条预设提示词`, Edit: "当前对话设置", diff --git a/app/locales/en.ts b/app/locales/en.ts index 697d09d1f4e..62823b05197 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -329,6 +329,10 @@ const en: LocaleType = { Success: "Copied to clipboard", Failed: "Copy failed, please grant permission to access clipboard", }, + Download: { + Success: "Content downloaded to your directory.", + Failed: "Download failed.", + }, Context: { Toast: (x: any) => `With ${x} contextual prompts`, Edit: "Current Chat Settings", diff --git a/app/locales/id.ts b/app/locales/id.ts index 244c5ade19a..7e1366f964e 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -301,6 +301,10 @@ const id: PartialLocaleType = { Failed: "Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)", }, + Download: { + Success: "Konten berhasil diunduh ke direktori Anda.", + Failed: "Unduhan gagal.", + }, Context: { Toast: (x: any) => `Dengan ${x} promp kontekstual`, Edit: "Pengaturan Obrolan Saat Ini", diff --git a/app/store/sync.ts b/app/store/sync.ts index c194162fcb0..c34ae7b9bbd 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -1,3 +1,4 @@ +import { getClientConfig } from "../config/client"; import { Updater } from "../typing"; import { ApiPath, STORAGE_KEY, StoreKey } from "../constant"; import { createPersistStore } from "../utils/store"; @@ -20,6 +21,7 @@ export interface WebDavConfig { password: string; } +const isApp = !!getClientConfig()?.isApp; export type SyncStore = GetStoreState; const DEFAULT_SYNC_STATE = { @@ -57,7 +59,11 @@ export const useSyncStore = createPersistStore( export() { const state = getLocalAppState(); - const fileName = `Backup-${new Date().toLocaleString()}.json`; + const datePart = isApp + ? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}` + : new Date().toLocaleString(); + + const fileName = `Backup-${datePart}.json`; downloadAs(JSON.stringify(state), fileName); }, diff --git a/app/utils.ts b/app/utils.ts index 37c17dd760d..19e55ce63e0 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) { } } -export function downloadAs(text: string, filename: string) { - const element = document.createElement("a"); - element.setAttribute( - "href", - "data:text/plain;charset=utf-8," + encodeURIComponent(text), - ); +export async function downloadAs(text: string, filename: string) { + if (window.__TAURI__) { + const result = await window.__TAURI__.dialog.save({ + defaultPath: `${filename}`, + filters: [ + { + name: `${filename.split('.').pop()} files`, + extensions: [`${filename.split('.').pop()}`], + }, + { + name: "All Files", + extensions: ["*"], + }, + ], + }); + + if (result !== null) { + try { + await window.__TAURI__.fs.writeBinaryFile( + result, + new Uint8Array([...text].map((c) => c.charCodeAt(0))) + ); + showToast(Locale.Download.Success); + } catch (error) { + showToast(Locale.Download.Failed); + } + } else { + showToast(Locale.Download.Failed); + } + } else { + const element = document.createElement("a"); + element.setAttribute( + "href", + "data:text/plain;charset=utf-8," + encodeURIComponent(text), + ); element.setAttribute("download", filename); element.style.display = "none"; @@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) { document.body.removeChild(element); } - +} export function readFromFile() { return new Promise((res, rej) => { const fileInput = document.createElement("input"); From ddfd05b008a80238eea870eba7e84e142dd74c47 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 3 Oct 2023 09:12:41 +0700 Subject: [PATCH 115/202] Fix & Feat Client App [Notification] [+] fix(update.ts): remove unnecessary notification sending when permission is not granted [+] feat(update.ts): add notification for already up to date version --- app/store/update.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/app/store/update.ts b/app/store/update.ts index facb5732163..5a08e36a3f5 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -86,24 +86,28 @@ export const useUpdateStore = createPersistStore( // Check if notification permission is granted await window.__TAURI__?.notification.isPermissionGranted().then((granted) => { if (!granted) { - // Send a notification without waiting for permission (because we don't neeed a permisison once client is already click "check") - window.__TAURI__?.notification.sendNotification({ - title: "ChatGPT Next Web", - body: `A new version (${remoteId}) is available.`, - icon: `${ChatGptIcon.src}`, - sound: "Default" - }); + return } else { // Request permission to show notifications window.__TAURI__?.notification.requestPermission().then((permission) => { if (permission === 'granted') { - // Show a notification using Tauri - window.__TAURI__?.notification.sendNotification({ - title: "ChatGPT Next Web", - body: `A new version (${remoteId}) is available.`, - icon: `${ChatGptIcon.src}`, - sound: "Default" - }); + if (version === remoteId) { + // Show a notification using Tauri + window.__TAURI__?.notification.sendNotification({ + title: "ChatGPT Next Web", + body: "Already up to date", + icon: `${ChatGptIcon.src}`, + sound: "Default" + }); + } else { + // Show a notification for the new version using Tauri + window.__TAURI__?.notification.sendNotification({ + title: "ChatGPT Next Web", + body: `A new version (${remoteId}) is available.`, + icon: `${ChatGptIcon.src}`, + sound: "Default" + }); + } } }); } From b558d1afc6a95d917500064bb77870864ccc1958 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 4 Oct 2023 02:10:26 +0700 Subject: [PATCH 116/202] Feat & Fix "Client App [Notification]" [+] feat(update.ts): add support for localization in update notifications [+] fix(update.ts): add missing semicolon in useUpdateStore function --- app/store/update.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/store/update.ts b/app/store/update.ts index 5a08e36a3f5..2b088a13d7a 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -3,6 +3,7 @@ import { api } from "../client/api"; import { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; import ChatGptIcon from "../icons/chatgpt.png"; +import Locale from "../locales"; const ONE_MINUTE = 60 * 1000; const isApp = !!getClientConfig()?.isApp; @@ -86,7 +87,7 @@ export const useUpdateStore = createPersistStore( // Check if notification permission is granted await window.__TAURI__?.notification.isPermissionGranted().then((granted) => { if (!granted) { - return + return; } else { // Request permission to show notifications window.__TAURI__?.notification.requestPermission().then((permission) => { @@ -95,15 +96,16 @@ export const useUpdateStore = createPersistStore( // Show a notification using Tauri window.__TAURI__?.notification.sendNotification({ title: "ChatGPT Next Web", - body: "Already up to date", + body: `${Locale.Settings.Update.IsLatest}`, icon: `${ChatGptIcon.src}`, sound: "Default" }); } else { + const updateMessage = Locale.Settings.Update.FoundUpdate(`${remoteId}`); // Show a notification for the new version using Tauri window.__TAURI__?.notification.sendNotification({ title: "ChatGPT Next Web", - body: `A new version (${remoteId}) is available.`, + body: updateMessage, icon: `${ChatGptIcon.src}`, sound: "Default" }); From ad5093ce0593e21df3e7c782a0396725662a425a Mon Sep 17 00:00:00 2001 From: KeithHello Date: Wed, 4 Oct 2023 16:38:28 +0900 Subject: [PATCH 117/202] Change log - config.ts - line 72 remove type confirmation of x as x always has type 'number' - line 135 remove redundant local variable - chat.ts - delete unused import --- app/locales/index.ts | 2 +- app/store/chat.ts | 3 --- app/store/config.ts | 10 ++++------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/locales/index.ts b/app/locales/index.ts index 79e314facdd..a32d3207c74 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -65,7 +65,7 @@ export const ALL_LANG_OPTIONS: Record = { }; const LANG_KEY = "lang"; -const DEFAULT_LANG = "en"; +const DEFAULT_LANG = "cn"; const fallbackLang = en; const targetLang = ALL_LANGS[getLang()] as LocaleType; diff --git a/app/store/chat.ts b/app/store/chat.ts index 269cc4a33c9..56ac8db6cc1 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,6 +1,3 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - import { trimTopic } from "../utils"; import Locale, { getLang } from "../locales"; diff --git a/app/store/config.ts b/app/store/config.ts index 956e5f3eb81..b6488479596 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -46,13 +46,13 @@ export const DEFAULT_CONFIG = { modelConfig: { model: "gpt-3.5-turbo" as ModelType, - temperature: 0.5, + temperature: 0.6, top_p: 1, max_tokens: 2000, presence_penalty: 0, frequency_penalty: 0, sendMemory: true, - historyMessageCount: 4, + historyMessageCount: 1, compressMessageLengthThreshold: 1000, enableInjectSystemPrompts: true, template: DEFAULT_INPUT_TEMPLATE, @@ -69,7 +69,7 @@ export function limitNumber( max: number, defaultValue: number, ) { - if (typeof x !== "number" || isNaN(x)) { + if (isNaN(x)) { return defaultValue; } @@ -132,9 +132,7 @@ export const useAppConfig = createPersistStore( .customModels.split(",") .filter((v) => !!v && v.length > 0) .map((m) => ({ name: m, available: true })); - - const models = get().models.concat(customModels); - return models; + return get().models.concat(customModels); }, }), { From 1505372e204d8363ec9c67a61d004806de81c200 Mon Sep 17 00:00:00 2001 From: KeithHello Date: Wed, 4 Oct 2023 18:08:29 +0900 Subject: [PATCH 118/202] Change log - config.ts - line 72: remove type confirmation of x as x always has type 'number' - line 135: remove the redundant local variable - chat.ts - delete the unused import --- app/locales/index.ts | 2 +- app/store/config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/locales/index.ts b/app/locales/index.ts index a32d3207c74..79e314facdd 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -65,7 +65,7 @@ export const ALL_LANG_OPTIONS: Record = { }; const LANG_KEY = "lang"; -const DEFAULT_LANG = "cn"; +const DEFAULT_LANG = "en"; const fallbackLang = en; const targetLang = ALL_LANGS[getLang()] as LocaleType; diff --git a/app/store/config.ts b/app/store/config.ts index b6488479596..ca230cc3c76 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -46,13 +46,13 @@ export const DEFAULT_CONFIG = { modelConfig: { model: "gpt-3.5-turbo" as ModelType, - temperature: 0.6, + temperature: 0.5, top_p: 1, max_tokens: 2000, presence_penalty: 0, frequency_penalty: 0, sendMemory: true, - historyMessageCount: 1, + historyMessageCount: 4, compressMessageLengthThreshold: 1000, enableInjectSystemPrompts: true, template: DEFAULT_INPUT_TEMPLATE, From 88f8c43472db225184287640fb09eae9aa122b3f Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Tue, 3 Oct 2023 21:09:41 +0800 Subject: [PATCH 119/202] Improve tw locale --- app/locales/tw.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 15f6648e659..e9f38d097e1 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -7,13 +7,13 @@ const tw: PartialLocaleType = { Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。", }, ChatItem: { - ChatItemCount: (count: number) => `${count} 條對話`, + ChatItemCount: (count: number) => `${count} 則對話`, }, Chat: { - SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`, + SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 則對話`, Actions: { - ChatList: "查看訊息列表", - CompressedHistory: "查看壓縮後的歷史 Prompt", + ChatList: "檢視訊息列表", + CompressedHistory: "檢視壓縮後的歷史 Prompt", Export: "匯出聊天紀錄", Copy: "複製", Stop: "停止", @@ -23,15 +23,15 @@ const tw: PartialLocaleType = { Rename: "重新命名對話", Typing: "正在輸入…", Input: (submitKey: string) => { - var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`; + var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可傳送`; if (submitKey === String(SubmitKey.Enter)) { inputHints += ",Shift + Enter 鍵換行"; } return inputHints; }, - Send: "發送", + Send: "傳送", Config: { - Reset: "重置預設", + Reset: "重設", SaveAs: "另存新檔", }, }, @@ -46,7 +46,7 @@ const tw: PartialLocaleType = { Title: "上下文記憶 Prompt", EmptyContent: "尚未記憶", Copy: "複製全部", - Send: "發送記憶", + Send: "傳送記憶", Reset: "重設對話", ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?", }, @@ -71,22 +71,22 @@ const tw: PartialLocaleType = { }, InjectSystemPrompts: { Title: "匯入系統提示", - SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示", + SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示", }, Update: { - Version: (x: string) => `當前版本:${x}`, + Version: (x: string) => `目前版本:${x}`, IsLatest: "已是最新版本", CheckUpdate: "檢查更新", IsChecking: "正在檢查更新...", FoundUpdate: (x: string) => `發現新版本:${x}`, GoToUpdate: "前往更新", }, - SendKey: "發送鍵", + SendKey: "傳送鍵", Theme: "主題", TightBorder: "緊湊邊框", SendPreviewBubble: { Title: "預覽氣泡", - SubTitle: "在預覽氣泡中預覽 Markdown 内容", + SubTitle: "在預覽氣泡中預覽 Markdown 內容", }, Mask: { Splash: { @@ -101,7 +101,7 @@ const tw: PartialLocaleType = { }, List: "自定義提示詞列表", ListCount: (builtin: number, custom: number) => - `內建 ${builtin} 條,用戶定義 ${custom} 條`, + `內建 ${builtin} 條,使用者定義 ${custom} 條`, Edit: "編輯", Modal: { Title: "提示詞列表", @@ -132,7 +132,7 @@ const tw: PartialLocaleType = { }, IsChecking: "正在檢查…", Check: "重新檢查", - NoAccess: "輸入API Key查看餘額", + NoAccess: "輸入 API Key 檢視餘額", }, AccessCode: { Title: "授權碼", @@ -150,7 +150,7 @@ const tw: PartialLocaleType = { }, PresencePenalty: { Title: "話題新穎度 (presence_penalty)", - SubTitle: "值越大,越有可能擴展到新話題", + SubTitle: "值越大,越有可能拓展到新話題", }, FrequencyPenalty: { Title: "頻率懲罰度 (frequency_penalty)", @@ -163,7 +163,7 @@ const tw: PartialLocaleType = { Error: "出錯了,請稍後再嘗試", Prompt: { History: (content: string) => - "這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content, + "這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content, Topic: "Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.", Summarize: @@ -192,16 +192,16 @@ const tw: PartialLocaleType = { Item: { Info: (count: number) => `包含 ${count} 條預設對話`, Chat: "對話", - View: "查看", + View: "檢視", Edit: "編輯", - Delete: "删除", - DeleteConfirm: "確認删除?", + Delete: "刪除", + DeleteConfirm: "確認刪除?", }, EditModal: { Title: (readonly: boolean) => - `編輯預設面具 ${readonly ? "(只读)" : ""}`, + `編輯預設面具 ${readonly ? "(只讀)" : ""}`, Download: "下載預設", - Clone: "克隆預設", + Clone: "複製預設", }, Config: { Avatar: "角色頭像", @@ -215,7 +215,7 @@ const tw: PartialLocaleType = { SubTitle: "現在開始,與面具背後的靈魂思維碰撞", More: "搜尋更多", NotShow: "不再呈現", - ConfirmNoShow: "確認禁用?禁用後可以随時在設定中重新啟用。", + ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。", }, UI: { Confirm: "確認", From e35c807216a3813cd902dc3c2cf20b818c639242 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sat, 7 Oct 2023 23:48:50 +0800 Subject: [PATCH 120/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 77b02a3bae8..8161e79e8e3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.7" + "version": "2.9.8" }, "tauri": { "allowlist": { From bdb49b11711977638a41845de36c6ea4166f2ef2 Mon Sep 17 00:00:00 2001 From: Kong Gaowen Date: Mon, 9 Oct 2023 18:50:10 +0800 Subject: [PATCH 121/202] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 似乎这里引入了一个逻辑判断错误,会导致打包之后的界面变宽变高。 --- app/components/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 07d5e88b373..dd22142d7c3 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -129,7 +129,7 @@ function Screen() { const isAuth = location.pathname === Path.Auth; const isMobileScreen = useMobileScreen(); const shouldTightBorder = - config.tightBorder && !isMobileScreen && !getClientConfig()?.isApp; + config.tightBorder && !isMobileScreen && getClientConfig()?.isApp; useEffect(() => { loadAsyncGoogleFont(); From f21f922160a6f024d0fe5e52a0ced59144c2d2cc Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 9 Oct 2023 22:46:36 +0800 Subject: [PATCH 122/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 68f9c07c037..e530203f680 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.8" + "version": "2.9.9" }, "tauri": { "allowlist": { From f54db695af55ea925369950be9b1b8988461544b Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 10 Oct 2023 11:54:25 +0800 Subject: [PATCH 123/202] fix: #2981 full screen button not works --- app/components/home.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index dd22142d7c3..811cbdf51cb 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -128,8 +128,7 @@ function Screen() { const isHome = location.pathname === Path.Home; const isAuth = location.pathname === Path.Auth; const isMobileScreen = useMobileScreen(); - const shouldTightBorder = - config.tightBorder && !isMobileScreen && getClientConfig()?.isApp; + const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen); useEffect(() => { loadAsyncGoogleFont(); From ea59ab51767fc0fe3e7ad611bf2f47fb357ddc11 Mon Sep 17 00:00:00 2001 From: Jason O'Gray <1228915+ograycode@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:24:45 -0400 Subject: [PATCH 124/202] Fix type in sync.ts Simple typo fix. --- app/store/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/sync.ts b/app/store/sync.ts index c34ae7b9bbd..b74f6895f6d 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -101,7 +101,7 @@ export const useSyncStore = createPersistStore( mergeAppState(localState, remoteState); setLocalAppState(localState); } catch (e) { - console.log("[Sync] failed to get remoate state", e); + console.log("[Sync] failed to get remote state", e); } await client.set(config.username, JSON.stringify(localState)); From 74fcaab5e9aa9538bff31e01f669dd9811a64c26 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 11 Oct 2023 04:36:04 +0700 Subject: [PATCH 125/202] Refactor Mask UI Page [Masks] [+] refactor(mask.tsx): refactor simple search to be case-insensitive --- app/components/mask.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 1ee1c239a74..9fe1d485a6b 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -393,11 +393,13 @@ export function MaskPage() { const [searchText, setSearchText] = useState(""); const masks = searchText.length > 0 ? searchMasks : allMasks; - // simple search, will refactor later + // refactored already, now it accurate const onSearch = (text: string) => { setSearchText(text); if (text.length > 0) { - const result = allMasks.filter((m) => m.name.includes(text)); + const result = allMasks.filter((m) => + m.name.toLowerCase().includes(text.toLowerCase()) + ); setSearchMasks(result); } else { setSearchMasks(allMasks); From ec33281ff5f4856e056d660861699198b584ee92 Mon Sep 17 00:00:00 2001 From: mcheping520 Date: Wed, 11 Oct 2023 21:29:50 +0800 Subject: [PATCH 126/202] =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E6=95=99=E7=A8=8B=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E7=BF=BB=E8=AF=91=E5=A4=9A=E5=9B=BD=E8=AF=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 ++++++++++++++++++++++++++++++++ README_CN.md | 32 ++++++++++++++++++++++++++++++++ README_JA.md | 32 ++++++++++++++++++++++++++++++++ README_KO.md | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) diff --git a/README.md b/README.md index 07455d00d82..6474f63e765 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,38 @@ If your proxy needs password, use: bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` +## Synchronizing Chat Records (UpStash) +### Prerequisites +- GitHub account +- Your own ChatGPT-Next-Web server set up +- [UpStash](https://upstash.com) + +### Getting Started +1. Register for an UpStash account. +2. Create a Database. + + ![Register and Log In](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) + + ![Create a Database](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) + + ![Select a Server](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) + +3. Find the REST API and copy both UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Caution⚠: Do not disclose your Token!) + + ![Copy](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) + +4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, and click **Check Availability**. + + ![Synchronization Step 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) + + If everything is in order, you've succeeded. + + ![Synchronization Availability Check Completed](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) + +5. Success! + + ![Great job!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) + ## Documentation > Please go to the [docs][./docs] directory for more documentation instructions. diff --git a/README_CN.md b/README_CN.md index b31e8b367ee..a3dd5c64bbb 100644 --- a/README_CN.md +++ b/README_CN.md @@ -169,6 +169,38 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。 +## 同步聊天记录(upStash) +### 准备工作 +- GitHub账号 +- 拥有自己搭建过的ChatGPT-Next-Web的服务器 +- [UpStash](https://upstash.com) + +### 开始教程 +1. 注册UpStash账号 +2. 创建数据库 + + ![注册登录](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) + + ![创建数据库](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) + + ![选择服务器](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) + +3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) + + ![复制](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) + +4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** + + ![同步1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) + + 如果没什么问题,那就成功了 + + ![同步可用性完成的样子](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) + +5. Success! + + ![好耶~!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) + ## 鸣谢 ### 捐赠者 diff --git a/README_JA.md b/README_JA.md index 72a0d5373f6..4735326d4d6 100644 --- a/README_JA.md +++ b/README_JA.md @@ -223,6 +223,38 @@ docker run -d -p 3000:3000 \ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` +## チャット履歴の同期(UpStash) +### 必要なもの +- GitHub アカウント +- 独自の ChatGPT-Next-Web サーバーのセットアップ +- [UpStash](https://upstash.com) + +### はじめに +1. UpStash アカウントを登録します。 +2. データベースを作成します。 + + ![登録とログイン](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) + + ![データベースの作成](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) + + ![サーバーの選択](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) + +3. REST API を見つけ、UPSTASH_REDIS_REST_URL および UPSTASH_REDIS_REST_TOKEN の両方をコピーします(⚠注意⚠:トークンを公開しないでください!)。 + + ![コピー](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) + +4. UPSTASH_REDIS_REST_URL および UPSTASH_REDIS_REST_TOKEN を同期設定にコピーし、**可用性を確認** をクリックします。 + + ![同期ステップ 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) + + すべてが正常であれば、成功です。 + + ![同期の可用性チェックが完了しました](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) + +5. 成功! + + ![おめでとうございます!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) + ## スクリーンショット ![Settings](./docs/images/settings.png) diff --git a/README_KO.md b/README_KO.md index 519dd9d9bb5..c0220168c83 100644 --- a/README_KO.md +++ b/README_KO.md @@ -169,6 +169,38 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요. +## 채팅 기록 동기화 (UpStash) +### 사전 요구 사항 +- GitHub 계정 +- 자체 ChatGPT-Next-Web 서버 설치 +- [UpStash](https://upstash.com) + +### 시작하기 +1. UpStash 계정 등록. +2. 데이터베이스 만들기. + + ![등록 및 로그인](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) + + ![데이터베이스 만들기](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) + + ![서버 선택](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) + +3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 노출하지 마세요!). + + ![복사](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) + +4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사하고 **가용성 확인**을 클릭합니다. + + ![동기화 단계 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) + + 모든 것이 정상이라면, 성공입니다. + + ![동기화 가능성 확인 완료](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) + +5. 성공! + + ![잘 했어요!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) + ## 감사의 말 ### 기부자 From 1e77df381a6b0d4eac1eeefb2610ae00780d0042 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 12 Oct 2023 17:31:29 +0800 Subject: [PATCH 127/202] Update constant.ts --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index 9e23ed510e4..e03e00971cc 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_CORS_HOST = "https://nb.nextweb.fun"; +export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun"; export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export enum Path { From b5d3348d996c5ca65aef870f4435cf1610a5ace0 Mon Sep 17 00:00:00 2001 From: mcheping520 Date: Thu, 12 Oct 2023 18:17:38 +0800 Subject: [PATCH 128/202] =?UTF-8?q?=E5=B7=B2=E6=8B=86=E5=88=86=E5=8D=95?= =?UTF-8?q?=E4=B8=AA=E6=96=87=E4=BB=B6=E3=80=81=E7=BF=BB=E8=AF=91=E5=A4=9A?= =?UTF-8?q?=E5=9B=BD=E8=AF=AD=E8=A8=80=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 +---------------------------- README_CN.md | 31 ------------------------------ README_JA.md | 32 ------------------------------- README_KO.md | 31 ------------------------------ docs/images/upstash-1.png | Bin 0 -> 23846 bytes docs/images/upstash-2.png | Bin 0 -> 66350 bytes docs/images/upstash-3.png | Bin 0 -> 50434 bytes docs/images/upstash-4.png | Bin 0 -> 37931 bytes docs/images/upstash-5.png | Bin 0 -> 68485 bytes docs/images/upstash-6.png | Bin 0 -> 67791 bytes docs/images/upstash-7.png | Bin 0 -> 85775 bytes docs/synchronise-chat-logs-cn.md | 31 ++++++++++++++++++++++++++++++ docs/synchronise-chat-logs-en.md | 31 ++++++++++++++++++++++++++++++ docs/synchronise-chat-logs-es.md | 31 ++++++++++++++++++++++++++++++ docs/synchronise-chat-logs-ja.md | 31 ++++++++++++++++++++++++++++++ docs/synchronise-chat-logs-ko.md | 31 ++++++++++++++++++++++++++++++ 16 files changed, 156 insertions(+), 123 deletions(-) create mode 100644 docs/images/upstash-1.png create mode 100644 docs/images/upstash-2.png create mode 100644 docs/images/upstash-3.png create mode 100644 docs/images/upstash-4.png create mode 100644 docs/images/upstash-5.png create mode 100644 docs/images/upstash-6.png create mode 100644 docs/images/upstash-7.png create mode 100644 docs/synchronise-chat-logs-cn.md create mode 100644 docs/synchronise-chat-logs-en.md create mode 100644 docs/synchronise-chat-logs-es.md create mode 100644 docs/synchronise-chat-logs-ja.md create mode 100644 docs/synchronise-chat-logs-ko.md diff --git a/README.md b/README.md index 6474f63e765..3fe76f4f06f 100644 --- a/README.md +++ b/README.md @@ -258,36 +258,8 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ``` ## Synchronizing Chat Records (UpStash) -### Prerequisites -- GitHub account -- Your own ChatGPT-Next-Web server set up -- [UpStash](https://upstash.com) -### Getting Started -1. Register for an UpStash account. -2. Create a Database. - - ![Register and Log In](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) - - ![Create a Database](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) - - ![Select a Server](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) - -3. Find the REST API and copy both UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Caution⚠: Do not disclose your Token!) - - ![Copy](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) - -4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, and click **Check Availability**. - - ![Synchronization Step 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) - - If everything is in order, you've succeeded. - - ![Synchronization Availability Check Completed](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) - -5. Success! - - ![Great job!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) +| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md) ## Documentation diff --git a/README_CN.md b/README_CN.md index a3dd5c64bbb..ce9309fd2a1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -169,37 +169,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。 -## 同步聊天记录(upStash) -### 准备工作 -- GitHub账号 -- 拥有自己搭建过的ChatGPT-Next-Web的服务器 -- [UpStash](https://upstash.com) - -### 开始教程 -1. 注册UpStash账号 -2. 创建数据库 - - ![注册登录](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) - - ![创建数据库](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) - - ![选择服务器](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) - -3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) - - ![复制](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) - -4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** - - ![同步1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) - - 如果没什么问题,那就成功了 - - ![同步可用性完成的样子](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) - -5. Success! - - ![好耶~!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) ## 鸣谢 diff --git a/README_JA.md b/README_JA.md index 4735326d4d6..72a0d5373f6 100644 --- a/README_JA.md +++ b/README_JA.md @@ -223,38 +223,6 @@ docker run -d -p 3000:3000 \ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` -## チャット履歴の同期(UpStash) -### 必要なもの -- GitHub アカウント -- 独自の ChatGPT-Next-Web サーバーのセットアップ -- [UpStash](https://upstash.com) - -### はじめに -1. UpStash アカウントを登録します。 -2. データベースを作成します。 - - ![登録とログイン](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) - - ![データベースの作成](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) - - ![サーバーの選択](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) - -3. REST API を見つけ、UPSTASH_REDIS_REST_URL および UPSTASH_REDIS_REST_TOKEN の両方をコピーします(⚠注意⚠:トークンを公開しないでください!)。 - - ![コピー](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) - -4. UPSTASH_REDIS_REST_URL および UPSTASH_REDIS_REST_TOKEN を同期設定にコピーし、**可用性を確認** をクリックします。 - - ![同期ステップ 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) - - すべてが正常であれば、成功です。 - - ![同期の可用性チェックが完了しました](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) - -5. 成功! - - ![おめでとうございます!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) - ## スクリーンショット ![Settings](./docs/images/settings.png) diff --git a/README_KO.md b/README_KO.md index c0220168c83..6ec7fe0e678 100644 --- a/README_KO.md +++ b/README_KO.md @@ -169,37 +169,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요. -## 채팅 기록 동기화 (UpStash) -### 사전 요구 사항 -- GitHub 계정 -- 자체 ChatGPT-Next-Web 서버 설치 -- [UpStash](https://upstash.com) - -### 시작하기 -1. UpStash 계정 등록. -2. 데이터베이스 만들기. - - ![등록 및 로그인](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-17-32.png) - - ![데이터베이스 만들기](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-24-04.png) - - ![서버 선택](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-25-49.png) - -3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 노출하지 마세요!). - - ![복사](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-28-12.png) - -4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사하고 **가용성 확인**을 클릭합니다. - - ![동기화 단계 1](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-04.png) - - 모든 것이 정상이라면, 성공입니다. - - ![동기화 가능성 확인 완료](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-33-52.png) - -5. 성공! - - ![잘 했어요!](https://mcheping520.gitee.io/graphic-bed/image/Snipaste_2023-10-11_19-34-15.png) ## 감사의 말 diff --git a/docs/images/upstash-1.png b/docs/images/upstash-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5813c521f3c9b4f33988d4a6257bc5dffe16b134 GIT binary patch literal 23846 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

oK?_RPpa_6X!+56upt8s(AbT(r5^|pbArE0^` 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 literal 0 HcmV?d00001 diff --git a/docs/images/upstash-3.png b/docs/images/upstash-3.png new file mode 100644 index 0000000000000000000000000000000000000000..06c53d190e14a5166f8967c37b10753d71ea4456 GIT binary patch literal 50434 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�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 VGU|OGIf8+qD5nZ3kum-B{{XAfItBm$ literal 0 HcmV?d00001 diff --git a/docs/images/upstash-4.png b/docs/images/upstash-4.png new file mode 100644 index 0000000000000000000000000000000000000000..ae301c4d349833777c748c260daae33f940a3f25 GIT binary patch literal 37931 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 literal 0 HcmV?d00001 diff --git a/docs/images/upstash-5.png b/docs/images/upstash-5.png new file mode 100644 index 0000000000000000000000000000000000000000..728a374600f5b568e4b0f94d8c71213015b85f0e GIT binary patch literal 68485 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%0D@n_}Gxxgz9VKTk&{Xgxc1 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 GfBp|ed?L&M literal 0 HcmV?d00001 diff --git a/docs/images/upstash-6.png b/docs/images/upstash-6.png new file mode 100644 index 0000000000000000000000000000000000000000..0991c89cf80ea13e7c9d451219e0a4a4f21d46d7 GIT binary patch literal 67791 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 literal 0 HcmV?d00001 diff --git a/docs/synchronise-chat-logs-cn.md b/docs/synchronise-chat-logs-cn.md new file mode 100644 index 00000000000..52d79268f8a --- /dev/null +++ b/docs/synchronise-chat-logs-cn.md @@ -0,0 +1,31 @@ +# 同步聊天记录(upStash) +## 准备工作 +- GitHub账号 +- 拥有自己搭建过的ChatGPT-Next-Web的服务器 +- [UpStash](https://upstash.com) + +## 开始教程 +1. 注册UpStash账号 +2. 创建数据库 + + ![注册登录](./images/upstash-1.png) + + ![创建数据库](./images/upstash-2.png) + + ![选择服务器](./images/upstash-3.png) + +3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!) + + ![复制](./images/upstash-4.png) + +4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性** + + ![同步1](./images/upstash-5.png) + + 如果没什么问题,那就成功了 + + ![同步可用性完成的样子](./images/upstash-6.png) + +5. Success! + + ![好耶~!](./images/upstash-7.png) diff --git a/docs/synchronise-chat-logs-en.md b/docs/synchronise-chat-logs-en.md new file mode 100644 index 00000000000..04d05607175 --- /dev/null +++ b/docs/synchronise-chat-logs-en.md @@ -0,0 +1,31 @@ +# Synchronize Chat Logs with UpStash +## Prerequisites +- GitHub account +- Your own ChatGPT-Next-Web server set up +- [UpStash](https://upstash.com) + +## Getting Started +1. Register for an UpStash account. +2. Create a database. + + ![Register and Login](./images/upstash-1.png) + + ![Create Database](./images/upstash-2.png) + + ![Select Server](./images/upstash-3.png) + +3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!) + + ![Copy](./images/upstash-4.png) + +4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**. + + ![Synchronize 1](./images/upstash-5.png) + + If everything is in order, you've successfully completed this step. + + ![Sync Availability Check Completed](./images/upstash-6.png) + +5. Success! + + ![Great job~!](./images/upstash-7.png) \ No newline at end of file diff --git a/docs/synchronise-chat-logs-es.md b/docs/synchronise-chat-logs-es.md new file mode 100644 index 00000000000..40135f1f620 --- /dev/null +++ b/docs/synchronise-chat-logs-es.md @@ -0,0 +1,31 @@ +# Sincronizzare i Log delle Chat con UpStash +## Prerequisiti +- Account GitHub +- Server ChatGPT-Next-Web di propria configurazione +- [UpStash](https://upstash.com) + +## Per iniziare +1. Registrarsi per un account UpStash. +2. Creare un database. + + ![Registrarsi ed Accedere](./images/upstash-1.png) + + ![Creare un Database](./images/upstash-2.png) + + ![Selezionare il Server](./images/upstash-3.png) + +3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!) + + ![Copia](./images/upstash-4.png) + +4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**. + + ![Sincronizzazione 1](./images/upstash-5.png) + + Se tutto è in ordine, hai completato con successo questa fase. + + ![Verifica la Disponibilità della Sincronizzazione Completata](./images/upstash-6.png) + +5. Successo! + + ![Ottimo lavoro~!](./images/upstash-7.png) \ No newline at end of file diff --git a/docs/synchronise-chat-logs-ja.md b/docs/synchronise-chat-logs-ja.md new file mode 100644 index 00000000000..ba75110fe68 --- /dev/null +++ b/docs/synchronise-chat-logs-ja.md @@ -0,0 +1,31 @@ +# UpStashを使用してチャットログを同期する +## 事前準備 +- GitHubアカウント +- 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること +- [UpStash](https://upstash.com) + +## 始める +1. UpStashアカウントを登録します。 +2. データベースを作成します。 + + ![登録とログイン](./images/upstash-1.png) + + ![データベースの作成](./images/upstash-2.png) + + ![サーバーの選択](./images/upstash-3.png) + +3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします(⚠重要⚠:トークンを共有しないでください!) + + ![コピー](./images/upstash-4.png) + +4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。 + + ![同期1](./images/upstash-5.png) + + すべてが正常であれば、このステップは成功です。 + + ![同期可用性チェックが完了しました](./images/upstash-6.png) + +5. 成功! + + ![お疲れ様でした~!](./images/upstash-7.png) \ No newline at end of file diff --git a/docs/synchronise-chat-logs-ko.md b/docs/synchronise-chat-logs-ko.md new file mode 100644 index 00000000000..88e6e2dda69 --- /dev/null +++ b/docs/synchronise-chat-logs-ko.md @@ -0,0 +1,31 @@ +# UpStash를 사용하여 채팅 기록 동기화 +## 사전 준비물 +- GitHub 계정 +- 자체 ChatGPT-Next-Web 서버 설정 +- [UpStash](https://upstash.com) + +## 시작하기 +1. UpStash 계정 등록 +2. 데이터베이스 생성 + + ![등록 및 로그인](./images/upstash-1.png) + + ![데이터베이스 생성](./images/upstash-2.png) + + ![서버 선택](./images/upstash-3.png) + +3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!) + + ![복사](./images/upstash-4.png) + +4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다. + + ![동기화 1](./images/upstash-5.png) + + 모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다. + + ![동기화 가용성 확인 완료](./images/upstash-6.png) + +5. 성공! + + ![잘 했어요~!](./images/upstash-7.png) \ No newline at end of file From 986d34fb3e3ba4a871f1f94609a3d1cae6b6b91b Mon Sep 17 00:00:00 2001 From: Surav Shrestha Date: Thu, 12 Oct 2023 21:22:35 +0545 Subject: [PATCH 129/202] docs: fix typo in app/masks/en.ts --- app/masks/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/masks/en.ts b/app/masks/en.ts index 1ab40d59b03..ed130351f1e 100644 --- a/app/masks/en.ts +++ b/app/masks/en.ts @@ -35,7 +35,7 @@ export const EN_MASKS: BuiltinMask[] = [ id: "prompt-improve-0", role: "user", content: - 'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.', + 'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finished and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.', date: "", }, { From 0dcbfd746e00b722d5b378f760684428f022e5ff Mon Sep 17 00:00:00 2001 From: Jesse <43443357+jessegpt@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:41:00 +0400 Subject: [PATCH 130/202] fix serviceWorker cache --- public/serviceWorker.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/public/serviceWorker.js b/public/serviceWorker.js index f5a24b70176..2e1d60b98d2 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -1,15 +1,21 @@ const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; +importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js'); + self.addEventListener("activate", function (event) { console.log("ServiceWorker activated."); }); -self.addEventListener("install", function (event) { - event.waitUntil( - caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) { - return cache.addAll([]); - }), - ); +workbox.core.clientsClaim(); +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } }); -self.addEventListener("fetch", (e) => {}); +workbox.routing.registerRoute( + new RegExp('/*'), + new workbox.strategies.StaleWhileRevalidate({ + cacheName: CHATGPT_NEXT_WEB_CACHE + }) +); From f5a2ce52aa1cdbf0c6fe8f9b1e053e0cd75651c2 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sat, 14 Oct 2023 15:22:41 +0900 Subject: [PATCH 131/202] Fix typo in README.md passsword -> password --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fe76f4f06f..fbb87a5ce64 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Your openai api key. ### `CODE` (optional) -Access passsword, separated by comma. +Access password, separated by comma. ### `BASE_URL` (optional) From 55bcf78efe0ab600abf67df0bd47dd62c8432b6c Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 15 Oct 2023 20:08:08 +0800 Subject: [PATCH 132/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3fe76f4f06f..76d928b6380 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ If you want to add a new translation, read this [document](./docs/translation.md [@AnsonHyq](https://github.com/AnsonHyq) [@synwith](https://github.com/synwith) [@piksonGit](https://github.com/piksonGit) +[@ouyangzhiping](https://github.com/ouyangzhiping) ### Contributor From 65c4a0c319c2264dcd5236d944fe7f541ef16441 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 16 Oct 2023 11:52:45 +0800 Subject: [PATCH 133/202] feat: close #3031 user can set larger font size --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 8ed6b77383c..795469a9681 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -753,7 +753,7 @@ export function Settings() { title={`${config.fontSize ?? 14}px`} value={config.fontSize} min="12" - max="18" + max="40" step="1" onChange={(e) => updateConfig( From f92fefb6cd00c8efb70768f277d2a06f8b149e66 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 23 Oct 2023 14:36:42 +0800 Subject: [PATCH 134/202] Update synchronise-chat-logs-cn.md --- docs/synchronise-chat-logs-cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/synchronise-chat-logs-cn.md b/docs/synchronise-chat-logs-cn.md index 52d79268f8a..59f2774292e 100644 --- a/docs/synchronise-chat-logs-cn.md +++ b/docs/synchronise-chat-logs-cn.md @@ -1,4 +1,4 @@ -# 同步聊天记录(upStash) +# 同步聊天记录 ## 准备工作 - GitHub账号 - 拥有自己搭建过的ChatGPT-Next-Web的服务器 From ccac85b079d62cf5886d5c1bc8543b2adebb5da6 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 23 Oct 2023 14:37:48 +0800 Subject: [PATCH 135/202] Update README_CN.md --- README_CN.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_CN.md b/README_CN.md index ce9309fd2a1..671e74b097d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -186,3 +186,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ## 开源协议 [MIT](https://opensource.org/license/mit/) + From e8a4ad10012dd6fa3e3370daa875835ad9897cfb Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 23 Oct 2023 14:38:04 +0800 Subject: [PATCH 136/202] Update README_KO.md From d1c3d900abaf8ec8eee92fbcfad59e9db64080af Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 23 Oct 2023 14:38:26 +0800 Subject: [PATCH 137/202] Update README_KO.md --- README_KO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_KO.md b/README_KO.md index 6ec7fe0e678..c4000c3a229 100644 --- a/README_KO.md +++ b/README_KO.md @@ -186,3 +186,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ## 오픈소스 라이센스 [MIT](https://opensource.org/license/mit/) + From 70e67a686b812c3ddf3a33c04e09cbd85ef9a48b Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 26 Oct 2023 00:06:09 +0800 Subject: [PATCH 138/202] Revert "fix serviceWorker cache: auto caching files by workbox" --- public/serviceWorker.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/public/serviceWorker.js b/public/serviceWorker.js index 2e1d60b98d2..f5a24b70176 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -1,21 +1,15 @@ const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; -importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js'); - self.addEventListener("activate", function (event) { console.log("ServiceWorker activated."); }); -workbox.core.clientsClaim(); -self.addEventListener("message", (event) => { - if (event.data && event.data.type === "SKIP_WAITING") { - self.skipWaiting(); - } +self.addEventListener("install", function (event) { + event.waitUntil( + caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) { + return cache.addAll([]); + }), + ); }); -workbox.routing.registerRoute( - new RegExp('/*'), - new workbox.strategies.StaleWhileRevalidate({ - cacheName: CHATGPT_NEXT_WEB_CACHE - }) -); +self.addEventListener("fetch", (e) => {}); From cd1f082c52e46b23ba844e6b1813c20f164d7116 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 7 Nov 2023 06:08:06 +0700 Subject: [PATCH 139/202] Feat Models [GPT-4] [GPT-3.5] [128K context] [+] feat(constant.ts): add new models to DEFAULT_MODELS array --- app/constant.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/constant.ts b/app/constant.ts index e03e00971cc..15db37a1ae6 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -100,6 +100,14 @@ export const DEFAULT_MODELS = [ name: "gpt-4-32k-0613", available: true, }, + { + name: "ggpt-4-1106-preview", + available: true, + }, + { + name: "gpt-4-vision-preview", + available: true, + }, { name: "gpt-3.5-turbo", available: true, @@ -112,6 +120,10 @@ export const DEFAULT_MODELS = [ name: "gpt-3.5-turbo-0613", available: true, }, + { + name: "gpt-3.5-turbo-1106", + available: true, + }, { name: "gpt-3.5-turbo-16k", available: true, From 9f26c8cecb997e3c1f81f5219f91043dc00342d9 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 7 Nov 2023 06:33:07 +0700 Subject: [PATCH 140/202] Fix Typo [GPT-4] [+] fix(constant.ts): fix typo in model name, change "ggpt-4-1106-preview" to "gpt-4-1106-preview" --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index 15db37a1ae6..5f3744ec181 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -101,7 +101,7 @@ export const DEFAULT_MODELS = [ available: true, }, { - name: "ggpt-4-1106-preview", + name: "gpt-4-1106-preview", available: true, }, { From d2d615c84a6a30879488528c4537e5a3ecaebca5 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 7 Nov 2023 06:42:55 +0700 Subject: [PATCH 141/202] Refactor KnowledgeCutoff [+] fix(constant.ts): update DEFAULT_SYSTEM_TEMPLATE to include knowledgeCutoff and time variables [+] feat(chat.ts): add support for injecting system prompts based on model configuration --- app/constant.ts | 5 +++-- app/store/chat.ts | 34 +++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 5f3744ec181..95de35627b9 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -69,9 +69,10 @@ export const OpenaiPath = { export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by OpenAI. -Knowledge cutoff: 2021-09 +Knowledge cutoff: {{knowledgeCutoff}} Current model: {{model}} -Current time: {{time}}`; +Current time: {{time}} +`; export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; diff --git a/app/store/chat.ts b/app/store/chat.ts index 56ac8db6cc1..9f73fdf2675 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -401,22 +401,26 @@ export const useChatStore = createPersistStore( // system prompts, to get close to OpenAI Web ChatGPT const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; - const systemPrompts = shouldInjectSystemPrompts - ? [ - createMessage({ - role: "system", - content: fillTemplateWith("", { - ...modelConfig, - template: DEFAULT_SYSTEM_TEMPLATE, - }), - }), - ] - : []; + let systemPrompts = shouldInjectSystemPrompts ? [] : []; + if (shouldInjectSystemPrompts) { - console.log( - "[Global System Prompt] ", - systemPrompts.at(0)?.content ?? "empty", - ); + const model = modelConfig.model; + let systemTemplate = DEFAULT_SYSTEM_TEMPLATE; + + if (model === "gpt-4-1106-preview" || model === "gpt-4-vision-preview") { + systemTemplate = systemTemplate.replace("{{knowledgeCutoff}}", "2023-04"); + } else { + systemTemplate = systemTemplate.replace("{{knowledgeCutoff}}", "2021-09"); + } + + const systemPrompt = createMessage({ + role: "system", + content: fillTemplateWith("", { + ...modelConfig, + template: systemTemplate, + }), + }); + console.log("[Global System Prompt] ", systemPrompt.content); } // long term memory From 6e52d14180345446abf61e933368eccbebad8694 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 7 Nov 2023 23:30:09 +0800 Subject: [PATCH 142/202] fix: #3016 disable sidebar transition on ios --- app/components/sidebar.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 6212d05d982..85af3ed2e98 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback } from "react"; +import { useEffect, useRef, useCallback, useMemo } from "react"; import styles from "./home.module.scss"; @@ -26,7 +26,7 @@ import { } from "../constant"; import { Link, useNavigate } from "react-router-dom"; -import { useMobileScreen } from "../utils"; +import { isIOS, useMobileScreen } from "../utils"; import dynamic from "next/dynamic"; import { showConfirm, showToast } from "./ui-lib"; @@ -134,6 +134,11 @@ export function SideBar(props: { className?: string }) { const { onDragStart, shouldNarrow } = useDragSideBar(); const navigate = useNavigate(); const config = useAppConfig(); + const isMobileScreen = useMobileScreen(); + const isIOSMobile = useMemo( + () => isIOS() && isMobileScreen, + [isMobileScreen], + ); useHotKey(); @@ -142,6 +147,10 @@ export function SideBar(props: { className?: string }) { className={`${styles.sidebar} ${props.className} ${ shouldNarrow && styles["narrow-sidebar"] }`} + style={{ + // #3016 disable transition on ios mobile screen + transition: isMobileScreen && isIOSMobile ? "none" : undefined, + }} >

From 836bf836d37b0d704eab132b7849447214b93d8c Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 7 Nov 2023 23:54:30 +0800 Subject: [PATCH 143/202] fix: #3152 system prompt should be injected --- app/constant.ts | 9 ++++++++- app/store/chat.ts | 39 ++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 95de35627b9..635fbeaeafa 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -69,13 +69,20 @@ export const OpenaiPath = { export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by OpenAI. -Knowledge cutoff: {{knowledgeCutoff}} +Knowledge cutoff: {{cutoff}} Current model: {{model}} Current time: {{time}} `; export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; +export const KnowledgeCutOffDate: Record = { + default: "2021-09", + "gpt-3.5-turbo-1106": "2023-04", + "gpt-4-1106-preview": "2023-04", + "gpt-4-vision-preview": "2023-04", +}; + export const DEFAULT_MODELS = [ { name: "gpt-4", diff --git a/app/store/chat.ts b/app/store/chat.ts index 9f73fdf2675..95822c19186 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -7,6 +7,7 @@ import { createEmptyMask, Mask } from "./mask"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE, + KnowledgeCutOffDate, StoreKey, SUMMARIZE_MODEL, } from "../constant"; @@ -116,7 +117,11 @@ function countMessages(msgs: ChatMessage[]) { } function fillTemplateWith(input: string, modelConfig: ModelConfig) { + let cutoff = + KnowledgeCutOffDate[modelConfig.model] ?? KnowledgeCutOffDate.default; + const vars = { + cutoff, model: modelConfig.model, time: new Date().toLocaleString(), lang: getLang(), @@ -401,26 +406,22 @@ export const useChatStore = createPersistStore( // system prompts, to get close to OpenAI Web ChatGPT const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; - let systemPrompts = shouldInjectSystemPrompts ? [] : []; - + const systemPrompts = shouldInjectSystemPrompts + ? [ + createMessage({ + role: "system", + content: fillTemplateWith("", { + ...modelConfig, + template: DEFAULT_SYSTEM_TEMPLATE, + }), + }), + ] + : []; if (shouldInjectSystemPrompts) { - const model = modelConfig.model; - let systemTemplate = DEFAULT_SYSTEM_TEMPLATE; - - if (model === "gpt-4-1106-preview" || model === "gpt-4-vision-preview") { - systemTemplate = systemTemplate.replace("{{knowledgeCutoff}}", "2023-04"); - } else { - systemTemplate = systemTemplate.replace("{{knowledgeCutoff}}", "2021-09"); - } - - const systemPrompt = createMessage({ - role: "system", - content: fillTemplateWith("", { - ...modelConfig, - template: systemTemplate, - }), - }); - console.log("[Global System Prompt] ", systemPrompt.content); + console.log( + "[Global System Prompt] ", + systemPrompts.at(0)?.content ?? "empty", + ); } // long term memory From 638fdd8c3e48837d4f060cca5bc73241d9bd9071 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 8 Nov 2023 00:20:34 +0800 Subject: [PATCH 144/202] feat: default disable balance query --- README.md | 4 +- README_CN.md | 7 +- README_ES.md | 173 ----------------------- README_JA.md | 275 ------------------------------------ README_KO.md | 189 ------------------------- app/components/chat.tsx | 3 +- app/components/settings.tsx | 19 ++- app/config/server.ts | 4 +- app/constant.ts | 1 + 9 files changed, 24 insertions(+), 651 deletions(-) delete mode 100644 README_ES.md delete mode 100644 README_JA.md delete mode 100644 README_KO.md diff --git a/README.md b/README.md index 33f6e85cbb0..b24470c64f5 100644 --- a/README.md +++ b/README.md @@ -185,11 +185,11 @@ If you do not want users to input their own API key, set this value to 1. If you do not want users to use GPT-4, set this value to 1. -### `HIDE_BALANCE_QUERY` (optional) +### `ENABLE_BALANCE_QUERY` (optional) > Default: Empty -If you do not want users to query balance, set this value to 1. +If you do want users to query balance, set this value to 1, or you should set it to 0. ## Requirements diff --git a/README_CN.md b/README_CN.md index 671e74b097d..e16c47a8ab1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -98,9 +98,9 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。 -### `HIDE_BALANCE_QUERY` (可选) +### `ENABLE_BALANCE_QUERY` (可选) -如果你不想让用户查询余额,将此环境变量设置为 1 即可。 +如果你想启用余额查询功能,将此环境变量设置为 1 即可。 ## 开发 @@ -169,7 +169,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s ⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。 - ## 鸣谢 ### 捐赠者 @@ -181,9 +180,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): 一站式大模型额度管理平台,支持市面上所有主流大语言模型 ## 开源协议 [MIT](https://opensource.org/license/mit/) - diff --git a/README_ES.md b/README_ES.md deleted file mode 100644 index a5787a996d0..00000000000 --- a/README_ES.md +++ /dev/null @@ -1,173 +0,0 @@ -
-预览 - -

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 deleted file mode 100644 index 72a0d5373f6..00000000000 --- a/README_JA.md +++ /dev/null @@ -1,275 +0,0 @@ -
-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 deleted file mode 100644 index c4000c3a229..00000000000 --- a/README_KO.md +++ /dev/null @@ -1,189 +0,0 @@ -
-프리뷰 - -

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/components/chat.tsx b/app/components/chat.tsx index cca096eb874..a30e505dd49 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -73,11 +73,10 @@ import { showPrompt, showToast, } from "./ui-lib"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { CHAT_PAGE_SIZE, LAST_INPUT_KEY, - MAX_RENDER_MSG_COUNT, Path, REQUEST_TIMEOUT_MS, UNFINISHED_INPUT, diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 795469a9681..cfe81ee48cf 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -50,7 +50,13 @@ import Locale, { } from "../locales"; import { copyToClipboard } from "../utils"; import Link from "next/link"; -import { Path, RELEASE_URL, STORAGE_KEY, UPDATE_URL } from "../constant"; +import { + OPENAI_BASE_URL, + Path, + RELEASE_URL, + STORAGE_KEY, + UPDATE_URL, +} from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { ErrorBoundary } from "./error"; import { InputRange } from "./input-range"; @@ -571,13 +577,19 @@ export function Settings() { console.log("[Update] remote version ", updateStore.remoteVersion); } + const accessStore = useAccessStore(); + const shouldHideBalanceQuery = useMemo(() => { + const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); + return accessStore.hideBalanceQuery || isOpenAiUrl; + }, [accessStore.hideBalanceQuery, accessStore.openaiUrl]); + const usage = { used: updateStore.used, subscription: updateStore.subscription, }; const [loadingUsage, setLoadingUsage] = useState(false); function checkUsage(force = false) { - if (accessStore.hideBalanceQuery) { + if (shouldHideBalanceQuery) { return; } @@ -587,7 +599,6 @@ export function Settings() { }); } - const accessStore = useAccessStore(); const enabledAccessControl = useMemo( () => accessStore.enabledAccessControl(), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -916,7 +927,7 @@ export function Settings() { ) : null} - {!accessStore.hideBalanceQuery ? ( + {!shouldHideBalanceQuery ? ( { isVercel: !!process.env.VERCEL, hideUserApiKey: !!process.env.HIDE_USER_API_KEY, disableGPT4: !!process.env.DISABLE_GPT4, - hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY, + hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, }; }; diff --git a/app/constant.ts b/app/constant.ts index 635fbeaeafa..8d36e0b5543 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -10,6 +10,7 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun"; export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; +export const OPENAI_BASE_URL = "https://api.openai.com"; export enum Path { Home = "/", From c5ca278253456c7d65bcd877b0ca40da0b8026eb Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 8 Nov 2023 00:30:02 +0800 Subject: [PATCH 145/202] feat: close #2908 allow to disable parse settings from link --- README.md | 6 ++++++ README_CN.md | 4 ++++ app/api/config/route.ts | 1 + app/components/chat.tsx | 9 ++++++--- app/components/settings.tsx | 12 +++++++++--- app/config/server.ts | 2 ++ app/store/access.ts | 10 +--------- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b24470c64f5..91e03d80049 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,12 @@ If you do not want users to use GPT-4, set this value to 1. If you do want users to query balance, set this value to 1, or you should set it to 0. +### `DISABLE_FAST_LINK` (optional) + +> Default: Empty + +If you want to disable parse settings from url, set this to 1. + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index e16c47a8ab1..13b97417d40 100644 --- a/README_CN.md +++ b/README_CN.md @@ -102,6 +102,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 如果你想启用余额查询功能,将此环境变量设置为 1 即可。 +### `DISABLE_FAST_LINK` (可选) + +如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 + ## 开发 点击下方按钮,开始二次开发: diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 0bfc955bfb8..44af8d3b9dd 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -11,6 +11,7 @@ const DANGER_CONFIG = { hideUserApiKey: serverConfig.hideUserApiKey, disableGPT4: serverConfig.disableGPT4, hideBalanceQuery: serverConfig.hideBalanceQuery, + disableFastLink: serverConfig.disableFastLink, }; declare global { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a30e505dd49..18353e8fcab 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -975,14 +975,17 @@ function _Chat() { doSubmit(text); }, code: (text) => { + if (accessStore.disableFastLink) return; console.log("[Command] got code from url: ", text); showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => { if (res) { - accessStore.updateCode(text); + accessStore.update((access) => (access.accessCode = text)); } }); }, settings: (text) => { + if (accessStore.disableFastLink) return; + try { const payload = JSON.parse(text) as { key?: string; @@ -998,10 +1001,10 @@ function _Chat() { ).then((res) => { if (!res) return; if (payload.key) { - accessStore.updateToken(payload.key); + accessStore.update((access) => (access.token = payload.key!)); } if (payload.url) { - accessStore.updateOpenAiUrl(payload.url); + accessStore.update((access) => (access.openaiUrl = payload.url!)); } }); } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index cfe81ee48cf..572c0743a11 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -888,7 +888,9 @@ export function Settings() { type="text" placeholder={Locale.Settings.AccessCode.Placeholder} onChange={(e) => { - accessStore.updateCode(e.currentTarget.value); + accessStore.update( + (access) => (access.accessCode = e.currentTarget.value), + ); }} /> @@ -907,7 +909,9 @@ export function Settings() { value={accessStore.openaiUrl} placeholder="https://api.openai.com/" onChange={(e) => - accessStore.updateOpenAiUrl(e.currentTarget.value) + accessStore.update( + (access) => (access.openaiUrl = e.currentTarget.value), + ) } > @@ -920,7 +924,9 @@ export function Settings() { type="text" placeholder={Locale.Settings.Token.Placeholder} onChange={(e) => { - accessStore.updateToken(e.currentTarget.value); + accessStore.update( + (access) => (access.token = e.currentTarget.value), + ); }} /> diff --git a/app/config/server.ts b/app/config/server.ts index 62b435e939d..2df806fed41 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -13,6 +13,7 @@ declare global { BUILD_MODE?: "standalone" | "export"; BUILD_APP?: string; // is building desktop app ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not + DISABLE_FAST_LINK?: string; // disallow parse settings from url or not } } } @@ -48,5 +49,6 @@ export const getServerSideConfig = () => { hideUserApiKey: !!process.env.HIDE_USER_API_KEY, disableGPT4: !!process.env.DISABLE_GPT4, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, + disableFastLink: !!process.env.DISABLE_FAST_LINK, }; }; diff --git a/app/store/access.ts b/app/store/access.ts index 9eaa81e5ea3..3d889f6e72e 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -16,6 +16,7 @@ const DEFAULT_ACCESS_STATE = { hideUserApiKey: false, hideBalanceQuery: false, disableGPT4: false, + disableFastLink: false, openaiUrl: DEFAULT_OPENAI_URL, }; @@ -29,15 +30,6 @@ export const useAccessStore = createPersistStore( return get().needCode; }, - updateCode(code: string) { - set(() => ({ accessCode: code?.trim() })); - }, - updateToken(token: string) { - set(() => ({ token: token?.trim() })); - }, - updateOpenAiUrl(url: string) { - set(() => ({ openaiUrl: url?.trim() })); - }, isAuthorized() { this.fetch(); From c7321fddfbe92dc0e9ef81862aac3daa93653881 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 8 Nov 2023 00:34:31 +0800 Subject: [PATCH 146/202] fixup --- app/components/auth.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index b82d0e894c7..577d7754240 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -12,11 +12,16 @@ import { getClientConfig } from "../config/client"; export function AuthPage() { const navigate = useNavigate(); - const access = useAccessStore(); + const accessStore = useAccessStore(); const goHome = () => navigate(Path.Home); const goChat = () => navigate(Path.Chat); - const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string + const resetAccessCode = () => { + accessStore.update((access) => { + access.token = ""; + access.accessCode = ""; + }); + }; // Reset access code to empty string useEffect(() => { if (getClientConfig()?.isApp) { @@ -38,21 +43,25 @@ export function AuthPage() { className={styles["auth-input"]} type="password" placeholder={Locale.Auth.Input} - value={access.accessCode} + value={accessStore.accessCode} onChange={(e) => { - access.updateCode(e.currentTarget.value); + accessStore.update( + (access) => (access.accessCode = e.currentTarget.value), + ); }} /> - {!access.hideUserApiKey ? ( + {!accessStore.hideUserApiKey ? ( <>
{Locale.Auth.SubTips}
{ - access.updateToken(e.currentTarget.value); + accessStore.update( + (access) => (access.token = e.currentTarget.value), + ); }} /> From 54a53328341af2d07db19e56db5febdaac225a87 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 8 Nov 2023 01:04:20 +0800 Subject: [PATCH 147/202] feat: sort model by name --- app/store/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/store/config.ts b/app/store/config.ts index 184355c94a3..0fbc26dfe0e 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -133,7 +133,9 @@ export const useAppConfig = createPersistStore( .customModels.split(",") .filter((v) => !!v && v.length > 0) .map((m) => ({ name: m, available: true })); - return get().models.concat(customModels); + const allModels = get().models.concat(customModels); + allModels.sort((a, b) => (a.name < b.name ? -1 : 1)); + return allModels; }, }), { From dbee5bc515fd9bad33fcdce0f8b176494db2d02a Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 8 Nov 2023 01:05:07 +0800 Subject: [PATCH 148/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e530203f680..649e3816db3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.9" + "version": "2.9.10" }, "tauri": { "allowlist": { From 9cb889c34f2653c812b044c1e6ebd264d98067a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E5=BF=86?= Date: Wed, 8 Nov 2023 07:09:52 +0800 Subject: [PATCH 149/202] docs: up to date --- .env.template | 8 ++++---- app/api/common.ts | 11 +++++++---- docker-compose.yml | 12 +++++++++--- docs/cloudflare-pages-cn.md | 7 +++++-- docs/cloudflare-pages-en.md | 13 +++++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.env.template b/.env.template index 1ff575f116a..5e73e6d850a 100644 --- a/.env.template +++ b/.env.template @@ -24,10 +24,10 @@ HIDE_USER_API_KEY= # (optional) # Default: Empty -# If you do not want users to use GPT-4, set this value to 1. -DISABLE_GPT4= +# If you do want users to query balance, set this value to 1. +ENABLE_BALANCE_QUERY= # (optional) # Default: Empty -# If you do not want users to query balance, set this value to 1. -HIDE_BALANCE_QUERY= +# If you want to disable parse settings from url, set this value to 1. +DISABLE_FAST_LINK= diff --git a/app/api/common.ts b/app/api/common.ts index cd2936ee39f..0af7761d88c 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -20,7 +20,7 @@ export async function requestOpenai(req: NextRequest) { baseUrl = `${PROTOCOL}://${baseUrl}`; } - if (baseUrl.endsWith('/')) { + if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, -1); } @@ -31,9 +31,12 @@ export async function requestOpenai(req: NextRequest) { console.log("[Org ID]", process.env.OPENAI_ORG_ID); } - const timeoutId = setTimeout(() => { - controller.abort(); - }, 10 * 60 * 1000); + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); const fetchUrl = `${baseUrl}/${openaiPath}`; const fetchOptions: RequestInit = { diff --git a/docker-compose.yml b/docker-compose.yml index c4312cfe3f3..57ca12e03f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ -version: '3.9' +version: "3.9" services: - chatgpt-next-web: + chatgpt-next-web: profiles: ["no-proxy"] container_name: chatgpt-next-web image: yidadaa/chatgpt-next-web @@ -13,8 +13,11 @@ services: - OPENAI_ORG_ID=$OPENAI_ORG_ID - HIDE_USER_API_KEY=$HIDE_USER_API_KEY - DISABLE_GPT4=$DISABLE_GPT4 + - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY + - DISABLE_FAST_LINK=$DISABLE_FAST_LINK + - OPENAI_SB=$OPENAI_SB - chatgpt-next-web-proxy: + chatgpt-next-web-proxy: profiles: ["proxy"] container_name: chatgpt-next-web-proxy image: yidadaa/chatgpt-next-web @@ -28,3 +31,6 @@ services: - OPENAI_ORG_ID=$OPENAI_ORG_ID - HIDE_USER_API_KEY=$HIDE_USER_API_KEY - DISABLE_GPT4=$DISABLE_GPT4 + - ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY + - DISABLE_FAST_LINK=$DISABLE_FAST_LINK + - OPENAI_SB=$OPENAI_SB diff --git a/docs/cloudflare-pages-cn.md b/docs/cloudflare-pages-cn.md index 2f9a99f2d41..e1567af03a7 100644 --- a/docs/cloudflare-pages-cn.md +++ b/docs/cloudflare-pages-cn.md @@ -1,6 +1,7 @@ # Cloudflare Pages 部署指南 ## 如何新建项目 + 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 1. 点击 "Create a project"。 @@ -30,10 +31,12 @@ - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` - + - `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能` + - `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置` + 12. 点击 "Save and Deploy"。 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 16. 前往 "Deployments",点击 "Retry deployment"。 -17. Enjoy. \ No newline at end of file +17. Enjoy. diff --git a/docs/cloudflare-pages-en.md b/docs/cloudflare-pages-en.md index 2279ff232a4..c5d55043872 100644 --- a/docs/cloudflare-pages-en.md +++ b/docs/cloudflare-pages-en.md @@ -1,6 +1,7 @@ # Cloudflare Pages Deployment Guide ## How to create a new project + Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 1. Click "Create a project". @@ -11,12 +12,13 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. 6. For "Project name" and "Production branch", use the default values or change them as needed. 7. In "Build Settings", choose the "Framework presets" option and select "Next.js". 8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command: - ``` - npx @cloudflare/next-on-pages --experimental-minify - ``` + ``` + npx @cloudflare/next-on-pages --experimental-minify + ``` 9. For "Build output directory", use the default value and do not modify it. 10. Do not modify "Root Directory". 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: + - `NODE_VERSION=20.1` - `NEXT_TELEMETRY_DISABLE=1` - `OPENAI_API_KEY=your_own_API_key` @@ -29,7 +31,10 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. - `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI` - `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key` - `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4` - + - `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance` + - `DISABLE_FAST_LINK=1 Optional, disable parse settings from url` + - `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API` + 12. Click "Save and Deploy". 13. Click "Cancel deployment" because you need to fill in Compatibility flags. 14. Go to "Build settings", "Functions", and find "Compatibility flags". From 3a519612a0fcd7a05fe3d5d01cb9f88c0caa2a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E5=BF=86?= Date: Wed, 8 Nov 2023 15:53:45 +0800 Subject: [PATCH 150/202] docs: fix typo in #3171 --- .env.template | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.template b/.env.template index 5e73e6d850a..3e32903695c 100644 --- a/.env.template +++ b/.env.template @@ -17,6 +17,11 @@ BASE_URL= # Default: Empty OPENAI_ORG_ID= +# (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 input their own API key, set this value to 1. From 6ab4c9be2e66ebb203d7dc50a1ecf4ab30fa6885 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 8 Nov 2023 17:40:28 +0800 Subject: [PATCH 151/202] Update cloudflare-pages-cn.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cloudflare 构建命令 --- docs/cloudflare-pages-cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cloudflare-pages-cn.md b/docs/cloudflare-pages-cn.md index e1567af03a7..137bb9dc328 100644 --- a/docs/cloudflare-pages-cn.md +++ b/docs/cloudflare-pages-cn.md @@ -13,7 +13,7 @@ 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令: ``` - npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify + npx @cloudflare/next-on-pages@1.5.0 ``` 9. 对于 "Build output directory",使用默认值并且不要修改。 10. 不要修改 "Root Directory"。 From 8c0ba1aee24f2f076c48e89a5e872466684afc85 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 9 Nov 2023 01:51:33 +0800 Subject: [PATCH 152/202] feat: close #2954 chat summary should be copyable --- app/components/chat.tsx | 1 + app/styles/globals.scss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 18353e8fcab..a0b7307c298 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -143,6 +143,7 @@ export function SessionConfigModel(props: { onClose: () => void }) { extraListItems={ session.mask.modelConfig.sendMemory ? ( diff --git a/app/styles/globals.scss b/app/styles/globals.scss index def28680c1a..aa22b7d4fd6 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -357,3 +357,7 @@ pre { overflow: hidden; text-overflow: ellipsis; } + +.copyable { + user-select: text; +} From fbc02367484416a98d20b86d9994d019869d78a8 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 9 Nov 2023 02:03:05 +0800 Subject: [PATCH 153/202] fix: #3174 should prompt to confirm to delete chat --- app/components/chat-list.tsx | 10 ++++++++-- app/components/sidebar.tsx | 5 +++-- app/store/chat.ts | 27 --------------------------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 7ba55585239..f76b369f12d 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -18,6 +18,7 @@ import { MaskAvatar } from "./mask"; import { Mask } from "../store/mask"; import { useRef, useEffect } from "react"; import { showConfirm } from "./ui-lib"; +import { useMobileScreen } from "../utils"; export function ChatItem(props: { onClick?: () => void; @@ -80,7 +81,11 @@ export function ChatItem(props: {
{ + props.onDelete?.(); + e.preventDefault(); + e.stopPropagation(); + }} >
@@ -101,6 +106,7 @@ export function ChatList(props: { narrow?: boolean }) { ); const chatStore = useChatStore(); const navigate = useNavigate(); + const isMobileScreen = useMobileScreen(); const onDragEnd: OnDragEndResponder = (result) => { const { destination, source } = result; @@ -142,7 +148,7 @@ export function ChatList(props: { narrow?: boolean }) { }} onDelete={async () => { if ( - !props.narrow || + (!props.narrow && !isMobileScreen) || (await showConfirm(Locale.Home.DeleteChat)) ) { chatStore.deleteSession(i); diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 85af3ed2e98..beeee865afe 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback, useMemo } from "react"; +import { useEffect, useRef, useMemo } from "react"; import styles from "./home.module.scss"; @@ -8,6 +8,7 @@ import GithubIcon from "../icons/github.svg"; import ChatGptIcon from "../icons/chatgpt.svg"; import AddIcon from "../icons/add.svg"; import CloseIcon from "../icons/close.svg"; +import DeleteIcon from "../icons/delete.svg"; import MaskIcon from "../icons/mask.svg"; import PluginIcon from "../icons/plugin.svg"; import DragIcon from "../icons/drag.svg"; @@ -202,7 +203,7 @@ export function SideBar(props: { className?: string }) {
} + icon={} onClick={async () => { if (await showConfirm(Locale.Home.DeleteChat)) { chatStore.deleteSession(chatStore.currentSessionIndex); diff --git a/app/store/chat.ts b/app/store/chat.ts index 95822c19186..ff7eb51b5c1 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -85,33 +85,6 @@ function getSummarizeModel(currentModel: string) { return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel; } -interface ChatStore { - sessions: ChatSession[]; - currentSessionIndex: number; - clearSessions: () => void; - moveSession: (from: number, to: number) => void; - selectSession: (index: number) => void; - newSession: (mask?: Mask) => void; - deleteSession: (index: number) => void; - currentSession: () => ChatSession; - nextSession: (delta: number) => void; - onNewMessage: (message: ChatMessage) => void; - onUserInput: (content: string) => Promise; - summarizeSession: () => void; - updateStat: (message: ChatMessage) => void; - updateCurrentSession: (updater: (session: ChatSession) => void) => void; - updateMessage: ( - sessionIndex: number, - messageIndex: number, - updater: (message?: ChatMessage) => void, - ) => void; - resetSession: () => void; - getMessagesWithMemory: () => ChatMessage[]; - getMemoryPrompt: () => ChatMessage; - - clearAllData: () => void; -} - function countMessages(msgs: ChatMessage[]) { return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0); } From d93f05f51163488525b3957bedfa0ed8a6167b8c Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 9 Nov 2023 03:01:29 +0800 Subject: [PATCH 154/202] feat: close #3187 use CUSTOM_MODELS to control model list --- README.md | 7 ++++++ README_CN.md | 6 +++++ app/api/common.ts | 31 ++++++++++++------------- app/api/config/route.ts | 1 + app/components/chat.tsx | 12 ++++------ app/components/model-config.tsx | 7 +++--- app/config/server.ts | 17 +++++++++++++- app/store/access.ts | 7 +----- app/store/config.ts | 10 +-------- app/utils/hooks.ts | 16 +++++++++++++ app/utils/model.ts | 40 +++++++++++++++++++++++++++++++++ 11 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 app/utils/hooks.ts create mode 100644 app/utils/model.ts diff --git a/README.md b/README.md index 91e03d80049..3973c84bfde 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,13 @@ If you do want users to query balance, set this value to 1, or you should set it If you want to disable parse settings from url, set this to 1. +### `CUSTOM_MODELS` (optional) + +> Default: Empty +> Example: `+llama,+claude-2,-gpt-3.5-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list. + +To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma. + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index 13b97417d40..d8e9553e183 100644 --- a/README_CN.md +++ b/README_CN.md @@ -106,6 +106,12 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 +### `CUSTOM_MODELS` (可选) + +> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。 + +用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。 + ## 开发 点击下方按钮,开始二次开发: diff --git a/app/api/common.ts b/app/api/common.ts index 0af7761d88c..a1decd42f5b 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,10 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; +import { getServerSideConfig } from "../config/server"; +import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; +import { collectModelTable, collectModels } from "../utils/model"; -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 DISABLE_GPT4 = !!process.env.DISABLE_GPT4; +const serverConfig = getServerSideConfig(); export async function requestOpenai(req: NextRequest) { const controller = new AbortController(); @@ -14,10 +13,10 @@ export async function requestOpenai(req: NextRequest) { "", ); - let baseUrl = BASE_URL; + let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { - baseUrl = `${PROTOCOL}://${baseUrl}`; + baseUrl = `https://${baseUrl}`; } if (baseUrl.endsWith("/")) { @@ -26,10 +25,7 @@ export async function requestOpenai(req: NextRequest) { console.log("[Proxy] ", openaiPath); console.log("[Base Url]", baseUrl); - - if (process.env.OPENAI_ORG_ID) { - console.log("[Org ID]", process.env.OPENAI_ORG_ID); - } + console.log("[Org ID]", serverConfig.openaiOrgId); const timeoutId = setTimeout( () => { @@ -58,18 +54,23 @@ export async function requestOpenai(req: NextRequest) { }; // #1815 try to refuse gpt4 request - if (DISABLE_GPT4 && req.body) { + if (serverConfig.customModels && req.body) { try { + const modelTable = collectModelTable( + DEFAULT_MODELS, + serverConfig.customModels, + ); const clonedBody = await req.text(); fetchOptions.body = clonedBody; - const jsonBody = JSON.parse(clonedBody); + const jsonBody = JSON.parse(clonedBody) as { model?: string }; - if ((jsonBody?.model ?? "").includes("gpt-4")) { + // not undefined and is false + if (modelTable[jsonBody?.model ?? ""] === false) { return NextResponse.json( { error: true, - message: "you are not allowed to use gpt-4 model", + message: `you are not allowed to use ${jsonBody?.model} model`, }, { status: 403, diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 44af8d3b9dd..db84fba175a 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -12,6 +12,7 @@ const DANGER_CONFIG = { disableGPT4: serverConfig.disableGPT4, hideBalanceQuery: serverConfig.hideBalanceQuery, disableFastLink: serverConfig.disableFastLink, + customModels: serverConfig.customModels, }; declare global { diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a0b7307c298..9afb49f7a66 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -88,6 +88,7 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command"; import { prettyObject } from "../utils/format"; import { ExportMessageModal } from "./exporter"; import { getClientConfig } from "../config/client"; +import { useAllModels } from "../utils/hooks"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -430,14 +431,9 @@ export function ChatActions(props: { // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; - const models = useMemo( - () => - config - .allModels() - .filter((m) => m.available) - .map((m) => m.name), - [config], - ); + const models = useAllModels() + .filter((m) => m.available) + .map((m) => m.name); const [showModelSelector, setShowModelSelector] = useState(false); return ( diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 63950a40d04..6e4c9bcb17b 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -1,14 +1,15 @@ -import { ModalConfigValidator, ModelConfig, useAppConfig } from "../store"; +import { ModalConfigValidator, ModelConfig } from "../store"; import Locale from "../locales"; import { InputRange } from "./input-range"; import { ListItem, Select } from "./ui-lib"; +import { useAllModels } from "../utils/hooks"; export function ModelConfigList(props: { modelConfig: ModelConfig; updateConfig: (updater: (config: ModelConfig) => void) => void; }) { - const config = useAppConfig(); + const allModels = useAllModels(); return ( <> @@ -24,7 +25,7 @@ export function ModelConfigList(props: { ); }} > - {config.allModels().map((v, i) => ( + {allModels.map((v, i) => ( diff --git a/app/config/server.ts b/app/config/server.ts index 2df806fed41..007c3973863 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -1,4 +1,5 @@ import md5 from "spark-md5"; +import { DEFAULT_MODELS } from "../constant"; declare global { namespace NodeJS { @@ -7,6 +8,7 @@ declare global { CODE?: string; BASE_URL?: string; PROXY_URL?: string; + OPENAI_ORG_ID?: string; VERCEL?: string; HIDE_USER_API_KEY?: string; // disable user's api key input DISABLE_GPT4?: string; // allow user to use gpt-4 or not @@ -14,6 +16,7 @@ declare global { BUILD_APP?: string; // is building desktop app ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not DISABLE_FAST_LINK?: string; // disallow parse settings from url or not + CUSTOM_MODELS?: string; // to control custom models } } } @@ -38,6 +41,16 @@ export const getServerSideConfig = () => { ); } + let disableGPT4 = !!process.env.DISABLE_GPT4; + let customModels = process.env.CUSTOM_MODELS ?? ""; + + if (disableGPT4) { + if (customModels) customModels += ","; + customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4")) + .map((m) => "-" + m.name) + .join(","); + } + return { apiKey: process.env.OPENAI_API_KEY, code: process.env.CODE, @@ -45,10 +58,12 @@ export const getServerSideConfig = () => { needCode: ACCESS_CODES.size > 0, baseUrl: process.env.BASE_URL, proxyUrl: process.env.PROXY_URL, + openaiOrgId: process.env.OPENAI_ORG_ID, isVercel: !!process.env.VERCEL, hideUserApiKey: !!process.env.HIDE_USER_API_KEY, - disableGPT4: !!process.env.DISABLE_GPT4, + disableGPT4, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, disableFastLink: !!process.env.DISABLE_FAST_LINK, + customModels, }; }; diff --git a/app/store/access.ts b/app/store/access.ts index 3d889f6e72e..f87e44a2ac4 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -17,6 +17,7 @@ const DEFAULT_ACCESS_STATE = { hideBalanceQuery: false, disableGPT4: false, disableFastLink: false, + customModels: "", openaiUrl: DEFAULT_OPENAI_URL, }; @@ -52,12 +53,6 @@ export const useAccessStore = createPersistStore( .then((res: DangerConfig) => { console.log("[Config] got config from server", res); set(() => ({ ...res })); - - if (res.disableGPT4) { - DEFAULT_MODELS.forEach( - (m: any) => (m.available = !m.name.startsWith("gpt-4")), - ); - } }) .catch(() => { console.error("[Config] failed to fetch config"); diff --git a/app/store/config.ts b/app/store/config.ts index 0fbc26dfe0e..5fcd6ff514c 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -128,15 +128,7 @@ export const useAppConfig = createPersistStore( })); }, - allModels() { - const customModels = get() - .customModels.split(",") - .filter((v) => !!v && v.length > 0) - .map((m) => ({ name: m, available: true })); - const allModels = get().models.concat(customModels); - allModels.sort((a, b) => (a.name < b.name ? -1 : 1)); - return allModels; - }, + allModels() {}, }), { name: StoreKey.Config, diff --git a/app/utils/hooks.ts b/app/utils/hooks.ts new file mode 100644 index 00000000000..f6bfae67323 --- /dev/null +++ b/app/utils/hooks.ts @@ -0,0 +1,16 @@ +import { useMemo } from "react"; +import { useAccessStore, useAppConfig } from "../store"; +import { collectModels } from "./model"; + +export function useAllModels() { + const accessStore = useAccessStore(); + const configStore = useAppConfig(); + const models = useMemo(() => { + return collectModels( + configStore.models, + [accessStore.customModels, configStore.customModels].join(","), + ); + }, [accessStore.customModels, configStore.customModels, configStore.models]); + + return models; +} diff --git a/app/utils/model.ts b/app/utils/model.ts new file mode 100644 index 00000000000..23090f9d2f3 --- /dev/null +++ b/app/utils/model.ts @@ -0,0 +1,40 @@ +import { LLMModel } from "../client/api"; + +export function collectModelTable( + models: readonly LLMModel[], + customModels: string, +) { + const modelTable: Record = {}; + + // default models + models.forEach((m) => (modelTable[m.name] = m.available)); + + // server custom models + customModels + .split(",") + .filter((v) => !!v && v.length > 0) + .map((m) => { + if (m.startsWith("+")) { + modelTable[m.slice(1)] = true; + } else if (m.startsWith("-")) { + modelTable[m.slice(1)] = false; + } else modelTable[m] = true; + }); + return modelTable; +} + +/** + * Generate full model table. + */ +export function collectModels( + models: readonly LLMModel[], + customModels: string, +) { + const modelTable = collectModelTable(models, customModels); + const allModels = Object.keys(modelTable).map((m) => ({ + name: m, + available: modelTable[m], + })); + + return allModels; +} From d0a1d910d4dae62351ae0273562cc6067e3e6ed9 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 9 Nov 2023 03:19:13 +0800 Subject: [PATCH 155/202] fix: #3186 enable max_tokens in chat payload --- app/client/platforms/openai.ts | 1 + app/components/model-config.tsx | 4 ++-- app/store/config.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index fd4eb59ce77..97392a004c9 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -70,6 +70,7 @@ export class ChatGPTApi implements LLMApi { presence_penalty: modelConfig.presence_penalty, frequency_penalty: modelConfig.frequency_penalty, top_p: modelConfig.top_p, + max_tokens: Math.max(modelConfig.max_tokens, 1024), }; console.log("[Request] openai payload: ", requestPayload); diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 6e4c9bcb17b..1c730e1449f 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -76,8 +76,8 @@ export function ModelConfigList(props: { > props.updateConfig( diff --git a/app/store/config.ts b/app/store/config.ts index 5fcd6ff514c..17eb88c30ed 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = { model: "gpt-3.5-turbo" as ModelType, temperature: 0.5, top_p: 1, - max_tokens: 2000, + max_tokens: 8192, presence_penalty: 0, frequency_penalty: 0, sendMemory: true, @@ -82,7 +82,7 @@ export const ModalConfigValidator = { return x as ModelType; }, max_tokens(x: number) { - return limitNumber(x, 0, 100000, 2000); + return limitNumber(x, 0, 512000, 1024); }, presence_penalty(x: number) { return limitNumber(x, -2, 2, 0); From 87e3d663a2955f7344f214b355f8a8d03032ea65 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 9 Nov 2023 11:00:09 +0800 Subject: [PATCH 156/202] fix: #3192 use smaller max_tokens as default --- app/store/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/config.ts b/app/store/config.ts index 17eb88c30ed..057e31b2565 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = { model: "gpt-3.5-turbo" as ModelType, temperature: 0.5, top_p: 1, - max_tokens: 8192, + max_tokens: 4000, presence_penalty: 0, frequency_penalty: 0, sendMemory: true, From 3b3ebda34bc5def7e7b72f9a3a7dcca2fa0c0aac Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 9 Nov 2023 11:01:41 +0800 Subject: [PATCH 157/202] fix: #3196 3.5-turbo-1106 should use old cutoff date --- app/constant.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index 8d36e0b5543..a97b8782292 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -79,7 +79,6 @@ export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; export const KnowledgeCutOffDate: Record = { default: "2021-09", - "gpt-3.5-turbo-1106": "2023-04", "gpt-4-1106-preview": "2023-04", "gpt-4-vision-preview": "2023-04", }; From fd2f441e02b1eecfd2139942fcb911b32ee3c1e4 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 9 Nov 2023 20:45:25 +0800 Subject: [PATCH 158/202] feat: wont send max_tokens --- app/client/platforms/openai.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 97392a004c9..4a5ddce7de6 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -70,7 +70,8 @@ export class ChatGPTApi implements LLMApi { presence_penalty: modelConfig.presence_penalty, frequency_penalty: modelConfig.frequency_penalty, top_p: modelConfig.top_p, - max_tokens: Math.max(modelConfig.max_tokens, 1024), + // max_tokens: Math.max(modelConfig.max_tokens, 1024), + // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; console.log("[Request] openai payload: ", requestPayload); From b7ffca031ebda555c373783820056541307ceba0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 02:43:30 +0800 Subject: [PATCH 159/202] feat: close #935 add azure support --- app/api/auth.ts | 18 ++- app/api/common.ts | 31 +++-- app/azure.ts | 9 ++ app/client/api.ts | 16 ++- app/client/platforms/openai.ts | 48 ++++++-- app/components/auth.tsx | 6 +- app/components/chat.tsx | 4 +- app/components/settings.tsx | 187 ++++++++++++++++++++++++------ app/components/ui-lib.module.scss | 2 +- app/components/ui-lib.tsx | 14 +-- app/config/server.ts | 35 ++++-- app/constant.ts | 11 ++ app/locales/cn.ts | 66 ++++++++--- app/locales/en.ts | 66 ++++++++--- app/store/access.ts | 57 +++++++-- app/utils/clone.ts | 7 ++ app/utils/store.ts | 51 ++++---- 17 files changed, 478 insertions(+), 150 deletions(-) create mode 100644 app/azure.ts diff --git a/app/api/auth.ts b/app/api/auth.ts index e0453b2b47f..c1f6e7fdec2 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -28,7 +28,7 @@ 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 { accessCode, apiKey } = parseApiKey(authToken); const hashedCode = md5.hash(accessCode ?? "").trim(); @@ -39,7 +39,7 @@ export function auth(req: NextRequest) { console.log("[User IP] ", getIP(req)); console.log("[Time] ", new Date().toLocaleString()); - if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { + if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) { return { error: true, msg: !accessCode ? "empty access code" : "wrong access code", @@ -47,11 +47,17 @@ export function auth(req: NextRequest) { } // if user does not provide an api key, inject system api key - if (!token) { - const apiKey = serverConfig.apiKey; - if (apiKey) { + if (!apiKey) { + const serverApiKey = serverConfig.isAzure + ? serverConfig.azureApiKey + : serverConfig.apiKey; + + if (serverApiKey) { console.log("[Auth] use system api key"); - req.headers.set("Authorization", `Bearer ${apiKey}`); + req.headers.set( + "Authorization", + `${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`, + ); } else { console.log("[Auth] admin did not provide an api key"); } diff --git a/app/api/common.ts b/app/api/common.ts index a1decd42f5b..fc877b02db2 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,19 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; -import { collectModelTable, collectModels } from "../utils/model"; +import { collectModelTable } from "../utils/model"; +import { makeAzurePath } from "../azure"; const serverConfig = getServerSideConfig(); export async function requestOpenai(req: NextRequest) { const controller = new AbortController(); + const authValue = req.headers.get("Authorization") ?? ""; - const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( + const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization"; + + let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( "/api/openai/", "", ); - let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL; + let baseUrl = + serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; @@ -23,7 +28,7 @@ export async function requestOpenai(req: NextRequest) { baseUrl = baseUrl.slice(0, -1); } - console.log("[Proxy] ", openaiPath); + console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); console.log("[Org ID]", serverConfig.openaiOrgId); @@ -34,14 +39,24 @@ export async function requestOpenai(req: NextRequest) { 10 * 60 * 1000, ); - const fetchUrl = `${baseUrl}/${openaiPath}`; + if (serverConfig.isAzure) { + if (!serverConfig.azureApiVersion) { + return NextResponse.json({ + error: true, + message: `missing AZURE_API_VERSION in server env vars`, + }); + } + path = makeAzurePath(path, serverConfig.azureApiVersion); + } + + const fetchUrl = `${baseUrl}/${path}`; const fetchOptions: RequestInit = { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", - Authorization: authValue, - ...(process.env.OPENAI_ORG_ID && { - "OpenAI-Organization": process.env.OPENAI_ORG_ID, + [authHeaderName]: authValue, + ...(serverConfig.openaiOrgId && { + "OpenAI-Organization": serverConfig.openaiOrgId, }), }, method: req.method, diff --git a/app/azure.ts b/app/azure.ts new file mode 100644 index 00000000000..48406c55ba5 --- /dev/null +++ b/app/azure.ts @@ -0,0 +1,9 @@ +export function makeAzurePath(path: string, apiVersion: string) { + // should omit /v1 prefix + path = path.replaceAll("v1/", ""); + + // should add api-key to query string + path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`; + + return path; +} diff --git a/app/client/api.ts b/app/client/api.ts index b04dd88b88c..eedd2c9ab48 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,5 +1,5 @@ import { getClientConfig } from "../config/client"; -import { ACCESS_CODE_PREFIX } from "../constant"; +import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant"; import { ChatMessage, ModelType, useAccessStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; @@ -127,22 +127,26 @@ export const api = new ClientApi(); export function getHeaders() { const accessStore = useAccessStore.getState(); - let headers: Record = { + const headers: Record = { "Content-Type": "application/json", "x-requested-with": "XMLHttpRequest", }; - const makeBearer = (token: string) => `Bearer ${token.trim()}`; + const isAzure = accessStore.provider === ServiceProvider.Azure; + const authHeader = isAzure ? "api-key" : "Authorization"; + const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey; + + const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`; const validString = (x: string) => x && x.length > 0; // use user's api key first - if (validString(accessStore.token)) { - headers.Authorization = makeBearer(accessStore.token); + if (validString(apiKey)) { + headers[authHeader] = makeBearer(apiKey); } else if ( accessStore.enabledAccessControl() && validString(accessStore.accessCode) ) { - headers.Authorization = makeBearer( + headers[authHeader] = makeBearer( ACCESS_CODE_PREFIX + accessStore.accessCode, ); } diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 4a5ddce7de6..930d606900a 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -1,8 +1,10 @@ import { + ApiPath, DEFAULT_API_HOST, DEFAULT_MODELS, OpenaiPath, REQUEST_TIMEOUT_MS, + ServiceProvider, } from "@/app/constant"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; @@ -14,6 +16,7 @@ import { } from "@fortaine/fetch-event-source"; import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; +import { makeAzurePath } from "@/app/azure"; export interface OpenAIListModelResponse { object: string; @@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi { private disableListModels = true; path(path: string): string { - let openaiUrl = useAccessStore.getState().openaiUrl; - const apiPath = "/api/openai"; + const accessStore = useAccessStore.getState(); - if (openaiUrl.length === 0) { + const isAzure = accessStore.provider === ServiceProvider.Azure; + + if (isAzure && !accessStore.isValidAzure()) { + throw Error( + "incomplete azure config, please check it in your settings page", + ); + } + + let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl; + + if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; - openaiUrl = isApp ? DEFAULT_API_HOST : apiPath; + baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI; } - if (openaiUrl.endsWith("/")) { - openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) { + baseUrl = "https://" + baseUrl; } - if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { - openaiUrl = "https://" + openaiUrl; + + if (isAzure) { + path = makeAzurePath(path, accessStore.azureApiVersion); } - return [openaiUrl, path].join("/"); + + return [baseUrl, path].join("/"); } extractMessage(res: any) { @@ -156,14 +174,20 @@ export class ChatGPTApi implements LLMApi { } const text = msg.data; try { - const json = JSON.parse(text); - const delta = json.choices[0].delta.content; + const json = JSON.parse(text) as { + choices: Array<{ + delta: { + content: string; + }; + }>; + }; + const delta = json.choices[0]?.delta?.content; if (delta) { responseText += delta; options.onUpdate?.(responseText, delta); } } catch (e) { - console.error("[Request] parse error", text, msg); + console.error("[Request] parse error", text); } }, onclose() { diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 577d7754240..3e1548a1325 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -18,7 +18,7 @@ export function AuthPage() { const goChat = () => navigate(Path.Chat); const resetAccessCode = () => { accessStore.update((access) => { - access.token = ""; + access.openaiApiKey = ""; access.accessCode = ""; }); }; // Reset access code to empty string @@ -57,10 +57,10 @@ export function AuthPage() { className={styles["auth-input"]} type="password" placeholder={Locale.Settings.Token.Placeholder} - value={accessStore.token} + value={accessStore.openaiApiKey} onChange={(e) => { accessStore.update( - (access) => (access.token = e.currentTarget.value), + (access) => (access.openaiApiKey = e.currentTarget.value), ); }} /> diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 9afb49f7a66..c27c3eee464 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -998,7 +998,9 @@ function _Chat() { ).then((res) => { if (!res) return; if (payload.key) { - accessStore.update((access) => (access.token = payload.key!)); + accessStore.update( + (access) => (access.openaiApiKey = payload.key!), + ); } if (payload.url) { accessStore.update((access) => (access.openaiUrl = payload.url!)); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 572c0743a11..178fcec57e9 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -51,10 +51,13 @@ import Locale, { import { copyToClipboard } from "../utils"; import Link from "next/link"; import { + Azure, OPENAI_BASE_URL, Path, RELEASE_URL, STORAGE_KEY, + ServiceProvider, + SlotID, UPDATE_URL, } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; @@ -580,8 +583,16 @@ export function Settings() { const accessStore = useAccessStore(); const shouldHideBalanceQuery = useMemo(() => { const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); - return accessStore.hideBalanceQuery || isOpenAiUrl; - }, [accessStore.hideBalanceQuery, accessStore.openaiUrl]); + return ( + accessStore.hideBalanceQuery || + isOpenAiUrl || + accessStore.provider === ServiceProvider.Azure + ); + }, [ + accessStore.hideBalanceQuery, + accessStore.openaiUrl, + accessStore.provider, + ]); const usage = { used: updateStore.used, @@ -877,16 +888,16 @@ export function Settings() { - - {showAccessCode ? ( + + {showAccessCode && ( { accessStore.update( (access) => (access.accessCode = e.currentTarget.value), @@ -894,44 +905,152 @@ export function Settings() { }} /> - ) : ( - <> )} - {!accessStore.hideUserApiKey ? ( + {!accessStore.hideUserApiKey && ( <> accessStore.update( - (access) => (access.openaiUrl = e.currentTarget.value), + (access) => + (access.useCustomConfig = e.currentTarget.checked), ) } > - - { - accessStore.update( - (access) => (access.token = e.currentTarget.value), - ); - }} - /> - + {accessStore.useCustomConfig && ( + <> + + + + + {accessStore.provider === "OpenAI" ? ( + <> + + + accessStore.update( + (access) => + (access.openaiUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.openaiApiKey = e.currentTarget.value), + ); + }} + /> + + + ) : ( + <> + + + accessStore.update( + (access) => + (access.azureUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.azureApiKey = e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.azureApiVersion = + e.currentTarget.value), + ) + } + > + + + )} + + )} - ) : null} + )} {!shouldHideBalanceQuery ? ( - | JSX.Element - | null - | undefined; -}) { - return
{props.children}
; +export function List(props: { children: React.ReactNode; id?: string }) { + return ( +
+ {props.children} +
+ ); } export function Loading() { diff --git a/app/config/server.ts b/app/config/server.ts index 007c3973863..2f2e7d7fd8a 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -4,19 +4,28 @@ import { DEFAULT_MODELS } from "../constant"; declare global { namespace NodeJS { interface ProcessEnv { + PROXY_URL?: string; // docker only + OPENAI_API_KEY?: string; CODE?: string; + BASE_URL?: string; - PROXY_URL?: string; - OPENAI_ORG_ID?: string; + OPENAI_ORG_ID?: string; // openai only + VERCEL?: string; - HIDE_USER_API_KEY?: string; // disable user's api key input - DISABLE_GPT4?: string; // allow user to use gpt-4 or not BUILD_MODE?: "standalone" | "export"; BUILD_APP?: string; // is building desktop app + + HIDE_USER_API_KEY?: string; // disable user's api key input + DISABLE_GPT4?: string; // allow user to use gpt-4 or not ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not DISABLE_FAST_LINK?: string; // disallow parse settings from url or not CUSTOM_MODELS?: string; // to control custom models + + // azure only + AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name} + AZURE_API_KEY?: string; + AZURE_API_VERSION?: string; } } } @@ -41,7 +50,7 @@ export const getServerSideConfig = () => { ); } - let disableGPT4 = !!process.env.DISABLE_GPT4; + const disableGPT4 = !!process.env.DISABLE_GPT4; let customModels = process.env.CUSTOM_MODELS ?? ""; if (disableGPT4) { @@ -51,15 +60,25 @@ export const getServerSideConfig = () => { .join(","); } + const isAzure = !!process.env.AZURE_URL; + return { + baseUrl: process.env.BASE_URL, apiKey: process.env.OPENAI_API_KEY, + openaiOrgId: process.env.OPENAI_ORG_ID, + + isAzure, + azureUrl: process.env.AZURE_URL, + azureApiKey: process.env.AZURE_API_KEY, + azureApiVersion: process.env.AZURE_API_VERSION, + + needCode: ACCESS_CODES.size > 0, code: process.env.CODE, codes: ACCESS_CODES, - needCode: ACCESS_CODES.size > 0, - baseUrl: process.env.BASE_URL, + proxyUrl: process.env.PROXY_URL, - openaiOrgId: process.env.OPENAI_ORG_ID, isVercel: !!process.env.VERCEL, + hideUserApiKey: !!process.env.HIDE_USER_API_KEY, disableGPT4, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, diff --git a/app/constant.ts b/app/constant.ts index a97b8782292..fbc0c72e378 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -23,10 +23,12 @@ export enum Path { export enum ApiPath { Cors = "/api/cors", + OpenAI = "/api/openai", } export enum SlotID { AppBody = "app-body", + CustomModel = "custom-model", } export enum FileName { @@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; +export enum ServiceProvider { + OpenAI = "OpenAI", + Azure = "Azure", +} + export const OpenaiPath = { ChatPath: "v1/chat/completions", UsagePath: "dashboard/billing/usage", @@ -67,6 +74,10 @@ export const OpenaiPath = { ListModelPath: "v1/models", }; +export const Azure = { + ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}", +}; + export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by OpenAI. diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 4cd963fb8e2..e721adef7ae 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -258,11 +258,6 @@ const cn = { Title: "历史消息长度压缩阈值", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", }, - Token: { - Title: "API Key", - SubTitle: "使用自己的 Key 可绕过密码访问限制", - Placeholder: "OpenAI API Key", - }, Usage: { Title: "余额查询", @@ -273,19 +268,56 @@ const cn = { Check: "重新检查", NoAccess: "输入 API Key 或访问密码查看余额", }, - AccessCode: { - Title: "访问密码", - SubTitle: "管理员已开启加密访问", - Placeholder: "请输入访问密码", - }, - Endpoint: { - Title: "接口地址", - SubTitle: "除默认地址外,必须包含 http(s)://", - }, - CustomModel: { - Title: "自定义模型名", - SubTitle: "增加自定义模型可选项,使用英文逗号隔开", + + Access: { + AccessCode: { + Title: "访问密码", + SubTitle: "管理员已开启加密访问", + Placeholder: "请输入访问密码", + }, + CustomEndpoint: { + Title: "自定义接口", + SubTitle: "是否使用自定义 Azure 或 OpenAI 服务", + }, + Provider: { + Title: "模型服务商", + SubTitle: "切换不同的服务商", + }, + OpenAI: { + ApiKey: { + Title: "API Key", + SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制", + Placeholder: "OpenAI API Key", + }, + + Endpoint: { + Title: "接口地址", + SubTitle: "除默认地址外,必须包含 http(s)://", + }, + }, + Azure: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Azure Key 绕过密码访问限制", + Placeholder: "Azure API Key", + }, + + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + + ApiVerion: { + Title: "接口版本 (azure api version)", + SubTitle: "选择指定的部分版本", + }, + }, + CustomModel: { + Title: "自定义模型名", + SubTitle: "增加自定义模型可选项,使用英文逗号隔开", + }, }, + Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 928c4b72d4e..c6e61ecab04 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -262,11 +262,7 @@ const en: LocaleType = { SubTitle: "Will compress if uncompressed messages length exceeds the value", }, - Token: { - Title: "API Key", - SubTitle: "Use your key to ignore access code limit", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "Account Balance", SubTitle(used: any, total: any) { @@ -276,19 +272,55 @@ const en: LocaleType = { Check: "Check", NoAccess: "Enter API Key to check balance", }, - AccessCode: { - Title: "Access Code", - SubTitle: "Access control enabled", - Placeholder: "Need Access Code", - }, - Endpoint: { - Title: "Endpoint", - SubTitle: "Custom endpoint must start with http(s)://", - }, - CustomModel: { - Title: "Custom Models", - SubTitle: "Add extra model options, separate by comma", + Access: { + AccessCode: { + Title: "Access Code", + SubTitle: "Access control Enabled", + Placeholder: "Enter Code", + }, + CustomEndpoint: { + Title: "Custom Endpoint", + SubTitle: "Use custom Azure or OpenAI service", + }, + Provider: { + Title: "Model Provider", + SubTitle: "Select Azure or OpenAI", + }, + OpenAI: { + ApiKey: { + Title: "OpenAI API Key", + SubTitle: "User custom OpenAI Api Key", + Placeholder: "sk-xxx", + }, + + Endpoint: { + Title: "OpenAI Endpoint", + SubTitle: "Must starts with http(s):// or use /api/openai as default", + }, + }, + Azure: { + ApiKey: { + Title: "Azure Api Key", + SubTitle: "Check your api key from Azure console", + Placeholder: "Azure Api Key", + }, + + Endpoint: { + Title: "Azure Endpoint", + SubTitle: "Example: ", + }, + + ApiVerion: { + Title: "Azure Api Version", + SubTitle: "Check your api version from azure console", + }, + }, + CustomModel: { + Title: "Custom Models", + SubTitle: "Custom model options, seperated by comma", + }, }, + Model: "Model", Temperature: { Title: "Temperature", diff --git a/app/store/access.ts b/app/store/access.ts index f87e44a2ac4..2abe1e3cc9f 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,25 +1,41 @@ -import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant"; +import { + ApiPath, + DEFAULT_API_HOST, + ServiceProvider, + StoreKey, +} from "../constant"; import { getHeaders } from "../client/api"; import { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; +import { ensure } from "../utils/clone"; let fetchState = 0; // 0 not fetch, 1 fetching, 2 done const DEFAULT_OPENAI_URL = - getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; -console.log("[API] default openai url", DEFAULT_OPENAI_URL); + getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI; const DEFAULT_ACCESS_STATE = { - token: "", accessCode: "", + useCustomConfig: false, + + provider: ServiceProvider.OpenAI, + + // openai + openaiUrl: DEFAULT_OPENAI_URL, + openaiApiKey: "", + + // azure + azureUrl: "", + azureApiKey: "", + azureApiVersion: "2023-08-01-preview", + + // server config needCode: true, hideUserApiKey: false, hideBalanceQuery: false, disableGPT4: false, disableFastLink: false, customModels: "", - - openaiUrl: DEFAULT_OPENAI_URL, }; export const useAccessStore = createPersistStore( @@ -31,12 +47,24 @@ export const useAccessStore = createPersistStore( return get().needCode; }, + + isValidOpenAI() { + return ensure(get(), ["openaiUrl", "openaiApiKey"]); + }, + + isValidAzure() { + return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]); + }, + isAuthorized() { this.fetch(); // has token or has code or disabled access control return ( - !!get().token || !!get().accessCode || !this.enabledAccessControl() + this.isValidOpenAI() || + this.isValidAzure() || + !this.enabledAccessControl() || + (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); }, fetch() { @@ -64,6 +92,19 @@ export const useAccessStore = createPersistStore( }), { name: StoreKey.Access, - version: 1, + version: 2, + migrate(persistedState, version) { + if (version < 2) { + const state = persistedState as { + token: string; + openaiApiKey: string; + azureApiVersion: string; + }; + state.openaiApiKey = state.token; + state.azureApiVersion = "2023-08-01-preview"; + } + + return persistedState as any; + }, }, ); diff --git a/app/utils/clone.ts b/app/utils/clone.ts index 2958b6b9c35..c42288f7789 100644 --- a/app/utils/clone.ts +++ b/app/utils/clone.ts @@ -1,3 +1,10 @@ export function deepClone(obj: T) { return JSON.parse(JSON.stringify(obj)); } + +export function ensure( + obj: T, + keys: Array<[keyof T][number]>, +) { + return keys.every((k) => obj[k] !== undefined && obj[k] !== null); +} diff --git a/app/utils/store.ts b/app/utils/store.ts index cd151dc4925..684a1911279 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { persist } from "zustand/middleware"; +import { combine, persist } from "zustand/middleware"; import { Updater } from "../typing"; import { deepClone } from "./clone"; @@ -23,33 +23,42 @@ type SetStoreState = ( replace?: boolean | undefined, ) => void; -export function createPersistStore( - defaultState: T, +export function createPersistStore( + state: T, methods: ( set: SetStoreState>, get: () => T & MakeUpdater, ) => M, persistOptions: SecondParam>>, ) { - return create>()( - persist((set, get) => { - return { - ...defaultState, - ...methods(set as any, get), - - lastUpdateTime: 0, - markUpdate() { - set({ lastUpdateTime: Date.now() } as Partial< - T & M & MakeUpdater - >); + return create( + persist( + combine( + { + ...state, + lastUpdateTime: 0, }, - update(updater) { - const state = deepClone(get()); - updater(state); - get().markUpdate(); - set(state); + (set, get) => { + return { + ...methods(set, get as any), + + markUpdate() { + set({ lastUpdateTime: Date.now() } as Partial< + T & M & MakeUpdater + >); + }, + update(updater) { + const state = deepClone(get()); + updater(state); + set({ + ...state, + lastUpdateTime: Date.now(), + }); + }, + } as M & MakeUpdater; }, - }; - }, persistOptions), + ), + persistOptions as any, + ), ); } From c9dd953817798d785abef6c92f6c9d047c5d16ca Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 02:50:50 +0800 Subject: [PATCH 160/202] fixup --- app/components/auth.tsx | 2 +- app/locales/ar.ts | 16 ++-------------- app/locales/bn.ts | 16 ++-------------- app/locales/cs.ts | 12 ++---------- app/locales/de.ts | 12 +----------- app/locales/es.ts | 12 ++---------- app/locales/fr.ts | 12 ++---------- app/locales/id.ts | 21 +++++---------------- app/locales/it.ts | 13 ++----------- app/locales/jp.ts | 15 ++++----------- app/locales/ko.ts | 12 ++---------- app/locales/no.ts | 13 ++----------- app/locales/ru.ts | 12 ++---------- app/locales/tr.ts | 12 ++---------- app/locales/tw.ts | 12 ++---------- app/locales/vi.ts | 12 ++---------- 16 files changed, 35 insertions(+), 169 deletions(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 3e1548a1325..7962d46bee4 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -56,7 +56,7 @@ export function AuthPage() { { accessStore.update( diff --git a/app/locales/ar.ts b/app/locales/ar.ts index d5844acd695..b58c3a2e8e5 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Title: "حد الضغط للتاريخ", SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد", }, - Token: { - Title: "مفتاح API", - SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول", - Placeholder: "مفتاح OpenAI API", - }, + Usage: { Title: "رصيد الحساب", SubTitle(used: any, total: any) { @@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد Check: "التحقق", NoAccess: "أدخل مفتاح API للتحقق من الرصيد", }, - AccessCode: { - Title: "رمز الوصول", - SubTitle: "تم تمكين التحكم في الوصول", - Placeholder: "رمز الوصول المطلوب", - }, - Endpoint: { - Title: "نقطة النهاية", - SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://", - }, + Model: "النموذج", Temperature: { Title: "الحرارة", diff --git a/app/locales/bn.ts b/app/locales/bn.ts index 2db132cecc2..6dfb0da9bc4 100644 --- a/app/locales/bn.ts +++ b/app/locales/bn.ts @@ -199,11 +199,7 @@ const bn: PartialLocaleType = { SubTitle: "নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে", }, - Token: { - Title: "অ্যাপি কী", - SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন", - Placeholder: "OpenAI API কী", - }, + Usage: { Title: "একাউন্ট ব্যালেন্স", SubTitle(used: any, total: any) { @@ -213,15 +209,7 @@ const bn: PartialLocaleType = { Check: "চেক", NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন", }, - AccessCode: { - Title: "অ্যাক্সেস কোড", - SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়", - Placeholder: "অ্যাক্সেস কোড প্রয়োজন", - }, - Endpoint: { - Title: "ইনটারপয়েন্ট", - SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে", - }, + Model: "মডেল", Temperature: { Title: "তাপমাত্রা", diff --git a/app/locales/cs.ts b/app/locales/cs.ts index 57aa803e42b..c1a84430fb3 100644 --- a/app/locales/cs.ts +++ b/app/locales/cs.ts @@ -124,11 +124,7 @@ const cs: PartialLocaleType = { SubTitle: "Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu", }, - Token: { - Title: "API klíč", - SubTitle: "Použitím klíče ignorujete omezení přístupového kódu", - Placeholder: "Klíč API OpenAI", - }, + Usage: { Title: "Stav účtu", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const cs: PartialLocaleType = { Check: "Zkontrolovat", NoAccess: "Pro kontrolu zůstatku zadejte klíč API", }, - AccessCode: { - Title: "Přístupový kód", - SubTitle: "Kontrola přístupu povolena", - Placeholder: "Potřebujete přístupový kód", - }, + Model: "Model", Temperature: { Title: "Teplota", diff --git a/app/locales/de.ts b/app/locales/de.ts index e0bdc52b749..2fe871bc9f0 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -124,12 +124,7 @@ const de: PartialLocaleType = { SubTitle: "Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet", }, - Token: { - Title: "API-Schlüssel", - SubTitle: - "Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren", - Placeholder: "OpenAI API-Schlüssel", - }, + Usage: { Title: "Kontostand", SubTitle(used: any, total: any) { @@ -139,11 +134,6 @@ const de: PartialLocaleType = { Check: "Erneut prüfen", NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen", }, - AccessCode: { - Title: "Zugangscode", - SubTitle: "Zugangskontrolle aktiviert", - Placeholder: "Zugangscode erforderlich", - }, Model: "Modell", Temperature: { Title: "Temperature", //Temperatur diff --git a/app/locales/es.ts b/app/locales/es.ts index a6ae154f44f..7d742d536e5 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -124,11 +124,7 @@ const es: PartialLocaleType = { SubTitle: "Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor", }, - Token: { - Title: "Clave de API", - SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso", - Placeholder: "Clave de la API de OpenAI", - }, + Usage: { Title: "Saldo de la cuenta", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const es: PartialLocaleType = { Check: "Comprobar de nuevo", NoAccess: "Introduzca la clave API para comprobar el saldo", }, - AccessCode: { - Title: "Código de acceso", - SubTitle: "Control de acceso habilitado", - Placeholder: "Necesita código de acceso", - }, + Model: "Modelo", Temperature: { Title: "Temperatura", diff --git a/app/locales/fr.ts b/app/locales/fr.ts index f5200f2719c..944754d62a7 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -173,11 +173,7 @@ const fr: PartialLocaleType = { SubTitle: "Comprimera si la longueur des messages non compressés dépasse cette valeur", }, - Token: { - Title: "Clé API", - SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès", - Placeholder: "Clé OpenAI API", - }, + Usage: { Title: "Solde du compte", SubTitle(used: any, total: any) { @@ -187,11 +183,7 @@ const fr: PartialLocaleType = { Check: "Vérifier", NoAccess: "Entrez la clé API pour vérifier le solde", }, - AccessCode: { - Title: "Code d'accès", - SubTitle: "Contrôle d'accès activé", - Placeholder: "Code d'accès requis", - }, + Model: "Modèle", Temperature: { Title: "Température", diff --git a/app/locales/id.ts b/app/locales/id.ts index b5e4a70b751..4da55948efc 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -4,8 +4,9 @@ import { PartialLocaleType } from "./index"; const id: PartialLocaleType = { WIP: "Coming Soon...", Error: { - Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).", - }, + Unauthorized: + "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).", + }, Auth: { Title: "Diperlukan Kode Akses", Tips: "Masukkan kode akses di bawah", @@ -237,11 +238,7 @@ const id: PartialLocaleType = { SubTitle: "Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", }, - Token: { - Title: "Kunci API", - SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses", - Placeholder: "Kunci API OpenAI", - }, + Usage: { Title: "Saldo Akun", SubTitle(used: any, total: any) { @@ -251,15 +248,7 @@ const id: PartialLocaleType = { Check: "Periksa", NoAccess: "Masukkan kunci API untuk memeriksa saldo", }, - AccessCode: { - Title: "Kode Akses", - SubTitle: "Kontrol akses diaktifkan", - Placeholder: "Diperlukan kode akses", - }, - Endpoint: { - Title: "Endpoint", - SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom", - }, + Model: "Model", Temperature: { Title: "Suhu", diff --git a/app/locales/it.ts b/app/locales/it.ts index bf20747b108..7f0a95846c2 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -124,12 +124,7 @@ const it: PartialLocaleType = { SubTitle: "Comprimerà se la lunghezza dei messaggi non compressi supera il valore", }, - Token: { - Title: "API Key", - SubTitle: - "Utilizzare la chiave per ignorare il limite del codice di accesso", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "Bilancio Account", SubTitle(used: any, total: any) { @@ -139,11 +134,7 @@ const it: PartialLocaleType = { Check: "Controlla ancora", NoAccess: "Inserire la chiave API per controllare il saldo", }, - AccessCode: { - Title: "Codice d'accesso", - SubTitle: "Controllo d'accesso abilitato", - Placeholder: "Inserisci il codice d'accesso", - }, + Model: "Modello GPT", Temperature: { Title: "Temperature", diff --git a/app/locales/jp.ts b/app/locales/jp.ts index b63e8ba3a56..e0ea07c755b 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -20,7 +20,8 @@ const jp: PartialLocaleType = { Stop: "停止", Retry: "リトライ", Pin: "ピン", - PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました", + PinToastContent: + "コンテキストプロンプトに1つのメッセージをピン留めしました", PinToastAction: "表示", Delete: "削除", Edit: "編集", @@ -146,11 +147,7 @@ const jp: PartialLocaleType = { SubTitle: "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", }, - Token: { - Title: "APIキー", - SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する", - Placeholder: "OpenAI APIキー", - }, + Usage: { Title: "残高照会", SubTitle(used: any, total: any) { @@ -160,11 +157,7 @@ const jp: PartialLocaleType = { Check: "再確認", NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示", }, - AccessCode: { - Title: "アクセスパスワード", - SubTitle: "暗号化アクセスが有効になっています", - Placeholder: "アクセスパスワードを入力してください", - }, + Model: "モデル (model)", Temperature: { Title: "ランダム性 (temperature)", diff --git a/app/locales/ko.ts b/app/locales/ko.ts index 717ce30b2f8..844459fc4ea 100644 --- a/app/locales/ko.ts +++ b/app/locales/ko.ts @@ -124,11 +124,7 @@ const ko: PartialLocaleType = { Title: "기록 압축 임계값", SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨", }, - Token: { - Title: "API 키", - SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용", - Placeholder: "OpenAI API 키", - }, + Usage: { Title: "계정 잔액", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const ko: PartialLocaleType = { Check: "확인", NoAccess: "잔액 확인을 위해 API 키를 입력하세요.", }, - AccessCode: { - Title: "액세스 코드", - SubTitle: "액세스 제어가 활성화됨", - Placeholder: "액세스 코드 입력", - }, + Model: "모델", Temperature: { Title: "온도 (temperature)", diff --git a/app/locales/no.ts b/app/locales/no.ts index 43c92916f3e..3a0e61107a4 100644 --- a/app/locales/no.ts +++ b/app/locales/no.ts @@ -106,12 +106,7 @@ const no: PartialLocaleType = { SubTitle: "Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien", }, - Token: { - Title: "API Key", - SubTitle: - "Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning", - Placeholder: "OpenAI API-nøkkel", - }, + Usage: { Title: "Saldo for konto", SubTitle(used: any, total: any) { @@ -121,11 +116,7 @@ const no: PartialLocaleType = { Check: "Sjekk", NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo", }, - AccessCode: { - Title: "Tilgangskode", - SubTitle: "Tilgangskontroll på", - Placeholder: "Trenger tilgangskode", - }, + Model: "Model", Temperature: { Title: "Temperatur", diff --git a/app/locales/ru.ts b/app/locales/ru.ts index bf98b4eb865..d12cf3e4258 100644 --- a/app/locales/ru.ts +++ b/app/locales/ru.ts @@ -125,11 +125,7 @@ const ru: PartialLocaleType = { SubTitle: "Будет сжимать, если длина несжатых сообщений превышает указанное значение", }, - Token: { - Title: "API ключ", - SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа", - Placeholder: "API ключ OpenAI", - }, + Usage: { Title: "Баланс аккаунта", SubTitle(used: any, total: any) { @@ -139,11 +135,7 @@ const ru: PartialLocaleType = { Check: "Проверить", NoAccess: "Введите API ключ, чтобы проверить баланс", }, - AccessCode: { - Title: "Код доступа", - SubTitle: "Контроль доступа включен", - Placeholder: "Требуется код доступа", - }, + Model: "Модель", Temperature: { Title: "Температура", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 06996d83dac..524c1b2c546 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -124,11 +124,7 @@ const tr: PartialLocaleType = { SubTitle: "Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır", }, - Token: { - Title: "API Anahtarı", - SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın", - Placeholder: "OpenAI API Anahtarı", - }, + Usage: { Title: "Hesap Bakiyesi", SubTitle(used: any, total: any) { @@ -138,11 +134,7 @@ const tr: PartialLocaleType = { Check: "Tekrar Kontrol Et", NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin", }, - AccessCode: { - Title: "Erişim Kodu", - SubTitle: "Erişim kontrolü etkinleştirme", - Placeholder: "Erişim Kodu Gerekiyor", - }, + Model: "Model", Temperature: { Title: "Gerçeklik", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index e9f38d097e1..af47e30ff71 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -120,11 +120,7 @@ const tw: PartialLocaleType = { Title: "歷史訊息長度壓縮閾值", SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮", }, - Token: { - Title: "API Key", - SubTitle: "使用自己的 Key 可規避授權存取限制", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "帳戶餘額", SubTitle(used: any, total: any) { @@ -134,11 +130,7 @@ const tw: PartialLocaleType = { Check: "重新檢查", NoAccess: "輸入 API Key 檢視餘額", }, - AccessCode: { - Title: "授權碼", - SubTitle: "目前是未授權存取狀態", - Placeholder: "請輸入授權碼", - }, + Model: "模型 (model)", Temperature: { Title: "隨機性 (temperature)", diff --git a/app/locales/vi.ts b/app/locales/vi.ts index 8f53a3dc1ee..3d95b566497 100644 --- a/app/locales/vi.ts +++ b/app/locales/vi.ts @@ -123,11 +123,7 @@ const vi: PartialLocaleType = { Title: "Ngưỡng nén lịch sử tin nhắn", SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng", }, - Token: { - Title: "API Key", - SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập", - Placeholder: "OpenAI API Key", - }, + Usage: { Title: "Hạn mức tài khoản", SubTitle(used: any, total: any) { @@ -137,11 +133,7 @@ const vi: PartialLocaleType = { Check: "Kiểm tra", NoAccess: "Nhập API Key để kiểm tra hạn mức", }, - AccessCode: { - Title: "Mã truy cập", - SubTitle: "Đã bật kiểm soát truy cập", - Placeholder: "Nhập mã truy cập", - }, + Model: "Mô hình", Temperature: { Title: "Tính ngẫu nhiên (temperature)", From c7e0a6f37fa4483c5211af45a64b8f00821c3586 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 02:57:40 +0800 Subject: [PATCH 161/202] doc: update azure env vars --- README.md | 22 ++++++++++++++++++---- README_CN.md | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3973c84bfde..11c8fdef671 100644 --- a/README.md +++ b/README.md @@ -153,14 +153,14 @@ After adding or modifying this environment variable, please redeploy the project > [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量) -### `OPENAI_API_KEY` (required) - -Your openai api key. - ### `CODE` (optional) Access password, separated by comma. +### `OPENAI_API_KEY` (required) + +Your openai api key. + ### `BASE_URL` (optional) > Default: `https://api.openai.com` @@ -173,6 +173,20 @@ Override openai api request base url. Specify OpenAI organization ID. +### `AZURE_URL` (optional) + +> Example: https://{azure-resource-url}/openai/deployments/{deploy-name} + +Azure deploy url. + +### `AZURE_API_KEY` (optional) + +Azure Api Key. + +### `AZURE_API_VERSION` (optional) + +Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions). + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index d8e9553e183..c82dfc044e7 100644 --- a/README_CN.md +++ b/README_CN.md @@ -90,6 +90,20 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 指定 OpenAI 中的组织 ID。 +### `AZURE_URL` (可选) + +> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name} + +Azure 部署地址。 + +### `AZURE_API_KEY` (可选) + +Azure 密钥。 + +### `AZURE_API_VERSION` (可选) + +Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 From 8032e6d68d6714e982287ad54b8c26cb17d7d818 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 10 Nov 2023 02:59:30 +0800 Subject: [PATCH 162/202] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 11c8fdef671..58d2b67a1a7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. - 🚀 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! +- 🚀 v2.9.11 you can use azure endpoint now. ## 主要功能 @@ -93,6 +94,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI. - 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com - 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 - 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 +- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 ## Get Started From e6b72ac1ff25c32e9f87b00b31811ec3e75d9f63 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 10 Nov 2023 02:59:47 +0800 Subject: [PATCH 163/202] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58d2b67a1a7..a7c862b4099 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

ChatGPT Next Web

-English / [简体中文](./README_CN.md) / [日本語](./README_JA.md) +English / [简体中文](./README_CN.md) One-Click to get well-designed cross-platform ChatGPT web UI. From cb140e482f522b5add2f31b42d80eda471764335 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 10 Nov 2023 15:15:43 +0800 Subject: [PATCH 164/202] fix: #3207 ensure corner case --- README_CN.md | 2 +- app/utils/clone.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README_CN.md b/README_CN.md index c82dfc044e7..72376374f86 100644 --- a/README_CN.md +++ b/README_CN.md @@ -138,7 +138,7 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro OPENAI_API_KEY= # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 -BASE_URL=https://nb.nextweb.fun/api/proxy +BASE_URL=https://ab.nextweb.fun/api/proxy ``` ### 本地开发 diff --git a/app/utils/clone.ts b/app/utils/clone.ts index c42288f7789..e4cd291111d 100644 --- a/app/utils/clone.ts +++ b/app/utils/clone.ts @@ -6,5 +6,7 @@ export function ensure( obj: T, keys: Array<[keyof T][number]>, ) { - return keys.every((k) => obj[k] !== undefined && obj[k] !== null); + return keys.every( + (k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "", + ); } From 2bfb362832eab66533eba1e455cec28762e86405 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 10 Nov 2023 15:19:31 +0800 Subject: [PATCH 165/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 649e3816db3..397ae0d8345 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.10" + "version": "2.9.11" }, "tauri": { "allowlist": { From 6b2db97347b4bc260146712997be1549c4968ec5 Mon Sep 17 00:00:00 2001 From: Ensteinjun Date: Fri, 10 Nov 2023 15:44:07 +0800 Subject: [PATCH 166/202] fixbug --- app/api/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/common.ts b/app/api/common.ts index fc877b02db2..adec611b2b3 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -18,7 +18,7 @@ export async function requestOpenai(req: NextRequest) { ); let baseUrl = - serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL; + serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL; if (!baseUrl.startsWith("http")) { baseUrl = `https://${baseUrl}`; From ca792669fce17493a58be64ec2388b4e9db9abb5 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 10 Nov 2023 18:20:51 +0800 Subject: [PATCH 167/202] Update constant.ts --- app/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constant.ts b/app/constant.ts index fbc0c72e378..561899769d4 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun"; +export const DEFAULT_CORS_HOST = "https://a.nextweb.fun"; export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export const OPENAI_BASE_URL = "https://api.openai.com"; From 943214c6a78bf3b9c6ed12e029efa682adaee659 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 10 Nov 2023 18:21:22 +0800 Subject: [PATCH 168/202] Update README_CN.md --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index 72376374f86..604771c5251 100644 --- a/README_CN.md +++ b/README_CN.md @@ -138,7 +138,7 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro OPENAI_API_KEY= # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 -BASE_URL=https://ab.nextweb.fun/api/proxy +BASE_URL=https://a.nextweb.fun/api/proxy ``` ### 本地开发 From be9774943bc17e30111ccf6ec1eb8242e61f3fa1 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 12 Nov 2023 00:29:36 +0800 Subject: [PATCH 169/202] feat: #3224 auto switch to first avaliable model --- app/components/chat.tsx | 22 +++++++++++++++++++--- app/utils/hooks.ts | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c27c3eee464..48f76e8ab48 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -431,11 +431,27 @@ export function ChatActions(props: { // switch model const currentModel = chatStore.currentSession().mask.modelConfig.model; - const models = useAllModels() - .filter((m) => m.available) - .map((m) => m.name); + const allModels = useAllModels(); + const models = useMemo( + () => allModels.filter((m) => m.available).map((m) => m.name), + [allModels], + ); const [showModelSelector, setShowModelSelector] = useState(false); + useEffect(() => { + // if current model is not available + // switch to first available model + const isUnavaliableModel = !models.includes(currentModel); + if (isUnavaliableModel && models.length > 0) { + const nextModel = models[0] as ModelType; + chatStore.updateCurrentSession( + (session) => (session.mask.modelConfig.model = nextModel), + ); + showToast(nextModel); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentModel, models]); + return (
{couldStop && ( diff --git a/app/utils/hooks.ts b/app/utils/hooks.ts index f6bfae67323..35d1f53a4c9 100644 --- a/app/utils/hooks.ts +++ b/app/utils/hooks.ts @@ -8,7 +8,7 @@ export function useAllModels() { const models = useMemo(() => { return collectModels( configStore.models, - [accessStore.customModels, configStore.customModels].join(","), + [configStore.customModels, accessStore.customModels].join(","), ); }, [accessStore.customModels, configStore.customModels, configStore.models]); From a5a1f2e8ad781e0c82a6f775746286477d806545 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 12 Nov 2023 00:46:21 +0800 Subject: [PATCH 170/202] feat: CUSTOM_MODELS support mapper --- app/api/common.ts | 2 +- app/components/chat.tsx | 10 +++++----- app/utils/model.ts | 32 +++++++++++++++++++++----------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/api/common.ts b/app/api/common.ts index adec611b2b3..dd1cc0bb80e 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -81,7 +81,7 @@ export async function requestOpenai(req: NextRequest) { const jsonBody = JSON.parse(clonedBody) as { model?: string }; // not undefined and is false - if (modelTable[jsonBody?.model ?? ""] === false) { + if (modelTable[jsonBody?.model ?? ""].available === false) { return NextResponse.json( { error: true, diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 48f76e8ab48..a088483e7b6 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -433,7 +433,7 @@ export function ChatActions(props: { const currentModel = chatStore.currentSession().mask.modelConfig.model; const allModels = useAllModels(); const models = useMemo( - () => allModels.filter((m) => m.available).map((m) => m.name), + () => allModels.filter((m) => m.available), [allModels], ); const [showModelSelector, setShowModelSelector] = useState(false); @@ -441,9 +441,9 @@ export function ChatActions(props: { useEffect(() => { // if current model is not available // switch to first available model - const isUnavaliableModel = !models.includes(currentModel); + const isUnavaliableModel = !models.some((m) => m.name === currentModel); if (isUnavaliableModel && models.length > 0) { - const nextModel = models[0] as ModelType; + const nextModel = models[0].name as ModelType; chatStore.updateCurrentSession( (session) => (session.mask.modelConfig.model = nextModel), ); @@ -531,8 +531,8 @@ export function ChatActions(props: { ({ - title: m, - value: m, + title: m.displayName, + value: m.name, }))} onClose={() => setShowModelSelector(false)} onSelection={(s) => { diff --git a/app/utils/model.ts b/app/utils/model.ts index 23090f9d2f3..d5c009c02d2 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -4,21 +4,34 @@ export function collectModelTable( models: readonly LLMModel[], customModels: string, ) { - const modelTable: Record = {}; + const modelTable: Record< + string, + { available: boolean; name: string; displayName: string } + > = {}; // default models - models.forEach((m) => (modelTable[m.name] = m.available)); + models.forEach( + (m) => + (modelTable[m.name] = { + ...m, + displayName: m.name, + }), + ); // server custom models customModels .split(",") .filter((v) => !!v && v.length > 0) .map((m) => { - if (m.startsWith("+")) { - modelTable[m.slice(1)] = true; - } else if (m.startsWith("-")) { - modelTable[m.slice(1)] = false; - } else modelTable[m] = true; + const available = !m.startsWith("-"); + const nameConfig = + m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; + const [name, displayName] = nameConfig.split(":"); + modelTable[name] = { + name, + displayName: displayName || name, + available, + }; }); return modelTable; } @@ -31,10 +44,7 @@ export function collectModels( customModels: string, ) { const modelTable = collectModelTable(models, customModels); - const allModels = Object.keys(modelTable).map((m) => ({ - name: m, - available: modelTable[m], - })); + const allModels = Object.values(modelTable); return allModels; } From 64647b0bb3d06b5c0ab17c96cf245753d8f4b48a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 12 Nov 2023 00:49:58 +0800 Subject: [PATCH 171/202] chore: update doc for mapped `CUSTOM_MODELS` --- README.md | 4 ++-- README_CN.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a7c862b4099..abe93306180 100644 --- a/README.md +++ b/README.md @@ -216,9 +216,9 @@ If you want to disable parse settings from url, set this to 1. ### `CUSTOM_MODELS` (optional) > Default: Empty -> Example: `+llama,+claude-2,-gpt-3.5-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list. +> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`. -To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma. +To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name:displayName` to customize model name, separated by comma. ## Requirements diff --git a/README_CN.md b/README_CN.md index 604771c5251..dde8c19b352 100644 --- a/README_CN.md +++ b/README_CN.md @@ -122,9 +122,9 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro ### `CUSTOM_MODELS` (可选) -> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。 +> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。 -用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。 +用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。 ## 开发 From be6d45e49f1df90daba4625117b95903189891c2 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 12 Nov 2023 01:21:39 +0800 Subject: [PATCH 172/202] feat: close #3222 share message list should start from clear context index --- app/components/exporter.tsx | 29 ++++++++++++++----- app/components/message-selector.module.scss | 10 +++++-- app/components/message-selector.tsx | 31 ++++++++++++++++----- app/components/ui-lib.tsx | 4 ++- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 0a885d87463..435e24953e6 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -27,7 +27,7 @@ import { Avatar } from "./emoji"; import dynamic from "next/dynamic"; import NextImage from "next/image"; -import { toBlob, toJpeg, toPng } from "html-to-image"; +import { toBlob, toPng } from "html-to-image"; import { DEFAULT_MASK_AVATAR } from "../store/mask"; import { api } from "../client/api"; import { prettyObject } from "../utils/format"; @@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { export function ExportMessageModal(props: { onClose: () => void }) { return (
- + + 只有清除上下文之后的消息会被展示 +
+ } + >
@@ -149,7 +164,7 @@ export function MessageExporter() { if (exportConfig.includeContext) { ret.push(...session.mask.context); } - ret.push(...session.messages.filter((m, i) => selection.has(m.id))); + ret.push(...session.messages.filter((m) => selection.has(m.id))); return ret; }, [ exportConfig.includeContext, @@ -437,13 +452,13 @@ export function ImagePreviewer(props: { showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; - + const isApp = getClientConfig()?.isApp; - + try { const blob = await toPng(dom); if (!blob) return; - + if (isMobile || (isApp && window.__TAURI__)) { if (isApp && window.__TAURI__) { const result = await window.__TAURI__.dialog.save({ @@ -459,7 +474,7 @@ export function ImagePreviewer(props: { }, ], }); - + if (result !== null) { const response = await fetch(blob); const buffer = await response.arrayBuffer(); diff --git a/app/components/message-selector.module.scss b/app/components/message-selector.module.scss index b4ba1a1412a..c8defb6b027 100644 --- a/app/components/message-selector.module.scss +++ b/app/components/message-selector.module.scss @@ -58,8 +58,8 @@ } .body { - flex-grow: 1; - max-width: calc(100% - 40px); + flex: 1; + max-width: calc(100% - 80px); .date { font-size: 12px; @@ -71,6 +71,12 @@ font-size: 12px; } } + + .checkbox { + display: flex; + justify-content: flex-end; + flex: 1; + } } } } diff --git a/app/components/message-selector.tsx b/app/components/message-selector.tsx index cadf52e643e..3d2321d0962 100644 --- a/app/components/message-selector.tsx +++ b/app/components/message-selector.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ChatMessage, useAppConfig, useChatStore } from "../store"; import { Updater } from "../typing"; import { IconButton } from "./button"; @@ -73,11 +73,23 @@ export function MessageSelector(props: { const chatStore = useChatStore(); const session = chatStore.currentSession(); const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming; - const messages = session.messages.filter( - (m, i) => - m.id && // message must have id - isValid(m) && - (i >= session.messages.length - 1 || isValid(session.messages[i + 1])), + const allMessages = useMemo(() => { + let startIndex = Math.max(0, session.clearContextIndex ?? 0); + if (startIndex === session.messages.length - 1) { + startIndex = 0; + } + return session.messages.slice(startIndex); + }, [session.messages, session.clearContextIndex]); + + const messages = useMemo( + () => + allMessages.filter( + (m, i) => + m.id && // message must have id + isValid(m) && + (i >= allMessages.length - 1 || isValid(allMessages[i + 1])), + ), + [allMessages], ); const messageCount = messages.length; const config = useAppConfig(); @@ -176,6 +188,8 @@ export function MessageSelector(props: {
{messages.map((m, i) => { if (!isInSearchResult(m.id!)) return null; + const id = m.id ?? i; + const isSelected = props.selection.has(id); return (
{ props.updateSelection((selection) => { - const id = m.id ?? i; selection.has(id) ? selection.delete(id) : selection.add(id); }); onClickIndex(i); @@ -206,6 +219,10 @@ export function MessageSelector(props: { {m.content}
+ +
+ +
); })} diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 0c927728a4e..f7e326fd318 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -97,8 +97,9 @@ export function Loading() { interface ModalProps { title: string; children?: any; - actions?: JSX.Element[]; + actions?: React.ReactNode[]; defaultMax?: boolean; + footer?: React.ReactNode; onClose?: () => void; } export function Modal(props: ModalProps) { @@ -147,6 +148,7 @@ export function Modal(props: ModalProps) {
{props.children}
+ {props.footer}
{props.actions?.map((action, i) => (
From 0f6ed9c2932f6fd31214ec5d1b9d1d6a5b1f56d5 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sun, 12 Nov 2023 00:53:15 +0700 Subject: [PATCH 173/202] Feat UI/UX Page Local Language [Exporter Message] [+] fix(exporter.tsx): update the text in the ExportMessageModal component to use the localized title from the locale file [+] feat(cn.ts, en.ts, id.ts): add localized title for the Exporter Description in the respective locale files --- app/components/exporter.tsx | 2 +- app/locales/cn.ts | 3 +++ app/locales/en.ts | 3 +++ app/locales/id.ts | 3 +++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 435e24953e6..571c281226c 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -53,7 +53,7 @@ export function ExportMessageModal(props: { onClose: () => void }) { opacity: 0.5, }} > - 只有清除上下文之后的消息会被展示 + {Locale.Exporter.Description.Title}
} > diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e721adef7ae..bb4baf50621 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -441,6 +441,9 @@ const cn = { Config: "配置", }, Exporter: { + Description : { + Title: "只有清除上下文之后的消息会被展示" + }, Model: "模型", Messages: "消息", Topic: "主题", diff --git a/app/locales/en.ts b/app/locales/en.ts index c6e61ecab04..f90cffd4cb1 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -442,6 +442,9 @@ const en: LocaleType = { Config: "Config", }, Exporter: { + Description: { + Title: "Only messages after clearing the context will be displayed" + }, Model: "Model", Messages: "Messages", Topic: "Topic", diff --git a/app/locales/id.ts b/app/locales/id.ts index 4da55948efc..571156a5776 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -368,6 +368,9 @@ const id: PartialLocaleType = { Edit: "Edit", }, Exporter: { + Description: { + Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan" + }, Model: "Model", Messages: "Pesan", Topic: "Topik", From a46f08154e74744ac6c0bec6d074e00df445a851 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sun, 12 Nov 2023 01:50:35 +0700 Subject: [PATCH 174/202] Fix UI/UX Page Local Language [Exporter Message] fix(locales): fix incorrect description title in en.ts and id.ts - Change "Only messages after clearing the context will be displayed" to "Only messages before clearing the context will be displayed" in en.ts - Change "Hanya pesan setelah menghapus konteks yang akan ditampilkan" to "Hanya pesan sebelum menghapus konteks yang akan ditampilkan" in id.ts --- app/locales/en.ts | 2 +- app/locales/id.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locales/en.ts b/app/locales/en.ts index f90cffd4cb1..7390b72840d 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -443,7 +443,7 @@ const en: LocaleType = { }, Exporter: { Description: { - Title: "Only messages after clearing the context will be displayed" + Title: "Only messages before clearing the context will be displayed" }, Model: "Model", Messages: "Messages", diff --git a/app/locales/id.ts b/app/locales/id.ts index 571156a5776..2b9b21e1122 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -369,7 +369,7 @@ const id: PartialLocaleType = { }, Exporter: { Description: { - Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan" + Title: "Hanya pesan sebelum menghapus konteks yang akan ditampilkan" }, Model: "Model", Messages: "Pesan", From 5ba3fc9321a126dce367c57d14649ec8a590dc82 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sun, 12 Nov 2023 01:56:31 +0700 Subject: [PATCH 175/202] Revert "Fix UI/UX Page Local Language [Exporter Message]" This reverts commit a46f08154e74744ac6c0bec6d074e00df445a851. Reason : better after instead of before --- app/locales/en.ts | 2 +- app/locales/id.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locales/en.ts b/app/locales/en.ts index 7390b72840d..f90cffd4cb1 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -443,7 +443,7 @@ const en: LocaleType = { }, Exporter: { Description: { - Title: "Only messages before clearing the context will be displayed" + Title: "Only messages after clearing the context will be displayed" }, Model: "Model", Messages: "Messages", diff --git a/app/locales/id.ts b/app/locales/id.ts index 2b9b21e1122..571156a5776 100644 --- a/app/locales/id.ts +++ b/app/locales/id.ts @@ -369,7 +369,7 @@ const id: PartialLocaleType = { }, Exporter: { Description: { - Title: "Hanya pesan sebelum menghapus konteks yang akan ditampilkan" + Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan" }, Model: "Model", Messages: "Pesan", From 3a654ba1998581ce5e319988277d2858854f0edf Mon Sep 17 00:00:00 2001 From: nanaya Date: Sun, 12 Nov 2023 11:18:14 +0800 Subject: [PATCH 176/202] UI (model selection): hide unavailable model options --- app/components/model-config.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 1c730e1449f..214a18c79b5 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -25,11 +25,13 @@ export function ModelConfigList(props: { ); }} > - {allModels.map((v, i) => ( - - ))} + {allModels + .filter((v) => v.available) + .map((v, i) => ( + + ))} Date: Sun, 12 Nov 2023 19:33:19 +0800 Subject: [PATCH 177/202] fix: #3189 should correct math eq in exporter --- app/components/chat-list.tsx | 5 ++++- app/components/chat.tsx | 7 ++++++- app/components/exporter.module.scss | 3 ++- app/components/exporter.tsx | 7 +++---- app/components/markdown.tsx | 2 +- app/components/mask.tsx | 18 +++++++++++------- app/components/message-selector.tsx | 5 ++++- app/components/new-chat.tsx | 16 ++++------------ app/constant.ts | 3 +++ app/locales/cn.ts | 4 ++-- 10 files changed, 40 insertions(+), 30 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index f76b369f12d..33967717d53 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -61,7 +61,10 @@ export function ChatItem(props: { {props.narrow ? (
- +
{props.count} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a088483e7b6..4d9de7259cc 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1176,7 +1176,12 @@ function _Chat() { {["system"].includes(message.role) ? ( ) : ( - + )} )} diff --git a/app/components/exporter.module.scss b/app/components/exporter.module.scss index c2046ffc09d..d3bfc81add1 100644 --- a/app/components/exporter.module.scss +++ b/app/components/exporter.module.scss @@ -186,7 +186,8 @@ box-shadow: var(--card-shadow); border: var(--border-in-light); - *:not(li) { + code, + pre { overflow: hidden; } } diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 435e24953e6..185cbb20b0e 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -1,5 +1,5 @@ /* eslint-disable @next/next/no-img-element */ -import { ChatMessage, useAppConfig, useChatStore } from "../store"; +import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store"; import Locale from "../locales"; import styles from "./exporter.module.scss"; import { @@ -275,7 +275,8 @@ export function RenderExport(props: { }); props.onRender(renderMsgs); - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -619,8 +620,6 @@ export function MarkdownPreviewer(props: { ); } -// modified by BackTrackZ now it's looks better - export function JsonPreviewer(props: { messages: ChatMessage[]; topic: string; diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 1a1fbf41677..b4cae9a21e1 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -11,7 +11,7 @@ import mermaid from "mermaid"; import LoadingIcon from "../icons/three-dots.svg"; import React from "react"; -import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; +import { useDebouncedCallback } from "use-debounce"; import { showImageModal } from "./ui-lib"; export function Mermaid(props: { code: string }) { diff --git a/app/components/mask.tsx b/app/components/mask.tsx index 9fe1d485a6b..3f616c3ac15 100644 --- a/app/components/mask.tsx +++ b/app/components/mask.tsx @@ -18,6 +18,7 @@ import { ChatMessage, createMessage, ModelConfig, + ModelType, useAppConfig, useChatStore, } from "../store"; @@ -58,11 +59,11 @@ function reorder(list: T[], startIndex: number, endIndex: number): T[] { return result; } -export function MaskAvatar(props: { mask: Mask }) { - return props.mask.avatar !== DEFAULT_MASK_AVATAR ? ( - +export function MaskAvatar(props: { avatar: string; model?: ModelType }) { + return props.avatar !== DEFAULT_MASK_AVATAR ? ( + ) : ( - + ); } @@ -123,7 +124,10 @@ export function MaskConfig(props: { onClick={() => setShowPicker(true)} style={{ cursor: "pointer" }} > - +
@@ -398,7 +402,7 @@ export function MaskPage() { setSearchText(text); if (text.length > 0) { const result = allMasks.filter((m) => - m.name.toLowerCase().includes(text.toLowerCase()) + m.name.toLowerCase().includes(text.toLowerCase()), ); setSearchMasks(result); } else { @@ -523,7 +527,7 @@ export function MaskPage() {
- +
{m.name}
diff --git a/app/components/message-selector.tsx b/app/components/message-selector.tsx index 3d2321d0962..c2015340139 100644 --- a/app/components/message-selector.tsx +++ b/app/components/message-selector.tsx @@ -208,7 +208,10 @@ export function MessageSelector(props: { {m.role === "user" ? ( ) : ( - + )}
diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index 76cbbeeb17e..54c646f237c 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -17,21 +17,13 @@ import { useCommand } from "../command"; import { showConfirm } from "./ui-lib"; import { BUILTIN_MASK_STORE } from "../masks"; -function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { - const xmin = Math.max(aRect.x, bRect.x); - const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width); - const ymin = Math.max(aRect.y, bRect.y); - const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height); - const width = xmax - xmin; - const height = ymax - ymin; - const intersectionArea = width < 0 || height < 0 ? 0 : width * height; - return intersectionArea; -} - function MaskItem(props: { mask: Mask; onClick?: () => void }) { return (
- +
{props.mask.name}
); diff --git a/app/constant.ts b/app/constant.ts index 561899769d4..779c6f7e76f 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -84,6 +84,9 @@ You are ChatGPT, a large language model trained by OpenAI. Knowledge cutoff: {{cutoff}} Current model: {{model}} Current time: {{time}} + +Latex inline: $x^2$ +Latex block: $$e=mc^2$$ `; export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e721adef7ae..f5b65559f3d 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -85,8 +85,8 @@ const cn = { Copy: "全部复制", Download: "下载文件", Share: "分享到 ShareGPT", - MessageFromYou: "来自你的消息", - MessageFromChatGPT: "来自 ChatGPT 的消息", + MessageFromYou: "用户", + MessageFromChatGPT: "ChatGPT", Format: { Title: "导出格式", SubTitle: "可以导出 Markdown 文本或者 PNG 图片", From a0cd939bfd560621b854b7533fa0b28a329dfa75 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 12 Nov 2023 19:45:58 +0800 Subject: [PATCH 178/202] fix: #2841 dollar sign conflict with latex math --- app/components/markdown.tsx | 26 ++++++++++++++++++++++++-- app/constant.ts | 1 - 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index b4cae9a21e1..f3a916cc535 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -5,7 +5,7 @@ import RemarkBreaks from "remark-breaks"; import RehypeKatex from "rehype-katex"; import RemarkGfm from "remark-gfm"; import RehypeHighlight from "rehype-highlight"; -import { useRef, useState, RefObject, useEffect } from "react"; +import { useRef, useState, RefObject, useEffect, useMemo } from "react"; import { copyToClipboard } from "../utils"; import mermaid from "mermaid"; @@ -99,7 +99,29 @@ export function PreCode(props: { children: any }) { ); } +function escapeDollarNumber(text: string) { + let escapedText = ""; + + for (let i = 0; i < text.length; i += 1) { + let char = text[i]; + const nextChar = text[i + 1] || " "; + + if (char === "$" && nextChar >= "0" && nextChar <= "9") { + char = "\\$"; + } + + escapedText += char; + } + + return escapedText; +} + function _MarkDownContent(props: { content: string }) { + const escapedContent = useMemo( + () => escapeDollarNumber(props.content), + [props.content], + ); + return ( - {props.content} + {escapedContent} ); } diff --git a/app/constant.ts b/app/constant.ts index 779c6f7e76f..69d5c511f0a 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -84,7 +84,6 @@ You are ChatGPT, a large language model trained by OpenAI. Knowledge cutoff: {{cutoff}} Current model: {{model}} Current time: {{time}} - Latex inline: $x^2$ Latex block: $$e=mc^2$$ `; From d033168d80b54636e306d6a38e604482f3999486 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 13 Nov 2023 10:53:30 +0800 Subject: [PATCH 179/202] fix: #3241 should not ensure openai url non-empty --- app/store/access.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/access.ts b/app/store/access.ts index 2abe1e3cc9f..3b9008ba84b 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -49,7 +49,7 @@ export const useAccessStore = createPersistStore( }, isValidOpenAI() { - return ensure(get(), ["openaiUrl", "openaiApiKey"]); + return ensure(get(), ["openaiApiKey"]); }, isValidAzure() { From 011b52d07d3d5d5c96a821ded5989d9be9fd7274 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Mon, 13 Nov 2023 16:53:36 +0800 Subject: [PATCH 180/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index abe93306180..d4e304f9d04 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,7 @@ If you want to add a new translation, read this [document](./docs/translation.md [@synwith](https://github.com/synwith) [@piksonGit](https://github.com/piksonGit) [@ouyangzhiping](https://github.com/ouyangzhiping) +[@wenjiavv](https://github.com/wenjiavv) ### Contributor From 1e5153173cbf9f938e45f32f643eca3c9144c8b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:45:37 +0000 Subject: [PATCH 181/202] chore(deps-dev): bump @types/node from 20.3.3 to 20.9.0 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.3.3 to 20.9.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0eed3bd9b5b..d19e3057955 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^1.4.0", - "@types/node": "^20.3.3", + "@types/node": "^20.9.0", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.7", "@types/react-katex": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 8adbf7dc24c..8068a6bf708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,12 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@^20.3.3": - version "20.3.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" - integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== +"@types/node@*", "@types/node@^20.9.0": + version "20.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298" + integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw== + dependencies: + undici-types "~5.26.4" "@types/parse-json@^4.0.0": version "4.0.0" @@ -5799,6 +5801,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From 5fbf4c394cbc91f35e48fee6b09879a00891f01f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:45:47 +0000 Subject: [PATCH 182/202] chore(deps-dev): bump @types/spark-md5 from 3.0.2 to 3.0.4 Bumps [@types/spark-md5](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/spark-md5) from 3.0.2 to 3.0.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/spark-md5) --- updated-dependencies: - dependency-name: "@types/spark-md5" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0eed3bd9b5b..178269e2533 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@types/react": "^18.2.14", "@types/react-dom": "^18.2.7", "@types/react-katex": "^3.0.0", - "@types/spark-md5": "^3.0.2", + "@types/spark-md5": "^3.0.4", "cross-env": "^7.0.3", "eslint": "^8.49.0", "eslint-config-next": "13.4.19", diff --git a/yarn.lock b/yarn.lock index 8adbf7dc24c..fc03cd9cce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1550,10 +1550,10 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== -"@types/spark-md5@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55" - integrity sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ== +"@types/spark-md5@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.4.tgz#c1221d63c069d95aba0c06a765b80661cacc12bf" + integrity sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q== "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" From a64c9dae42b84b97847b558aa28fa786deb83a2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:45:59 +0000 Subject: [PATCH 183/202] chore(deps): bump mermaid from 10.3.1 to 10.6.1 Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.3.1 to 10.6.1. - [Release notes](https://github.com/mermaid-js/mermaid/releases) - [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md) - [Commits](https://github.com/mermaid-js/mermaid/compare/v10.3.1...v10.6.1) --- updated-dependencies: - dependency-name: mermaid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0eed3bd9b5b..9b2747e356a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "emoji-picker-react": "^4.5.1", "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", - "mermaid": "^10.3.1", + "mermaid": "^10.6.1", "nanoid": "^4.0.2", "next": "^13.4.9", "node-fetch": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index 8adbf7dc24c..8e21ab08e98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4314,10 +4314,10 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.3.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.3.1.tgz#2f3c7e9f6bd7a8da2bef71cce2a542c8eba2a62e" - integrity sha512-hkenh7WkuRWPcob3oJtrN3W+yzrrIYuWF1OIfk/d0xGE8UWlvDhfexaHmDwwe8DKQgqMLI8DWEPwGprxkumjuw== +mermaid@^10.6.1: + version "10.6.1" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc" + integrity sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A== dependencies: "@braintree/sanitize-url" "^6.0.1" "@types/d3-scale" "^4.0.3" From 7d1fae32cd1ce5d07104da22a96e9680bff66f7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:22:50 +0000 Subject: [PATCH 184/202] chore(deps): bump emoji-picker-react from 4.5.1 to 4.5.15 Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.5.1 to 4.5.15. - [Release notes](https://github.com/ealush/emoji-picker-react/releases) - [Commits](https://github.com/ealush/emoji-picker-react/commits) --- updated-dependencies: - dependency-name: emoji-picker-react dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 9b2747e356a..acf0d5c3b04 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@hello-pangea/dnd": "^16.3.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", - "emoji-picker-react": "^4.5.1", + "emoji-picker-react": "^4.5.15", "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", "mermaid": "^10.6.1", diff --git a/yarn.lock b/yarn.lock index 8e21ab08e98..ba1e7399067 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2126,11 +2126,6 @@ client-only@0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2762,12 +2757,10 @@ elkjs@^0.8.2: resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== -emoji-picker-react@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195" - integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw== - dependencies: - clsx "^1.2.1" +emoji-picker-react@^4.5.15: + version "4.5.15" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.15.tgz#e12797c50584cb8af8aee7eb6c7c8fd953e41f7e" + integrity sha512-BTqo+pNUE8kqX8BKFTbD4fhlxcA69qfie5En4PerReLaaPfXVyRlDJ1uf85nKj2u5esUQ999iUf8YyqcPsM2Qw== emoji-regex@^8.0.0: version "8.0.0" From a9d605ed301adb78379da7e577847b5839ac88d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:22:51 +0000 Subject: [PATCH 185/202] chore(deps): bump nanoid from 4.0.2 to 5.0.3 Bumps [nanoid](https://github.com/ai/nanoid) from 4.0.2 to 5.0.3. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.0.3) --- updated-dependencies: - dependency-name: nanoid dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9b2747e356a..72fc5ae3f1b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", "mermaid": "^10.6.1", - "nanoid": "^4.0.2", + "nanoid": "^5.0.3", "next": "^13.4.9", "node-fetch": "^3.3.1", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 8e21ab08e98..fad107c2836 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4690,10 +4690,10 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== -nanoid@^4.0.2: - version "4.0.2" - resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" - integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== +nanoid@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.3.tgz#6c97f53d793a7a1de6a38ebb46f50f95bf9793c7" + integrity sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA== natural-compare@^1.4.0: version "1.4.0" From f1772f46254eb6df9bdefe39987bb008273fe7c5 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Tue, 14 Nov 2023 15:04:03 +0700 Subject: [PATCH 186/202] Fix UI/UX Page [Settings] [+] fix(settings.tsx): fix condition to show balance query in settings component [+] fix(settings.tsx): add condition to hide balance query for app clients --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 178fcec57e9..1edb3c6bae4 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1052,7 +1052,7 @@ export function Settings() { )} - {!shouldHideBalanceQuery ? ( + {!shouldHideBalanceQuery && !clientConfig?.isApp ? ( Date: Thu, 16 Nov 2023 00:53:11 +0800 Subject: [PATCH 187/202] fix: #3275 refuse on server side if hide user api key --- app/api/auth.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/api/auth.ts b/app/api/auth.ts index c1f6e7fdec2..b41e34e059b 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -46,6 +46,13 @@ export function auth(req: NextRequest) { }; } + if (serverConfig.hideUserApiKey && !!apiKey) { + return { + error: true, + msg: "you are not allowed to access openai with your own api key", + }; + } + // if user does not provide an api key, inject system api key if (!apiKey) { const serverApiKey = serverConfig.isAzure From 74eb42c11148511cb026d51f44ab1ab931e994fc Mon Sep 17 00:00:00 2001 From: Eric-2369 Date: Thu, 16 Nov 2023 11:33:10 +0800 Subject: [PATCH 188/202] config: exclude hkg1 from the deployment regions of the edge function --- app/api/openai/[...path]/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/openai/[...path]/route.ts b/app/api/openai/[...path]/route.ts index 9df005a317a..2addd53a52d 100644 --- a/app/api/openai/[...path]/route.ts +++ b/app/api/openai/[...path]/route.ts @@ -75,3 +75,4 @@ export const GET = handle; export const POST = handle; export const runtime = "edge"; +export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1']; From 401fa198c970d012a333a4a9acf0cd08090533e0 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 17 Nov 2023 11:29:52 +0800 Subject: [PATCH 189/202] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 397ae0d8345..182d0079256 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.11" + "version": "2.9.12" }, "tauri": { "allowlist": { From 3c510cfaf0296184deecebe55d97f42019180af7 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 17 Nov 2023 11:56:42 +0800 Subject: [PATCH 190/202] chore: update gh actions nodejs version to 18 --- .github/workflows/app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index b928ad6c15f..aebba28f7e2 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -18,7 +18,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: get version run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV - name: create release @@ -59,7 +59,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: From 7bdb68eecf58ce3d1531701c51ff3fbbbdd4d932 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 17 Nov 2023 15:10:51 +0800 Subject: [PATCH 191/202] config: disable ip forward in vercel json config --- vercel.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/vercel.json b/vercel.json index 1890a0f7dba..21dc9cc2497 100644 --- a/vercel.json +++ b/vercel.json @@ -2,23 +2,4 @@ "github": { "silent": true }, - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "X-Real-IP", - "value": "$remote_addr" - }, - { - "key": "X-Forwarded-For", - "value": "$proxy_add_x_forwarded_for" - }, - { - "key": "Host", - "value": "$http_host" - } - ] - } - ] } From 914f4fb86211b7377bc1f8133b8f031503da4423 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 17 Nov 2023 15:11:03 +0800 Subject: [PATCH 192/202] Update vercel.json --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 21dc9cc2497..0cae358a188 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,5 @@ { "github": { "silent": true - }, + } } From cf220dd2eb08589c7bd99b5a2c05eddf8f2e106f Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 17 Nov 2023 15:22:11 +0800 Subject: [PATCH 193/202] Update vercel.json --- vercel.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 0cae358a188..1890a0f7dba 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,24 @@ { "github": { "silent": true - } + }, + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "X-Real-IP", + "value": "$remote_addr" + }, + { + "key": "X-Forwarded-For", + "value": "$proxy_add_x_forwarded_for" + }, + { + "key": "Host", + "value": "$http_host" + } + ] + } + ] } From 6f135a0ccebe055559ba81c3f002096ea49dc391 Mon Sep 17 00:00:00 2001 From: Yuri Filipe Date: Sat, 18 Nov 2023 15:12:15 -0300 Subject: [PATCH 194/202] Translation of the application into Brazilian Portuguese --- app/locales/index.ts | 3 + app/locales/pt.ts | 466 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 app/locales/pt.ts diff --git a/app/locales/index.ts b/app/locales/index.ts index 79e314facdd..1d84de22cf1 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -1,5 +1,6 @@ import cn from "./cn"; import en from "./en"; +import pt from "./pt"; import tw from "./tw"; import id from "./id"; import fr from "./fr"; @@ -23,6 +24,7 @@ export type { LocaleType, PartialLocaleType } from "./cn"; const ALL_LANGS = { cn, en, + pt, tw, jp, ko, @@ -47,6 +49,7 @@ export const AllLangs = Object.keys(ALL_LANGS) as Lang[]; export const ALL_LANG_OPTIONS: Record = { cn: "简体中文", en: "English", + pt: "Português", tw: "繁體中文", jp: "日本語", ko: "한국어", diff --git a/app/locales/pt.ts b/app/locales/pt.ts new file mode 100644 index 00000000000..e984dc99a65 --- /dev/null +++ b/app/locales/pt.ts @@ -0,0 +1,466 @@ +import { SubmitKey } from "../store/config"; +import { LocaleType } from "../locales/index"; +import { getClientConfig } from "../config/client"; + +const isApp = !!getClientConfig()?.isApp; + +const pt: LocaleType = { + WIP: "Em breve...", + Error: { + Unauthorized: isApp + ? "Chave API inválida, por favor verifique em [Configurações](/#/settings)." + : "Acesso não autorizado, por favor insira o código de acesso em [auth](/#/auth) ou insira sua Chave API OpenAI.", + }, + Auth: { + Title: "Necessário Código de Acesso", + Tips: "Por favor, insira o código de acesso abaixo", + SubTips: "Ou insira sua Chave API OpenAI", + Input: "código de acesso", + Confirm: "Confirmar", + Later: "Depois", + }, + ChatItem: { + ChatItemCount: (count: number) => `${count} mensagens`, + }, + Chat: { + SubTitle: (count: number) => `${count} mensagens`, + EditMessage: { + Title: "Editar Todas as Mensagens", + Topic: { + Title: "Tópico", + SubTitle: "Mudar o tópico atual", + }, + }, + Actions: { + ChatList: "Ir Para Lista de Chat", + CompressedHistory: "Prompt de Memória Histórica Comprimida", + Export: "Exportar Todas as Mensagens como Markdown", + Copy: "Copiar", + Stop: "Parar", + Retry: "Tentar Novamente", + Pin: "Fixar", + PinToastContent: "Fixada 1 mensagem para prompts contextuais", + PinToastAction: "Visualizar", + Delete: "Deletar", + Edit: "Editar", + }, + Commands: { + new: "Iniciar um novo chat", + newm: "Iniciar um novo chat com máscara", + next: "Próximo Chat", + prev: "Chat Anterior", + clear: "Limpar Contexto", + del: "Deletar Chat", + }, + InputActions: { + Stop: "Parar", + ToBottom: "Para o Mais Recente", + Theme: { + auto: "Automático", + light: "Tema Claro", + dark: "Tema Escuro", + }, + Prompt: "Prompts", + Masks: "Máscaras", + Clear: "Limpar Contexto", + Settings: "Configurações", + }, + Rename: "Renomear Chat", + Typing: "Digitando…", + Input: (submitKey: string) => { + var inputHints = `${submitKey} para enviar`; + if (submitKey === String(SubmitKey.Enter)) { + inputHints += ", Shift + Enter para quebrar linha"; + } + return inputHints + ", / para buscar prompts, : para usar comandos"; + }, + Send: "Enviar", + Config: { + Reset: "Redefinir para Padrão", + SaveAs: "Salvar como Máscara", + }, + IsContext: "Prompt Contextual", + }, + Export: { + Title: "Exportar Mensagens", + Copy: "Copiar Tudo", + Download: "Baixar", + MessageFromYou: "Mensagem De Você", + MessageFromChatGPT: "Mensagem De ChatGPT", + Share: "Compartilhar para ShareGPT", + Format: { + Title: "Formato de Exportação", + SubTitle: "Markdown ou Imagem PNG", + }, + IncludeContext: { + Title: "Incluindo Contexto", + SubTitle: "Exportar prompts de contexto na máscara ou não", + }, + Steps: { + Select: "Selecionar", + Preview: "Pré-visualizar", + }, + Image: { + Toast: "Capturando Imagem...", + Modal: + "Pressione longamente ou clique com o botão direito para salvar a imagem", + }, + }, + Select: { + Search: "Buscar", + All: "Selecionar Tudo", + Latest: "Selecionar Mais Recente", + Clear: "Limpar", + }, + Memory: { + Title: "Prompt de Memória", + EmptyContent: "Nada ainda.", + Send: "Enviar Memória", + Copy: "Copiar Memória", + Reset: "Resetar Sessão", + ResetConfirm: + "Resetar irá limpar o histórico de conversa atual e a memória histórica. Você tem certeza que quer resetar?", + }, + Home: { + NewChat: "Novo Chat", + DeleteChat: "Confirmar para deletar a conversa selecionada?", + DeleteToast: "Chat Deletado", + Revert: "Reverter", + }, + Settings: { + Title: "Configurações", + SubTitle: "Todas as Configurações", + Danger: { + Reset: { + Title: "Resetar Todas as Configurações", + SubTitle: "Resetar todos os itens de configuração para o padrão", + Action: "Resetar", + Confirm: "Confirmar para resetar todas as configurações para o padrão?", + }, + Clear: { + Title: "Limpar Todos os Dados", + SubTitle: "Limpar todas as mensagens e configurações", + Action: "Limpar", + Confirm: "Confirmar para limpar todas as mensagens e configurações?", + }, + }, + Lang: { + Name: "Idioma", + All: "Todos os Idiomas", + }, + Avatar: "Avatar", + FontSize: { + Title: "Tamanho da Fonte", + SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat", + }, + InjectSystemPrompts: { + Title: "Inserir Prompts de Sistema", + SubTitle: "Inserir um prompt de sistema global para cada requisição", + }, + InputTemplate: { + Title: "Modelo de Entrada", + SubTitle: "A mensagem mais recente será preenchida neste modelo", + }, + + Update: { + Version: (x: string) => `Versão: ${x}`, + IsLatest: "Última versão", + CheckUpdate: "Verificar Atualização", + IsChecking: "Verificando atualização...", + FoundUpdate: (x: string) => `Nova versão encontrada: ${x}`, + GoToUpdate: "Atualizar", + }, + SendKey: "Tecla de Envio", + Theme: "Tema", + TightBorder: "Borda Ajustada", + SendPreviewBubble: { + Title: "Bolha de Pré-visualização de Envio", + SubTitle: "Pré-visualizar markdown na bolha", + }, + AutoGenerateTitle: { + Title: "Gerar Título Automaticamente", + SubTitle: "Gerar um título adequado baseado no conteúdo da conversa", + }, + Sync: { + CloudState: "Última Atualização", + NotSyncYet: "Ainda não sincronizado", + Success: "Sincronização bem sucedida", + Fail: "Falha na sincronização", + + Config: { + Modal: { + Title: "Configurar Sincronização", + Check: "Verificar Conexão", + }, + SyncType: { + Title: "Tipo de Sincronização", + SubTitle: "Escolha seu serviço de sincronização favorito", + }, + Proxy: { + Title: "Habilitar Proxy CORS", + SubTitle: "Habilitar um proxy para evitar restrições de cross-origin", + }, + ProxyUrl: { + Title: "Endpoint de Proxy", + SubTitle: "Apenas aplicável ao proxy CORS embutido para este projeto", + }, + + WebDav: { + Endpoint: "Endpoint WebDAV", + UserName: "Nome de Usuário", + Password: "Senha", + }, + + UpStash: { + Endpoint: "URL REST Redis UpStash", + UserName: "Nome do Backup", + Password: "Token REST Redis UpStash", + }, + }, + + LocalState: "Dados Locais", + Overview: (overview: any) => { + return `${overview.chat} chats,${overview.message} mensagens,${overview.prompt} prompts,${overview.mask} máscaras`; + }, + ImportFailed: "Falha ao importar do arquivo", + }, + Mask: { + Splash: { + Title: "Tela de Início da Máscara", + SubTitle: + "Mostrar uma tela de início da máscara antes de iniciar novo chat", + }, + Builtin: { + Title: "Esconder Máscaras Embutidas", + SubTitle: "Esconder máscaras embutidas na lista de máscaras", + }, + }, + Prompt: { + Disable: { + Title: "Desabilitar auto-completar", + SubTitle: "Digite / para acionar auto-completar", + }, + List: "Lista de Prompts", + ListCount: (builtin: number, custom: number) => + `${builtin} embutidos, ${custom} definidos pelo usuário`, + Edit: "Editar", + Modal: { + Title: "Lista de Prompts", + Add: "Adicionar Um", + Search: "Buscar Prompts", + }, + EditModal: { + Title: "Editar Prompt", + }, + }, + HistoryCount: { + Title: "Contagem de Mensagens Anexadas", + SubTitle: "Número de mensagens enviadas anexadas por requisição", + }, + CompressThreshold: { + Title: "Limite de Compressão de Histórico", + SubTitle: + "Irá comprimir se o comprimento das mensagens não comprimidas exceder o valor", + }, + + Usage: { + Title: "Saldo da Conta", + SubTitle(used: any, total: any) { + return `Usado este mês ${used}, assinatura ${total}`; + }, + IsChecking: "Verificando...", + Check: "Verificar", + NoAccess: "Insira a Chave API para verificar o saldo", + }, + Access: { + AccessCode: { + Title: "Código de Acesso", + SubTitle: "Controle de Acesso Habilitado", + Placeholder: "Insira o Código", + }, + CustomEndpoint: { + Title: "Endpoint Personalizado", + SubTitle: "Use serviço personalizado Azure ou OpenAI", + }, + Provider: { + Title: "Provedor do Modelo", + SubTitle: "Selecione Azure ou OpenAI", + }, + OpenAI: { + ApiKey: { + Title: "Chave API OpenAI", + SubTitle: "Usar Chave API OpenAI personalizada", + Placeholder: "sk-xxx", + }, + + Endpoint: { + Title: "Endpoint OpenAI", + SubTitle: + "Deve começar com http(s):// ou usar /api/openai como padrão", + }, + }, + Azure: { + ApiKey: { + Title: "Chave API Azure", + SubTitle: "Verifique sua chave API do console Azure", + Placeholder: "Chave API Azure", + }, + + Endpoint: { + Title: "Endpoint Azure", + SubTitle: "Exemplo: ", + }, + + ApiVerion: { + Title: "Versão API Azure", + SubTitle: "Verifique sua versão API do console Azure", + }, + }, + CustomModel: { + Title: "Modelos Personalizados", + SubTitle: "Opções de modelo personalizado, separados por vírgula", + }, + }, + + Model: "Modelo", + Temperature: { + Title: "Temperatura", + SubTitle: "Um valor maior torna a saída mais aleatória", + }, + TopP: { + Title: "Top P", + SubTitle: "Não altere este valor junto com a temperatura", + }, + MaxTokens: { + Title: "Máximo de Tokens", + SubTitle: "Comprimento máximo de tokens de entrada e tokens gerados", + }, + PresencePenalty: { + Title: "Penalidade de Presença", + SubTitle: + "Um valor maior aumenta a probabilidade de falar sobre novos tópicos", + }, + FrequencyPenalty: { + Title: "Penalidade de Frequência", + SubTitle: + "Um valor maior diminui a probabilidade de repetir a mesma linha", + }, + }, + Store: { + DefaultTopic: "Nova Conversa", + BotHello: "Olá! Como posso ajudá-lo hoje?", + Error: "Algo deu errado, por favor tente novamente mais tarde.", + Prompt: { + History: (content: string) => + "Este é um resumo do histórico de chat como um recapitulativo: " + + content, + Topic: + "Por favor, gere um título de quatro a cinco palavras resumindo nossa conversa sem qualquer introdução, pontuação, aspas, períodos, símbolos ou texto adicional. Remova as aspas que o envolvem.", + Summarize: + "Resuma a discussão brevemente em 200 palavras ou menos para usar como um prompt para o contexto futuro.", + }, + }, + Copy: { + Success: "Copiado para a área de transferência", + Failed: + "Falha na cópia, por favor conceda permissão para acessar a área de transferência", + }, + Download: { + Success: "Conteúdo baixado para seu diretório.", + Failed: "Falha no download.", + }, + Context: { + Toast: (x: any) => `Com ${x} prompts contextuais`, + Edit: "Configurações do Chat Atual", + Add: "Adicionar um Prompt", + Clear: "Contexto Limpo", + Revert: "Reverter", + }, + Plugin: { + Name: "Plugin", + }, + FineTuned: { + Sysmessage: "Você é um assistente que", + }, + Mask: { + Name: "Máscara", + Page: { + Title: "Template de Prompt", + SubTitle: (count: number) => `${count} templates de prompt`, + Search: "Buscar Templates", + Create: "Criar", + }, + Item: { + Info: (count: number) => `${count} prompts`, + Chat: "Chat", + View: "Visualizar", + Edit: "Editar", + Delete: "Deletar", + DeleteConfirm: "Confirmar para deletar?", + }, + EditModal: { + Title: (readonly: boolean) => + `Editar Template de Prompt ${readonly ? "(somente leitura)" : ""}`, + Download: "Baixar", + Clone: "Clonar", + }, + Config: { + Avatar: "Avatar do Bot", + Name: "Nome do Bot", + Sync: { + Title: "Usar Configuração Global", + SubTitle: "Usar configuração global neste chat", + Confirm: + "Confirmar para substituir a configuração personalizada pela configuração global?", + }, + HideContext: { + Title: "Esconder Prompts de Contexto", + SubTitle: "Não mostrar prompts de contexto no chat", + }, + Share: { + Title: "Compartilhar Esta Máscara", + SubTitle: "Gerar um link para esta máscara", + Action: "Copiar Link", + }, + }, + }, + NewChat: { + Return: "Retornar", + Skip: "Apenas Começar", + Title: "Escolher uma Máscara", + SubTitle: "Converse com a Alma por trás da Máscara", + More: "Encontre Mais", + NotShow: "Nunca Mostrar Novamente", + ConfirmNoShow: + "Confirmar para desabilitar?Você pode habilitar nas configurações depois.", + }, + + UI: { + Confirm: "Confirmar", + Cancel: "Cancelar", + Close: "Fechar", + Create: "Criar", + Edit: "Editar", + Export: "Exportar", + Import: "Importar", + Sync: "Sincronizar", + Config: "Configurar", + }, + Exporter: { + Description: { + Title: "Apenas mensagens após a limpeza do contexto serão exibidas", + }, + Model: "Modelo", + Messages: "Mensagens", + Topic: "Tópico", + Time: "Tempo", + }, + + URLCommand: { + Code: "Código de acesso detectado a partir da url, confirmar para aplicar? ", + Settings: + "Configurações detectadas a partir da url, confirmar para aplicar?", + }, +}; + +export default pt; From 16b2a3e66edf1565b8cf6ad5685adf76534c9106 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 19 Nov 2023 04:23:25 +0800 Subject: [PATCH 195/202] Update vercel.json --- vercel.json | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/vercel.json b/vercel.json index 1890a0f7dba..0cae358a188 100644 --- a/vercel.json +++ b/vercel.json @@ -1,24 +1,5 @@ { "github": { "silent": true - }, - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "X-Real-IP", - "value": "$remote_addr" - }, - { - "key": "X-Forwarded-For", - "value": "$proxy_add_x_forwarded_for" - }, - { - "key": "Host", - "value": "$http_host" - } - ] - } - ] + } } From 536ace8e10553c6101308ec09f2fa65bc84d2416 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 19 Nov 2023 18:24:51 +0800 Subject: [PATCH 196/202] feat: animate streaming response to make more smooth --- app/client/platforms/openai.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 930d606900a..dc79d2cece7 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -115,12 +115,33 @@ export class ChatGPTApi implements LLMApi { if (shouldStream) { let responseText = ""; + let remainText = ""; let finished = false; + // animate response to make it looks smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + return; + } + + if (remainText.length > 0) { + responseText += remainText[0]; + remainText = remainText.slice(1); + options.onUpdate?.(responseText, remainText[0]); + } + + requestAnimationFrame(animateResponseText); + } + + // start animaion + animateResponseText(); + const finish = () => { if (!finished) { - options.onFinish(responseText); finished = true; + options.onFinish(responseText + remainText); } }; @@ -183,8 +204,7 @@ export class ChatGPTApi implements LLMApi { }; const delta = json.choices[0]?.delta?.content; if (delta) { - responseText += delta; - options.onUpdate?.(responseText, delta); + remainText += delta; } } catch (e) { console.error("[Request] parse error", text); From dc7159a4504682f6bfad104d5d03168412c550f1 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 19 Nov 2023 18:32:54 +0800 Subject: [PATCH 197/202] feat: close #3301 enable or diable default models with -all / +all --- README.md | 6 ++++-- README_CN.md | 5 +++-- app/utils/model.ts | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d4e304f9d04..ef7f2e1d9fb 100644 --- a/README.md +++ b/README.md @@ -216,9 +216,11 @@ If you want to disable parse settings from url, set this to 1. ### `CUSTOM_MODELS` (optional) > Default: Empty -> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`. +> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`. -To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name:displayName` to customize model name, separated by comma. +To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name=displayName` to customize model name, separated by comma. + +User `-all` to disable all default models, `+all` to enable all default models. ## Requirements diff --git a/README_CN.md b/README_CN.md index dde8c19b352..3b713255a47 100644 --- a/README_CN.md +++ b/README_CN.md @@ -122,9 +122,10 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro ### `CUSTOM_MODELS` (可选) -> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。 +> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。 +> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo` -用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。 +用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 ## 开发 diff --git a/app/utils/model.ts b/app/utils/model.ts index d5c009c02d2..bf7300806ff 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -27,6 +27,12 @@ export function collectModelTable( const nameConfig = m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; const [name, displayName] = nameConfig.split(":"); + + // enable or disable all models + if (name === "all") { + Object.values(modelTable).forEach((m) => (m.available = available)); + } + modelTable[name] = { name, displayName: displayName || name, From 45b88ebb2a720c62d60e63a873004d3cd9734801 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 19 Nov 2023 18:34:39 +0800 Subject: [PATCH 198/202] feat: close #3304 use `=` instead of `:` to map model name in CUSTOM_MODELS --- app/utils/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/model.ts b/app/utils/model.ts index bf7300806ff..74b28a66ae8 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -26,7 +26,7 @@ export function collectModelTable( const available = !m.startsWith("-"); const nameConfig = m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m; - const [name, displayName] = nameConfig.split(":"); + const [name, displayName] = nameConfig.split("="); // enable or disable all models if (name === "all") { From 6aade62ce2f131caeaefc18689fea502ec1a3966 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 19 Nov 2023 18:42:30 +0800 Subject: [PATCH 199/202] feat: close #3300 support multiple api keys --- README.md | 2 +- README_CN.md | 2 +- app/config/server.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef7f2e1d9fb..3050fcc95e1 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Access password, separated by comma. ### `OPENAI_API_KEY` (required) -Your openai api key. +Your openai api key, join multiple api keys with comma. ### `BASE_URL` (optional) diff --git a/README_CN.md b/README_CN.md index 3b713255a47..0ef508f61ac 100644 --- a/README_CN.md +++ b/README_CN.md @@ -68,7 +68,7 @@ code1,code2,code3 ### `OPENAI_API_KEY` (必填项) -OpanAI 密钥,你在 openai 账户页面申请的 api key。 +OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。 ### `CODE` (可选) diff --git a/app/config/server.ts b/app/config/server.ts index 2f2e7d7fd8a..2398805a264 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -62,9 +62,17 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; + const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; + const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); + const randomIndex = Math.floor(Math.random() * apiKeys.length); + const apiKey = apiKeys[randomIndex]; + console.log( + `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, + ); + return { baseUrl: process.env.BASE_URL, - apiKey: process.env.OPENAI_API_KEY, + apiKey, openaiOrgId: process.env.OPENAI_ORG_ID, isAzure, From f2485931d9b3680234f4816f4526759c8d4b741e Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 19 Nov 2023 19:15:11 +0800 Subject: [PATCH 200/202] feat: better animation speed --- app/client/platforms/openai.ts | 8 +++++--- app/locales/index.ts | 2 +- app/locales/pt.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index dc79d2cece7..8ea864692d5 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -127,9 +127,11 @@ export class ChatGPTApi implements LLMApi { } if (remainText.length > 0) { - responseText += remainText[0]; - remainText = remainText.slice(1); - options.onUpdate?.(responseText, remainText[0]); + const fetchCount = Math.max(1, Math.round(remainText.length / 60)); + const fetchText = remainText.slice(0, fetchCount); + responseText += fetchText; + remainText = remainText.slice(fetchCount); + options.onUpdate?.(responseText, fetchText); } requestAnimationFrame(animateResponseText); diff --git a/app/locales/index.ts b/app/locales/index.ts index 1d84de22cf1..cfbdff2977e 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -24,8 +24,8 @@ export type { LocaleType, PartialLocaleType } from "./cn"; const ALL_LANGS = { cn, en, - pt, tw, + pt, jp, ko, id, diff --git a/app/locales/pt.ts b/app/locales/pt.ts index e984dc99a65..55a40497045 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -145,7 +145,7 @@ const pt: LocaleType = { }, }, Lang: { - Name: "Idioma", + Name: "Language", All: "Todos os Idiomas", }, Avatar: "Avatar", From 06de3f5e6989e8ca131dfb1b3b788cb620a73367 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 24 Nov 2023 10:33:33 +0800 Subject: [PATCH 201/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3050fcc95e1..0b7dfa901e8 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,7 @@ If you want to add a new translation, read this [document](./docs/translation.md [@piksonGit](https://github.com/piksonGit) [@ouyangzhiping](https://github.com/ouyangzhiping) [@wenjiavv](https://github.com/wenjiavv) +[@LeXwDeX](https://github.com/LeXwDeX) ### Contributor From b21931c667abf2825eca43d59f794471420c1db0 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Fri, 24 Nov 2023 11:36:14 +0800 Subject: [PATCH 202/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0b7dfa901e8..372b870963f 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,7 @@ If you want to add a new translation, read this [document](./docs/translation.md [@ouyangzhiping](https://github.com/ouyangzhiping) [@wenjiavv](https://github.com/wenjiavv) [@LeXwDeX](https://github.com/LeXwDeX) +[@Licoy](https://github.com/Licoy) ### Contributor