Compare commits

...

99 Commits

Author SHA1 Message Date
Yifei Zhang
cae4655785 Update tauri.conf.json 2023-12-11 15:59:37 +08:00
Yifei Zhang
28c12606a4 Merge pull request #3455 from H0llyW00dzZ/trimtopic 2023-12-06 18:58:38 +08:00
H0llyW00dzZ
54df355014 Fix Trim Topic in Indonesia Language
- [+] fix(utils.ts): fix trimTopic function to remove double quotes from the start and end of the string, and remove specified punctuation from the end of the string
2023-12-05 07:26:11 +07:00
Yifei Zhang
cf50299b14 Update README_CN.md 2023-11-29 11:39:21 +08:00
Yifei Zhang
2c12be62c4 Merge pull request #3383 from H0llyW00dzZ/auth 2023-11-27 15:51:27 +08:00
Yifei Zhang
4636a75b5e Merge pull request #3382 from H0llyW00dzZ/docs 2023-11-27 15:47:39 +08:00
Yifei Zhang
03756e364a Merge pull request #3372 from H0llyW00dzZ/fixserverside 2023-11-27 15:47:24 +08:00
Yifei Zhang
ce1715c79e Merge pull request #3369 from H0llyW00dzZ/chatz 2023-11-27 15:47:01 +08:00
Yifei Zhang
a62ab3c649 Merge pull request #3354 from AnYiEE/fix-type 2023-11-27 15:45:42 +08:00
Yifei Zhang
bfb7b988f4 Merge pull request #3356 from durianice/pr_merge 2023-11-27 15:44:03 +08:00
Yifei Zhang
84f41262f5 Update README.md 2023-11-27 15:28:34 +08:00
H0llyW00dzZ
dda40e29f4 Fix & Refactor UI/UX Page [Settings]
- [+] fix(settings.tsx): fix conditional rendering of ListItem based on clientConfig.isApp
- [+] refactor(settings.tsx): improve readability of conditional rendering code
2023-11-27 08:07:53 +07:00
H0llyW00dzZ
7df868e22a Revert "Fix & Refactor UI/UX Page [Auth]"
This reverts commit d76e744eab.

Reason: Move to commits bf5e7aaa48
2023-11-27 07:12:57 +07:00
H0llyW00dzZ
bf5e7aaa48 Feat UI/UX Page [Settings]
- [+] feat(settings.tsx): set useCustomConfig to true if clientConfig.isApp is truthy
2023-11-27 07:03:41 +07:00
H0llyW00dzZ
d76e744eab Fix & Refactor UI/UX Page [Auth]
- [+] fix(auth.tsx): fix condition to set custom endpoint to true if it's app
- [+] refactor(auth.tsx): update accessStore to set useCustomConfig to true
2023-11-27 05:44:26 +07:00
H0llyW00dzZ
6f5699fe09 Chore Docs (README.md)
- [+] chore(README.md): add support for Korean and Indonesian languages
2023-11-27 04:49:15 +07:00
H0llyW00dzZ
f9d916925e Fixup Api Common [Server Side] [Console Log]
- [+] fix(common.ts): fix condition to check if serverConfig.openaiOrgId is not undefined
2023-11-25 10:23:16 +07:00
H0llyW00dzZ
f9258878db Fix Api Common [Server Side] [Console Log]
- [+] fix(common.ts): fix console.log statement for [Org ID] to handle undefined value
2023-11-25 10:10:06 +07:00
H0llyW00dzZ
ef9e86b50d Fix UI/UX Page Chats [Memory Prompt] [Stored Local Storage]
- [+] fix(chat.ts): update the memory prompt in onFinish callback
- [+] feat(chat.ts): update the current session with lastSummarizeIndex and memoryPrompt
2023-11-25 05:03:34 +07:00
Yifei Zhang
b21931c667 Update README.md 2023-11-24 11:36:14 +08:00
Yifei Zhang
06de3f5e69 Update README.md 2023-11-24 10:33:33 +08:00
durian
261a8fd83b fix(CUSTOM_MODELS):#3349 Dependency chatStore otherwise the session and view will not updated 2023-11-23 15:37:57 +08:00
安忆
6527074cde fix(locales): type error in pt.ts 2023-11-23 11:02:20 +08:00
Yidadaa
f2485931d9 feat: better animation speed 2023-11-19 19:15:11 +08:00
Yifei Zhang
4f8a0b7711 Merge pull request #3308 from yuri-filipe/main 2023-11-19 18:45:31 +08:00
Yifei Zhang
2dde55050e Merge pull request #3311 from Yidadaa/bugfix-1119 2023-11-19 18:44:14 +08:00
Yidadaa
6aade62ce2 feat: close #3300 support multiple api keys 2023-11-19 18:42:30 +08:00
Yidadaa
45b88ebb2a feat: close #3304 use = instead of : to map model name in CUSTOM_MODELS 2023-11-19 18:34:39 +08:00
Yidadaa
dc7159a450 feat: close #3301 enable or diable default models with -all / +all 2023-11-19 18:32:54 +08:00
Yidadaa
536ace8e10 feat: animate streaming response to make more smooth 2023-11-19 18:24:51 +08:00
Yifei Zhang
16b2a3e66e Update vercel.json 2023-11-19 04:23:25 +08:00
Yuri Filipe
6f135a0cce Translation of the application into Brazilian Portuguese 2023-11-18 15:12:15 -03:00
Yifei Zhang
cf220dd2eb Update vercel.json 2023-11-17 15:22:11 +08:00
Yifei Zhang
914f4fb862 Update vercel.json 2023-11-17 15:11:03 +08:00
Yifei Zhang
7bdb68eecf config: disable ip forward in vercel json config 2023-11-17 15:10:51 +08:00
Yifei Zhang
3c510cfaf0 chore: update gh actions nodejs version to 18 2023-11-17 11:56:42 +08:00
Yifei Zhang
401fa198c9 Update tauri.conf.json 2023-11-17 11:29:52 +08:00
Yifei Zhang
600df4f63a Merge pull request #3290 from Eric-2369/main 2023-11-17 11:26:21 +08:00
Eric-2369
74eb42c111 config: exclude hkg1 from the deployment regions of the edge function 2023-11-16 11:33:10 +08:00
Yidadaa
9876a1aeca fix: #3275 refuse on server side if hide user api key 2023-11-16 00:53:11 +08:00
Yifei Zhang
d898ffce23 Merge pull request #3261 from H0llyW00dzZ/clients 2023-11-14 16:07:16 +08:00
H0llyW00dzZ
f1772f4625 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
2023-11-14 15:04:57 +07:00
Yifei Zhang
9da455a7ea Merge pull request #3248 from Yidadaa/dependabot/npm_and_yarn/nanoid-5.0.3 2023-11-14 01:24:10 +08:00
Yifei Zhang
2bd6342309 Merge pull request #3249 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.15 2023-11-14 01:23:50 +08:00
Yifei Zhang
5e73577088 Merge pull request #3250 from Yidadaa/dependabot/npm_and_yarn/types/node-20.9.0 2023-11-14 01:23:37 +08:00
Yifei Zhang
5156a80738 Merge pull request #3251 from Yidadaa/dependabot/npm_and_yarn/types/spark-md5-3.0.4 2023-11-14 01:23:17 +08:00
dependabot[bot]
a9d605ed30 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] <support@github.com>
2023-11-13 17:22:51 +00:00
dependabot[bot]
7d1fae32cd 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] <support@github.com>
2023-11-13 17:22:50 +00:00
Yifei Zhang
9f17b45bc4 Merge pull request #3252 from Yidadaa/dependabot/npm_and_yarn/mermaid-10.6.1 2023-11-14 01:21:11 +08:00
dependabot[bot]
a64c9dae42 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] <support@github.com>
2023-11-13 10:45:59 +00:00
dependabot[bot]
5fbf4c394c 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] <support@github.com>
2023-11-13 10:45:47 +00:00
dependabot[bot]
1e5153173c 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] <support@github.com>
2023-11-13 10:45:37 +00:00
Yifei Zhang
011b52d07d Update README.md 2023-11-13 16:53:36 +08:00
Yifei Zhang
d033168d80 fix: #3241 should not ensure openai url non-empty 2023-11-13 10:53:30 +08:00
Yifei Zhang
fdca9e59de Merge pull request #3233 from nanaya-tachibana/nanaya/model-selection 2023-11-13 10:44:03 +08:00
Yifei Zhang
549a2fd206 Merge pull request #3237 from Yidadaa/latex 2023-11-12 19:47:26 +08:00
Yidadaa
a0cd939bfd fix: #2841 dollar sign conflict with latex math 2023-11-12 19:45:58 +08:00
Yifei Zhang
4f52679ec6 Merge pull request #3236 from Yidadaa/latex 2023-11-12 19:35:22 +08:00
Yidadaa
b52e237044 fix: #3189 should correct math eq in exporter 2023-11-12 19:33:19 +08:00
Yifei Zhang
17b329bb63 Merge pull request #3232 from H0llyW00dzZ/language 2023-11-12 18:00:02 +08:00
nanaya
3a654ba199 UI (model selection): hide unavailable model options 2023-11-12 11:18:14 +08:00
H0llyW00dzZ
5ba3fc9321 Revert "Fix UI/UX Page Local Language [Exporter Message]"
This reverts commit a46f08154e.
Reason : better after instead of before
2023-11-12 01:56:31 +07:00
H0llyW00dzZ
a46f08154e 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
2023-11-12 01:50:35 +07:00
H0llyW00dzZ
0f6ed9c293 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
2023-11-12 00:53:15 +07:00
Yifei Zhang
295864d36b Merge pull request #3231 from Yidadaa/bugfix-1112 2023-11-12 01:23:56 +08:00
Yidadaa
be6d45e49f feat: close #3222 share message list should start from clear context index 2023-11-12 01:21:39 +08:00
Yifei Zhang
22b6987249 Merge pull request #3230 from Yidadaa/bugfix-1112 2023-11-12 00:52:21 +08:00
Yidadaa
64647b0bb3 chore: update doc for mapped CUSTOM_MODELS 2023-11-12 00:49:58 +08:00
Yidadaa
a5a1f2e8ad feat: CUSTOM_MODELS support mapper 2023-11-12 00:46:21 +08:00
Yifei Zhang
8bd39f33e0 Merge pull request #3229 from Yidadaa/bugfix-1112 2023-11-12 00:31:15 +08:00
Yidadaa
be9774943b feat: #3224 auto switch to first avaliable model 2023-11-12 00:29:36 +08:00
Yifei Zhang
943214c6a7 Update README_CN.md 2023-11-10 18:21:22 +08:00
Yifei Zhang
ca792669fc Update constant.ts 2023-11-10 18:20:51 +08:00
Yifei Zhang
78d68a9949 Merge pull request #3214 from Ensteinjun/main 2023-11-10 15:58:28 +08:00
Ensteinjun
6b2db97347 fixbug 2023-11-10 15:44:07 +08:00
Yifei Zhang
2bfb362832 Update tauri.conf.json 2023-11-10 15:19:31 +08:00
Yidadaa
cb140e482f fix: #3207 ensure corner case 2023-11-10 15:15:43 +08:00
Yifei Zhang
e6b72ac1ff Update README.md 2023-11-10 02:59:47 +08:00
Yifei Zhang
8032e6d68d Update README.md 2023-11-10 02:59:30 +08:00
Yidadaa
c7e0a6f37f doc: update azure env vars 2023-11-10 02:57:46 +08:00
Yifei Zhang
1141cd2e6e Merge pull request #3206 from Yidadaa/azure 2023-11-10 02:55:12 +08:00
Yidadaa
c9dd953817 fixup 2023-11-10 02:50:50 +08:00
Yidadaa
b7ffca031e feat: close #935 add azure support 2023-11-10 02:43:37 +08:00
Yifei Zhang
fd2f441e02 feat: wont send max_tokens 2023-11-09 20:45:25 +08:00
Yifei Zhang
3b3ebda34b fix: #3196 3.5-turbo-1106 should use old cutoff date 2023-11-09 11:01:41 +08:00
Yifei Zhang
87e3d663a2 fix: #3192 use smaller max_tokens as default 2023-11-09 11:00:09 +08:00
Yidadaa
d0a1d910d4 fix: #3186 enable max_tokens in chat payload 2023-11-09 03:19:13 +08:00
Yifei Zhang
33b97082fa Merge pull request #3192 from Yidadaa/bugfix-1109 2023-11-09 03:05:41 +08:00
Yidadaa
d93f05f511 feat: close #3187 use CUSTOM_MODELS to control model list 2023-11-09 03:01:29 +08:00
Yifei Zhang
3dc29197e1 Merge pull request #3191 from Yidadaa/bugfix-1109 2023-11-09 02:23:20 +08:00
Yidadaa
fbc0236748 fix: #3174 should prompt to confirm to delete chat 2023-11-09 02:03:05 +08:00
Yifei Zhang
1304a3943a Merge pull request #3190 from Yidadaa/bugfix-1109 2023-11-09 01:54:09 +08:00
Yidadaa
8c0ba1aee2 feat: close #2954 chat summary should be copyable 2023-11-09 01:51:33 +08:00
Yifei Zhang
47154773f2 Merge pull request #3181 from AnYiEE/docs-typo 2023-11-08 18:37:59 +08:00
Yifei Zhang
deca9bf06d Merge pull request #3183 from hekbobo/patch-1 2023-11-08 18:37:35 +08:00
bob
6ab4c9be2e Update cloudflare-pages-cn.md
cloudflare 构建命令
2023-11-08 17:40:28 +08:00
安忆
3a519612a0 docs: fix typo in #3171 2023-11-08 15:53:45 +08:00
Yifei Zhang
d1ec26ae83 Merge pull request #3171 from AnYiEE/docs-update 2023-11-08 15:31:17 +08:00
安忆
9cb889c34f docs: up to date 2023-11-08 07:09:52 +08:00
63 changed files with 1477 additions and 546 deletions

