Compare commits

..

137 Commits

Author SHA1 Message Date
Yifei Zhang
77f27b7948 Merge pull request #3166 from Yidadaa/bugfix-1107 2023-11-08 01:08:00 +08:00
Yifei Zhang
dbee5bc515 Update tauri.conf.json 2023-11-08 01:05:07 +08:00
Yidadaa
54a5332834 feat: sort model by name 2023-11-08 01:04:20 +08:00
Yifei Zhang
8af164c99f Merge pull request #3165 from Yidadaa/bugfix-1107 2023-11-08 00:36:09 +08:00
Yidadaa
c7321fddfb fixup 2023-11-08 00:34:31 +08:00
Yidadaa
c5ca278253 feat: close #2908 allow to disable parse settings from link 2023-11-08 00:30:02 +08:00
Yidadaa
638fdd8c3e feat: default disable balance query 2023-11-08 00:20:34 +08:00
Yifei Zhang
3407b57a51 Merge pull request #3163 from Yidadaa/bugfix-1107 2023-11-07 23:56:29 +08:00
Yidadaa
836bf836d3 fix: #3152 system prompt should be injected 2023-11-07 23:54:30 +08:00
Yidadaa
6e52d14180 fix: #3016 disable sidebar transition on ios 2023-11-07 23:30:09 +08:00
Yifei Zhang
1096fe55e1 Merge pull request #3143 from H0llyW00dzZ/newmodels 2023-11-07 10:58:40 +08:00
H0llyW00dzZ
d2d615c84a Refactor KnowledgeCutoff
[+] fix(constant.ts): update DEFAULT_SYSTEM_TEMPLATE to include knowledgeCutoff and time variables
[+] feat(chat.ts): add support for injecting system prompts based on model configuration
2023-11-07 06:42:55 +07:00
H0llyW00dzZ
9f26c8cecb Fix Typo [GPT-4]
[+] fix(constant.ts): fix typo in model name, change "ggpt-4-1106-preview" to "gpt-4-1106-preview"
2023-11-07 06:33:07 +07:00
H0llyW00dzZ
cd1f082c52 Feat Models [GPT-4] [GPT-3.5] [128K context]
[+] feat(constant.ts): add new models to DEFAULT_MODELS array
2023-11-07 06:08:06 +07:00
Yifei Zhang
b90dfb48ee Merge pull request #3095 from Yidadaa/revert-3013-main 2023-10-26 18:34:44 +08:00
Yifei Zhang
70e67a686b Revert "fix serviceWorker cache: auto caching files by workbox" 2023-10-26 00:06:09 +08:00
Yifei Zhang
d1c3d900ab Update README_KO.md 2023-10-23 14:38:26 +08:00
Yifei Zhang
e8a4ad1001 Update README_KO.md 2023-10-23 14:38:04 +08:00
Yifei Zhang
ccac85b079 Update README_CN.md 2023-10-23 14:37:48 +08:00
Yifei Zhang
f92fefb6cd Update synchronise-chat-logs-cn.md 2023-10-23 14:36:42 +08:00
Yifei Zhang
cc565cf57e Merge pull request #3013 from jessegpt/main 2023-10-16 12:02:26 +08:00
Yifei Zhang
4a56e9c884 Merge pull request #3019 from eltociear/patch-2 2023-10-16 12:02:04 +08:00
Yifei Zhang
d11d906fa2 Merge pull request #3033 from Yidadaa/Yidadaa-patch-1 2023-10-16 11:58:36 +08:00
Yifei Zhang
65c4a0c319 feat: close #3031 user can set larger font size 2023-10-16 11:52:45 +08:00
Yifei Zhang
55bcf78efe Update README.md 2023-10-15 20:08:08 +08:00
Ikko Eltociear Ashimine
f5a2ce52aa Fix typo in README.md
passsword -> password
2023-10-14 15:22:41 +09:00
Jesse
0dcbfd746e fix serviceWorker cache 2023-10-13 15:41:00 +04:00
Yifei Zhang
06e043d3f1 Merge pull request #3003 from suravshrestha/patch1 2023-10-13 00:41:16 +08:00
Surav Shrestha
986d34fb3e docs: fix typo in app/masks/en.ts 2023-10-12 21:22:35 +05:45
Yifei Zhang
5296ab1b09 Merge pull request #2996 from MCheping8108/main 2023-10-12 18:22:52 +08:00
mcheping520
b5d3348d99 已拆分单个文件、翻译多国语言版本 2023-10-12 18:17:38 +08:00
Yifei Zhang
1e77df381a Update constant.ts 2023-10-12 17:31:29 +08:00
mcheping520
ec33281ff5 提供同步聊天记录教程,同时翻译多国语言 2023-10-11 21:29:50 +08:00
Yifei Zhang
ba67f13ad5 Merge pull request #2993 from H0llyW00dzZ/masks 2023-10-11 11:42:45 +08:00
H0llyW00dzZ
1604ed91fb Merge branch 'Yidadaa:main' into masks 2023-10-11 07:35:40 +07:00
H0llyW00dzZ
74fcaab5e9 Refactor Mask UI Page [Masks]
[+] refactor(mask.tsx): refactor simple search to be case-insensitive
2023-10-11 04:36:04 +07:00
Yifei Zhang
adee435ea6 Merge pull request #2992 from ograycode/patch-1 2023-10-11 00:42:52 +08:00
Jason O'Gray
ea59ab5176 Fix type in sync.ts
Simple typo fix.
2023-10-10 11:24:45 -04:00
Yifei Zhang
f78008c1b2 Merge pull request #2983 from Yidadaa/Yidadaa-patch-1 2023-10-10 11:56:24 +08:00
Yifei Zhang
f54db695af fix: #2981 full screen button not works 2023-10-10 11:54:25 +08:00
Yifei Zhang
f21f922160 Update tauri.conf.json 2023-10-09 22:46:36 +08:00
Yifei Zhang
fd413c7b52 Merge pull request #2947 from KeithHello/main 2023-10-09 22:45:56 +08:00
Yifei Zhang
3e2c5af4b5 Merge pull request #2975 from luckykong/patch-1 2023-10-09 22:38:45 +08:00
Kong Gaowen
bdb49b1171 修改界面
似乎这里引入了一个逻辑判断错误,会导致打包之后的界面变宽变高。
2023-10-09 18:50:10 +08:00
GH Action - Upstream Sync
5933b3d7eb Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-10-08 00:19:29 +00:00
Yifei Zhang
4c8d606fae Merge pull request #2950 from PeterDaveHello/locale-tw 2023-10-08 00:20:39 +08:00
Yifei Zhang
13c1d2fd2b Merge pull request #2946 from H0llyW00dzZ/ClientApp 2023-10-07 23:50:49 +08:00
Yifei Zhang
e35c807216 Update tauri.conf.json 2023-10-07 23:48:50 +08:00
Yifei Zhang
5a2cc6f154 Merge pull request #2927 from lifeeric/MACOS 2023-10-07 23:10:43 +08:00
Peter Dave Hello
88f8c43472 Improve tw locale 2023-10-07 23:06:20 +08:00
Yifei Zhang
ef3e8e6fac Merge pull request #2933 from H0llyW00dzZ/authpage 2023-10-07 23:03:56 +08:00
KeithHello
1505372e20 Change log
-   config.ts
    -   line 72: remove type confirmation of x as x always has type 'number'
    -   line 135: remove the redundant local variable
-   chat.ts
    -   delete the unused import
2023-10-04 18:08:29 +09:00
KeithHello
ad5093ce05 Change log
-   config.ts
    -   line 72 remove type confirmation of x as x always has type 'number'
    -   line 135 remove redundant local variable
-   chat.ts
    -   delete unused import
2023-10-04 17:40:23 +09:00
H0llyW00dzZ
b558d1afc6 Feat & Fix "Client App [Notification]"
[+] feat(update.ts): add support for localization in update notifications
[+] fix(update.ts): add missing semicolon in useUpdateStore function
2023-10-04 02:10:26 +07:00
H0llyW00dzZ
ddfd05b008 Fix & Feat Client App [Notification]
[+] fix(update.ts): remove unnecessary notification sending when permission is not granted
[+] feat(update.ts): add notification for already up to date version
2023-10-03 09:12:41 +07:00
H0llyW00dzZ
d2ad01a9ff Client App Fix Issue [Bug] 'export' button does not work #2884
[+] fix(exporter.tsx): add async keyword to download function
[+] feat(exporter.tsx): add support for saving image file using window.__TAURI__ API
[+] feat(global.d.ts): add types for window.__TAURI__ API methods
[+] feat(locales): add translations for download success and failure messages
[+] feat(sync.ts): add support for generating backup file name with date and time
[+] fix(utils.ts): add async keyword to downloadAs function and add support for saving file using window.__TAURI__ API
2023-10-03 08:49:03 +07:00
H0llyW00dzZ
64a17abfe2 Client App [Notification]
[+] feat(global.d.ts): add support for window.__TAURI__.notification methods
[+] feat(update.ts): add notification for new version availability
[+] fix(Cargo.toml): add tauri feature "notification-all" to enable notifications
[+] fix(tauri.conf.json): enable all notification features in tauri configuration
2023-10-03 08:08:11 +07:00
H0llyW00dzZ
04b638aa06 Fix & Refactor UI Page [Auth Page]
[+] fix(auth.tsx): fix conditional rendering of token input field
[+] refactor(auth.tsx): improve code readability by using conditional rendering for token input field
2023-10-01 00:23:19 +07:00
H0llyW00dzZ
31e30906d0 Refactor Locale Indonesia
[+] refactor(id.ts): remove unused import and isApp variable
[+] fix(id.ts): update Unauthorized error message
2023-09-30 22:59:24 +07:00
H0llyW00dzZ
bc00be9065 Feat & Fix UI Page [Auth Page]
[+] feat(auth.tsx): add goChat function to navigate to chat page
[+] fix(auth.tsx): change onClick event from goHome to goChat
2023-09-30 22:33:18 +07:00
H0llyW00dzZ
4a599e986f UI Page [Auth Page]
[+] feat(auth.tsx): add support for resetting access token in resetAccessCode function
[+] fix(auth.tsx): fix formatting issue in resetAccessCode function
[+] feat(locales): add support for sub tips in Auth component for multiple languages :
  - Add sub tips in Arabic locale (ar.ts)
  - Add sub tips in Bengali locale (bn.ts)
  - Add sub tips in Chinese locale (cn.ts)
  - Add sub tips in English locale (en.ts)
  - Add sub tips in Indonesian locale (id.ts)
2023-09-30 22:16:34 +07:00
EricGit
f1ca03e378 [FIXED] now it should detects all macintosh 2023-09-28 13:21:17 -04:00
Eric R
f3d5fc7a84 [FIXED] now the default key should be CMD on MacOS 2023-09-28 06:50:31 -04:00
Eric R
3bfcdf9c41 [ADDED] MacOS detect 2023-09-28 06:10:22 -04:00
Yifei Zhang
144200e315 Merge pull request #2912 from Algorithm5838/Algorithm5838-patch-1 2023-09-27 17:18:15 +08:00
Algorithm5838
398e229c77 Update chat.tsx 2023-09-26 21:49:17 +03:00
Algorithm5838
6a61fe5776 Update chat.tsx 2023-09-26 20:45:09 +03:00
Yifei Zhang
9835206452 Merge pull request #2905 from H0llyW00dzZ/ref#2895 2023-09-26 10:50:49 +08:00
H0llyW00dzZ
70b0580fb7 UI Page [sidebar]
[+] fix(sidebar.tsx): update onClick function to conditionally navigate to different paths based on config.dontShowMaskSplashScreen value

Ref : [Feature] Make the mask selection more streamlined Yidadaa#2895
2023-09-26 04:59:19 +07:00
Yifei Zhang
23eb7732d7 feat: support more http status check for webdav 2023-09-20 17:47:35 +08:00
Yifei Zhang
26e50cefea Update chat.tsx 2023-09-20 02:09:14 +08:00
Yifei Zhang
588e907181 Merge pull request #2873 from Yidadaa/bugfix-0919 2023-09-19 23:42:09 +08:00
Yidadaa
eae7d6260f fix: should not tight border in desktop app 2023-09-19 23:26:52 +08:00
Yifei Zhang
175b4e7f92 Update README_CN.md 2023-09-19 11:04:10 +08:00
Yifei Zhang
b050417ab1 Update tauri.conf.json 2023-09-19 11:03:22 +08:00
Yifei Zhang
37b49400db Update constant.ts 2023-09-19 11:03:03 +08:00
Yifei Zhang
ebcb2e7837 Merge pull request #2867 from Yidadaa/bugfix-0919 2023-09-19 03:34:45 +08:00
Yidadaa
f1e7db6a88 feat: auto fill upstash backup name 2023-09-19 03:33:17 +08:00
Yifei Zhang
2ba0929458 Merge pull request #2866 from Yidadaa/bugfix-0919 2023-09-19 03:20:37 +08:00
Yidadaa
83fed42997 feat: add upstash redis cloud sync 2023-09-19 03:18:34 +08:00
Yifei Zhang
2c4626709c Merge pull request #2865 from Yidadaa/bugfix-0919 2023-09-19 02:25:35 +08:00
Yidadaa
59fbadd9eb fixup 2023-09-19 02:21:31 +08:00
Yidadaa
adb860b464 fix: #2820 try to fix 520 error code 2023-09-19 02:12:43 +08:00
Yifei Zhang
8d8790586d Merge pull request #2864 from Yidadaa/bugfix-0919 2023-09-19 02:02:18 +08:00
Yidadaa
61ca60c550 fix: #2817 min-height for landscape orientation on mobile phone 2023-09-19 01:58:52 +08:00
Yidadaa
d713d01600 feat: close #2848 click drag icon to toggle sidebar width 2023-09-19 01:47:15 +08:00
Yifei Zhang
372ea0f845 Merge pull request #2853 from a6z6/main 2023-09-18 17:33:09 +08:00
Yifei Zhang
61888708f5 Merge pull request #2856 from koho/fix-font 2023-09-18 17:32:56 +08:00
Gerhard Tan
c900459f73 Encode google font url 2023-09-18 10:22:07 +08:00
Amor Zara
2c92f75c86 Update .env.template 2023-09-17 17:32:42 +08:00
Yifei Zhang
3e1514239c Merge pull request #2839 from a6z6/main 2023-09-15 15:17:46 +08:00
Yifei Zhang
0707a1d49a Merge pull request #2840 from Algorithm5838/Algorithm5838-patch-1 2023-09-15 15:17:35 +08:00
Amor Zara
9521f19507 Merge pull request #1 from a6z6/a6z6-patch-1
Update route.ts
2023-09-15 11:23:42 +08:00
Amor Zara
bd69116df2 Update route.ts
Correct typo error and make warning more specific.
2023-09-15 11:21:42 +08:00
Algorithm5838
6535986484 Update markdown.tsx 2023-09-15 06:06:34 +03:00
Yifei Zhang
e03db9c2d5 Merge pull request #2826 from H0llyW00dzZ/language 2023-09-14 10:49:44 +08:00
Yifei Zhang
038790370c Merge pull request #2822 from yhua1998/main 2023-09-13 22:40:03 +08:00
H0llyW00dzZ
261bf0b298 Changes "Nama" -> "User" 2023-09-13 20:49:22 +07:00
H0llyW00dzZ
5a7bdcfe59 [+] Some improvements to the Indonesian language. 2023-09-13 20:17:30 +07:00
H0llyW00dzZ
4f3261b262 [+] Some improvements to the Indonesian language. 2023-09-13 20:05:58 +07:00
yhua1998
48e6087b1b fix: The width of the sidebar changes abruptly by dragging it multiple times over and over again (bouncing) 2023-09-13 16:55:31 +08:00
yhua1998
28103c901d Refactor: sidebar drag 2023-09-13 16:24:21 +08:00
yhua1998
368701610f fix: Width changes abruptly when dragging the sidebar (jumps)
In useRef is non-responsive, we can't get the modified config.sidebarWidth dynamic modification value in handleMouseUp
2023-09-13 13:57:30 +08:00
Yifei Zhang
b589f48aa9 Update tauri.conf.json 2023-09-13 03:01:28 +08:00
Yifei Zhang
74e32efa7d Merge pull request #2819 from Yidadaa/webdav 2023-09-13 02:56:10 +08:00
Yidadaa
dc555b2206 fixup 2023-09-13 02:52:28 +08:00
Yidadaa
859cf6930f fixup 2023-09-13 02:51:57 +08:00
Yidadaa
6f83fbd212 feat: add webdav support 2023-09-13 02:51:02 +08:00
Yifei Zhang
a7be4780ba Merge pull request #2809 from kfear1337/auth-page 2023-09-12 18:14:13 +08:00
B0zal
b5e89d4440 [+] Updated Auth Page
- Made changes to the Auth Page to reset the input field for the access code when the "Later" button is clicked.
This ensures that only expected user is logged by entering access code or entering their OpenAI API Key, mitigating the risk of small bug issue
2023-09-12 07:00:23 +07:00
Yifei Zhang
89bc98d26b Merge pull request #2804 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.1 2023-09-11 20:37:16 +08:00
Yifei Zhang
31a73273ac Merge pull request #2805 from Yidadaa/dependabot/npm_and_yarn/react-router-dom-6.15.0 2023-09-11 20:37:01 +08:00
Yifei Zhang
2a820ea6e7 Merge pull request #2808 from Yidadaa/dependabot/npm_and_yarn/eslint-8.49.0 2023-09-11 20:36:47 +08:00
Yifei Zhang
6484d4f0bd Merge pull request #2807 from Yidadaa/dependabot/npm_and_yarn/typescript-5.2.2 2023-09-11 20:36:33 +08:00
Yifei Zhang
461e48be32 Merge pull request #2799 from kfear1337/CodeQL-Report 2023-09-11 20:35:04 +08:00
dependabot[bot]
ff60ffca3e chore(deps-dev): bump eslint from 8.44.0 to 8.49.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.49.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.49.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:34 +00:00
dependabot[bot]
1bbf310c46 chore(deps-dev): bump typescript from 4.9.5 to 5.2.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.2.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.2.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:25 +00:00
dependabot[bot]
8469f448b5 chore(deps): bump react-router-dom from 6.14.1 to 6.15.0
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.14.1 to 6.15.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.15.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:00 +00:00
dependabot[bot]
e36abc3ac6 chore(deps): bump emoji-picker-react from 4.4.8 to 4.5.1
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.4.8 to 4.5.1.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:31:50 +00:00
B0zal
605dd72354 [+] CodeQL Report Fix log injection vulnerability in useSyncStore
Severity : High
Sanitize the 'res' object before logging it in the 'check' method of useSyncStore to prevent log injection attacks.
The 'res' object is now sanitized by extracting only the necessary properties ('status', 'statusText', and 'headers') and logging the sanitized object instead.
This ensures that only safe and expected data is logged, mitigating the risk of log injection vulnerabilities.
2023-09-11 08:49:08 +07:00
Yifei Zhang
9770b65146 Merge pull request #2798 from Yidadaa/backup 2023-09-11 00:41:25 +08:00
Yidadaa
57158890c3 fixup 2023-09-11 00:39:56 +08:00
Yifei Zhang
6f8f490fdd Merge pull request #2797 from Yidadaa/backup 2023-09-11 00:36:24 +08:00
Yidadaa
415e9dc913 fixup: minor sync fixup 2023-09-11 00:34:51 +08:00
Yifei Zhang
1487762925 Merge pull request #2796 from Yidadaa/backup 2023-09-11 00:27:51 +08:00
Yidadaa
c73a91a0f5 fixup: fix type errors 2023-09-11 00:24:05 +08:00
Yidadaa
5dced28088 fixup: add en locales 2023-09-11 00:22:14 +08:00
Yidadaa
38f6956e71 feat: close #2754 add import/export to file 2023-09-11 00:20:23 +08:00
Yifei Zhang
93da9dd1f4 Merge pull request #2791 from shoito/patch-1 2023-09-10 21:47:17 +08:00
shoito
505c8cde81 improve japanese translations 2023-09-09 16:10:24 +09:00
Yifei Zhang
f7a6fa9873 Update README_CN.md 2023-09-07 17:43:17 +08:00
Yifei Zhang
70a2d11b8c Merge pull request #2781 from rickyrobinett/main 2023-09-07 10:40:26 +08:00
Ricky Robinett
2c077aca5a fix cloudflare deployment instructions 2023-09-06 15:36:12 -04:00
Yifei Zhang
efef6b098d Merge pull request #2764 from Yidadaa/bugfix-0905 2023-09-05 02:10:53 +08:00
Yidadaa
885f2a3226 feat: close #2752 auto re-fill unfinished input 2023-09-05 01:54:28 +08:00
Yifei Zhang
1f94ae7888 Merge pull request #2721 from Yidadaa/revert-2673-dependabot/npm_and_yarn/next-13.4.19 2023-08-28 11:10:34 +08:00
Yifei Zhang
05aa520669 Revert "chore(deps): bump next from 13.4.9 to 13.4.19" 2023-08-28 11:06:12 +08:00
67 changed files with 2441 additions and 1705 deletions

View File

@@ -15,7 +15,6 @@ BASE_URL=
# Specify OpenAI organization ID.(optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
OPENAI_ORG_ID=
# (optional)
@@ -31,4 +30,4 @@ DISABLE_GPT4=
# (optional)
# Default: Empty
# If you do not want users to query balance, set this value to 1.
HIDE_BALANCE_QUERY=
HIDE_BALANCE_QUERY=

View File

@@ -159,7 +159,7 @@ Your openai api key.
### `CODE` (optional)
Access passsword, separated by comma.
Access password, separated by comma.
### `BASE_URL` (optional)
@@ -185,11 +185,17 @@ If you do not want users to input their own API key, set this value to 1.
If you do not want users to use GPT-4, set this value to 1.
### `HIDE_BALANCE_QUERY` (optional)
### `ENABLE_BALANCE_QUERY` (optional)
> Default: Empty
If you do not want users to query balance, set this value to 1.
If you do want users to query balance, set this value to 1, or you should set it to 0.
### `DISABLE_FAST_LINK` (optional)
> Default: Empty
If you want to disable parse settings from url, set this to 1.
## Requirements
@@ -257,6 +263,10 @@ If your proxy needs password, use:
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## Synchronizing Chat Records (UpStash)
| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
## Documentation
> Please go to the [docs][./docs] directory for more documentation instructions.
@@ -309,6 +319,7 @@ If you want to add a new translation, read this [document](./docs/translation.md
[@AnsonHyq](https://github.com/AnsonHyq)
[@synwith](https://github.com/synwith)
[@piksonGit](https://github.com/piksonGit)
[@ouyangzhiping](https://github.com/ouyangzhiping)
### Contributor

View File

@@ -98,9 +98,13 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
如果你不想让用户使用 GPT-4将此环境变量设置为 1 即可。
### `HIDE_BALANCE_QUERY` (可选)
### `ENABLE_BALANCE_QUERY` (可选)
如果你不想让用户查询余额,将此环境变量设置为 1 即可。
如果你想启用余额查询功能,将此环境变量设置为 1 即可。
### `DISABLE_FAST_LINK` (可选)
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
## 开发
@@ -114,7 +118,7 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
BASE_URL=https://nb.nextweb.fun/api/proxy
```
### 本地开发
@@ -180,6 +184,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
### 相关项目
- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
## 开源协议

View File

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

View File

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

View File

@@ -1,187 +0,0 @@
<div align="center">
<img src="./docs/images/icon.svg" alt="프리뷰"/>
<h1 align="center">ChatGPT Next Web</h1>
개인 ChatGPT 웹 애플리케이션을 한 번의 클릭으로 무료로 배포하세요.
[데모 Demo](https://chat-gpt-next-web.vercel.app/) / [피드백 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discord 참여](https://discord.gg/zrhvHCr79N) / [QQ 그룹](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [개발자에게 기부](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [기부 Donate](#기부-donate-usdt)
[![Vercel로 배포하기](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
[![Gitpod에서 열기](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![메인 화면](./docs/images/cover.png)
</div>
## 사용 시작
1. [OpenAI API Key](https://platform.openai.com/account/api-keys)를 준비합니다.
2. 오른쪽 버튼을 클릭하여 배포를 시작하십시오:
[![Vercel로 배포하기](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web). Github 계정으로 바로 로그인하십시오. API Key와 [페이지 접근 비밀번호](#페이지-접근-비밀번호-설정) CODE를 환경 변수 페이지에 입력하십시오.
3. 배포가 완료되면 사용을 시작하십시오.
4. (선택 사항) [사용자 정의 도메인 바인딩](https://vercel.com/docs/concepts/projects/domains/add-a-domain) : Vercel에서 할당한 도메인 DNS가 일부 지역에서 오염되어 있습니다. 사용자 정의 도메인을 바인딩하면 직접 연결할 수 있습니다.
## 업데이트 유지
위의 단계대로 프로젝트를 배포한 경우 "업데이트가 있습니다"라는 메시지가 항상 표시될 수 있습니다. 이는 Vercel이 기본적으로 새 프로젝트를 생성하고이 프로젝트를 포크하지 않기 때문입니다. 이 문제는 업데이트를 올바르게 감지할 수 없습니다.
아래 단계를 따라 다시 배포하십시오:
- 기존 저장소를 삭제합니다.
- 페이지 오른쪽 상단의 포크 버튼을 사용하여 이 프로젝트를 포크합니다.
- Vercel에서 다시 선택하여 배포하십시오. [자세한 튜토리얼 보기](./docs/vercel-cn.md#새-프로젝트-만드는-방법).
### 자동 업데이트 활성화
> Upstream Sync 오류가 발생한 경우 수동으로 Sync Fork를 한 번 실행하십시오!
프로젝트를 포크한 후 GitHub의 제한으로 인해 포크한 프로젝트의 동작 페이지에서 워크플로우를 수동으로 활성화해야 합니다. Upstream Sync Action을 활성화하면 매시간마다 자동 업데이트가 활성화됩니다:
![자동 업데이트](./docs/images/enable-actions.jpg)
![자동 업데이트 활성화](./docs/images/enable-actions-sync.jpg)
### 수동으로 코드 업데이트
수동으로 즉시 업데이트하려면 [GitHub 문서](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)에서 포크된 프로젝트를 어떻게 원본 코드와 동기화하는지 확인하십시오.
이 프로젝트에 별표/감시를 부여하거나 작성자를 팔로우하여 새 기능 업데이트 알림을 받을 수 있습니다.
## 페이지 접근 비밀번호 설정
> 비밀번호가 설정된 후, 사용자는 설정 페이지에서 접근 코드를 수동으로 입력하여 정상적으로 채팅할 수 있습니다. 그렇지 않으면 메시지를 통해 권한이 없는 상태가 표시됩니다.
> **경고** : 비밀번호의 길이를 충분히 길게 설정하십시오. 최소 7 자리 이상이 좋습니다. 그렇지 않으면 [해킹될 수 있습니다](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518).
이 프로젝트는 제한된 권한 제어 기능을 제공합니다. Vercel 프로젝트 컨트롤 패널의 환경 변수 페이지에서 `CODE`라는 환경 변수를 추가하십시오. 값은 쉼표로 구분된 사용자 정의 비밀번호로 설정됩니다. (아래 예시의 경우 `code1` `code2` `code3` 3개의 비밀번호가 생성됩니다.)
```
code1,code2,code3
```
이 환경 변수를 추가하거나 수정한 후에는 프로젝트를 다시 배포하여 변경 사항을 적용해야 합니다.
## 환경 변수
> 이 프로젝트에서 대부분의 설정 요소들은 환경 변수를 통해 설정됩니다. [Vercel 환경변수 수정 방법.](./docs/vercel-ko.md)。
## OPENAI_API_KEY (필수 항목)
OpenAI 키로, openai 계정 페이지에서 신청한 api key입니다.
## CODE (선택 가능)
접근 비밀번호로, 선택적입니다. 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다.
**경고** : 이 항목을 입력하지 않으면, 누구나 여러분이 배포한 웹사이트를 직접 사용할 수 있게 됩니다. 이로 인해 토큰이 빠르게 소진될 수 있으므로, 이 항목을 반드시 입력하는 것이 좋습니다.
## BASE_URL (선택 가능)
> 기본값: `https://api.openai.com`
> 예시: `http://your-openai-proxy.com`
OpenAI 인터페이스 프록시 URL입니다. 만약, 수동으로 openai 인터페이스 proxy를 설정했다면, 이 항목을 입력하셔야 합니다.
**참고**: SSL 인증서 문제가 발생한 경우, BASE_URL의 프로토콜을 http로 설정하세요.
## OPENAI_ORG_ID (선택 가능)
OpenAI 내의 조직 ID를 지정합니다.
## HIDE_USER_API_KEY (선택 가능)
사용자가 API Key를 직접 입력하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요.
## DISABLE_GPT4 (선택 가능)
사용자가 GPT-4를 사용하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요.
## HIDE_BALANCE_QUERY (선택 가능)
사용자가 잔액을 조회하는 것을 원하지 않는 경우, 이 환경 변수를 1로 설정하세요.
## 개발
아래 버튼을 클릭하여 개발을 시작하세요:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
코드 작성을 전, 프로젝트 루트 디렉토리에 `.env.local` 파일을 새로 만들고 해당 파일에 환경 변수를 입력해야 합니다:
```
OPENAI_API_KEY=<여기에 여러분의 api 키를 입력하세요>
#중국 사용자들은 이 프로젝트에 포함된 프록시를 사용하여 개발할 수 있습니다. 또는 다른 프록시 주소를 자유롭게 선택할 수 있습니다.
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
```
### 로컬 환경에서의 개발
1. nodejs 18과 yarn을 설치하세요. 자세한 사항은 ChatGPT에 문의하십시오.
2. `yarn install && yarn dev` 명령을 실행하세요. ⚠️ 주의: 이 명령은 로컬 개발 전용입니다. 배포용으로 사용하지 마십시오!
3. 로컬에서 배포하고 싶다면, `yarn install && yarn build && yarn start` 명령을 사용하세요. pm2와 함께 사용하여 프로세스를 보호하고, 강제 종료되지 않도록 할 수 있습니다. 자세한 내용은 ChatGPT에 문의하세요.
## 배포
### 컨테이너 배포 (추천)
> Docker 버전은 20 이상이어야 합니다. 그렇지 않으면 이미지를 찾을 수 없다는 메시지가 표시됩니다.
> ⚠️ 주의: docker 버전은 대부분의 경우 최신 버전보다 1~2일 뒤처집니다. 따라서 배포 후 "업데이트 가능" 알림이 지속적으로 나타날 수 있으며, 이는 정상적인 현상입니다.
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=페이지 접근 비밀번호 \
yidadaa/chatgpt-next-web
```
프록시를 지정하려면 다음을 사용하세요:
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=페이지 접근 비밀번호 \
--net=host \
-e PROXY_URL=http://127.0.0.1:7890 \
yidadaa/chatgpt-next-web
```
로컬 프록시에 사용자 이름과 비밀번호가 필요한 경우, 아래와 같이 사용하세요:
```shell
-e PROXY_URL="http://127.0.0.1:7890 사용자이름 비밀번호"
```
다른 환경 변수를 지정해야 하는 경우, 위의 명령에 `-e 환경변수=환경변수값`을 추가하여 지정하세요.
### 로컬 배포
콘솔에서 아래의 명령을 실행하세요:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요.
## 감사의 말
### 기부자
> 영문 버전 참조.
### 기여자
[프로젝트 기여자 목록 보기](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
### 관련 프로젝트
- [one-api](https://github.com/songquanpeng/one-api): 통합 대형 모델 할당 관리 플랫폼, 주요 대형 언어 모델 모두 지원
## 오픈소스 라이센스
[MIT](https://opensource.org/license/mit/)

View File

@@ -4,13 +4,14 @@ import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// Danger! Do not hard code any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,
hideUserApiKey: serverConfig.hideUserApiKey,
disableGPT4: serverConfig.disableGPT4,
hideBalanceQuery: serverConfig.hideBalanceQuery,
disableFastLink: serverConfig.disableFastLink,
};
declare global {

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from "next/server";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const [protocol, ...subpath] = params.path;
const targetUrl = `${protocol}://${subpath.join("/")}`;
const method = req.headers.get("method") ?? undefined;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
return fetchResult;
}
export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "nodejs";

View File

@@ -12,9 +12,16 @@ import { getClientConfig } from "../config/client";
export function AuthPage() {
const navigate = useNavigate();
const access = useAccessStore();
const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => {
accessStore.update((access) => {
access.token = "";
access.accessCode = "";
});
}; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@@ -36,19 +43,43 @@ export function AuthPage() {
className={styles["auth-input"]}
type="password"
placeholder={Locale.Auth.Input}
value={access.accessCode}
value={accessStore.accessCode}
onChange={(e) => {
access.updateCode(e.currentTarget.value);
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
);
}}
/>
{!accessStore.hideUserApiKey ? (
<>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Token.Placeholder}
value={accessStore.token}
onChange={(e) => {
accessStore.update(
(access) => (access.token = e.currentTarget.value),
);
}}
/>
</>
) : null}
<div className={styles["auth-actions"]}>
<IconButton
text={Locale.Auth.Confirm}
type="primary"
onClick={goHome}
onClick={goChat}
/>
<IconButton
text={Locale.Auth.Later}
onClick={() => {
resetAccessCode();
goHome();
}}
/>
<IconButton text={Locale.Auth.Later} onClick={goHome} />
</div>
</div>
);

View File

@@ -73,13 +73,13 @@ import {
showPrompt,
showToast,
} from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import {
CHAT_PAGE_SIZE,
LAST_INPUT_KEY,
MAX_RENDER_MSG_COUNT,
Path,
REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT,
} from "../constant";
import { Avatar } from "./emoji";
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@@ -935,7 +935,8 @@ function _Chat() {
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
const isHitBottom = bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10);
const isHitBottom =
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
@@ -974,14 +975,17 @@ function _Chat() {
doSubmit(text);
},
code: (text) => {
if (accessStore.disableFastLink) return;
console.log("[Command] got code from url: ", text);
showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
if (res) {
accessStore.updateCode(text);
accessStore.update((access) => (access.accessCode = text));
}
});
},
settings: (text) => {
if (accessStore.disableFastLink) return;
try {
const payload = JSON.parse(text) as {
key?: string;
@@ -997,10 +1001,10 @@ function _Chat() {
).then((res) => {
if (!res) return;
if (payload.key) {
accessStore.updateToken(payload.key);
accessStore.update((access) => (access.token = payload.key!));
}
if (payload.url) {
accessStore.updateOpenAiUrl(payload.url);
accessStore.update((access) => (access.openaiUrl = payload.url!));
}
});
}
@@ -1013,6 +1017,23 @@ function _Chat() {
// edit / insert message modal
const [isEditingMessage, setIsEditingMessage] = useState(false);
// remember unfinished input
useEffect(() => {
// try to load from local storage
const key = UNFINISHED_INPUT(session.id);
const mayBeUnfinishedInput = localStorage.getItem(key);
if (mayBeUnfinishedInput && userInput.length === 0) {
setUserInput(mayBeUnfinishedInput);
localStorage.removeItem(key);
}
const dom = inputRef.current;
return () => {
localStorage.setItem(key, dom?.value ?? "");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region>
@@ -1136,7 +1157,13 @@ function _Chat() {
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<MaskAvatar mask={session.mask} />
<>
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar mask={session.mask} />
)}
</>
)}
</div>

View File

@@ -4,8 +4,8 @@ import GithubIcon from "../icons/github.svg";
import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { downloadAs } from "../utils";
import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync";
interface IErrorBoundaryState {
hasError: boolean;
@@ -26,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
clearAndSaveData() {
try {
downloadAs(
JSON.stringify(localStorage),
"chatgpt-next-web-snapshot.json",
);
useSyncStore.getState().export();
} finally {
localStorage.clear();
location.reload();

View File

@@ -433,25 +433,55 @@ export function ImagePreviewer(props: {
const isMobile = useMobileScreen();
const download = () => {
const download = async () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toPng(dom)
.then((blob) => {
if (!blob) return;
if (isMobile || getClientConfig()?.isApp) {
showImageModal(blob);
const isApp = getClientConfig()?.isApp;
try {
const blob = await toPng(dom);
if (!blob) return;
if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
defaultPath: `${props.topic}.png`,
filters: [
{
name: "PNG Files",
extensions: ["png"],
},
{
name: "All Files",
extensions: ["*"],
},
],
});
if (result !== null) {
const response = await fetch(blob);
const buffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
showToast(Locale.Download.Success);
} else {
showToast(Locale.Download.Failed);
}
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
refreshPreview();
showImageModal(blob);
}
})
.catch((e) => console.log("[Export Image] ", e));
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
refreshPreview();
}
} catch (error) {
showToast(Locale.Download.Failed);
}
};
const refreshPreview = () => {

View File

@@ -6,7 +6,7 @@
color: var(--black);
background-color: var(--white);
min-width: 600px;
min-height: 480px;
min-height: 370px;
max-width: 1200px;
display: flex;

View File

@@ -115,7 +115,10 @@ const loadAsyncGoogleFont = () => {
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
linkEl.rel = "stylesheet";
linkEl.href =
googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap";
googleFontUrl +
"/css2?family=" +
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
"&display=swap";
document.head.appendChild(linkEl);
};
@@ -125,6 +128,7 @@ function Screen() {
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
useEffect(() => {
loadAsyncGoogleFont();
@@ -134,11 +138,9 @@ function Screen() {
<div
className={
styles.container +
` ${
config.tightBorder && !isMobileScreen
? styles["tight-container"]
: styles.container
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
getLang() === "ar" ? styles["rtl-screen"] : ""
}`
}
>
{isAuth ? (

View File

@@ -151,6 +151,7 @@ export function Markdown(
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
dir="auto"
>
{props.loading ? (
<LoadingIcon />

View File

@@ -393,11 +393,13 @@ export function MaskPage() {
const [searchText, setSearchText] = useState("");
const masks = searchText.length > 0 ? searchMasks : allMasks;
// simple search, will refactor later
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) => m.name.includes(text));
const result = allMasks.filter((m) =>
m.name.toLowerCase().includes(text.toLowerCase())
);
setSearchMasks(result);
} else {
setSearchMasks(allMasks);
@@ -410,7 +412,7 @@ export function MaskPage() {
const closeMaskModal = () => setEditingMaskId(undefined);
const downloadAll = () => {
downloadAs(JSON.stringify(masks), FileName.Masks);
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
};
const importFromFile = () => {
@@ -452,11 +454,13 @@ export function MaskPage() {
icon={<DownloadIcon />}
bordered
onClick={downloadAll}
text={Locale.UI.Export}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<UploadIcon />}
text={Locale.UI.Import}
bordered
onClick={() => importFromFile()}
/>
@@ -604,7 +608,7 @@ export function MaskPage() {
<MaskConfig
mask={editingMask}
updateMask={(updater) =>
maskStore.update(editingMaskId!, updater)
maskStore.updateMask(editingMaskId!, updater)
}
readonly={editingMask.builtin}
/>

View File

@@ -10,6 +10,15 @@ import ClearIcon from "../icons/clear.svg";
import LoadingIcon from "../icons/three-dots.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
import ConfigIcon from "../icons/config.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ConnectionIcon from "../icons/connection.svg";
import CloudSuccessIcon from "../icons/cloud-success.svg";
import CloudFailIcon from "../icons/cloud-fail.svg";
import {
Input,
List,
@@ -19,6 +28,7 @@ import {
Popover,
Select,
showConfirm,
showToast,
} from "./ui-lib";
import { ModelConfigList } from "./model-config";
@@ -40,7 +50,13 @@ import Locale, {
} from "../locales";
import { copyToClipboard } from "../utils";
import Link from "next/link";
import { Path, RELEASE_URL, UPDATE_URL } from "../constant";
import {
OPENAI_BASE_URL,
Path,
RELEASE_URL,
STORAGE_KEY,
UPDATE_URL,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
@@ -49,6 +65,8 @@ import { Avatar, AvatarPicker } from "./emoji";
import { getClientConfig } from "../config/client";
import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
@@ -75,7 +93,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
readOnly={!prompt.isUser}
className={styles["edit-prompt-title"]}
onInput={(e) =>
promptStore.update(
promptStore.updatePrompt(
props.id,
(prompt) => (prompt.title = e.currentTarget.value),
)
@@ -87,7 +105,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
className={styles["edit-prompt-content"]}
rows={10}
onInput={(e) =>
promptStore.update(
promptStore.updatePrompt(
props.id,
(prompt) => (prompt.content = e.currentTarget.value),
)
@@ -127,14 +145,15 @@ function UserPromptModal(props: { onClose?: () => void }) {
actions={[
<IconButton
key="add"
onClick={() =>
promptStore.add({
onClick={() => {
const promptId = promptStore.add({
id: nanoid(),
createdAt: Date.now(),
title: "Empty Prompt",
content: "Empty Prompt Content",
})
}
});
setEditingPromptId(promptId);
}}
icon={<AddIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
@@ -241,75 +260,297 @@ function DangerItems() {
);
}
function SyncItems() {
function CheckButton() {
const syncStore = useSyncStore();
const webdav = syncStore.webDavConfig;
// not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332
return null;
const couldCheck = useMemo(() => {
return syncStore.coundSync();
}, [syncStore]);
const [checkState, setCheckState] = useState<
"none" | "checking" | "success" | "failed"
>("none");
async function check() {
setCheckState("checking");
const valid = await syncStore.check();
setCheckState(valid ? "success" : "failed");
}
if (!couldCheck) return null;
return (
<List>
<ListItem
title={"上次同步:" + new Date().toLocaleString()}
subTitle={"20 次对话100 条消息200 提示词20 面具"}
<IconButton
text={Locale.Settings.Sync.Config.Modal.Check}
bordered
onClick={check}
icon={
checkState === "none" ? (
<ConnectionIcon />
) : checkState === "checking" ? (
<LoadingIcon />
) : checkState === "success" ? (
<CloudSuccessIcon />
) : checkState === "failed" ? (
<CloudFailIcon />
) : (
<ConnectionIcon />
)
}
></IconButton>
);
}
function SyncConfigModal(props: { onClose?: () => void }) {
const syncStore = useSyncStore();
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Sync.Config.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<CheckButton key="check" />,
<IconButton
key="confirm"
onClick={props.onClose}
icon={<ConfirmIcon />}
bordered
text={Locale.UI.Confirm}
/>,
]}
>
<IconButton
icon={<ResetIcon />}
text="同步"
onClick={() => {
syncStore.check().then(console.log);
}}
/>
</ListItem>
<List>
<ListItem
title={Locale.Settings.Sync.Config.SyncType.Title}
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
>
<select
value={syncStore.provider}
onChange={(e) => {
syncStore.update(
(config) =>
(config.provider = e.target.value as ProviderType),
);
}}
>
{Object.entries(ProviderType).map(([k, v]) => (
<option value={v} key={k}>
{k}
</option>
))}
</select>
</ListItem>
<ListItem
title={"本地备份"}
subTitle={"20 次对话100 条消息200 提示词20 面具"}
></ListItem>
<ListItem
title={Locale.Settings.Sync.Config.Proxy.Title}
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
>
<input
type="checkbox"
checked={syncStore.useProxy}
onChange={(e) => {
syncStore.update(
(config) => (config.useProxy = e.currentTarget.checked),
);
}}
></input>
</ListItem>
{syncStore.useProxy ? (
<ListItem
title={Locale.Settings.Sync.Config.ProxyUrl.Title}
subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
>
<input
type="text"
value={syncStore.proxyUrl}
onChange={(e) => {
syncStore.update(
(config) => (config.proxyUrl = e.currentTarget.value),
);
}}
></input>
</ListItem>
) : null}
</List>
<ListItem
title={"Web Dav Server"}
subTitle={Locale.Settings.AccessCode.SubTitle}
>
<input
value={webdav.server}
type="text"
placeholder={"https://example.com"}
onChange={(e) => {
syncStore.update(
(config) => (config.server = e.currentTarget.value),
);
}}
/>
</ListItem>
{syncStore.provider === ProviderType.WebDAV && (
<>
<List>
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
<input
type="text"
value={syncStore.webdav.endpoint}
onChange={(e) => {
syncStore.update(
(config) =>
(config.webdav.endpoint = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title="Web Dav User Name" subTitle="user name here">
<input
value={webdav.username}
type="text"
placeholder={"username"}
onChange={(e) => {
syncStore.update(
(config) => (config.username = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
<input
type="text"
value={syncStore.webdav.username}
onChange={(e) => {
syncStore.update(
(config) =>
(config.webdav.username = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
<PasswordInput
value={syncStore.webdav.password}
onChange={(e) => {
syncStore.update(
(config) =>
(config.webdav.password = e.currentTarget.value),
);
}}
></PasswordInput>
</ListItem>
</List>
</>
)}
<ListItem title="Web Dav Password" subTitle="password here">
<input
value={webdav.password}
type="text"
placeholder={"password"}
onChange={(e) => {
syncStore.update(
(config) => (config.password = e.currentTarget.value),
);
}}
/>
</ListItem>
</List>
{syncStore.provider === ProviderType.UpStash && (
<List>
<ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
<input
type="text"
value={syncStore.upstash.endpoint}
onChange={(e) => {
syncStore.update(
(config) =>
(config.upstash.endpoint = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
<input
type="text"
value={syncStore.upstash.username}
placeholder={STORAGE_KEY}
onChange={(e) => {
syncStore.update(
(config) =>
(config.upstash.username = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
<PasswordInput
value={syncStore.upstash.apiKey}
onChange={(e) => {
syncStore.update(
(config) => (config.upstash.apiKey = e.currentTarget.value),
);
}}
></PasswordInput>
</ListItem>
</List>
)}
</Modal>
</div>
);
}
function SyncItems() {
const syncStore = useSyncStore();
const chatStore = useChatStore();
const promptStore = usePromptStore();
const maskStore = useMaskStore();
const couldSync = useMemo(() => {
return syncStore.coundSync();
}, [syncStore]);
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
const stateOverview = useMemo(() => {
const sessions = chatStore.sessions;
const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
return {
chat: sessions.length,
message: messageCount,
prompt: Object.keys(promptStore.prompts).length,
mask: Object.keys(maskStore.masks).length,
};
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
return (
<>
<List>
<ListItem
title={Locale.Settings.Sync.CloudState}
subTitle={
syncStore.lastProvider
? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
syncStore.lastProvider
}]`
: Locale.Settings.Sync.NotSyncYet
}
>
<div style={{ display: "flex" }}>
<IconButton
icon={<ConfigIcon />}
text={Locale.UI.Config}
onClick={() => {
setShowSyncConfigModal(true);
}}
/>
{couldSync && (
<IconButton
icon={<ResetIcon />}
text={Locale.UI.Sync}
onClick={async () => {
try {
await syncStore.sync();
showToast(Locale.Settings.Sync.Success);
} catch (e) {
showToast(Locale.Settings.Sync.Fail);
console.error("[Sync]", e);
}
}}
/>
)}
</div>
</ListItem>
<ListItem
title={Locale.Settings.Sync.LocalState}
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
>
<div style={{ display: "flex" }}>
<IconButton
icon={<UploadIcon />}
text={Locale.UI.Export}
onClick={() => {
syncStore.export();
}}
/>
<IconButton
icon={<DownloadIcon />}
text={Locale.UI.Import}
onClick={() => {
syncStore.import();
}}
/>
</div>
</ListItem>
</List>
{showSyncConfigModal && (
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
)}
</>
);
}
@@ -336,13 +577,19 @@ export function Settings() {
console.log("[Update] remote version ", updateStore.remoteVersion);
}
const accessStore = useAccessStore();
const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
return accessStore.hideBalanceQuery || isOpenAiUrl;
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]);
const usage = {
used: updateStore.used,
subscription: updateStore.subscription,
};
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage(force = false) {
if (accessStore.hideBalanceQuery) {
if (shouldHideBalanceQuery) {
return;
}
@@ -352,7 +599,6 @@ export function Settings() {
});
}
const accessStore = useAccessStore();
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -518,7 +764,7 @@ export function Settings() {
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
max="18"
max="40"
step="1"
onChange={(e) =>
updateConfig(
@@ -562,6 +808,8 @@ export function Settings() {
</ListItem>
</List>
<SyncItems />
<List>
<ListItem
title={Locale.Settings.Mask.Splash.Title}
@@ -640,7 +888,9 @@ export function Settings() {
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
onChange={(e) => {
accessStore.updateCode(e.currentTarget.value);
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
);
}}
/>
</ListItem>
@@ -659,7 +909,9 @@ export function Settings() {
value={accessStore.openaiUrl}
placeholder="https://api.openai.com/"
onChange={(e) =>
accessStore.updateOpenAiUrl(e.currentTarget.value)
accessStore.update(
(access) => (access.openaiUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
@@ -672,14 +924,16 @@ export function Settings() {
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
accessStore.update(
(access) => (access.token = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
) : null}
{!accessStore.hideBalanceQuery ? (
{!shouldHideBalanceQuery ? (
<ListItem
title={Locale.Settings.Usage.Title}
subTitle={
@@ -722,8 +976,6 @@ export function Settings() {
</ListItem>
</List>
<SyncItems />
<List>
<ModelConfigList
modelConfig={config.modelConfig}

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useCallback, useMemo } from "react";
import styles from "./home.module.scss";
@@ -17,6 +17,7 @@ import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH,
@@ -25,7 +26,7 @@ import {
} from "../constant";
import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils";
import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { showConfirm, showToast } from "./ui-lib";
@@ -57,31 +58,57 @@ function useDragSideBar() {
const config = useAppConfig();
const startX = useRef(0);
const startDragWidth = useRef(config.sidebarWidth ?? 300);
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 50) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current);
});
const onDragMouseDown = (e: MouseEvent) => {
startX.current = e.clientX;
window.addEventListener("mousemove", handleMouseMove.current);
window.addEventListener("mouseup", handleMouseUp.current);
const toggleSideBar = () => {
config.update((config) => {
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
}
});
};
const onDragStart = (e: MouseEvent) => {
// Remembers the initial width each time the mouse is pressed
startX.current = e.clientX;
startDragWidth.current = config.sidebarWidth;
const dragStartTime = Date.now();
const handleDragMove = (e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 20) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => {
if (nextWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = nextWidth;
}
});
};
const handleDragEnd = () => {
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
window.removeEventListener("pointermove", handleDragMove);
window.removeEventListener("pointerup", handleDragEnd);
// if user click the drag icon, should toggle the sidebar
const shouldFireClick = Date.now() - dragStartTime < 300;
if (shouldFireClick) {
toggleSideBar();
}
};
window.addEventListener("pointermove", handleDragMove);
window.addEventListener("pointerup", handleDragEnd);
};
const isMobileScreen = useMobileScreen();
const shouldNarrow =
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
@@ -89,13 +116,13 @@ function useDragSideBar() {
useEffect(() => {
const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH
: limit(config.sidebarWidth ?? 300);
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return {
onDragMouseDown,
onDragStart,
shouldNarrow,
};
}
@@ -104,9 +131,14 @@ export function SideBar(props: { className?: string }) {
const chatStore = useChatStore();
// drag side bar
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
const { onDragStart, shouldNarrow } = useDragSideBar();
const navigate = useNavigate();
const config = useAppConfig();
const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo(
() => isIOS() && isMobileScreen,
[isMobileScreen],
);
useHotKey();
@@ -115,6 +147,10 @@ export function SideBar(props: { className?: string }) {
className={`${styles.sidebar} ${props.className} ${
shouldNarrow && styles["narrow-sidebar"]
}`}
style={{
// #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
}}
>
<div className={styles["sidebar-header"]} data-tauri-drag-region>
<div className={styles["sidebar-title"]} data-tauri-drag-region>
@@ -133,7 +169,13 @@ export function SideBar(props: { className?: string }) {
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
} else {
navigate(Path.Masks, { state: { fromHome: true } });
}
}}
shadow
/>
<IconButton
@@ -198,7 +240,7 @@ export function SideBar(props: { className?: string }) {
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
onPointerDown={(e) => onDragStart(e as any)}
>
<DragIcon />
</div>

View File

@@ -12,7 +12,8 @@ declare global {
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app
HIDE_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
}
}
}
@@ -47,6 +48,7 @@ export const getServerSideConfig = () => {
isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
disableGPT4: !!process.env.DISABLE_GPT4,
hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
disableFastLink: !!process.env.DISABLE_FAST_LINK,
};
};

View File

@@ -7,7 +7,10 @@ export const RELEASE_URL = `${REPO_URL}/releases`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy";
export const DEFAULT_CORS_HOST = "https://ab.nextweb.fun";
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
export const OPENAI_BASE_URL = "https://api.openai.com";
export enum Path {
Home = "/",
@@ -18,6 +21,10 @@ export enum Path {
Auth = "/auth",
}
export enum ApiPath {
Cors = "/api/cors",
}
export enum SlotID {
AppBody = "app-body",
}
@@ -37,6 +44,7 @@ export enum StoreKey {
Sync = "sync",
}
export const DEFAULT_SIDEBAR_WIDTH = 300;
export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;
@@ -44,6 +52,9 @@ export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "nk-";
export const LAST_INPUT_KEY = "last-input";
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
export const STORAGE_KEY = "chatgpt-next-web";
export const REQUEST_TIMEOUT_MS = 60000;
@@ -59,12 +70,20 @@ export const OpenaiPath = {
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2021-09
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}`;
Current time: {{time}}
`;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
export const KnowledgeCutOffDate: Record<string, string> = {
default: "2021-09",
"gpt-3.5-turbo-1106": "2023-04",
"gpt-4-1106-preview": "2023-04",
"gpt-4-vision-preview": "2023-04",
};
export const DEFAULT_MODELS = [
{
name: "gpt-4",
@@ -90,6 +109,14 @@ export const DEFAULT_MODELS = [
name: "gpt-4-32k-0613",
available: true,
},
{
name: "gpt-4-1106-preview",
available: true,
},
{
name: "gpt-4-vision-preview",
available: true,
},
{
name: "gpt-3.5-turbo",
available: true,
@@ -102,6 +129,10 @@ export const DEFAULT_MODELS = [
name: "gpt-3.5-turbo-0613",
available: true,
},
{
name: "gpt-3.5-turbo-1106",
available: true,
},
{
name: "gpt-3.5-turbo-16k",
available: true,

12
app/global.d.ts vendored
View File

@@ -13,5 +13,17 @@ declare module "*.svg";
declare interface Window {
__TAURI__?: {
writeText(text: string): Promise<void>;
invoke(command: string, payload?: Record<string, unknown>): Promise<any>;
dialog: {
save(options?: Record<string, unknown>): Promise<string | null>;
};
fs: {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
};
notification:{
requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void;
};
};
}

1
app/icons/cloud-fail.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M4.00337,11.6633M4.00337,11.6633c-0.8391,0 -1.5514,-0.20763 -2.13691,-0.6229c-0.79984,-0.56727 -1.19975,-1.41519 -1.19975,-2.54377c0,-1.2521 0.52073,-2.20348 1.56218,-2.85415c0.68561,-0.42835 1.38711,-0.64252 2.10448,-0.64252v0.66667l-0.64163,-0.18098l0.64163,0.18098l-0.64163,-0.18098c0.65543,-2.32379 2.09264,-3.48569 4.31163,-3.48569c2.30635,0 3.74023,0.99523 4.30163,2.98569l-0.6416,0.18098l0.0729,-0.66267l-0.0729,0.66267l0.0729,-0.66267c2.3958,0.26354 3.5937,1.5411 3.5937,3.83267c0,2.21778 -1.10887,3.32667 -3.3266,3.32667c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.00853 -0.0846,-0.0212 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02427 -0.07007,-0.0519 -0.101,-0.0829c-0.031,-0.03093 -0.05863,-0.0646 -0.0829,-0.101c-0.02433,-0.0364 -0.04487,-0.07483 -0.0616,-0.1153c-0.0168,-0.0404 -0.02947,-0.08207 -0.038,-0.125c-0.00853,-0.04293 -0.0128,-0.0863 -0.0128,-0.1301c0,-0.04373 0.00427,-0.08707 0.0128,-0.13c0.00853,-0.04293 0.0212,-0.08463 0.038,-0.1251c0.01673,-0.04047 0.03727,-0.0789 0.0616,-0.1153c0.02427,-0.0364 0.0519,-0.07007 0.0829,-0.101c0.03093,-0.03093 0.0646,-0.05857 0.101,-0.0829c0.0364,-0.02433 0.07483,-0.04487 0.1153,-0.0616c0.0404,-0.01673 0.08207,-0.02937 0.125,-0.0379c0.04293,-0.00853 0.0863,-0.0128 0.1301,-0.0128c1.32887,0 1.9933,-0.66446 1.9933,-1.99337c0,-1.4951 -0.80207,-2.33088 -2.4062,-2.50733c-0.27,-0.02971 -0.495,-0.22026 0,0c-0.27,-0.02971 -0.495,-0.22026 -0.5688,-0.4817c-0.37873,-1.34287 -1.38484,-2.01431 -3.01833,-2.01431c-1.54613,0 -2.55559,0.8381 -3.02836,2.51431c-0.08103,0.28728 -0.34314,0.48569 0,0c-0.08103,0.28728 -0.34314,0.48569 -0.64164,0.48569c-0.46252,0 -0.92852,0.14666 -1.39801,0.43998c-0.62355,0.38957 -0.93532,0.96402 -0.93532,1.72336c0,0.66927 0.21258,1.15467 0.63775,1.45621c0.35449,0.25144 0.80969,0.37716 1.36558,0.37716c0.04378,0 0.08713,0.00427 0.13006,0.0128c0.04293,0.00853 0.08462,0.02117 0.12507,0.0379c0.04044,0.01673 0.07886,0.03727 0.11525,0.0616c0.0364,0.02433 0.07008,0.05197 0.10103,0.0829c0.03095,0.03093 0.05859,0.0646 0.08291,0.101c0.02432,0.0364 0.04485,0.07483 0.0616,0.1153c0.01675,0.04047 0.0294,0.08217 0.03794,0.1251c0.00854,0.04293 0.01281,0.08627 0.01281,0.13c0,0.0438 -0.00427,0.08717 -0.01281,0.1301c-0.00854,0.04293 -0.02119,0.0846 -0.03794,0.125c-0.01675,0.04047 -0.03728,0.0789 -0.0616,0.1153c-0.02432,0.0364 -0.05196,0.07007 -0.08291,0.101c-0.03095,0.031 -0.06463,0.05863 -0.10103,0.0829c-0.03639,0.02433 -0.07481,0.04487 -0.11525,0.0616c-0.04045,0.0168 -0.08214,0.02947 -0.12507,0.038c-0.04293,0.00853 -0.08628,0.0128 -0.13006,0.0128z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.42578,10.4903l2,1.66l-0.42578,0.513l-0.52012,-0.4171l2.67002,-3.32998c0.0274,-0.03415 0.05783,-0.06531 0.0913,-0.09346c0.03353,-0.02815 0.0695,-0.05277 0.1079,-0.07384c0.03833,-0.02107 0.07837,-0.0382 0.1201,-0.05138c0.04173,-0.01319 0.08437,-0.02217 0.1279,-0.02696c0.04353,-0.00479 0.0871,-0.00528 0.1307,-0.00149c0.0436,0.00379 0.0864,0.01181 0.1284,0.02404c0.04207,0.01223 0.0825,0.02844 0.1213,0.04863c0.03887,0.0202 0.07537,0.04399 0.1095,0.07137c0.0342,0.02738 0.06537,0.05783 0.0935,0.09135c0.02813,0.03352 0.05273,0.06946 0.0738,0.10783c0.02107,0.03837 0.0382,0.07843 0.0514,0.12017c0.0132,0.04174 0.0222,0.08437 0.027,0.12788c0.00473,0.04351 0.00523,0.08707 0.0015,0.13068c-0.0038,0.04361 -0.01183,0.08643 -0.0241,0.12846c-0.0122,0.04203 -0.0284,0.08247 -0.0486,0.1213c-0.0202,0.03884 -0.044,0.07534 -0.0714,0.10949l-2.66998,3.33001c-0.23308,0.2907 -0.65919,0.3339 -0.9459,0.0959l-2,-1.66c-0.03369,-0.02793 -0.06432,-0.0589 -0.0919,-0.0929c-0.02758,-0.034 -0.05158,-0.07037 -0.072,-0.1091c-0.02042,-0.03867 -0.03687,-0.079 -0.04934,-0.121c-0.01248,-0.04193 -0.02074,-0.0847 -0.02479,-0.1283c-0.00405,-0.0436 -0.0038,-0.08717 0.00073,-0.1307c0.00453,-0.04353 0.01326,-0.0862 0.0262,-0.128c0.01294,-0.0418 0.02983,-0.08197 0.05068,-0.1205c0.02085,-0.03847 0.04526,-0.07453 0.07321,-0.1082c0.02796,-0.03373 0.05893,-0.06437 0.09292,-0.0919c0.03399,-0.0276 0.07035,-0.0516 0.10907,-0.072c0.03872,-0.02047 0.07906,-0.03693 0.12102,-0.0494c0.04196,-0.01247 0.08473,-0.02073 0.12831,-0.0248c0.04359,-0.004 0.08715,-0.00373 0.13069,0.0008c0.04354,0.00453 0.08622,0.01327 0.12804,0.0262c0.04181,0.01293 0.08197,0.02983 0.12046,0.0507c0.03849,0.0208 0.07457,0.0452 0.10826,0.0732z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

1
app/icons/config.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

1
app/icons/connection.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -10,6 +10,7 @@ const ar: PartialLocaleType = {
Auth: {
Title: "تحتاج إلى رمز الوصول",
Tips: "يرجى إدخال رمز الوصول أدناه",
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
Input: "رمز الوصول",
Confirm: "تأكيد",
Later: "لاحقًا",

View File

@@ -10,6 +10,7 @@ const bn: PartialLocaleType = {
Auth: {
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
Input: "অ্যাক্সেস কোড",
Confirm: "নিশ্চিত করুন",
Later: "পরে",

View File

@@ -13,6 +13,7 @@ const cn = {
Auth: {
Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码",
SubTips: "或者输入你的 OpenAI API 密钥",
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
@@ -178,6 +179,49 @@ const cn = {
Title: "自动生成标题",
SubTitle: "根据对话内容生成合适的标题",
},
Sync: {
CloudState: "云端数据",
NotSyncYet: "还没有进行过同步",
Success: "同步成功",
Fail: "同步失败",
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
},
Proxy: {
Title: "启用代理",
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
},
ProxyUrl: {
Title: "代理地址",
SubTitle: "仅适用于本项目自带的跨域代理",
},
WebDav: {
Endpoint: "WebDAV 地址",
UserName: "用户名",
Password: "密码",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "备份名称",
Password: "UpStash Redis REST Token",
},
},
LocalState: "本地数据",
Overview: (overview: any) => {
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
},
ImportFailed: "导入失败",
},
Mask: {
Splash: {
Title: "面具启动页",
@@ -280,6 +324,10 @@ const cn = {
Success: "已写入剪切板",
Failed: "复制失败,请赋予剪切板权限",
},
Download: {
Success: "内容已下载到您的目录。",
Failed: "下载失败。",
},
Context: {
Toast: (x: any) => `包含 ${x} 条预设提示词`,
Edit: "当前对话设置",
@@ -355,6 +403,10 @@ const cn = {
Close: "关闭",
Create: "新建",
Edit: "编辑",
Export: "导出",
Import: "导入",
Sync: "同步",
Config: "配置",
},
Exporter: {
Model: "模型",

View File

@@ -15,6 +15,7 @@ const en: LocaleType = {
Auth: {
Title: "Need Access Code",
Tips: "Please enter access code below",
SubTips: "Or enter your OpenAI API Key",
Input: "access code",
Confirm: "Confirm",
Later: "Later",
@@ -180,6 +181,50 @@ const en: LocaleType = {
Title: "Auto Generate Title",
SubTitle: "Generate a suitable title based on the conversation content",
},
Sync: {
CloudState: "Last Update",
NotSyncYet: "Not sync yet",
Success: "Sync Success",
Fail: "Sync Fail",
Config: {
Modal: {
Title: "Config Sync",
Check: "Check Connection",
},
SyncType: {
Title: "Sync Type",
SubTitle: "Choose your favorite sync service",
},
Proxy: {
Title: "Enable CORS Proxy",
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
},
ProxyUrl: {
Title: "Proxy Endpoint",
SubTitle:
"Only applicable to the built-in CORS proxy for this project",
},
WebDav: {
Endpoint: "WebDAV Endpoint",
UserName: "User Name",
Password: "Password",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "Backup Name",
Password: "UpStash Redis REST Token",
},
},
LocalState: "Local Data",
Overview: (overview: any) => {
return `${overview.chat} chats${overview.message} messages${overview.prompt} prompts${overview.mask} masks`;
},
ImportFailed: "Failed to import from file",
},
Mask: {
Splash: {
Title: "Mask Splash Screen",
@@ -285,6 +330,10 @@ const en: LocaleType = {
Success: "Copied to clipboard",
Failed: "Copy failed, please grant permission to access clipboard",
},
Download: {
Success: "Content downloaded to your directory.",
Failed: "Download failed.",
},
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Current Chat Settings",
@@ -355,6 +404,10 @@ const en: LocaleType = {
Close: "Close",
Create: "Create",
Edit: "Edit",
Export: "Export",
Import: "Import",
Sync: "Sync",
Config: "Config",
},
Exporter: {
Model: "Model",

View File

@@ -4,12 +4,12 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = {
WIP: "Coming Soon...",
Error: {
Unauthorized:
"Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.",
},
Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
},
Auth: {
Title: "Diperlukan Kode Akses",
Tips: "Masukkan kode akses di bawah",
SubTips: "Atau masukkan kunci API OpenAI Anda",
Input: "Kode Akses",
Confirm: "Konfirmasi",
Later: "Nanti",
@@ -60,7 +60,9 @@ const id: PartialLocaleType = {
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter untuk membalut";
}
return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah";
return (
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
);
},
Send: "Kirim",
Config: {
@@ -117,33 +119,35 @@ const id: PartialLocaleType = {
Title: "Setel Ulang Semua Pengaturan",
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
Action: "Setel Ulang",
Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
Confirm:
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
},
Clear: {
Title: "Hapus Semua Data",
SubTitle: "Menghapus semua pesan dan pengaturan",
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
Action: "Hapus",
Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?",
Confirm:
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
},
},
Lang: {
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Semua Bahasa",
},
Avatar: "Avatar",
FontSize: {
Title: "Ukuran Font",
SubTitle: "Ubah ukuran font konten chat",
},
InjectSystemPrompts: {
Title: "Suntikkan Petunjuk Sistem",
SubTitle:
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
},
InputTemplate: {
Title: "Template Input",
SubTitle: "Pesan baru akan diisi menggunakan template ini",
},
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Semua Bahasa",
},
Avatar: "Avatar",
FontSize: {
Title: "Ukuran Font",
SubTitle: "Ubah ukuran font konten chat",
},
InjectSystemPrompts: {
Title: "Suntikkan Petunjuk Sistem",
SubTitle:
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
},
InputTemplate: {
Title: "Template Input",
SubTitle: "Pesan baru akan diisi menggunakan template ini",
},
Update: {
Version: (x: string) => `Version: ${x}`,
@@ -154,9 +158,40 @@ const id: PartialLocaleType = {
GoToUpdate: "Perbarui Sekarang",
},
AutoGenerateTitle: {
Title: "Hasilkan Judul Otomatis",
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
Title: "Hasilkan Judul Otomatis",
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
},
Sync: {
CloudState: "Pembaruan Terakhir",
NotSyncYet: "Belum disinkronkan",
Success: "Sinkronisasi Berhasil",
Fail: "Sinkronisasi Gagal",
Config: {
Modal: {
Title: "Konfigurasi Sinkronisasi",
},
SyncType: {
Title: "Tipe Sinkronisasi",
SubTitle: "Pilih layanan sinkronisasi favorit Anda",
},
Proxy: {
Title: "Aktifkan Proxy CORS",
SubTitle:
"Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber",
},
ProxyUrl: {
Title: "Lokasi Titik Akhir Proxy CORS",
SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini",
},
WebDav: {
Endpoint: "Lokasi Titik Akhir WebDAV",
UserName: "User Pengguna",
Password: "Kata Sandi",
},
},
},
SendKey: "Kirim",
Theme: "Tema",
TightBorder: "Batas Ketat",
@@ -176,76 +211,77 @@ const id: PartialLocaleType = {
},
},
Prompt: {
Disable: {
Title: "Nonaktifkan Otomatisasi",
SubTitle: "Aktifkan/Matikan otomatisasi",
},
List: "Daftar Prompt",
ListCount: (builtin: number, custom: number) =>
`${builtin} bawaan, ${custom} penggunaan khusus`,
Edit: "Edit",
Modal: {
Title: "Daftar Prompt",
Add: "Tambahkan",
Search: "Cari Prompt",
},
EditModal: {
Title: "Edit Prompt",
},
Disable: {
Title: "Nonaktifkan Otomatisasi",
SubTitle: "Aktifkan/Matikan otomatisasi",
},
HistoryCount: {
Title: "Jumlah Pesan Riwayat",
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
List: "Daftar Prompt",
ListCount: (builtin: number, custom: number) =>
`${builtin} bawaan, ${custom} penggunaan khusus`,
Edit: "Edit",
Modal: {
Title: "Daftar Prompt",
Add: "Tambahkan",
Search: "Cari Prompt",
},
CompressThreshold: {
Title: "Batas Kompresi Riwayat",
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
Token: {
Title: "Kunci API",
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
Placeholder: "Kunci API OpenAI",
EditModal: {
Title: "Edit Prompt",
},
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
},
IsChecking: "Memeriksa...",
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
HistoryCount: {
Title: "Jumlah Pesan Riwayat",
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
},
CompressThreshold: {
Title: "Batas Kompresi Riwayat",
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
Token: {
Title: "Kunci API",
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
Placeholder: "Kunci API OpenAI",
},
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
},
AccessCode: {
Title: "Kode Akses",
SubTitle: "Kontrol akses diaktifkan",
Placeholder: "Diperlukan kode akses",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
},
Model: "Model",
Temperature: {
Title: "Suhu",
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
},
TopP: {
Title: "Top P",
SubTitle: "Tidak mengubah nilai dengan suhu",
},
MaxTokens: {
Title: "Token Maksimum",
SubTitle: "Panjang maksimum token input dan output",
},
PresencePenalty: {
Title: "Penalti Kehadiran",
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
},
FrequencyPenalty: {
Title: "Penalti Frekuensi",
SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
},
IsChecking: "Memeriksa...",
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
AccessCode: {
Title: "Kode Akses",
SubTitle: "Kontrol akses diaktifkan",
Placeholder: "Diperlukan kode akses",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
},
Model: "Model",
Temperature: {
Title: "Suhu",
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
},
TopP: {
Title: "Top P",
SubTitle: "Tidak mengubah nilai dengan suhu",
},
MaxTokens: {
Title: "Token Maksimum",
SubTitle: "Panjang maksimum token input dan output",
},
PresencePenalty: {
Title: "Penalti Kehadiran",
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
},
FrequencyPenalty: {
Title: "Penalti Frekuensi",
SubTitle:
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
},
},
Store: {
DefaultTopic: "Percakapan Baru",
@@ -261,8 +297,13 @@ const id: PartialLocaleType = {
},
},
Copy: {
Success: "Berhasil disalin ke clipboard",
Failed: "Gagal menyalin, berikan izin untuk memberikan izin",
Success: "Tersalin ke clipboard",
Failed:
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
},
Download: {
Success: "Konten berhasil diunduh ke direktori Anda.",
Failed: "Unduhan gagal.",
},
Context: {
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
@@ -341,7 +382,7 @@ const id: PartialLocaleType = {
Model: "Model",
Messages: "Pesan",
Topic: "Topik",
Time: "Waktu",
Time: "Tanggal & Waktu",
},
URLCommand: {
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",

View File

@@ -19,7 +19,11 @@ const jp: PartialLocaleType = {
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
Pin: "ピン",
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastAction: "表示",
Delete: "削除",
Edit: "編集",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
@@ -33,7 +37,7 @@ const jp: PartialLocaleType = {
Send: "送信",
Config: {
Reset: "リセット",
SaveAs: "另存为面具",
SaveAs: "保存",
},
},
Export: {

View File

@@ -7,13 +7,13 @@ const tw: PartialLocaleType = {
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 對話`,
ChatItemCount: (count: number) => `${count} 對話`,
},
Chat: {
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 對話`,
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 對話`,
Actions: {
ChatList: "查看訊息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
ChatList: "檢視訊息列表",
CompressedHistory: "檢視壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
@@ -23,15 +23,15 @@ const tw: PartialLocaleType = {
Rename: "重新命名對話",
Typing: "正在輸入…",
Input: (submitKey: string) => {
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可`;
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 鍵換行";
}
return inputHints;
},
Send: "送",
Send: "送",
Config: {
Reset: "重置預設",
Reset: "重設",
SaveAs: "另存新檔",
},
},
@@ -46,7 +46,7 @@ const tw: PartialLocaleType = {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
Send: "送記憶",
Send: "送記憶",
Reset: "重設對話",
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
},
@@ -71,22 +71,22 @@ const tw: PartialLocaleType = {
},
InjectSystemPrompts: {
Title: "匯入系統提示",
SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示",
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
},
Update: {
Version: (x: string) => `前版本:${x}`,
Version: (x: string) => `前版本:${x}`,
IsLatest: "已是最新版本",
CheckUpdate: "檢查更新",
IsChecking: "正在檢查更新...",
FoundUpdate: (x: string) => `發現新版本:${x}`,
GoToUpdate: "前往更新",
},
SendKey: "送鍵",
SendKey: "送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
SendPreviewBubble: {
Title: "預覽氣泡",
SubTitle: "在預覽氣泡中預覽 Markdown 容",
SubTitle: "在預覽氣泡中預覽 Markdown 容",
},
Mask: {
Splash: {
@@ -101,7 +101,7 @@ const tw: PartialLocaleType = {
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
`內建 ${builtin} 條,用戶定義 ${custom}`,
`內建 ${builtin} 條,使用者定義 ${custom}`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
@@ -132,7 +132,7 @@ const tw: PartialLocaleType = {
},
IsChecking: "正在檢查…",
Check: "重新檢查",
NoAccess: "輸入API Key查看餘額",
NoAccess: "輸入 API Key 檢視餘額",
},
AccessCode: {
Title: "授權碼",
@@ -150,7 +150,7 @@ const tw: PartialLocaleType = {
},
PresencePenalty: {
Title: "話題新穎度 (presence_penalty)",
SubTitle: "值越大,越有可能展到新話題",
SubTitle: "值越大,越有可能展到新話題",
},
FrequencyPenalty: {
Title: "頻率懲罰度 (frequency_penalty)",
@@ -163,7 +163,7 @@ const tw: PartialLocaleType = {
Error: "出錯了,請稍後再嘗試",
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
"這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content,
Topic:
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
Summarize:
@@ -192,16 +192,16 @@ const tw: PartialLocaleType = {
Item: {
Info: (count: number) => `包含 ${count} 條預設對話`,
Chat: "對話",
View: "查看",
View: "檢視",
Edit: "編輯",
Delete: "除",
DeleteConfirm: "確認除?",
Delete: "除",
DeleteConfirm: "確認除?",
},
EditModal: {
Title: (readonly: boolean) =>
`編輯預設面具 ${readonly ? "(只" : ""}`,
`編輯預設面具 ${readonly ? "(只" : ""}`,
Download: "下載預設",
Clone: "克隆預設",
Clone: "複製預設",
},
Config: {
Avatar: "角色頭像",
@@ -215,7 +215,7 @@ const tw: PartialLocaleType = {
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
More: "搜尋更多",
NotShow: "不再呈現",
ConfirmNoShow: "確認用?用後可以時在設定中重新啟用。",
ConfirmNoShow: "確認用?用後可以時在設定中重新啟用。",
},
UI: {
Confirm: "確認",

View File

@@ -35,7 +35,7 @@ export const EN_MASKS: BuiltinMask[] = [
id: "prompt-improve-0",
role: "user",
content:
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finished and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
date: "",
},
{

View File

@@ -1,28 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
import { getHeaders } from "../client/api";
import { BOT_HELLO } from "./chat";
import { getClientConfig } from "../config/client";
export interface AccessControlStore {
accessCode: string;
token: string;
needCode: boolean;
hideUserApiKey: boolean;
hideBalanceQuery: boolean;
disableGPT4: boolean;
openaiUrl: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
updateOpenAiUrl: (_: string) => void;
enabledAccessControl: () => boolean;
isAuthorized: () => boolean;
fetch: () => void;
}
import { createPersistStore } from "../utils/store";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
@@ -30,72 +9,66 @@ const DEFAULT_OPENAI_URL =
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
const DEFAULT_ACCESS_STATE = {
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
disableFastLink: false,
openaiUrl: DEFAULT_OPENAI_URL,
openaiUrl: DEFAULT_OPENAI_URL,
};
enabledAccessControl() {
get().fetch();
export const useAccessStore = createPersistStore(
{ ...DEFAULT_ACCESS_STATE },
return get().needCode;
},
updateCode(code: string) {
set(() => ({ accessCode: code?.trim() }));
},
updateToken(token: string) {
set(() => ({ token: token?.trim() }));
},
updateOpenAiUrl(url: string) {
set(() => ({ openaiUrl: url?.trim() }));
},
isAuthorized() {
get().fetch();
(set, get) => ({
enabledAccessControl() {
this.fetch();
// has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !get().enabledAccessControl()
);
},
fetch() {
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
fetchState = 1;
fetch("/api/config", {
method: "post",
body: null,
headers: {
...getHeaders(),
},
})
.then((res) => res.json())
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
if (res.disableGPT4) {
DEFAULT_MODELS.forEach(
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
);
}
})
.catch(() => {
console.error("[Config] failed to fetch config");
})
.finally(() => {
fetchState = 2;
});
},
}),
{
name: StoreKey.Access,
version: 1,
return get().needCode;
},
),
isAuthorized() {
this.fetch();
// has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !this.enabledAccessControl()
);
},
fetch() {
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
fetchState = 1;
fetch("/api/config", {
method: "post",
body: null,
headers: {
...getHeaders(),
},
})
.then((res) => res.json())
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
if (res.disableGPT4) {
DEFAULT_MODELS.forEach(
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
);
}
})
.catch(() => {
console.error("[Config] failed to fetch config");
})
.finally(() => {
fetchState = 2;
});
},
}),
{
name: StoreKey.Access,
version: 1,
},
);

View File

@@ -1,6 +1,3 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { trimTopic } from "../utils";
import Locale, { getLang } from "../locales";
@@ -10,6 +7,7 @@ import { createEmptyMask, Mask } from "./mask";
import {
DEFAULT_INPUT_TEMPLATE,
DEFAULT_SYSTEM_TEMPLATE,
KnowledgeCutOffDate,
StoreKey,
SUMMARIZE_MODEL,
} from "../constant";
@@ -18,6 +16,7 @@ import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format";
import { estimateTokenLength } from "../utils/token";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export type ChatMessage = RequestMessage & {
date: string;
@@ -118,7 +117,11 @@ function countMessages(msgs: ChatMessage[]) {
}
function fillTemplateWith(input: string, modelConfig: ModelConfig) {
let cutoff =
KnowledgeCutOffDate[modelConfig.model] ?? KnowledgeCutOffDate.default;
const vars = {
cutoff,
model: modelConfig.model,
time: new Date().toLocaleString(),
lang: getLang(),
@@ -140,12 +143,22 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output;
}
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()],
currentSessionIndex: 0,
};
export const useChatStore = createPersistStore(
DEFAULT_CHAT_STATE,
(set, _get) => {
function get() {
return {
..._get(),
...methods,
};
}
const methods = {
clearSessions() {
set(() => ({
sessions: [createEmptySession()],
@@ -184,7 +197,7 @@ export const useChatStore = create<ChatStore>()(
});
},
newSession(mask) {
newSession(mask?: Mask) {
const session = createEmptySession();
if (mask) {
@@ -207,14 +220,14 @@ export const useChatStore = create<ChatStore>()(
}));
},
nextSession(delta) {
nextSession(delta: number) {
const n = get().sessions.length;
const limit = (x: number) => (x + n) % n;
const i = get().currentSessionIndex;
get().selectSession(limit(i + delta));
},
deleteSession(index) {
deleteSession(index: number) {
const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index);
@@ -271,7 +284,7 @@ export const useChatStore = create<ChatStore>()(
return session;
},
onNewMessage(message) {
onNewMessage(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
session.lastUpdate = Date.now();
@@ -280,7 +293,7 @@ export const useChatStore = create<ChatStore>()(
get().summarizeSession();
},
async onUserInput(content) {
async onUserInput(content: string) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
@@ -580,14 +593,14 @@ export const useChatStore = create<ChatStore>()(
}
},
updateStat(message) {
updateStat(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
updateCurrentSession(updater: (session: ChatSession) => void) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
@@ -598,56 +611,60 @@ export const useChatStore = create<ChatStore>()(
localStorage.clear();
location.reload();
},
}),
{
name: StoreKey.Chat,
version: 3.1,
migrate(persistedState, version) {
const state = persistedState as any;
const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
};
if (version < 2) {
newState.sessions = [];
return methods;
},
{
name: StoreKey.Chat,
version: 3.1,
migrate(persistedState, version) {
const state = persistedState as any;
const newState = JSON.parse(
JSON.stringify(state),
) as typeof DEFAULT_CHAT_STATE;
const oldSessions = state.sessions;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
newSession.mask.modelConfig.sendMemory = true;
newSession.mask.modelConfig.historyMessageCount = 4;
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
newState.sessions.push(newSession);
if (version < 2) {
newState.sessions = [];
const oldSessions = state.sessions;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
newSession.mask.modelConfig.sendMemory = true;
newSession.mask.modelConfig.historyMessageCount = 4;
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
newState.sessions.push(newSession);
}
}
if (version < 3) {
// migrate id to nanoid
newState.sessions.forEach((s) => {
s.id = nanoid();
s.messages.forEach((m) => (m.id = nanoid()));
});
}
// Enable `enableInjectSystemPrompts` attribute for old sessions.
// Resolve issue of old sessions not automatically enabling.
if (version < 3.1) {
newState.sessions.forEach((s) => {
if (
// Exclude those already set by user
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
) {
// Because users may have changed this configuration,
// the user's current configuration is used instead of the default
const config = useAppConfig.getState();
s.mask.modelConfig.enableInjectSystemPrompts =
config.modelConfig.enableInjectSystemPrompts;
}
}
});
}
if (version < 3) {
// migrate id to nanoid
newState.sessions.forEach((s) => {
s.id = nanoid();
s.messages.forEach((m) => (m.id = nanoid()));
});
}
// Enable `enableInjectSystemPrompts` attribute for old sessions.
// Resolve issue of old sessions not automatically enabling.
if (version < 3.1) {
newState.sessions.forEach((s) => {
if (
// Exclude those already set by user
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
) {
// Because users may have changed this configuration,
// the user's current configuration is used instead of the default
const config = useAppConfig.getState();
s.mask.modelConfig.enableInjectSystemPrompts =
config.modelConfig.enableInjectSystemPrompts;
}
});
}
return newState;
},
return newState as any;
},
),
},
);

View File

@@ -1,8 +1,13 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { LLMModel } from "../client/api";
import { isMacOS } from "../utils";
import { getClientConfig } from "../config/client";
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
import {
DEFAULT_INPUT_TEMPLATE,
DEFAULT_MODELS,
DEFAULT_SIDEBAR_WIDTH,
StoreKey,
} from "../constant";
import { createPersistStore } from "../utils/store";
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
@@ -21,14 +26,16 @@ export enum Theme {
}
export const DEFAULT_CONFIG = {
submitKey: SubmitKey.CtrlEnter as SubmitKey,
lastUpdate: Date.now(), // timestamp, to merge state
submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: !!getClientConfig()?.isApp,
sendPreviewBubble: true,
enableAutoGenerateTitle: true,
sidebarWidth: 300,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
disablePromptHint: false,
@@ -55,13 +62,6 @@ export const DEFAULT_CONFIG = {
export type ChatConfig = typeof DEFAULT_CONFIG;
export type ChatConfigStore = ChatConfig & {
reset: () => void;
update: (updater: (config: ChatConfig) => void) => void;
mergeModels: (newModels: LLMModel[]) => void;
allModels: () => LLMModel[];
};
export type ModelConfig = ChatConfig["modelConfig"];
export function limitNumber(
@@ -70,7 +70,7 @@ export function limitNumber(
max: number,
defaultValue: number,
) {
if (typeof x !== "number" || isNaN(x)) {
if (isNaN(x)) {
return defaultValue;
}
@@ -98,85 +98,80 @@ export const ModalConfigValidator = {
},
};
export const useAppConfig = create<ChatConfigStore>()(
persist(
(set, get) => ({
...DEFAULT_CONFIG,
reset() {
set(() => ({ ...DEFAULT_CONFIG }));
},
update(updater) {
const config = { ...get() };
updater(config);
set(() => config);
},
mergeModels(newModels) {
if (!newModels || newModels.length === 0) {
return;
}
const oldModels = get().models;
const modelMap: Record<string, LLMModel> = {};
for (const model of oldModels) {
model.available = false;
modelMap[model.name] = model;
}
for (const model of newModels) {
model.available = true;
modelMap[model.name] = model;
}
set(() => ({
models: Object.values(modelMap),
}));
},
allModels() {
const customModels = get()
.customModels.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => ({ name: m, available: true }));
const models = get().models.concat(customModels);
return models;
},
}),
{
name: StoreKey.Config,
version: 3.7,
migrate(persistedState, version) {
const state = persistedState as ChatConfig;
if (version < 3.4) {
state.modelConfig.sendMemory = true;
state.modelConfig.historyMessageCount = 4;
state.modelConfig.compressMessageLengthThreshold = 1000;
state.modelConfig.frequency_penalty = 0;
state.modelConfig.top_p = 1;
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
state.dontShowMaskSplashScreen = false;
state.hideBuiltinMasks = false;
}
if (version < 3.5) {
state.customModels = "claude,claude-100k";
}
if (version < 3.6) {
state.modelConfig.enableInjectSystemPrompts = true;
}
if (version < 3.7) {
state.enableAutoGenerateTitle = true;
}
return state as any;
},
export const useAppConfig = createPersistStore(
{ ...DEFAULT_CONFIG },
(set, get) => ({
reset() {
set(() => ({ ...DEFAULT_CONFIG }));
},
),
mergeModels(newModels: LLMModel[]) {
if (!newModels || newModels.length === 0) {
return;
}
const oldModels = get().models;
const modelMap: Record<string, LLMModel> = {};
for (const model of oldModels) {
model.available = false;
modelMap[model.name] = model;
}
for (const model of newModels) {
model.available = true;
modelMap[model.name] = model;
}
set(() => ({
models: Object.values(modelMap),
}));
},
allModels() {
const customModels = get()
.customModels.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => ({ name: m, available: true }));
const allModels = get().models.concat(customModels);
allModels.sort((a, b) => (a.name < b.name ? -1 : 1));
return allModels;
},
}),
{
name: StoreKey.Config,
version: 3.8,
migrate(persistedState, version) {
const state = persistedState as ChatConfig;
if (version < 3.4) {
state.modelConfig.sendMemory = true;
state.modelConfig.historyMessageCount = 4;
state.modelConfig.compressMessageLengthThreshold = 1000;
state.modelConfig.frequency_penalty = 0;
state.modelConfig.top_p = 1;
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
state.dontShowMaskSplashScreen = false;
state.hideBuiltinMasks = false;
}
if (version < 3.5) {
state.customModels = "claude,claude-100k";
}
if (version < 3.6) {
state.modelConfig.enableInjectSystemPrompts = true;
}
if (version < 3.7) {
state.enableAutoGenerateTitle = true;
}
if (version < 3.8) {
state.lastUpdate = Date.now();
}
return state as any;
},
},
);

View File

@@ -1,11 +1,10 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, useAppConfig } from "./config";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export type Mask = {
id: string;
@@ -25,14 +24,6 @@ export const DEFAULT_MASK_STATE = {
};
export type MaskState = typeof DEFAULT_MASK_STATE;
type MaskStore = MaskState & {
create: (mask?: Partial<Mask>) => Mask;
update: (id: string, updater: (mask: Mask) => void) => void;
delete: (id: string) => void;
search: (text: string) => Mask[];
get: (id?: string) => Mask | null;
getAll: () => Mask[];
};
export const DEFAULT_MASK_AVATAR = "gpt-bot";
export const createEmptyMask = () =>
@@ -46,89 +37,90 @@ export const createEmptyMask = () =>
lang: getLang(),
builtin: false,
createdAt: Date.now(),
} as Mask);
}) as Mask;
export const useMaskStore = create<MaskStore>()(
persist(
(set, get) => ({
...DEFAULT_MASK_STATE,
export const useMaskStore = createPersistStore(
{ ...DEFAULT_MASK_STATE },
create(mask) {
const masks = get().masks;
const id = nanoid();
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
(set, get) => ({
create(mask?: Partial<Mask>) {
const masks = get().masks;
const id = nanoid();
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
set(() => ({ masks }));
set(() => ({ masks }));
get().markUpdate();
return masks[id];
},
update(id, updater) {
const masks = get().masks;
const mask = masks[id];
if (!mask) return;
const updateMask = { ...mask };
updater(updateMask);
masks[id] = updateMask;
set(() => ({ masks }));
},
delete(id) {
const masks = get().masks;
delete masks[id];
set(() => ({ masks }));
},
get(id) {
return get().masks[id ?? 1145141919810];
},
getAll() {
const userMasks = Object.values(get().masks).sort(
(a, b) => b.createdAt - a.createdAt,
);
const config = useAppConfig.getState();
if (config.hideBuiltinMasks) return userMasks;
const buildinMasks = BUILTIN_MASKS.map(
(m) =>
({
...m,
modelConfig: {
...config.modelConfig,
...m.modelConfig,
},
} as Mask),
);
return userMasks.concat(buildinMasks);
},
search(text) {
return Object.values(get().masks);
},
}),
{
name: StoreKey.Mask,
version: 3.1,
migrate(state, version) {
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
// migrate mask id to nanoid
if (version < 3) {
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
}
if (version < 3.1) {
const updatedMasks: Record<string, Mask> = {};
Object.values(newState.masks).forEach((m) => {
updatedMasks[m.id] = m;
});
newState.masks = updatedMasks;
}
return newState as any;
},
return masks[id];
},
),
updateMask(id: string, updater: (mask: Mask) => void) {
const masks = get().masks;
const mask = masks[id];
if (!mask) return;
const updateMask = { ...mask };
updater(updateMask);
masks[id] = updateMask;
set(() => ({ masks }));
get().markUpdate();
},
delete(id: string) {
const masks = get().masks;
delete masks[id];
set(() => ({ masks }));
get().markUpdate();
},
get(id?: string) {
return get().masks[id ?? 1145141919810];
},
getAll() {
const userMasks = Object.values(get().masks).sort(
(a, b) => b.createdAt - a.createdAt,
);
const config = useAppConfig.getState();
if (config.hideBuiltinMasks) return userMasks;
const buildinMasks = BUILTIN_MASKS.map(
(m) =>
({
...m,
modelConfig: {
...config.modelConfig,
...m.modelConfig,
},
}) as Mask,
);
return userMasks.concat(buildinMasks);
},
search(text: string) {
return Object.values(get().masks);
},
}),
{
name: StoreKey.Mask,
version: 3.1,
migrate(state, version) {
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
// migrate mask id to nanoid
if (version < 3) {
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
}
if (version < 3.1) {
const updatedMasks: Record<string, Mask> = {};
Object.values(newState.masks).forEach((m) => {
updatedMasks[m.id] = m;
});
newState.masks = updatedMasks;
}
return newState as any;
},
},
);

View File

@@ -1,9 +1,8 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
import { getLang } from "../locales";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export interface Prompt {
id: string;
@@ -13,19 +12,6 @@ export interface Prompt {
createdAt: number;
}
export interface PromptStore {
counter: number;
prompts: Record<string, Prompt>;
add: (prompt: Prompt) => string;
get: (id: string) => Prompt | undefined;
remove: (id: string) => void;
search: (text: string) => Prompt[];
update: (id: string, updater: (prompt: Prompt) => void) => void;
getUserPrompts: () => Prompt[];
}
export const SearchService = {
ready: false,
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
@@ -62,130 +48,136 @@ export const SearchService = {
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
counter: 0,
latestId: 0,
prompts: {},
export const usePromptStore = createPersistStore(
{
counter: 0,
prompts: {} as Record<string, Prompt>,
},
add(prompt) {
const prompts = get().prompts;
prompt.id = nanoid();
prompt.isUser = true;
prompt.createdAt = Date.now();
prompts[prompt.id] = prompt;
(set, get) => ({
add(prompt: Prompt) {
const prompts = get().prompts;
prompt.id = nanoid();
prompt.isUser = true;
prompt.createdAt = Date.now();
prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
prompts: prompts,
}));
set(() => ({
prompts: prompts,
}));
return prompt.id!;
},
get(id) {
const targetPrompt = get().prompts[id];
if (!targetPrompt) {
return SearchService.builtinPrompts.find((v) => v.id === id);
}
return targetPrompt;
},
remove(id) {
const prompts = get().prompts;
delete prompts[id];
SearchService.remove(id);
set(() => ({
prompts,
counter: get().counter + 1,
}));
},
getUserPrompts() {
const userPrompts = Object.values(get().prompts ?? {});
userPrompts.sort((a, b) =>
b.id && a.id ? b.createdAt - a.createdAt : 0,
);
return userPrompts;
},
update(id, updater) {
const prompt = get().prompts[id] ?? {
title: "",
content: "",
id,
};
SearchService.remove(id);
updater(prompt);
const prompts = get().prompts;
prompts[id] = prompt;
set(() => ({ prompts }));
SearchService.add(prompt);
},
search(text) {
if (text.length === 0) {
// return all rompts
return get().getUserPrompts().concat(SearchService.builtinPrompts);
}
return SearchService.search(text) as Prompt[];
},
}),
{
name: StoreKey.Prompt,
version: 3,
migrate(state, version) {
const newState = JSON.parse(JSON.stringify(state)) as PromptStore;
if (version < 3) {
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
}
return newState;
},
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>;
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
let fetchPrompts = [res.en, res.cn];
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts.map(
(promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
id: nanoid(),
title,
content,
createdAt: Date.now(),
} as Prompt),
);
},
);
const userPrompts =
usePromptStore.getState().getUserPrompts() ?? [];
const allPromptsForSearch = builtinPrompts
.reduce((pre, cur) => pre.concat(cur), [])
.filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch, userPrompts);
});
},
return prompt.id!;
},
),
get(id: string) {
const targetPrompt = get().prompts[id];
if (!targetPrompt) {
return SearchService.builtinPrompts.find((v) => v.id === id);
}
return targetPrompt;
},
remove(id: string) {
const prompts = get().prompts;
delete prompts[id];
Object.entries(prompts).some(([key, prompt]) => {
if (prompt.id === id) {
delete prompts[key];
return true;
}
return false;
});
SearchService.remove(id);
set(() => ({
prompts,
counter: get().counter + 1,
}));
},
getUserPrompts() {
const userPrompts = Object.values(get().prompts ?? {});
userPrompts.sort((a, b) =>
b.id && a.id ? b.createdAt - a.createdAt : 0,
);
return userPrompts;
},
updatePrompt(id: string, updater: (prompt: Prompt) => void) {
const prompt = get().prompts[id] ?? {
title: "",
content: "",
id,
};
SearchService.remove(id);
updater(prompt);
const prompts = get().prompts;
prompts[id] = prompt;
set(() => ({ prompts }));
SearchService.add(prompt);
},
search(text: string) {
if (text.length === 0) {
// return all rompts
return this.getUserPrompts().concat(SearchService.builtinPrompts);
}
return SearchService.search(text) as Prompt[];
},
}),
{
name: StoreKey.Prompt,
version: 3,
migrate(state, version) {
const newState = JSON.parse(JSON.stringify(state)) as {
prompts: Record<string, Prompt>;
};
if (version < 3) {
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
}
return newState as any;
},
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>;
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
let fetchPrompts = [res.en, res.cn];
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts.map((promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
id: nanoid(),
title,
content,
createdAt: Date.now(),
}) as Prompt,
);
});
const userPrompts = usePromptStore.getState().getUserPrompts() ?? [];
const allPromptsForSearch = builtinPrompts
.reduce((pre, cur) => pre.concat(cur), [])
.filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch, userPrompts);
});
},
},
);

View File

@@ -1,7 +1,19 @@
import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
import { createPersistStore } from "../utils/store";
import {
AppState,
getLocalAppState,
GetStoreState,
mergeAppState,
setLocalAppState,
} from "../utils/sync";
import { downloadAs, readFromFile } from "../utils";
import { showToast } from "../components/ui-lib";
import Locale from "../locales";
import { createSyncClient, ProviderType } from "../utils/cloud";
import { corsPath } from "../utils/cors";
export interface WebDavConfig {
server: string;
@@ -9,79 +21,111 @@ export interface WebDavConfig {
password: string;
}
export interface SyncStore {
webDavConfig: WebDavConfig;
lastSyncTime: number;
const isApp = !!getClientConfig()?.isApp;
export type SyncStore = GetStoreState<typeof useSyncStore>;
update: Updater<WebDavConfig>;
check: () => Promise<boolean>;
const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
path: (path: string) => string;
headers: () => { Authorization: string };
}
webdav: {
endpoint: "",
username: "",
password: "",
},
const FILE = {
root: "/chatgpt-next-web/",
upstash: {
endpoint: "",
username: STORAGE_KEY,
apiKey: "",
},
lastSyncTime: 0,
lastProvider: "",
};
export const useSyncStore = create<SyncStore>()(
persist(
(set, get) => ({
webDavConfig: {
server: "",
username: "",
password: "",
},
lastSyncTime: 0,
update(updater) {
const config = { ...get().webDavConfig };
updater(config);
set({ webDavConfig: config });
},
async check() {
try {
const res = await fetch(this.path(""), {
method: "PROFIND",
headers: this.headers(),
});
console.log(res);
return res.status === 207;
} catch (e) {
console.error("[Sync] ", e);
return false;
}
},
path(path: string) {
let url = get().webDavConfig.server;
if (!url.endsWith("/")) {
url += "/";
}
if (path.startsWith("/")) {
path = path.slice(1);
}
return url + path;
},
headers() {
const auth = btoa(
[get().webDavConfig.username, get().webDavConfig.password].join(":"),
);
return {
Authorization: `Basic ${auth}`,
};
},
}),
{
name: StoreKey.Sync,
version: 1,
export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(set, get) => ({
coundSync() {
const config = get()[get().provider];
return Object.values(config).every((c) => c.toString().length > 0);
},
),
markSyncTime() {
set({ lastSyncTime: Date.now(), lastProvider: get().provider });
},
export() {
const state = getLocalAppState();
const datePart = isApp
? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}`
: new Date().toLocaleString();
const fileName = `Backup-${datePart}.json`;
downloadAs(JSON.stringify(state), fileName);
},
async import() {
const rawContent = await readFromFile();
try {
const remoteState = JSON.parse(rawContent) as AppState;
const localState = getLocalAppState();
mergeAppState(localState, remoteState);
setLocalAppState(localState);
location.reload();
} catch (e) {
console.error("[Import]", e);
showToast(Locale.Settings.Sync.ImportFailed);
}
},
getClient() {
const provider = get().provider;
const client = createSyncClient(provider, get());
return client;
},
async sync() {
const localState = getLocalAppState();
const provider = get().provider;
const config = get()[provider];
const client = this.getClient();
try {
const remoteState = JSON.parse(
await client.get(config.username),
) as AppState;
mergeAppState(localState, remoteState);
setLocalAppState(localState);
} catch (e) {
console.log("[Sync] failed to get remote state", e);
}
await client.set(config.username, JSON.stringify(localState));
this.markSyncTime();
},
async check() {
const client = this.getClient();
return await client.check();
},
}),
{
name: StoreKey.Sync,
version: 1.1,
migrate(persistedState, version) {
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
if (version < 1.1) {
newState.upstash.username = STORAGE_KEY;
}
return newState as any;
},
},
);

View File

@@ -1,26 +1,12 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { api } from "../client/api";
import { getClientConfig } from "../config/client";
export interface UpdateStore {
versionType: "date" | "tag";
lastUpdate: number;
version: string;
remoteVersion: string;
used?: number;
subscription?: number;
lastUpdateUsage: number;
getLatestVersion: (force?: boolean) => Promise<void>;
updateUsage: (force?: boolean) => Promise<void>;
formatVersion: (version: string) => string;
}
import { createPersistStore } from "../utils/store";
import ChatGptIcon from "../icons/chatgpt.png";
import Locale from "../locales";
const ONE_MINUTE = 60 * 1000;
const isApp = !!getClientConfig()?.isApp;
function formatVersionDate(t: string) {
const d = new Date(+t);
@@ -35,7 +21,9 @@ function formatVersionDate(t: string) {
].join("");
}
async function getVersion(type: "date" | "tag") {
type VersionType = "date" | "tag";
async function getVersion(type: VersionType) {
if (type === "date") {
const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as {
commit: {
@@ -55,75 +43,108 @@ async function getVersion(type: "date" | "tag") {
}
}
export const useUpdateStore = create<UpdateStore>()(
persist(
(set, get) => ({
versionType: "tag",
lastUpdate: 0,
version: "unknown",
remoteVersion: "",
export const useUpdateStore = createPersistStore(
{
versionType: "tag" as VersionType,
lastUpdate: 0,
version: "unknown",
remoteVersion: "",
used: 0,
subscription: 0,
lastUpdateUsage: 0,
formatVersion(version: string) {
if (get().versionType === "date") {
version = formatVersionDate(version);
}
return version;
},
async getLatestVersion(force = false) {
const versionType = get().versionType;
let version =
versionType === "date"
? getClientConfig()?.commitDate
: getClientConfig()?.version;
set(() => ({ version }));
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
if (!force && !shouldCheck) return;
set(() => ({
lastUpdate: Date.now(),
}));
try {
const remoteId = await getVersion(versionType);
set(() => ({
remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
}
},
async updateUsage(force = false) {
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
if (!overOneMinute && !force) return;
set(() => ({
lastUpdateUsage: Date.now(),
}));
try {
const usage = await api.llm.usage();
if (usage) {
set(() => ({
used: usage.used,
subscription: usage.total,
}));
}
} catch (e) {
console.error((e as Error).message);
}
},
}),
{
name: StoreKey.Update,
version: 1,
lastUpdateUsage: 0,
},
(set, get) => ({
formatVersion(version: string) {
if (get().versionType === "date") {
version = formatVersionDate(version);
}
return version;
},
),
async getLatestVersion(force = false) {
const versionType = get().versionType;
let version =
versionType === "date"
? getClientConfig()?.commitDate
: getClientConfig()?.version;
set(() => ({ version }));
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
if (!force && !shouldCheck) return;
set(() => ({
lastUpdate: Date.now(),
}));
try {
const remoteId = await getVersion(versionType);
set(() => ({
remoteVersion: remoteId,
}));
if (window.__TAURI__?.notification && isApp) {
// Check if notification permission is granted
await window.__TAURI__?.notification.isPermissionGranted().then((granted) => {
if (!granted) {
return;
} else {
// Request permission to show notifications
window.__TAURI__?.notification.requestPermission().then((permission) => {
if (permission === 'granted') {
if (version === remoteId) {
// Show a notification using Tauri
window.__TAURI__?.notification.sendNotification({
title: "ChatGPT Next Web",
body: `${Locale.Settings.Update.IsLatest}`,
icon: `${ChatGptIcon.src}`,
sound: "Default"
});
} else {
const updateMessage = Locale.Settings.Update.FoundUpdate(`${remoteId}`);
// Show a notification for the new version using Tauri
window.__TAURI__?.notification.sendNotification({
title: "ChatGPT Next Web",
body: updateMessage,
icon: `${ChatGptIcon.src}`,
sound: "Default"
});
}
}
});
}
});
}
console.log("[Got Upstream] ", remoteId);
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
}
},
async updateUsage(force = false) {
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
if (!overOneMinute && !force) return;
set(() => ({
lastUpdateUsage: Date.now(),
}));
try {
const usage = await api.llm.usage();
if (usage) {
set(() => ({
used: usage.used,
subscription: usage.total,
}));
}
} catch (e) {
console.error((e as Error).message);
}
},
}),
{
name: StoreKey.Update,
version: 1,
},
);

View File

@@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) {
}
}
export function downloadAs(text: string, filename: string) {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
export async function downloadAs(text: string, filename: string) {
if (window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
defaultPath: `${filename}`,
filters: [
{
name: `${filename.split('.').pop()} files`,
extensions: [`${filename.split('.').pop()}`],
},
{
name: "All Files",
extensions: ["*"],
},
],
});
if (result !== null) {
try {
await window.__TAURI__.fs.writeBinaryFile(
result,
new Uint8Array([...text].map((c) => c.charCodeAt(0)))
);
showToast(Locale.Download.Success);
} catch (error) {
showToast(Locale.Download.Failed);
}
} else {
showToast(Locale.Download.Failed);
}
} else {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
element.setAttribute("download", filename);
element.style.display = "none";
@@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) {
document.body.removeChild(element);
}
}
export function readFromFile() {
return new Promise<string>((res, rej) => {
const fileInput = document.createElement("input");
@@ -173,3 +202,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
export function getCSSVar(varName: string) {
return getComputedStyle(document.body).getPropertyValue(varName).trim();
}
/**
* Detects Macintosh
*/
export function isMacOS(): boolean {
if (typeof window !== "undefined") {
let userAgent = window.navigator.userAgent.toLocaleLowerCase();
const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent)
return !!macintosh
}
return false
}

3
app/utils/clone.ts Normal file
View File

@@ -0,0 +1,3 @@
export function deepClone<T>(obj: T) {
return JSON.parse(JSON.stringify(obj));
}

33
app/utils/cloud/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import { createWebDavClient } from "./webdav";
import { createUpstashClient } from "./upstash";
export enum ProviderType {
WebDAV = "webdav",
UpStash = "upstash",
}
export const SyncClients = {
[ProviderType.UpStash]: createUpstashClient,
[ProviderType.WebDAV]: createWebDavClient,
} as const;
type SyncClientConfig = {
[K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends (
_: infer C,
) => any
? C
: never;
};
export type SyncClient = {
get: (key: string) => Promise<string>;
set: (key: string, value: string) => Promise<void>;
check: () => Promise<boolean>;
};
export function createSyncClient<T extends ProviderType>(
provider: T,
config: SyncClientConfig[T],
): SyncClient {
return SyncClients[provider](config as any) as any;
}

101
app/utils/cloud/upstash.ts Normal file
View File

@@ -0,0 +1,101 @@
import { STORAGE_KEY } from "@/app/constant";
import { SyncStore } from "@/app/store/sync";
import { corsFetch } from "../cors";
import { chunks } from "../format";
export type UpstashConfig = SyncStore["upstash"];
export type UpStashClient = ReturnType<typeof createUpstashClient>;
export function createUpstashClient(store: SyncStore) {
const config = store.upstash;
const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username;
const chunkCountKey = `${storeKey}-chunk-count`;
const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`;
const proxyUrl =
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
return {
async check() {
try {
const res = await corsFetch(this.path(`get/${storeKey}`), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[Upstash] check", res.status, res.statusText);
return [200].includes(res.status);
} catch (e) {
console.error("[Upstash] failed to check", e);
}
return false;
},
async redisGet(key: string) {
const res = await corsFetch(this.path(`get/${key}`), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[Upstash] get key = ", key, res.status, res.statusText);
const resJson = (await res.json()) as { result: string };
return resJson.result;
},
async redisSet(key: string, value: string) {
const res = await corsFetch(this.path(`set/${key}`), {
method: "POST",
headers: this.headers(),
body: value,
proxyUrl,
});
console.log("[Upstash] set key = ", key, res.status, res.statusText);
},
async get() {
const chunkCount = Number(await this.redisGet(chunkCountKey));
if (!Number.isInteger(chunkCount)) return;
const chunks = await Promise.all(
new Array(chunkCount)
.fill(0)
.map((_, i) => this.redisGet(chunkIndexKey(i))),
);
console.log("[Upstash] get full chunks", chunks);
return chunks.join("");
},
async set(_: string, value: string) {
// upstash limit the max request size which is 1Mb for “Free” and “Pay as you go”
// so we need to split the data to chunks
let index = 0;
for await (const chunk of chunks(value)) {
await this.redisSet(chunkIndexKey(index), chunk);
index += 1;
}
await this.redisSet(chunkCountKey, index.toString());
},
headers() {
return {
Authorization: `Bearer ${config.apiKey}`,
};
},
path(path: string) {
let url = config.endpoint;
if (!url.endsWith("/")) {
url += "/";
}
if (path.startsWith("/")) {
path = path.slice(1);
}
return url + path;
},
};
}

76
app/utils/cloud/webdav.ts Normal file
View File

@@ -0,0 +1,76 @@
import { STORAGE_KEY } from "@/app/constant";
import { SyncStore } from "@/app/store/sync";
import { corsFetch } from "../cors";
export type WebDAVConfig = SyncStore["webdav"];
export type WebDavClient = ReturnType<typeof createWebDavClient>;
export function createWebDavClient(store: SyncStore) {
const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`;
const config = store.webdav;
const proxyUrl =
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
return {
async check() {
try {
const res = await corsFetch(this.path(folder), {
method: "MKCOL",
headers: this.headers(),
proxyUrl,
});
console.log("[WebDav] check", res.status, res.statusText);
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
} catch (e) {
console.error("[WebDav] failed to check", e);
}
return false;
},
async get(key: string) {
const res = await corsFetch(this.path(fileName), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[WebDav] get key = ", key, res.status, res.statusText);
return await res.text();
},
async set(key: string, value: string) {
const res = await corsFetch(this.path(fileName), {
method: "PUT",
headers: this.headers(),
body: value,
proxyUrl,
});
console.log("[WebDav] set key = ", key, res.status, res.statusText);
},
headers() {
const auth = btoa(config.username + ":" + config.password);
return {
authorization: `Basic ${auth}`,
};
},
path(path: string) {
let url = config.endpoint;
if (!url.endsWith("/")) {
url += "/";
}
if (path.startsWith("/")) {
path = path.slice(1);
}
return url + path;
},
};
}

50
app/utils/cors.ts Normal file
View File

@@ -0,0 +1,50 @@
import { getClientConfig } from "../config/client";
import { ApiPath, DEFAULT_CORS_HOST } from "../constant";
export function corsPath(path: string) {
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : "";
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path += "/";
}
return `${baseUrl}${path}`;
}
export function corsFetch(
url: string,
options: RequestInit & {
proxyUrl?: string;
},
) {
if (!url.startsWith("http")) {
throw Error("[CORS Fetch] url must starts with http/https");
}
let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors);
if (!proxyUrl.endsWith("/")) {
proxyUrl += "/";
}
url = url.replace("://", "/");
const corsOptions = {
...options,
method: "POST",
headers: options.method
? {
...options.headers,
method: options.method,
}
: options.headers,
};
const corsUrl = proxyUrl + url;
console.info("[CORS] target = ", corsUrl);
return fetch(corsUrl, corsOptions);
}

View File

@@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
}
return ["```json", msg, "```"].join("\n");
}
export function* chunks(s: string, maxBytes = 1000 * 1000) {
const decoder = new TextDecoder("utf-8");
let buf = new TextEncoder().encode(s);
while (buf.length) {
let i = buf.lastIndexOf(32, maxBytes + 1);
// If no space found, try forward search
if (i < 0) i = buf.indexOf(32, maxBytes);
// If there's no space at all, take all
if (i < 0) i = buf.length;
// This is a safe cut-off point; never half-way a multi-byte
yield decoder.decode(buf.slice(0, i));
buf = buf.slice(i + 1); // Skip space (if any)
}
}

55
app/utils/store.ts Normal file
View File

@@ -0,0 +1,55 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { Updater } from "../typing";
import { deepClone } from "./clone";
type SecondParam<T> = T extends (
_f: infer _F,
_s: infer S,
...args: infer _U
) => any
? S
: never;
type MakeUpdater<T> = {
lastUpdateTime: number;
markUpdate: () => void;
update: Updater<T>;
};
type SetStoreState<T> = (
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
replace?: boolean | undefined,
) => void;
export function createPersistStore<T, M>(
defaultState: T,
methods: (
set: SetStoreState<T & MakeUpdater<T>>,
get: () => T & MakeUpdater<T>,
) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) {
return create<T & M & MakeUpdater<T>>()(
persist((set, get) => {
return {
...defaultState,
...methods(set as any, get),
lastUpdateTime: 0,
markUpdate() {
set({ lastUpdateTime: Date.now() } as Partial<
T & M & MakeUpdater<T>
>);
},
update(updater) {
const state = deepClone(get());
updater(state);
get().markUpdate();
set(state);
},
};
}, persistOptions),
);
}

165
app/utils/sync.ts Normal file
View File

@@ -0,0 +1,165 @@
import {
ChatSession,
useAccessStore,
useAppConfig,
useChatStore,
} from "../store";
import { useMaskStore } from "../store/mask";
import { usePromptStore } from "../store/prompt";
import { StoreKey } from "../constant";
import { merge } from "./merge";
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
}[keyof T];
type NonFunctionFields<T> = Pick<T, NonFunctionKeys<T>>;
export function getNonFunctionFileds<T extends object>(obj: T) {
const ret: any = {};
Object.entries(obj).map(([k, v]) => {
if (typeof v !== "function") {
ret[k] = v;
}
});
return ret as NonFunctionFields<T>;
}
export type GetStoreState<T> = T extends { getState: () => infer U }
? NonFunctionFields<U>
: never;
const LocalStateSetters = {
[StoreKey.Chat]: useChatStore.setState,
[StoreKey.Access]: useAccessStore.setState,
[StoreKey.Config]: useAppConfig.setState,
[StoreKey.Mask]: useMaskStore.setState,
[StoreKey.Prompt]: usePromptStore.setState,
} as const;
const LocalStateGetters = {
[StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()),
[StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()),
[StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()),
[StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()),
[StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()),
} as const;
export type AppState = {
[k in keyof typeof LocalStateGetters]: ReturnType<
(typeof LocalStateGetters)[k]
>;
};
type Merger<T extends keyof AppState, U = AppState[T]> = (
localState: U,
remoteState: U,
) => U;
type StateMerger = {
[K in keyof AppState]: Merger<K>;
};
// we merge remote state to local state
const MergeStates: StateMerger = {
[StoreKey.Chat]: (localState, remoteState) => {
// merge sessions
const localSessions: Record<string, ChatSession> = {};
localState.sessions.forEach((s) => (localSessions[s.id] = s));
remoteState.sessions.forEach((remoteSession) => {
// skip empty chats
if (remoteSession.messages.length === 0) return;
const localSession = localSessions[remoteSession.id];
if (!localSession) {
// if remote session is new, just merge it
localState.sessions.push(remoteSession);
} else {
// if both have the same session id, merge the messages
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
remoteSession.messages.forEach((m) => {
if (!localMessageIds.has(m.id)) {
localSession.messages.push(m);
}
});
// sort local messages with date field in asc order
localSession.messages.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
);
}
});
// sort local sessions with date field in desc order
localState.sessions.sort(
(a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
);
return localState;
},
[StoreKey.Prompt]: (localState, remoteState) => {
localState.prompts = {
...remoteState.prompts,
...localState.prompts,
};
return localState;
},
[StoreKey.Mask]: (localState, remoteState) => {
localState.masks = {
...remoteState.masks,
...localState.masks,
};
return localState;
},
[StoreKey.Config]: mergeWithUpdate<AppState[StoreKey.Config]>,
[StoreKey.Access]: mergeWithUpdate<AppState[StoreKey.Access]>,
};
export function getLocalAppState() {
const appState = Object.fromEntries(
Object.entries(LocalStateGetters).map(([key, getter]) => {
return [key, getter()];
}),
) as AppState;
return appState;
}
export function setLocalAppState(appState: AppState) {
Object.entries(LocalStateSetters).forEach(([key, setter]) => {
setter(appState[key as keyof AppState]);
});
}
export function mergeAppState(localState: AppState, remoteState: AppState) {
Object.keys(localState).forEach(<T extends keyof AppState>(k: string) => {
const key = k as T;
const localStoreState = localState[key];
const remoteStoreState = remoteState[key];
MergeStates[key](localStoreState, remoteStoreState);
});
return localState;
}
/**
* Merge state with `lastUpdateTime`, older state will be override
*/
export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
localState: T,
remoteState: T,
) {
const localUpdateTime = localState.lastUpdateTime ?? 0;
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
if (localUpdateTime < remoteUpdateTime) {
merge(remoteState, localState);
return { ...remoteState };
} else {
merge(localState, remoteState);
return { ...localState };
}
}

View File

@@ -12,7 +12,7 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
```
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
npx @cloudflare/next-on-pages --experimental-minify
```
9. For "Build output directory", use the default value and do not modify it.
10. Do not modify "Root Directory".
@@ -35,4 +35,4 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
14. Go to "Build settings", "Functions", and find "Compatibility flags".
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
16. Go to "Deployments" and click "Retry deployment".
17. Enjoy.
17. Enjoy.

BIN
docs/images/upstash-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/images/upstash-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/images/upstash-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/images/upstash-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
docs/images/upstash-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
docs/images/upstash-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/images/upstash-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,31 @@
# 同步聊天记录
## 准备工作
- GitHub账号
- 拥有自己搭建过的ChatGPT-Next-Web的服务器
- [UpStash](https://upstash.com)
## 开始教程
1. 注册UpStash账号
2. 创建数据库
![注册登录](./images/upstash-1.png)
![创建数据库](./images/upstash-2.png)
![选择服务器](./images/upstash-3.png)
3. 找到REST API分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN⚠切记⚠不要泄露Token!
![复制](./images/upstash-4.png)
4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置点击**检查可用性**
![同步1](./images/upstash-5.png)
如果没什么问题,那就成功了
![同步可用性完成的样子](./images/upstash-6.png)
5. Success!
![好耶~](./images/upstash-7.png)

View File

@@ -0,0 +1,31 @@
# Synchronize Chat Logs with UpStash
## Prerequisites
- GitHub account
- Your own ChatGPT-Next-Web server set up
- [UpStash](https://upstash.com)
## Getting Started
1. Register for an UpStash account.
2. Create a database.
![Register and Login](./images/upstash-1.png)
![Create Database](./images/upstash-2.png)
![Select Server](./images/upstash-3.png)
3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!)
![Copy](./images/upstash-4.png)
4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**.
![Synchronize 1](./images/upstash-5.png)
If everything is in order, you've successfully completed this step.
![Sync Availability Check Completed](./images/upstash-6.png)
5. Success!
![Great job~!](./images/upstash-7.png)

View File

@@ -0,0 +1,31 @@
# Sincronizzare i Log delle Chat con UpStash
## Prerequisiti
- Account GitHub
- Server ChatGPT-Next-Web di propria configurazione
- [UpStash](https://upstash.com)
## Per iniziare
1. Registrarsi per un account UpStash.
2. Creare un database.
![Registrarsi ed Accedere](./images/upstash-1.png)
![Creare un Database](./images/upstash-2.png)
![Selezionare il Server](./images/upstash-3.png)
3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!)
![Copia](./images/upstash-4.png)
4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**.
![Sincronizzazione 1](./images/upstash-5.png)
Se tutto è in ordine, hai completato con successo questa fase.
![Verifica la Disponibilità della Sincronizzazione Completata](./images/upstash-6.png)
5. Successo!
![Ottimo lavoro~!](./images/upstash-7.png)

View File

@@ -0,0 +1,31 @@
# UpStashを使用してチャットログを同期する
## 事前準備
- GitHubアカウント
- 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること
- [UpStash](https://upstash.com)
## 始める
1. UpStashアカウントを登録します。
2. データベースを作成します。
![登録とログイン](./images/upstash-1.png)
![データベースの作成](./images/upstash-2.png)
![サーバーの選択](./images/upstash-3.png)
3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします⚠重要⚠トークンを共有しないでください
![コピー](./images/upstash-4.png)
4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。
![同期1](./images/upstash-5.png)
すべてが正常であれば、このステップは成功です。
![同期可用性チェックが完了しました](./images/upstash-6.png)
5. 成功!
![お疲れ様でした~](./images/upstash-7.png)

View File

@@ -0,0 +1,31 @@
# UpStash를 사용하여 채팅 기록 동기화
## 사전 준비물
- GitHub 계정
- 자체 ChatGPT-Next-Web 서버 설정
- [UpStash](https://upstash.com)
## 시작하기
1. UpStash 계정 등록
2. 데이터베이스 생성
![등록 및 로그인](./images/upstash-1.png)
![데이터베이스 생성](./images/upstash-2.png)
![서버 선택](./images/upstash-3.png)
3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!)
![복사](./images/upstash-4.png)
4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다.
![동기화 1](./images/upstash-5.png)
모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다.
![동기화 가용성 확인 완료](./images/upstash-6.png)
5. 성공!
![잘 했어요~!](./images/upstash-7.png)

View File

@@ -35,27 +35,29 @@ const nextConfig = {
},
};
const CorsHeaders = [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{
key: "Access-Control-Allow-Methods",
value: "*",
},
{
key: "Access-Control-Allow-Headers",
value: "*",
},
{
key: "Access-Control-Max-Age",
value: "86400",
},
];
if (mode !== "export") {
nextConfig.headers = async () => {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{
key: "Access-Control-Allow-Methods",
value: "*",
},
{
key: "Access-Control-Allow-Headers",
value: "*",
},
{
key: "Access-Control-Max-Age",
value: "86400",
},
],
headers: CorsHeaders,
},
];
};
@@ -76,15 +78,6 @@ if (mode !== "export") {
},
];
const apiUrl = process.env.API_URL;
if (apiUrl) {
console.log("[Next] using api url ", apiUrl);
ret.push({
source: "/api/:path*",
destination: `${apiUrl}/:path*`,
});
}
return {
beforeFiles: ret,
};

View File

@@ -20,17 +20,17 @@
"@hello-pangea/dnd": "^16.3.0",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"emoji-picker-react": "^4.5.1",
"fuse.js": "^6.6.2",
"html-to-image": "^1.11.11",
"mermaid": "^10.3.1",
"nanoid": "^4.0.2",
"next": "^13.4.19",
"next": "^13.4.9",
"node-fetch": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.14.1",
"react-router-dom": "^6.15.0",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.3",
"remark-breaks": "^3.0.2",
@@ -49,14 +49,14 @@
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"cross-env": "^7.0.3",
"eslint": "^8.44.0",
"eslint": "^8.49.0",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.2.2",
"prettier": "^3.0.2",
"typescript": "4.9.5",
"typescript": "5.2.2",
"webpack": "^5.88.1"
},
"resolutions": {

View File

@@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri = { version = "1.3.0", features = ["notification-all", "fs-all", "clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features]

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "ChatGPT Next Web",
"version": "2.9.5"
"version": "2.9.10"
},
"tauri": {
"allowlist": {
@@ -44,6 +44,12 @@
"startDragging": true,
"unmaximize": true,
"unminimize": true
},
"fs": {
"all": true
},
"notification": {
"all": true
}
},
"bundle": {

242
yarn.lock
View File

@@ -1012,15 +1012,15 @@
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
"@eslint-community/regexpp@^4.6.1":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005"
integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==
"@eslint/eslintrc@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
"@eslint/eslintrc@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1032,10 +1032,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.44.0":
version "8.44.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
"@eslint/js@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333"
integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==
"@fortaine/fetch-event-source@^3.0.6":
version "3.0.6"
@@ -1055,10 +1055,10 @@
redux "^4.2.1"
use-memo-one "^1.1.3"
"@humanwhocodes/config-array@^0.11.10":
version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
debug "^4.1.1"
@@ -1131,10 +1131,10 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@next/env@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3"
integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==
"@next/env@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e"
integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw==
"@next/eslint-plugin-next@13.4.19":
version "13.4.19"
@@ -1143,50 +1143,50 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6"
integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==
"@next/swc-darwin-arm64@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677"
integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ==
"@next/swc-darwin-x64@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a"
integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==
"@next/swc-darwin-x64@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8"
integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A==
"@next/swc-linux-arm64-gnu@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6"
integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==
"@next/swc-linux-arm64-gnu@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1"
integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg==
"@next/swc-linux-arm64-musl@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86"
integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==
"@next/swc-linux-arm64-musl@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0"
integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw==
"@next/swc-linux-x64-gnu@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1"
integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==
"@next/swc-linux-x64-gnu@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a"
integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg==
"@next/swc-linux-x64-musl@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8"
integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==
"@next/swc-linux-x64-musl@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a"
integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A==
"@next/swc-win32-arm64-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577"
integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==
"@next/swc-win32-arm64-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a"
integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA==
"@next/swc-win32-ia32-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd"
integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==
"@next/swc-win32-ia32-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190"
integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA==
"@next/swc-win32-x64-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce"
integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==
"@next/swc-win32-x64-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b"
integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1221,10 +1221,10 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"
"@remix-run/router@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498"
integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==
"@remix-run/router@1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
"@rushstack/eslint-patch@^1.1.3":
version "1.2.0"
@@ -1779,7 +1779,7 @@ ajv-keywords@^3.5.2:
resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2762,10 +2762,10 @@ elkjs@^0.8.2:
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
emoji-picker-react@^4.4.7:
version "4.4.8"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.4.8.tgz#cd18e942720d0d01e3d488a008f5e79aa315ec87"
integrity sha512-5bbj0PCvpjB64PZj31wZ35EoebF2mKoHqEEx9u2ZLghx7sGoD1MgyDhse851rqROypjhmK9IUY15QBa7mCLP0g==
emoji-picker-react@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195"
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw==
dependencies:
clsx "^1.2.1"
@@ -3050,40 +3050,40 @@ eslint-scope@5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.44.0:
version "8.44.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
eslint@^8.49.0:
version "8.49.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42"
integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
"@eslint/eslintrc" "^2.1.0"
"@eslint/js" "8.44.0"
"@humanwhocodes/config-array" "^0.11.10"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.2"
"@eslint/js" "8.49.0"
"@humanwhocodes/config-array" "^0.11.11"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.10.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.0"
eslint-visitor-keys "^3.4.1"
espree "^9.6.0"
eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@@ -3093,7 +3093,6 @@ eslint@^8.44.0:
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
@@ -3105,13 +3104,12 @@ eslint@^8.44.0:
natural-compare "^1.4.0"
optionator "^0.9.3"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
espree@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f"
integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
dependencies:
acorn "^8.9.0"
acorn-jsx "^5.3.2"
@@ -3635,7 +3633,7 @@ immutable@^4.0.0:
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
import-fresh@^3.0.0, import-fresh@^3.2.1:
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -4707,12 +4705,12 @@ neo-async@^2.6.2:
resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next@^13.4.19:
version "13.4.19"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f"
integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==
next@^13.4.9:
version "13.4.9"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba"
integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA==
dependencies:
"@next/env" "13.4.19"
"@next/env" "13.4.9"
"@swc/helpers" "0.5.1"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
@@ -4721,15 +4719,15 @@ next@^13.4.19:
watchpack "2.4.0"
zod "3.21.4"
optionalDependencies:
"@next/swc-darwin-arm64" "13.4.19"
"@next/swc-darwin-x64" "13.4.19"
"@next/swc-linux-arm64-gnu" "13.4.19"
"@next/swc-linux-arm64-musl" "13.4.19"
"@next/swc-linux-x64-gnu" "13.4.19"
"@next/swc-linux-x64-musl" "13.4.19"
"@next/swc-win32-arm64-msvc" "13.4.19"
"@next/swc-win32-ia32-msvc" "13.4.19"
"@next/swc-win32-x64-msvc" "13.4.19"
"@next/swc-darwin-arm64" "13.4.9"
"@next/swc-darwin-x64" "13.4.9"
"@next/swc-linux-arm64-gnu" "13.4.9"
"@next/swc-linux-arm64-musl" "13.4.9"
"@next/swc-linux-x64-gnu" "13.4.9"
"@next/swc-linux-x64-musl" "13.4.9"
"@next/swc-win32-arm64-msvc" "13.4.9"
"@next/swc-win32-ia32-msvc" "13.4.9"
"@next/swc-win32-x64-msvc" "13.4.9"
node-domexception@^1.0.0:
version "1.0.0"
@@ -5092,20 +5090,20 @@ react-redux@^8.1.1:
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react-router-dom@^6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f"
integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==
react-router-dom@^6.15.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
dependencies:
"@remix-run/router" "1.7.1"
react-router "6.14.1"
"@remix-run/router" "1.8.0"
react-router "6.15.0"
react-router@6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810"
integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==
react-router@6.15.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
dependencies:
"@remix-run/router" "1.7.1"
"@remix-run/router" "1.8.0"
react@^18.2.0:
version "18.2.0"
@@ -5588,7 +5586,7 @@ strip-final-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -5786,10 +5784,10 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript@4.9.5:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
unbox-primitive@^1.0.2:
version "1.0.2"