View File

@@ -17,11 +17,6 @@ BASE_URL=
# Default: Empty # Default: Empty
OPENAI_ORG_ID= OPENAI_ORG_ID=
# (optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
HIDE_USER_API_KEY=
# (optional) # (optional)
# Default: Empty # Default: Empty
# If you do not want users to use GPT-4, set this value to 1. # If you do not want users to use GPT-4, set this value to 1.
@@ -29,5 +24,15 @@ DISABLE_GPT4=
# (optional) # (optional)
# Default: Empty # Default: Empty
# If you do not want users to query balance, set this value to 1. # If you do not want users to input their own API key, set this value to 1.
HIDE_BALANCE_QUERY= HIDE_USER_API_KEY=
# (optional)
# Default: Empty
# If you do want users to query balance, set this value to 1.
ENABLE_BALANCE_QUERY=
# (optional)
# Default: Empty
# If you want to disable parse settings from url, set this value to 1.
DISABLE_FAST_LINK=

View File

@@ -18,7 +18,7 @@ jobs:
- name: setup node - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
- name: get version - name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
- name: create release - name: create release
@@ -59,7 +59,7 @@ jobs:
- name: setup node - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
- name: install Rust stable - name: install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:

View File

@@ -3,7 +3,7 @@
<h1 align="center">ChatGPT Next Web</h1> <h1 align="center">ChatGPT Next Web</h1>
English / [简体中文](./README_CN.md) / [日本語](./README_JA.md) English / [简体中文](./README_CN.md)
One-Click to get well-designed cross-platform ChatGPT web UI. One-Click to get well-designed cross-platform ChatGPT web UI.
@@ -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.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.7 let's share conversations as image, or share to ShareGPT!
- 🚀 v2.8 now we have a client that runs across all platforms! - 🚀 v2.8 now we have a client that runs across all platforms!
- 🚀 v2.9.11 you can use azure endpoint now.
## 主要功能 ## 主要功能
@@ -74,7 +75,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话 - 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话 - 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština - 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问 - 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划 ## 开发计划
@@ -93,6 +94,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com - 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 - 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 - 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
## Get Started ## Get Started
@@ -153,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量) > [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
Your openai api key.
### `CODE` (optional) ### `CODE` (optional)
Access password, separated by comma. Access password, separated by comma.
### `OPENAI_API_KEY` (required)
Your openai api key, join multiple api keys with comma.
### `BASE_URL` (optional) ### `BASE_URL` (optional)
> Default: `https://api.openai.com` > Default: `https://api.openai.com`
@@ -173,6 +175,20 @@ Override openai api request base url.
Specify OpenAI organization ID. 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) ### `HIDE_USER_API_KEY` (optional)
> Default: Empty > Default: Empty
@@ -197,6 +213,15 @@ 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. 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`.
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 ## Requirements
NodeJS >= 18, Docker >= 20 NodeJS >= 18, Docker >= 20
@@ -320,6 +345,10 @@ If you want to add a new translation, read this [document](./docs/translation.md
[@synwith](https://github.com/synwith) [@synwith](https://github.com/synwith)
[@piksonGit](https://github.com/piksonGit) [@piksonGit](https://github.com/piksonGit)
[@ouyangzhiping](https://github.com/ouyangzhiping) [@ouyangzhiping](https://github.com/ouyangzhiping)
[@wenjiavv](https://github.com/wenjiavv)
[@LeXwDeX](https://github.com/LeXwDeX)
[@Licoy](https://github.com/Licoy)
[@shangmin2009](https://github.com/shangmin2009)
### Contributor ### Contributor

View File

@@ -68,7 +68,7 @@ code1,code2,code3
### `OPENAI_API_KEY` (必填项) ### `OPENAI_API_KEY` (必填项)
OpanAI 密钥,你在 openai 账户页面申请的 api key。 OpanAI 密钥,你在 openai 账户页面申请的 api key使用英文逗号隔开多个 key这样可以随机轮询这些 key。
### `CODE` (可选) ### `CODE` (可选)
@@ -90,6 +90,20 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
指定 OpenAI 中的组织 ID。 指定 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` (可选) ### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。 如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@@ -106,6 +120,13 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `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`。
> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
## 开发 ## 开发
点击下方按钮,开始二次开发: 点击下方按钮,开始二次开发:
@@ -118,7 +139,7 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
OPENAI_API_KEY=<your api key here> OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址 # 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://nb.nextweb.fun/api/proxy BASE_URL=https://b.nextweb.fun/api/proxy
``` ```
### 本地开发 ### 本地开发

View File

@@ -28,7 +28,7 @@ export function auth(req: NextRequest) {
const authToken = req.headers.get("Authorization") ?? ""; const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token // 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(); const hashedCode = md5.hash(accessCode ?? "").trim();
@@ -39,19 +39,32 @@ export function auth(req: NextRequest) {
console.log("[User IP] ", getIP(req)); console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString()); console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) { if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return { return {
error: true, error: true,
msg: !accessCode ? "empty access code" : "wrong access code", msg: !accessCode ? "empty access code" : "wrong access code",
}; };
} }
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 user does not provide an api key, inject system api key
if (!token) { if (!apiKey) {
const apiKey = serverConfig.apiKey; const serverApiKey = serverConfig.isAzure
if (apiKey) { ? serverConfig.azureApiKey
: serverConfig.apiKey;
if (serverApiKey) {
console.log("[Auth] use system api key"); console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${apiKey}`); req.headers.set(
"Authorization",
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
);
} else { } else {
console.log("[Auth] admin did not provide an api key"); console.log("[Auth] admin did not provide an api key");
} }

View File

@@ -1,48 +1,65 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
import { collectModelTable } from "../utils/model";
import { makeAzurePath } from "../azure";
export const OPENAI_URL = "api.openai.com"; const serverConfig = getServerSideConfig();
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;
export async function requestOpenai(req: NextRequest) { export async function requestOpenai(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
const authValue = req.headers.get("Authorization") ?? ""; 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/", "/api/openai/",
"", "",
); );
let baseUrl = BASE_URL; let baseUrl =
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) { if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`; baseUrl = `https://${baseUrl}`;
} }
if (baseUrl.endsWith('/')) { if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1); baseUrl = baseUrl.slice(0, -1);
} }
console.log("[Proxy] ", openaiPath); console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl); console.log("[Base Url]", baseUrl);
// this fix [Org ID] undefined in server side if not using custom point
if (process.env.OPENAI_ORG_ID) { if (serverConfig.openaiOrgId !== undefined) {
console.log("[Org ID]", process.env.OPENAI_ORG_ID); console.log("[Org ID]", serverConfig.openaiOrgId);
} }
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(
controller.abort(); () => {
}, 10 * 60 * 1000); controller.abort();
},
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 = { const fetchOptions: RequestInit = {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-store", "Cache-Control": "no-store",
Authorization: authValue, [authHeaderName]: authValue,
...(process.env.OPENAI_ORG_ID && { ...(serverConfig.openaiOrgId && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID, "OpenAI-Organization": serverConfig.openaiOrgId,
}), }),
}, },
method: req.method, method: req.method,
@@ -55,18 +72,23 @@ export async function requestOpenai(req: NextRequest) {
}; };
// #1815 try to refuse gpt4 request // #1815 try to refuse gpt4 request
if (DISABLE_GPT4 && req.body) { if (serverConfig.customModels && req.body) {
try { try {
const modelTable = collectModelTable(
DEFAULT_MODELS,
serverConfig.customModels,
);
const clonedBody = await req.text(); const clonedBody = await req.text();
fetchOptions.body = clonedBody; 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 ?? ""].available === false) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
message: "you are not allowed to use gpt-4 model", message: `you are not allowed to use ${jsonBody?.model} model`,
}, },
{ {
status: 403, status: 403,

View File

@@ -12,6 +12,7 @@ const DANGER_CONFIG = {
disableGPT4: serverConfig.disableGPT4, disableGPT4: serverConfig.disableGPT4,
hideBalanceQuery: serverConfig.hideBalanceQuery, hideBalanceQuery: serverConfig.hideBalanceQuery,
disableFastLink: serverConfig.disableFastLink, disableFastLink: serverConfig.disableFastLink,
customModels: serverConfig.customModels,
}; };
declare global { declare global {

View File

@@ -75,3 +75,4 @@ export const GET = handle;
export const POST = handle; export const POST = handle;
export const runtime = "edge"; export const runtime = "edge";
export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1'];

9
app/azure.ts Normal file
View File

@@ -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;
}

View File

@@ -1,5 +1,5 @@
import { getClientConfig } from "../config/client"; 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 { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai"; import { ChatGPTApi } from "./platforms/openai";
@@ -127,22 +127,26 @@ export const api = new ClientApi();
export function getHeaders() { export function getHeaders() {
const accessStore = useAccessStore.getState(); const accessStore = useAccessStore.getState();
let headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest", "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; const validString = (x: string) => x && x.length > 0;
// use user's api key first // use user's api key first
if (validString(accessStore.token)) { if (validString(apiKey)) {
headers.Authorization = makeBearer(accessStore.token); headers[authHeader] = makeBearer(apiKey);
} else if ( } else if (
accessStore.enabledAccessControl() && accessStore.enabledAccessControl() &&
validString(accessStore.accessCode) validString(accessStore.accessCode)
) { ) {
headers.Authorization = makeBearer( headers[authHeader] = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode, ACCESS_CODE_PREFIX + accessStore.accessCode,
); );
} }

View File

@@ -1,8 +1,10 @@
import { import {
ApiPath,
DEFAULT_API_HOST, DEFAULT_API_HOST,
DEFAULT_MODELS, DEFAULT_MODELS,
OpenaiPath, OpenaiPath,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
@@ -14,6 +16,7 @@ import {
} from "@fortaine/fetch-event-source"; } from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { makeAzurePath } from "@/app/azure";
export interface OpenAIListModelResponse { export interface OpenAIListModelResponse {
object: string; object: string;
@@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi {
private disableListModels = true; private disableListModels = true;
path(path: string): string { path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl; const accessStore = useAccessStore.getState();
const apiPath = "/api/openai";
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; 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 (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) { if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
openaiUrl = "https://" + openaiUrl; baseUrl = "https://" + baseUrl;
} }
return [openaiUrl, path].join("/");
if (isAzure) {
path = makeAzurePath(path, accessStore.azureApiVersion);
}
return [baseUrl, path].join("/");
} }
extractMessage(res: any) { extractMessage(res: any) {
@@ -70,6 +88,8 @@ export class ChatGPTApi implements LLMApi {
presence_penalty: modelConfig.presence_penalty, presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty, frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p, top_p: modelConfig.top_p,
// 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); console.log("[Request] openai payload: ", requestPayload);
@@ -95,12 +115,35 @@ export class ChatGPTApi implements LLMApi {
if (shouldStream) { if (shouldStream) {
let responseText = ""; let responseText = "";
let remainText = "";
let finished = false; 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) {
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);
}
// start animaion
animateResponseText();
const finish = () => { const finish = () => {
if (!finished) { if (!finished) {
options.onFinish(responseText);
finished = true; finished = true;
options.onFinish(responseText + remainText);
} }
}; };
@@ -154,14 +197,19 @@ export class ChatGPTApi implements LLMApi {
} }
const text = msg.data; const text = msg.data;
try { try {
const json = JSON.parse(text); const json = JSON.parse(text) as {
const delta = json.choices[0].delta.content; choices: Array<{
delta: {
content: string;
};
}>;
};
const delta = json.choices[0]?.delta?.content;
if (delta) { if (delta) {
responseText += delta; remainText += delta;
options.onUpdate?.(responseText, delta);
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text, msg); console.error("[Request] parse error", text);
} }
}, },
onclose() { onclose() {

View File

@@ -18,7 +18,7 @@ export function AuthPage() {
const goChat = () => navigate(Path.Chat); const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => { const resetAccessCode = () => {
accessStore.update((access) => { accessStore.update((access) => {
access.token = ""; access.openaiApiKey = "";
access.accessCode = ""; access.accessCode = "";
}); });
}; // Reset access code to empty string }; // Reset access code to empty string
@@ -56,11 +56,11 @@ export function AuthPage() {
<input <input
className={styles["auth-input"]} className={styles["auth-input"]}
type="password" type="password"
placeholder={Locale.Settings.Token.Placeholder} placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
value={accessStore.token} value={accessStore.openaiApiKey}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.token = e.currentTarget.value), (access) => (access.openaiApiKey = e.currentTarget.value),
); );
}} }}
/> />

View File

@@ -18,6 +18,7 @@ import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask"; import { Mask } from "../store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils";
export function ChatItem(props: { export function ChatItem(props: {
onClick?: () => void; onClick?: () => void;
@@ -60,7 +61,10 @@ export function ChatItem(props: {
{props.narrow ? ( {props.narrow ? (
<div className={styles["chat-item-narrow"]}> <div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}> <div className={styles["chat-item-avatar"] + " no-dark"}>
<MaskAvatar mask={props.mask} /> <MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div> </div>
<div className={styles["chat-item-narrow-count"]}> <div className={styles["chat-item-narrow-count"]}>
{props.count} {props.count}
@@ -80,7 +84,11 @@ export function ChatItem(props: {
<div <div
className={styles["chat-item-delete"]} className={styles["chat-item-delete"]}
onClickCapture={props.onDelete} onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
e.stopPropagation();
}}
> >
<DeleteIcon /> <DeleteIcon />
</div> </div>
@@ -101,6 +109,7 @@ export function ChatList(props: { narrow?: boolean }) {
); );
const chatStore = useChatStore(); const chatStore = useChatStore();
const navigate = useNavigate(); const navigate = useNavigate();
const isMobileScreen = useMobileScreen();
const onDragEnd: OnDragEndResponder = (result) => { const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result; const { destination, source } = result;
@@ -142,7 +151,7 @@ export function ChatList(props: { narrow?: boolean }) {
}} }}
onDelete={async () => { onDelete={async () => {
if ( if (
!props.narrow || (!props.narrow && !isMobileScreen) ||
(await showConfirm(Locale.Home.DeleteChat)) (await showConfirm(Locale.Home.DeleteChat))
) { ) {
chatStore.deleteSession(i); chatStore.deleteSession(i);

View File

@@ -88,6 +88,7 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter"; import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
@@ -143,6 +144,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
extraListItems={ extraListItems={
session.mask.modelConfig.sendMemory ? ( session.mask.modelConfig.sendMemory ? (
<ListItem <ListItem
className="copyable"
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`} title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent} subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
></ListItem> ></ListItem>
@@ -429,16 +431,26 @@ export function ChatActions(props: {
// switch model // switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model; const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels();
const models = useMemo( const models = useMemo(
() => () => allModels.filter((m) => m.available),
config [allModels],
.allModels()
.filter((m) => m.available)
.map((m) => m.name),
[config],
); );
const [showModelSelector, setShowModelSelector] = useState(false); const [showModelSelector, setShowModelSelector] = useState(false);
useEffect(() => {
// if current model is not available
// switch to first available model
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
if (isUnavaliableModel && models.length > 0) {
const nextModel = models[0].name as ModelType;
chatStore.updateCurrentSession(
(session) => (session.mask.modelConfig.model = nextModel),
);
showToast(nextModel);
}
}, [chatStore, currentModel, models]);
return ( return (
<div className={styles["chat-input-actions"]}> <div className={styles["chat-input-actions"]}>
{couldStop && ( {couldStop && (
@@ -518,8 +530,8 @@ export function ChatActions(props: {
<Selector <Selector
defaultSelectedValue={currentModel} defaultSelectedValue={currentModel}
items={models.map((m) => ({ items={models.map((m) => ({
title: m, title: m.displayName,
value: m, value: m.name,
}))} }))}
onClose={() => setShowModelSelector(false)} onClose={() => setShowModelSelector(false)}
onSelection={(s) => { onSelection={(s) => {
@@ -1001,7 +1013,9 @@ function _Chat() {
).then((res) => { ).then((res) => {
if (!res) return; if (!res) return;
if (payload.key) { if (payload.key) {
accessStore.update((access) => (access.token = payload.key!)); accessStore.update(
(access) => (access.openaiApiKey = payload.key!),
);
} }
if (payload.url) { if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!)); accessStore.update((access) => (access.openaiUrl = payload.url!));
@@ -1161,7 +1175,12 @@ function _Chat() {
{["system"].includes(message.role) ? ( {["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" /> <Avatar avatar="2699-fe0f" />
) : ( ) : (
<MaskAvatar mask={session.mask} /> <MaskAvatar
avatar={session.mask.avatar}
model={
message.model || session.mask.modelConfig.model
}
/>
)} )}
</> </>
)} )}

View File

@@ -186,7 +186,8 @@
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border: var(--border-in-light); border: var(--border-in-light);
*:not(li) { code,
pre {
overflow: hidden; overflow: hidden;
} }
} }

View File

@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */ /* 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 Locale from "../locales";
import styles from "./exporter.module.scss"; import styles from "./exporter.module.scss";
import { import {
@@ -27,7 +27,7 @@ import { Avatar } from "./emoji";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import NextImage from "next/image"; 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 { DEFAULT_MASK_AVATAR } from "../store/mask";
import { api } from "../client/api"; import { api } from "../client/api";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
@@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
export function ExportMessageModal(props: { onClose: () => void }) { export function ExportMessageModal(props: { onClose: () => void }) {
return ( return (
<div className="modal-mask"> <div className="modal-mask">
<Modal title={Locale.Export.Title} onClose={props.onClose}> <Modal
title={Locale.Export.Title}
onClose={props.onClose}
footer={
<div
style={{
width: "100%",
textAlign: "center",
fontSize: 14,
opacity: 0.5,
}}
>
{Locale.Exporter.Description.Title}
</div>
}
>
<div style={{ minHeight: "40vh" }}> <div style={{ minHeight: "40vh" }}>
<MessageExporter /> <MessageExporter />
</div> </div>
@@ -149,7 +164,7 @@ export function MessageExporter() {
if (exportConfig.includeContext) { if (exportConfig.includeContext) {
ret.push(...session.mask.context); 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; return ret;
}, [ }, [
exportConfig.includeContext, exportConfig.includeContext,
@@ -260,7 +275,8 @@ export function RenderExport(props: {
}); });
props.onRender(renderMsgs); props.onRender(renderMsgs);
}); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<div ref={domRef}> <div ref={domRef}>
@@ -437,13 +453,13 @@ export function ImagePreviewer(props: {
showToast(Locale.Export.Image.Toast); showToast(Locale.Export.Image.Toast);
const dom = previewRef.current; const dom = previewRef.current;
if (!dom) return; if (!dom) return;
const isApp = getClientConfig()?.isApp; const isApp = getClientConfig()?.isApp;
try { try {
const blob = await toPng(dom); const blob = await toPng(dom);
if (!blob) return; if (!blob) return;
if (isMobile || (isApp && window.__TAURI__)) { if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) { if (isApp && window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({ const result = await window.__TAURI__.dialog.save({
@@ -459,7 +475,7 @@ export function ImagePreviewer(props: {
}, },
], ],
}); });
if (result !== null) { if (result !== null) {
const response = await fetch(blob); const response = await fetch(blob);
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
@@ -604,8 +620,6 @@ export function MarkdownPreviewer(props: {
); );
} }
// modified by BackTrackZ now it's looks better
export function JsonPreviewer(props: { export function JsonPreviewer(props: {
messages: ChatMessage[]; messages: ChatMessage[];
topic: string; topic: string;

View File

@@ -5,13 +5,13 @@ import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex"; import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm"; import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight"; 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 { copyToClipboard } from "../utils";
import mermaid from "mermaid"; import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import React from "react"; import React from "react";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { showImageModal } from "./ui-lib"; import { showImageModal } from "./ui-lib";
export function Mermaid(props: { code: string }) { export function Mermaid(props: { code: string }) {
@@ -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 }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(
() => escapeDollarNumber(props.content),
[props.content],
);
return ( return (
<ReactMarkdown <ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
@@ -124,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
}, },
}} }}
> >
{props.content} {escapedContent}
</ReactMarkdown> </ReactMarkdown>
); );
} }

View File

@@ -18,6 +18,7 @@ import {
ChatMessage, ChatMessage,
createMessage, createMessage,
ModelConfig, ModelConfig,
ModelType,
useAppConfig, useAppConfig,
useChatStore, useChatStore,
} from "../store"; } from "../store";
@@ -58,11 +59,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
return result; return result;
} }
export function MaskAvatar(props: { mask: Mask }) { export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? ( return props.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.mask.avatar} /> <Avatar avatar={props.avatar} />
) : ( ) : (
<Avatar model={props.mask.modelConfig.model} /> <Avatar model={props.model} />
); );
} }
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
onClick={() => setShowPicker(true)} onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<MaskAvatar mask={props.mask} /> <MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div> </div>
</Popover> </Popover>
</ListItem> </ListItem>
@@ -398,7 +402,7 @@ export function MaskPage() {
setSearchText(text); setSearchText(text);
if (text.length > 0) { if (text.length > 0) {
const result = allMasks.filter((m) => const result = allMasks.filter((m) =>
m.name.toLowerCase().includes(text.toLowerCase()) m.name.toLowerCase().includes(text.toLowerCase()),
); );
setSearchMasks(result); setSearchMasks(result);
} else { } else {
@@ -523,7 +527,7 @@ export function MaskPage() {
<div className={styles["mask-item"]} key={m.id}> <div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}> <div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}> <div className={styles["mask-icon"]}>
<MaskAvatar mask={m} /> <MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
</div> </div>
<div className={styles["mask-title"]}> <div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div> <div className={styles["mask-name"]}>{m.name}</div>

View File

@@ -58,8 +58,8 @@
} }
.body { .body {
flex-grow: 1; flex: 1;
max-width: calc(100% - 40px); max-width: calc(100% - 80px);
.date { .date {
font-size: 12px; font-size: 12px;
@@ -71,6 +71,12 @@
font-size: 12px; font-size: 12px;
} }
} }
.checkbox {
display: flex;
justify-content: flex-end;
flex: 1;
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { ChatMessage, useAppConfig, useChatStore } from "../store"; import { ChatMessage, useAppConfig, useChatStore } from "../store";
import { Updater } from "../typing"; import { Updater } from "../typing";
import { IconButton } from "./button"; import { IconButton } from "./button";
@@ -73,11 +73,23 @@ export function MessageSelector(props: {
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming; const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
const messages = session.messages.filter( const allMessages = useMemo(() => {
(m, i) => let startIndex = Math.max(0, session.clearContextIndex ?? 0);
m.id && // message must have id if (startIndex === session.messages.length - 1) {
isValid(m) && startIndex = 0;
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])), }
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 messageCount = messages.length;
const config = useAppConfig(); const config = useAppConfig();
@@ -176,6 +188,8 @@ export function MessageSelector(props: {
<div className={styles["messages"]}> <div className={styles["messages"]}>
{messages.map((m, i) => { {messages.map((m, i) => {
if (!isInSearchResult(m.id!)) return null; if (!isInSearchResult(m.id!)) return null;
const id = m.id ?? i;
const isSelected = props.selection.has(id);
return ( return (
<div <div
@@ -185,7 +199,6 @@ export function MessageSelector(props: {
key={i} key={i}
onClick={() => { onClick={() => {
props.updateSelection((selection) => { props.updateSelection((selection) => {
const id = m.id ?? i;
selection.has(id) ? selection.delete(id) : selection.add(id); selection.has(id) ? selection.delete(id) : selection.add(id);
}); });
onClickIndex(i); onClickIndex(i);
@@ -195,7 +208,10 @@ export function MessageSelector(props: {
{m.role === "user" ? ( {m.role === "user" ? (
<Avatar avatar={config.avatar}></Avatar> <Avatar avatar={config.avatar}></Avatar>
) : ( ) : (
<MaskAvatar mask={session.mask} /> <MaskAvatar
avatar={session.mask.avatar}
model={m.model || session.mask.modelConfig.model}
/>
)} )}
</div> </div>
<div className={styles["body"]}> <div className={styles["body"]}>
@@ -206,6 +222,10 @@ export function MessageSelector(props: {
{m.content} {m.content}
</div> </div>
</div> </div>
<div className={styles["checkbox"]}>
<input type="checkbox" checked={isSelected}></input>
</div>
</div> </div>
); );
})} })}

View File

@@ -1,14 +1,15 @@
import { ModalConfigValidator, ModelConfig, useAppConfig } from "../store"; import { ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void; updateConfig: (updater: (config: ModelConfig) => void) => void;
}) { }) {
const config = useAppConfig(); const allModels = useAllModels();
return ( return (
<> <>
@@ -24,11 +25,13 @@ export function ModelConfigList(props: {
); );
}} }}
> >
{config.allModels().map((v, i) => ( {allModels
<option value={v.name} key={i} disabled={!v.available}> .filter((v) => v.available)
{v.name} .map((v, i) => (
</option> <option value={v.name} key={i}>
))} {v.displayName}
</option>
))}
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem
@@ -75,8 +78,8 @@ export function ModelConfigList(props: {
> >
<input <input
type="number" type="number"
min={100} min={1024}
max={100000} max={512000}
value={props.modelConfig.max_tokens} value={props.modelConfig.max_tokens}
onChange={(e) => onChange={(e) =>
props.updateConfig( props.updateConfig(

View File

@@ -17,21 +17,13 @@ import { useCommand } from "../command";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks"; 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 }) { function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return ( return (
<div className={styles["mask"]} onClick={props.onClick}> <div className={styles["mask"]} onClick={props.onClick}>
<MaskAvatar mask={props.mask} /> <MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div> <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
</div> </div>
); );

View File

@@ -51,10 +51,13 @@ import Locale, {
import { copyToClipboard } from "../utils"; import { copyToClipboard } from "../utils";
import Link from "next/link"; import Link from "next/link";
import { import {
Azure,
OPENAI_BASE_URL, OPENAI_BASE_URL,
Path, Path,
RELEASE_URL, RELEASE_URL,
STORAGE_KEY, STORAGE_KEY,
ServiceProvider,
SlotID,
UPDATE_URL, UPDATE_URL,
} from "../constant"; } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { Prompt, SearchService, usePromptStore } from "../store/prompt";
@@ -580,8 +583,16 @@ export function Settings() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const shouldHideBalanceQuery = useMemo(() => { const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
return accessStore.hideBalanceQuery || isOpenAiUrl; return (
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]); accessStore.hideBalanceQuery ||
isOpenAiUrl ||
accessStore.provider === ServiceProvider.Azure
);
}, [
accessStore.hideBalanceQuery,
accessStore.openaiUrl,
accessStore.provider,
]);
const usage = { const usage = {
used: updateStore.used, used: updateStore.used,
@@ -624,6 +635,11 @@ export function Settings() {
navigate(Path.Home); navigate(Path.Home);
} }
}; };
if (clientConfig?.isApp) { // Force to set custom endpoint to true if it's app
accessStore.update((state) => {
state.useCustomConfig = true;
});
}
document.addEventListener("keydown", keydownEvent); document.addEventListener("keydown", keydownEvent);
return () => { return () => {
document.removeEventListener("keydown", keydownEvent); document.removeEventListener("keydown", keydownEvent);
@@ -877,16 +893,16 @@ export function Settings() {
</ListItem> </ListItem>
</List> </List>
<List> <List id={SlotID.CustomModel}>
{showAccessCode ? ( {showAccessCode && (
<ListItem <ListItem
title={Locale.Settings.AccessCode.Title} title={Locale.Settings.Access.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle} subTitle={Locale.Settings.Access.AccessCode.SubTitle}
> >
<PasswordInput <PasswordInput
value={accessStore.accessCode} value={accessStore.accessCode}
type="text" type="text"
placeholder={Locale.Settings.AccessCode.Placeholder} placeholder={Locale.Settings.Access.AccessCode.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.accessCode = e.currentTarget.value), (access) => (access.accessCode = e.currentTarget.value),
@@ -894,46 +910,159 @@ export function Settings() {
}} }}
/> />
</ListItem> </ListItem>
) : (
<></>
)} )}
{!accessStore.hideUserApiKey ? ( {!accessStore.hideUserApiKey && (
<> <>
<ListItem {
title={Locale.Settings.Endpoint.Title} // Conditionally render the following ListItem based on clientConfig.isApp
subTitle={Locale.Settings.Endpoint.SubTitle} !clientConfig?.isApp && ( // only show if isApp is false
> <ListItem
<input title={Locale.Settings.Access.CustomEndpoint.Title}
type="text" subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
value={accessStore.openaiUrl} >
placeholder="https://api.openai.com/" <input
onChange={(e) => type="checkbox"
accessStore.update( checked={accessStore.useCustomConfig}
(access) => (access.openaiUrl = e.currentTarget.value), onChange={(e) =>
) accessStore.update(
} (access) =>
></input> (access.useCustomConfig = e.currentTarget.checked),
</ListItem> )
<ListItem }
title={Locale.Settings.Token.Title} ></input>
subTitle={Locale.Settings.Token.SubTitle} </ListItem>
> )
<PasswordInput }
value={accessStore.token} {accessStore.useCustomConfig && (
type="text" <>
placeholder={Locale.Settings.Token.Placeholder} <ListItem
onChange={(e) => { title={Locale.Settings.Access.Provider.Title}
accessStore.update( subTitle={Locale.Settings.Access.Provider.SubTitle}
(access) => (access.token = e.currentTarget.value), >
); <Select
}} value={accessStore.provider}
/> onChange={(e) => {
</ListItem> accessStore.update(
</> (access) =>
) : null} (access.provider = e.target
.value as ServiceProvider),
);
}}
>
{Object.entries(ServiceProvider).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</Select>
</ListItem>
{!shouldHideBalanceQuery ? ( {accessStore.provider === "OpenAI" ? (
<>
<ListItem
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
subTitle={
Locale.Settings.Access.OpenAI.Endpoint.SubTitle
}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
onChange={(e) =>
accessStore.update(
(access) =>
(access.openaiUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.openaiApiKey}
type="text"
placeholder={
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.openaiApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
) : (
<>
<ListItem
title={Locale.Settings.Access.Azure.Endpoint.Title}
subTitle={
Locale.Settings.Access.Azure.Endpoint.SubTitle +
Azure.ExampleEndpoint
}
>
<input
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiKey.Title}
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<PasswordInput
value={accessStore.azureApiKey}
type="text"
placeholder={
Locale.Settings.Access.Azure.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) =>
(access.azureApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiVerion.Title}
subTitle={
Locale.Settings.Access.Azure.ApiVerion.SubTitle
}
>
<input
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
onChange={(e) =>
accessStore.update(
(access) =>
(access.azureApiVersion =
e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
</>
)}
</>
)}
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
<ListItem <ListItem
title={Locale.Settings.Usage.Title} title={Locale.Settings.Usage.Title}
subTitle={ subTitle={
@@ -960,8 +1089,8 @@ export function Settings() {
) : null} ) : null}
<ListItem <ListItem
title={Locale.Settings.CustomModel.Title} title={Locale.Settings.Access.CustomModel.Title}
subTitle={Locale.Settings.CustomModel.SubTitle} subTitle={Locale.Settings.Access.CustomModel.SubTitle}
> >
<input <input
type="text" type="text"

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useMemo } from "react"; import { useEffect, useRef, useMemo } from "react";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@@ -8,6 +8,7 @@ import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg"; import ChatGptIcon from "../icons/chatgpt.svg";
import AddIcon from "../icons/add.svg"; import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import MaskIcon from "../icons/mask.svg"; import MaskIcon from "../icons/mask.svg";
import PluginIcon from "../icons/plugin.svg"; import PluginIcon from "../icons/plugin.svg";
import DragIcon from "../icons/drag.svg"; import DragIcon from "../icons/drag.svg";
@@ -202,7 +203,7 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-actions"]}> <div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<DeleteIcon />}
onClick={async () => { onClick={async () => {
if (await showConfirm(Locale.Home.DeleteChat)) { if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex); chatStore.deleteSession(chatStore.currentSessionIndex);

View File

@@ -235,7 +235,7 @@
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);
padding: 10px 25px 10px 10px; padding: 10px 35px 10px 10px;
border-radius: 10px; border-radius: 10px;
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;

View File

@@ -70,14 +70,12 @@ export function ListItem(props: {
); );
} }
export function List(props: { export function List(props: { children: React.ReactNode; id?: string }) {
children: return (
| Array<JSX.Element | null | undefined> <div className={styles.list} id={props.id}>
| JSX.Element {props.children}
| null </div>
| undefined; );
}) {
return <div className={styles.list}>{props.children}</div>;
} }
export function Loading() { export function Loading() {
@@ -99,8 +97,9 @@ export function Loading() {
interface ModalProps { interface ModalProps {
title: string; title: string;
children?: any; children?: any;
actions?: JSX.Element[]; actions?: React.ReactNode[];
defaultMax?: boolean; defaultMax?: boolean;
footer?: React.ReactNode;
onClose?: () => void; onClose?: () => void;
} }
export function Modal(props: ModalProps) { export function Modal(props: ModalProps) {
@@ -149,6 +148,7 @@ export function Modal(props: ModalProps) {
<div className={styles["modal-content"]}>{props.children}</div> <div className={styles["modal-content"]}>{props.children}</div>
<div className={styles["modal-footer"]}> <div className={styles["modal-footer"]}>
{props.footer}
<div className={styles["modal-actions"]}> <div className={styles["modal-actions"]}>
{props.actions?.map((action, i) => ( {props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}> <div key={i} className={styles["modal-action"]}>

View File

@@ -1,19 +1,31 @@
import md5 from "spark-md5"; import md5 from "spark-md5";
import { DEFAULT_MODELS } from "../constant";
declare global { declare global {
namespace NodeJS { namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
PROXY_URL?: string; // docker only
OPENAI_API_KEY?: string; OPENAI_API_KEY?: string;
CODE?: string; CODE?: string;
BASE_URL?: string; BASE_URL?: string;
PROXY_URL?: string; OPENAI_ORG_ID?: string; // openai only
VERCEL?: 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
BUILD_MODE?: "standalone" | "export"; BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app 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 ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url 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;
} }
} }
} }
@@ -38,17 +50,47 @@ export const getServerSideConfig = () => {
); );
} }
const 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(",");
}
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 { return {
apiKey: process.env.OPENAI_API_KEY, baseUrl: process.env.BASE_URL,
apiKey,
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, code: process.env.CODE,
codes: ACCESS_CODES, codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
baseUrl: process.env.BASE_URL,
proxyUrl: process.env.PROXY_URL, proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL, isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY, hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
disableGPT4: !!process.env.DISABLE_GPT4, disableGPT4,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
disableFastLink: !!process.env.DISABLE_FAST_LINK, disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels,
}; };
}; };

View File

@@ -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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; 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 DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
export const OPENAI_BASE_URL = "https://api.openai.com"; export const OPENAI_BASE_URL = "https://api.openai.com";
@@ -23,10 +23,12 @@ export enum Path {
export enum ApiPath { export enum ApiPath {
Cors = "/api/cors", Cors = "/api/cors",
OpenAI = "/api/openai",
} }
export enum SlotID { export enum SlotID {
AppBody = "app-body", AppBody = "app-body",
CustomModel = "custom-model",
} }
export enum FileName { export enum FileName {
@@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export enum ServiceProvider {
OpenAI = "OpenAI",
Azure = "Azure",
}
export const OpenaiPath = { export const OpenaiPath = {
ChatPath: "v1/chat/completions", ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage", UsagePath: "dashboard/billing/usage",
@@ -67,19 +74,24 @@ export const OpenaiPath = {
ListModelPath: "v1/models", 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_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = ` export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI. You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: {{cutoff}} Knowledge cutoff: {{cutoff}}
Current model: {{model}} Current model: {{model}}
Current time: {{time}} Current time: {{time}}
Latex inline: $x^2$
Latex block: $$e=mc^2$$
`; `;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
export const KnowledgeCutOffDate: Record<string, string> = { export const KnowledgeCutOffDate: Record<string, string> = {
default: "2021-09", default: "2021-09",
"gpt-3.5-turbo-1106": "2023-04",
"gpt-4-1106-preview": "2023-04", "gpt-4-1106-preview": "2023-04",
"gpt-4-vision-preview": "2023-04", "gpt-4-vision-preview": "2023-04",
}; };

View File

@@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Title: "حد الضغط للتاريخ", Title: "حد الضغط للتاريخ",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد", SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
}, },
Token: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
Placeholder: "مفتاح OpenAI API",
},
Usage: { Usage: {
Title: "رصيد الحساب", Title: "رصيد الحساب",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Check: "التحقق", Check: "التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد", NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
}, },
AccessCode: {
Title: "رمز الوصول",
SubTitle: "تم تمكين التحكم في الوصول",
Placeholder: "رمز الوصول المطلوب",
},
Endpoint: {
Title: "نقطة النهاية",
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
},
Model: "النموذج", Model: "النموذج",
Temperature: { Temperature: {
Title: "الحرارة", Title: "الحرارة",

View File

@@ -199,11 +199,7 @@ const bn: PartialLocaleType = {
SubTitle: SubTitle:
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে", "নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
}, },
Token: {
Title: "অ্যাপি কী",
SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন",
Placeholder: "OpenAI API কী",
},
Usage: { Usage: {
Title: "একাউন্ট ব্যালেন্স", Title: "একাউন্ট ব্যালেন্স",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -213,15 +209,7 @@ const bn: PartialLocaleType = {
Check: "চেক", Check: "চেক",
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন", NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
}, },
AccessCode: {
Title: "অ্যাক্সেস কোড",
SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়",
Placeholder: "অ্যাক্সেস কোড প্রয়োজন",
},
Endpoint: {
Title: "ইনটারপয়েন্ট",
SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে",
},
Model: "মডেল", Model: "মডেল",
Temperature: { Temperature: {
Title: "তাপমাত্রা", Title: "তাপমাত্রা",

View File

@@ -85,8 +85,8 @@ const cn = {
Copy: "全部复制", Copy: "全部复制",
Download: "下载文件", Download: "下载文件",
Share: "分享到 ShareGPT", Share: "分享到 ShareGPT",
MessageFromYou: "来自你的消息", MessageFromYou: "用户",
MessageFromChatGPT: "来自 ChatGPT 的消息", MessageFromChatGPT: "ChatGPT",
Format: { Format: {
Title: "导出格式", Title: "导出格式",
SubTitle: "可以导出 Markdown 文本或者 PNG 图片", SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
@@ -258,11 +258,6 @@ const cn = {
Title: "历史消息长度压缩阈值", Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
}, },
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: { Usage: {
Title: "余额查询", Title: "余额查询",
@@ -273,19 +268,56 @@ const cn = {
Check: "重新检查", Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额", NoAccess: "输入 API Key 或访问密码查看余额",
}, },
AccessCode: {
Title: "访问密码", Access: {
SubTitle: "管理员已开启加密访问", AccessCode: {
Placeholder: "请输入访问密码", Title: "访问密码",
}, SubTitle: "管理员已开启加密访问",
Endpoint: { Placeholder: "请输入访问密码",
Title: "接口地址", },
SubTitle: "除默认地址外,必须包含 http(s)://", CustomEndpoint: {
}, Title: "自定义接口",
CustomModel: { SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
Title: "自定义模型名", },
SubTitle: "增加自定义模型可选项,使用英文逗号隔开", 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)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
@@ -409,6 +441,9 @@ const cn = {
Config: "配置", Config: "配置",
}, },
Exporter: { Exporter: {
Description : {
Title: "只有清除上下文之后的消息会被展示"
},
Model: "模型", Model: "模型",
Messages: "消息", Messages: "消息",
Topic: "主题", Topic: "主题",

View File

@@ -124,11 +124,7 @@ const cs: PartialLocaleType = {
SubTitle: SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu", "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: { Usage: {
Title: "Stav účtu", Title: "Stav účtu",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const cs: PartialLocaleType = {
Check: "Zkontrolovat", Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API", 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", Model: "Model",
Temperature: { Temperature: {
Title: "Teplota", Title: "Teplota",

View File

@@ -124,12 +124,7 @@ const de: PartialLocaleType = {
SubTitle: SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet", "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: { Usage: {
Title: "Kontostand", Title: "Kontostand",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -139,11 +134,6 @@ const de: PartialLocaleType = {
Check: "Erneut prüfen", Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen", NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
}, },
AccessCode: {
Title: "Zugangscode",
SubTitle: "Zugangskontrolle aktiviert",
Placeholder: "Zugangscode erforderlich",
},
Model: "Modell", Model: "Modell",
Temperature: { Temperature: {
Title: "Temperature", //Temperatur Title: "Temperature", //Temperatur

View File

@@ -262,11 +262,7 @@ const en: LocaleType = {
SubTitle: SubTitle:
"Will compress if uncompressed messages length exceeds the value", "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: { Usage: {
Title: "Account Balance", Title: "Account Balance",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -276,19 +272,55 @@ const en: LocaleType = {
Check: "Check", Check: "Check",
NoAccess: "Enter API Key to check balance", NoAccess: "Enter API Key to check balance",
}, },
AccessCode: { Access: {
Title: "Access Code", AccessCode: {
SubTitle: "Access control enabled", Title: "Access Code",
Placeholder: "Need Access Code", SubTitle: "Access control Enabled",
}, Placeholder: "Enter Code",
Endpoint: { },
Title: "Endpoint", CustomEndpoint: {
SubTitle: "Custom endpoint must start with http(s)://", Title: "Custom Endpoint",
}, SubTitle: "Use custom Azure or OpenAI service",
CustomModel: { },
Title: "Custom Models", Provider: {
SubTitle: "Add extra model options, separate by comma", 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", Model: "Model",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",
@@ -410,6 +442,9 @@ const en: LocaleType = {
Config: "Config", Config: "Config",
}, },
Exporter: { Exporter: {
Description: {
Title: "Only messages after clearing the context will be displayed"
},
Model: "Model", Model: "Model",
Messages: "Messages", Messages: "Messages",
Topic: "Topic", Topic: "Topic",

View File

@@ -124,11 +124,7 @@ const es: PartialLocaleType = {
SubTitle: SubTitle:
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor", "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: { Usage: {
Title: "Saldo de la cuenta", Title: "Saldo de la cuenta",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const es: PartialLocaleType = {
Check: "Comprobar de nuevo", Check: "Comprobar de nuevo",
NoAccess: "Introduzca la clave API para comprobar el saldo", 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", Model: "Modelo",
Temperature: { Temperature: {
Title: "Temperatura", Title: "Temperatura",

View File

@@ -173,11 +173,7 @@ const fr: PartialLocaleType = {
SubTitle: SubTitle:
"Comprimera si la longueur des messages non compressés dépasse cette valeur", "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: { Usage: {
Title: "Solde du compte", Title: "Solde du compte",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -187,11 +183,7 @@ const fr: PartialLocaleType = {
Check: "Vérifier", Check: "Vérifier",
NoAccess: "Entrez la clé API pour vérifier le solde", 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", Model: "Modèle",
Temperature: { Temperature: {
Title: "Température", Title: "Température",

View File

@@ -4,8 +4,9 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = { const id: PartialLocaleType = {
WIP: "Coming Soon...", WIP: "Coming Soon...",
Error: { 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: { Auth: {
Title: "Diperlukan Kode Akses", Title: "Diperlukan Kode Akses",
Tips: "Masukkan kode akses di bawah", Tips: "Masukkan kode akses di bawah",
@@ -237,11 +238,7 @@ const id: PartialLocaleType = {
SubTitle: SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi", "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: { Usage: {
Title: "Saldo Akun", Title: "Saldo Akun",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -251,15 +248,7 @@ const id: PartialLocaleType = {
Check: "Periksa", Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo", 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", Model: "Model",
Temperature: { Temperature: {
Title: "Suhu", Title: "Suhu",
@@ -379,6 +368,9 @@ const id: PartialLocaleType = {
Edit: "Edit", Edit: "Edit",
}, },
Exporter: { Exporter: {
Description: {
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
},
Model: "Model", Model: "Model",
Messages: "Pesan", Messages: "Pesan",
Topic: "Topik", Topic: "Topik",

View File

@@ -1,5 +1,6 @@
import cn from "./cn"; import cn from "./cn";
import en from "./en"; import en from "./en";
import pt from "./pt";
import tw from "./tw"; import tw from "./tw";
import id from "./id"; import id from "./id";
import fr from "./fr"; import fr from "./fr";
@@ -24,6 +25,7 @@ const ALL_LANGS = {
cn, cn,
en, en,
tw, tw,
pt,
jp, jp,
ko, ko,
id, id,
@@ -47,6 +49,7 @@ export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
export const ALL_LANG_OPTIONS: Record<Lang, string> = { export const ALL_LANG_OPTIONS: Record<Lang, string> = {
cn: "简体中文", cn: "简体中文",
en: "English", en: "English",
pt: "Português",
tw: "繁體中文", tw: "繁體中文",
jp: "日本語", jp: "日本語",
ko: "한국어", ko: "한국어",

View File

@@ -124,12 +124,7 @@ const it: PartialLocaleType = {
SubTitle: SubTitle:
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore", "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: { Usage: {
Title: "Bilancio Account", Title: "Bilancio Account",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -139,11 +134,7 @@ const it: PartialLocaleType = {
Check: "Controlla ancora", Check: "Controlla ancora",
NoAccess: "Inserire la chiave API per controllare il saldo", 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", Model: "Modello GPT",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",

View File

@@ -20,7 +20,8 @@ const jp: PartialLocaleType = {
Stop: "停止", Stop: "停止",
Retry: "リトライ", Retry: "リトライ",
Pin: "ピン", Pin: "ピン",
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました", PinToastContent:
"コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastAction: "表示", PinToastAction: "表示",
Delete: "削除", Delete: "削除",
Edit: "編集", Edit: "編集",
@@ -146,11 +147,7 @@ const jp: PartialLocaleType = {
SubTitle: SubTitle:
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。", "圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
}, },
Token: {
Title: "APIキー",
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
Placeholder: "OpenAI APIキー",
},
Usage: { Usage: {
Title: "残高照会", Title: "残高照会",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -160,11 +157,7 @@ const jp: PartialLocaleType = {
Check: "再確認", Check: "再確認",
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示", NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
}, },
AccessCode: {
Title: "アクセスパスワード",
SubTitle: "暗号化アクセスが有効になっています",
Placeholder: "アクセスパスワードを入力してください",
},
Model: "モデル (model)", Model: "モデル (model)",
Temperature: { Temperature: {
Title: "ランダム性 (temperature)", Title: "ランダム性 (temperature)",

View File

@@ -124,11 +124,7 @@ const ko: PartialLocaleType = {
Title: "기록 압축 임계값", Title: "기록 압축 임계값",
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨", SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
}, },
Token: {
Title: "API 키",
SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용",
Placeholder: "OpenAI API 키",
},
Usage: { Usage: {
Title: "계정 잔액", Title: "계정 잔액",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const ko: PartialLocaleType = {
Check: "확인", Check: "확인",
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.", NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
}, },
AccessCode: {
Title: "액세스 코드",
SubTitle: "액세스 제어가 활성화됨",
Placeholder: "액세스 코드 입력",
},
Model: "모델", Model: "모델",
Temperature: { Temperature: {
Title: "온도 (temperature)", Title: "온도 (temperature)",

View File

@@ -106,12 +106,7 @@ const no: PartialLocaleType = {
SubTitle: SubTitle:
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien", "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: { Usage: {
Title: "Saldo for konto", Title: "Saldo for konto",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -121,11 +116,7 @@ const no: PartialLocaleType = {
Check: "Sjekk", Check: "Sjekk",
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo", NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
}, },
AccessCode: {
Title: "Tilgangskode",
SubTitle: "Tilgangskontroll på",
Placeholder: "Trenger tilgangskode",
},
Model: "Model", Model: "Model",
Temperature: { Temperature: {
Title: "Temperatur", Title: "Temperatur",

466
app/locales/pt.ts Normal file
View File

@@ -0,0 +1,466 @@
import { SubmitKey } from "../store/config";
import { PartialLocaleType } from "../locales/index";
import { getClientConfig } from "../config/client";
const isApp = !!getClientConfig()?.isApp;
const pt: PartialLocaleType = {
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: "Language",
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 desabilitarVocê 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;

View File

@@ -125,11 +125,7 @@ const ru: PartialLocaleType = {
SubTitle: SubTitle:
"Будет сжимать, если длина несжатых сообщений превышает указанное значение", "Будет сжимать, если длина несжатых сообщений превышает указанное значение",
}, },
Token: {
Title: "API ключ",
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
Placeholder: "API ключ OpenAI",
},
Usage: { Usage: {
Title: "Баланс аккаунта", Title: "Баланс аккаунта",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -139,11 +135,7 @@ const ru: PartialLocaleType = {
Check: "Проверить", Check: "Проверить",
NoAccess: "Введите API ключ, чтобы проверить баланс", NoAccess: "Введите API ключ, чтобы проверить баланс",
}, },
AccessCode: {
Title: "Код доступа",
SubTitle: "Контроль доступа включен",
Placeholder: "Требуется код доступа",
},
Model: "Модель", Model: "Модель",
Temperature: { Temperature: {
Title: "Температура", Title: "Температура",

View File

@@ -124,11 +124,7 @@ const tr: PartialLocaleType = {
SubTitle: SubTitle:
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır", "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: { Usage: {
Title: "Hesap Bakiyesi", Title: "Hesap Bakiyesi",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const tr: PartialLocaleType = {
Check: "Tekrar Kontrol Et", Check: "Tekrar Kontrol Et",
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin", 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", Model: "Model",
Temperature: { Temperature: {
Title: "Gerçeklik", Title: "Gerçeklik",

View File

@@ -120,11 +120,7 @@ const tw: PartialLocaleType = {
Title: "歷史訊息長度壓縮閾值", Title: "歷史訊息長度壓縮閾值",
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮", SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
}, },
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可規避授權存取限制",
Placeholder: "OpenAI API Key",
},
Usage: { Usage: {
Title: "帳戶餘額", Title: "帳戶餘額",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -134,11 +130,7 @@ const tw: PartialLocaleType = {
Check: "重新檢查", Check: "重新檢查",
NoAccess: "輸入 API Key 檢視餘額", NoAccess: "輸入 API Key 檢視餘額",
}, },
AccessCode: {
Title: "授權碼",
SubTitle: "目前是未授權存取狀態",
Placeholder: "請輸入授權碼",
},
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "隨機性 (temperature)", Title: "隨機性 (temperature)",

View File

@@ -123,11 +123,7 @@ const vi: PartialLocaleType = {
Title: "Ngưỡng nén lịch sử tin nhắn", 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", 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: { Usage: {
Title: "Hạn mức tài khoản", Title: "Hạn mức tài khoản",
SubTitle(used: any, total: any) { SubTitle(used: any, total: any) {
@@ -137,11 +133,7 @@ const vi: PartialLocaleType = {
Check: "Kiểm tra", Check: "Kiểm tra",
NoAccess: "Nhập API Key để kiểm tra hạn mức", 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", Model: "Mô hình",
Temperature: { Temperature: {
Title: "Tính ngẫu nhiên (temperature)", Title: "Tính ngẫu nhiên (temperature)",

View File

@@ -1,24 +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 { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { ensure } from "../utils/clone";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
const DEFAULT_OPENAI_URL = const DEFAULT_OPENAI_URL =
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/"; getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
const DEFAULT_ACCESS_STATE = { const DEFAULT_ACCESS_STATE = {
token: "",
accessCode: "", accessCode: "",
useCustomConfig: false,
provider: ServiceProvider.OpenAI,
// openai
openaiUrl: DEFAULT_OPENAI_URL,
openaiApiKey: "",
// azure
azureUrl: "",
azureApiKey: "",
azureApiVersion: "2023-08-01-preview",
// server config
needCode: true, needCode: true,
hideUserApiKey: false, hideUserApiKey: false,
hideBalanceQuery: false, hideBalanceQuery: false,
disableGPT4: false, disableGPT4: false,
disableFastLink: false, disableFastLink: false,
customModels: "",
openaiUrl: DEFAULT_OPENAI_URL,
}; };
export const useAccessStore = createPersistStore( export const useAccessStore = createPersistStore(
@@ -30,12 +47,24 @@ export const useAccessStore = createPersistStore(
return get().needCode; return get().needCode;
}, },
isValidOpenAI() {
return ensure(get(), ["openaiApiKey"]);
},
isValidAzure() {
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
},
isAuthorized() { isAuthorized() {
this.fetch(); this.fetch();
// has token or has code or disabled access control // has token or has code or disabled access control
return ( return (
!!get().token || !!get().accessCode || !this.enabledAccessControl() this.isValidOpenAI() ||
this.isValidAzure() ||
!this.enabledAccessControl() ||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
); );
}, },
fetch() { fetch() {
@@ -52,12 +81,6 @@ export const useAccessStore = createPersistStore(
.then((res: DangerConfig) => { .then((res: DangerConfig) => {
console.log("[Config] got config from server", res); console.log("[Config] got config from server", res);
set(() => ({ ...res })); set(() => ({ ...res }));
if (res.disableGPT4) {
DEFAULT_MODELS.forEach(
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
);
}
}) })
.catch(() => { .catch(() => {
console.error("[Config] failed to fetch config"); console.error("[Config] failed to fetch config");
@@ -69,6 +92,19 @@ export const useAccessStore = createPersistStore(
}), }),
{ {
name: StoreKey.Access, 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;
},
}, },
); );

View File

@@ -85,33 +85,6 @@ function getSummarizeModel(currentModel: string) {
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel; 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<void>;
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[]) { function countMessages(msgs: ChatMessage[]) {
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0); return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
} }
@@ -584,7 +557,10 @@ export const useChatStore = createPersistStore(
}, },
onFinish(message) { onFinish(message) {
console.log("[Memory] ", message); console.log("[Memory] ", message);
session.lastSummarizeIndex = lastSummarizeIndex; get().updateCurrentSession((session) => {
session.lastSummarizeIndex = lastSummarizeIndex;
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
});
}, },
onError(err) { onError(err) {
console.error("[Summarize] ", err); console.error("[Summarize] ", err);

View File

@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = {
model: "gpt-3.5-turbo" as ModelType, model: "gpt-3.5-turbo" as ModelType,
temperature: 0.5, temperature: 0.5,
top_p: 1, top_p: 1,
max_tokens: 2000, max_tokens: 4000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0, frequency_penalty: 0,
sendMemory: true, sendMemory: true,
@@ -82,7 +82,7 @@ export const ModalConfigValidator = {
return x as ModelType; return x as ModelType;
}, },
max_tokens(x: number) { max_tokens(x: number) {
return limitNumber(x, 0, 100000, 2000); return limitNumber(x, 0, 512000, 1024);
}, },
presence_penalty(x: number) { presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0); return limitNumber(x, -2, 2, 0);
@@ -128,15 +128,7 @@ export const useAppConfig = createPersistStore(
})); }));
}, },
allModels() { 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;
},
}), }),
{ {
name: StoreKey.Config, name: StoreKey.Config,

View File

@@ -357,3 +357,7 @@ pre {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.copyable {
user-select: text;
}

View File

@@ -3,7 +3,10 @@ import { showToast } from "./components/ui-lib";
import Locale from "./locales"; import Locale from "./locales";
export function trimTopic(topic: string) { export function trimTopic(topic: string) {
return topic.replace(/[,。!?”“"、,.!?]*$/, ""); // Fix an issue where double quotes still show in the Indonesian language
// This will remove the specified punctuation from the end of the string
// and also trim quotes from both the start and end if they exist.
return topic.replace(/^["“”]+|["“”]+$/g, "").replace(/[,。!?”“"、,.!?]*$/, "");
} }
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {

View File

@@ -1,3 +1,12 @@
export function deepClone<T>(obj: T) { export function deepClone<T>(obj: T) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
export function ensure<T extends object>(
obj: T,
keys: Array<[keyof T][number]>,
) {
return keys.every(
(k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "",
);
}

16
app/utils/hooks.ts Normal file
View File

@@ -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,
[configStore.customModels, accessStore.customModels].join(","),
);
}, [accessStore.customModels, configStore.customModels, configStore.models]);
return models;
}

56
app/utils/model.ts Normal file
View File

@@ -0,0 +1,56 @@
import { LLMModel } from "../client/api";
export function collectModelTable(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable: Record<
string,
{ available: boolean; name: string; displayName: string }
> = {};
// default models
models.forEach(
(m) =>
(modelTable[m.name] = {
...m,
displayName: m.name,
}),
);
// server custom models
customModels
.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => {
const available = !m.startsWith("-");
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,
available,
};
});
return modelTable;
}
/**
* Generate full model table.
*/
export function collectModels(
models: readonly LLMModel[],
customModels: string,
) {
const modelTable = collectModelTable(models, customModels);
const allModels = Object.values(modelTable);
return allModels;
}

View File

@@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { combine, persist } from "zustand/middleware";
import { Updater } from "../typing"; import { Updater } from "../typing";
import { deepClone } from "./clone"; import { deepClone } from "./clone";
@@ -23,33 +23,42 @@ type SetStoreState<T> = (
replace?: boolean | undefined, replace?: boolean | undefined,
) => void; ) => void;
export function createPersistStore<T, M>( export function createPersistStore<T extends object, M>(
defaultState: T, state: T,
methods: ( methods: (
set: SetStoreState<T & MakeUpdater<T>>, set: SetStoreState<T & MakeUpdater<T>>,
get: () => T & MakeUpdater<T>, get: () => T & MakeUpdater<T>,
) => M, ) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>, persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) { ) {
return create<T & M & MakeUpdater<T>>()( return create(
persist((set, get) => { persist(
return { combine(
...defaultState, {
...methods(set as any, get), ...state,
lastUpdateTime: 0,
},
(set, get) => {
return {
...methods(set, get as any),
lastUpdateTime: 0, markUpdate() {
markUpdate() { set({ lastUpdateTime: Date.now() } as Partial<
set({ lastUpdateTime: Date.now() } as Partial< T & M & MakeUpdater<T>
T & M & MakeUpdater<T> >);
>); },
update(updater) {
const state = deepClone(get());
updater(state);
set({
...state,
lastUpdateTime: Date.now(),
});
},
} as M & MakeUpdater<T>;
}, },
update(updater) { ),
const state = deepClone(get()); persistOptions as any,
updater(state); ),
get().markUpdate();
set(state);
},
};
}, persistOptions),
); );
} }

View File

@@ -1,6 +1,6 @@
version: '3.9' version: "3.9"
services: services:
chatgpt-next-web: chatgpt-next-web:
profiles: ["no-proxy"] profiles: ["no-proxy"]
container_name: chatgpt-next-web container_name: chatgpt-next-web
image: yidadaa/chatgpt-next-web image: yidadaa/chatgpt-next-web
@@ -13,8 +13,11 @@ services:
- OPENAI_ORG_ID=$OPENAI_ORG_ID - OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY - HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=$DISABLE_GPT4 - 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"] profiles: ["proxy"]
container_name: chatgpt-next-web-proxy container_name: chatgpt-next-web-proxy
image: yidadaa/chatgpt-next-web image: yidadaa/chatgpt-next-web
@@ -28,3 +31,6 @@ services:
- OPENAI_ORG_ID=$OPENAI_ORG_ID - OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY - HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=$DISABLE_GPT4 - DISABLE_GPT4=$DISABLE_GPT4
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
- OPENAI_SB=$OPENAI_SB

View File

@@ -1,6 +1,7 @@
# Cloudflare Pages 部署指南 # Cloudflare Pages 部署指南
## 如何新建项目 ## 如何新建项目
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。 在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
1. 点击 "Create a project"。 1. 点击 "Create a project"。
@@ -12,7 +13,7 @@
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。 7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
8. 由于 node:buffer 的 bug暂时不要使用默认的 "Build command"。请使用以下命令: 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",使用默认值并且不要修改。 9. 对于 "Build output directory",使用默认值并且不要修改。
10. 不要修改 "Root Directory"。 10. 不要修改 "Root Directory"。
@@ -30,10 +31,12 @@
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID` - `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key` - `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4` - `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
- `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能`
- `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置`
12. 点击 "Save and Deploy"。 12. 点击 "Save and Deploy"。
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。 13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。 14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。 15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
16. 前往 "Deployments",点击 "Retry deployment"。 16. 前往 "Deployments",点击 "Retry deployment"。
17. Enjoy. 17. Enjoy.

View File

@@ -1,6 +1,7 @@
# Cloudflare Pages Deployment Guide # Cloudflare Pages Deployment Guide
## How to create a new project ## How to create a new project
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages. Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
1. Click "Create a project". 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. 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". 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: 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. 9. For "Build output directory", use the default value and do not modify it.
10. Do not modify "Root Directory". 10. Do not modify "Root Directory".
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information: 11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
- `NODE_VERSION=20.1` - `NODE_VERSION=20.1`
- `NEXT_TELEMETRY_DISABLE=1` - `NEXT_TELEMETRY_DISABLE=1`
- `OPENAI_API_KEY=your_own_API_key` - `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` - `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` - `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` - `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 Optionaluse the third-party OpenAI-SB API`
12. Click "Save and Deploy". 12. Click "Save and Deploy".
13. Click "Cancel deployment" because you need to fill in Compatibility flags. 13. Click "Cancel deployment" because you need to fill in Compatibility flags.
14. Go to "Build settings", "Functions", and find "Compatibility flags". 14. Go to "Build settings", "Functions", and find "Compatibility flags".

View File

@@ -20,11 +20,11 @@
"@hello-pangea/dnd": "^16.3.0", "@hello-pangea/dnd": "^16.3.0",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.5.1", "emoji-picker-react": "^4.5.15",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"mermaid": "^10.3.1", "mermaid": "^10.6.1",
"nanoid": "^4.0.2", "nanoid": "^5.0.3",
"next": "^13.4.9", "next": "^13.4.9",
"node-fetch": "^3.3.1", "node-fetch": "^3.3.1",
"react": "^18.2.0", "react": "^18.2.0",
@@ -43,11 +43,11 @@
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@types/node": "^20.3.3", "@types/node": "^20.9.0",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-katex": "^3.0.0", "@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2", "@types/spark-md5": "^3.0.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.49.0", "eslint": "^8.49.0",
"eslint-config-next": "13.4.19", "eslint-config-next": "13.4.19",

View File

@@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "ChatGPT Next Web", "productName": "ChatGPT Next Web",
"version": "2.9.10" "version": "2.9.13"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@@ -1,24 +1,5 @@
{ {
"github": { "github": {
"silent": true "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"
}
]
}
]
} }

View File

@@ -1507,10 +1507,12 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*", "@types/node@^20.3.3": "@types/node@*", "@types/node@^20.9.0":
version "20.3.3" version "20.9.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298"
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==
dependencies:
undici-types "~5.26.4"
"@types/parse-json@^4.0.0": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
@@ -1550,10 +1552,10 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@types/spark-md5@^3.0.2": "@types/spark-md5@^3.0.4":
version "3.0.2" version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55" resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.4.tgz#c1221d63c069d95aba0c06a765b80661cacc12bf"
integrity sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ== integrity sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==
"@types/unist@*", "@types/unist@^2.0.0": "@types/unist@*", "@types/unist@^2.0.0":
version "2.0.6" version "2.0.6"
@@ -2126,11 +2128,6 @@ client-only@0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== 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: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -2762,12 +2759,10 @@ elkjs@^0.8.2:
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
emoji-picker-react@^4.5.1: emoji-picker-react@^4.5.15:
version "4.5.1" version "4.5.15"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195" resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.15.tgz#e12797c50584cb8af8aee7eb6c7c8fd953e41f7e"
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw== integrity sha512-BTqo+pNUE8kqX8BKFTbD4fhlxcA69qfie5En4PerReLaaPfXVyRlDJ1uf85nKj2u5esUQ999iUf8YyqcPsM2Qw==
dependencies:
clsx "^1.2.1"
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
@@ -4314,10 +4309,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@^10.3.1: mermaid@^10.6.1:
version "10.3.1" version "10.6.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.3.1.tgz#2f3c7e9f6bd7a8da2bef71cce2a542c8eba2a62e" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc"
integrity sha512-hkenh7WkuRWPcob3oJtrN3W+yzrrIYuWF1OIfk/d0xGE8UWlvDhfexaHmDwwe8DKQgqMLI8DWEPwGprxkumjuw== integrity sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==
dependencies: dependencies:
"@braintree/sanitize-url" "^6.0.1" "@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3" "@types/d3-scale" "^4.0.3"
@@ -4690,10 +4685,10 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^4.0.2: nanoid@^5.0.3:
version "4.0.2" version "5.0.3"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.3.tgz#6c97f53d793a7a1de6a38ebb46f50f95bf9793c7"
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== integrity sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
@@ -5799,6 +5794,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3" has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2" 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: unicode-canonical-property-names-ecmascript@^2.0.0:
version "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" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"