Compare commits

...

316 Commits
v1.1 ... v1.9.4

Author SHA1 Message Date
Yifei Zhang
fec055d8a8 Merge pull request #583 from Yidadaa/Yidadaa-patch-2
fix: #537 delete chat button style
2023-04-07 11:26:35 +08:00
Yifei Zhang
6420f61566 fix: #537 delete chat button style 2023-04-07 11:23:10 +08:00
Yifei Zhang
905bf41cd8 Update utils.ts 2023-04-07 11:08:20 +08:00
Yifei Zhang
0acb877b03 Merge pull request #576 from XGZepto/main
Add FAQ-EN.
2023-04-07 10:54:13 +08:00
Zepto
52f80e0c44 Refine China Specific Issues 2023-04-07 02:59:36 +08:00
Zepto
a28bfdbaf8 Add FAQ-EN 2023-04-06 18:53:11 +00:00
Yifei Zhang
4d675c11e8 Update faq-cn.md 2023-04-07 00:58:22 +08:00
Yifei Zhang
cc56121e67 Merge pull request #574 from Yidadaa/bugfix0406
fix: toast, renaming and revert delete session
2023-04-07 00:50:43 +08:00
Yidadaa
5952064362 feat: #499 revert delete session 2023-04-07 00:14:27 +08:00
Yidadaa
806587c8ea fix: #512 Mobile renaming should not return to chat list 2023-04-06 23:47:47 +08:00
Yidadaa
f3dbe5a251 fix: #513 show toast after copying 2023-04-06 23:18:51 +08:00
Yifei Zhang
74cff2639b Merge pull request #567 from leedom92/optimize-send-button
refactor: optimize component `IconButton`
2023-04-06 23:10:09 +08:00
leedom
cd671066f7 remove unnecessary judgment 2023-04-06 22:52:18 +08:00
Leedom
fb3f5a414a Update button.module.scss 2023-04-06 21:34:00 +08:00
Yifei Zhang
9a952f0e45 Merge pull request #565 from xiaotianxt/main
fix: distinguish PC/Mobile behavior on auto-scroll
2023-04-06 21:24:25 +08:00
Yifei Zhang
cd3a11b16f Merge pull request #566 from latorc/main
docs: 更新中文FAQ faq-cn.md
2023-04-06 21:24:02 +08:00
leedom
b7cdea1b82 refactor: optimize send button 2023-04-06 21:02:48 +08:00
latorc
2092f30af5 Update faq-cn.md 2023-04-06 20:55:54 +08:00
xiaotianxt
dd20c36a55 fix: distinguish PC/Mobile behavior on auto-scroll
The chat list should be set to auto-scroll on mobile screen when the
input textarea is focused. It should not behave like that on PC screen
because user may want to refer to previous content.
2023-04-06 20:38:21 +08:00
Yifei Zhang
85bf4ac077 fix: #559 custom input ui style 2023-04-06 18:16:49 +08:00
Yifei Zhang
29bc9a45df Merge pull request #535 from zhongmeizhi/main
fix: 兼容不同浏览器的input range
2023-04-06 17:47:05 +08:00
Yifei Zhang
09fde0528a Update README.md 2023-04-06 17:38:03 +08:00
Yifei Zhang
f7624f29b1 Merge pull request #557 from Yidadaa/Yidadaa-patch-1
Update requests.ts
2023-04-06 17:35:37 +08:00
Yifei Zhang
f7e42179d0 fixup 2023-04-06 17:34:17 +08:00
Yifei Zhang
6823839f4b fixup 2023-04-06 17:28:09 +08:00
Yifei Zhang
a68721fcf2 Update requests.ts 2023-04-06 17:14:19 +08:00
Yifei Zhang
03a2a4e534 Update README.md 2023-04-06 17:02:47 +08:00
Mokou
d92108453f fix: 兼容不同浏览器的input range兼容 2023-04-06 12:39:31 +08:00
Yifei Zhang
03b3f16472 Update README_CN.md 2023-04-06 04:50:32 +08:00
Yifei Zhang
6bd2c2f121 Create CODE_OF_CONDUCT.md 2023-04-06 04:43:31 +08:00
Yifei Zhang
998c5b2969 Update issue templates 2023-04-06 04:27:38 +08:00
Yifei Zhang
f509cc73ca Update issue templates 2023-04-06 04:17:20 +08:00
Yifei Zhang
f71354faed Merge pull request #509 from xiaotianxt/feat/dnd-xiaotianxt
Drag & Drop support for ChatList
2023-04-06 04:12:00 +08:00
Yifei Zhang
796eafbf8f Update README_CN.md 2023-04-06 04:11:12 +08:00
Yifei Zhang
46daafd190 Update README_CN.md 2023-04-06 04:10:51 +08:00
Yifei Zhang
b1e26a5063 Merge pull request #510 from Yidadaa/user-prompt
feat: split docs, clear messages and fix stop response
2023-04-06 04:02:48 +08:00
Yidadaa
c77f946be1 fixup 2023-04-06 04:01:53 +08:00
Yidadaa
acfe6eec18 fix: #463 add subscrption total amount 2023-04-06 03:56:54 +08:00
Yidadaa
dce2546f5f fix: #451 override default model config 2023-04-06 03:26:42 +08:00
Yidadaa
8e560d2b2e fix: #410 can not stop response 2023-04-06 03:19:33 +08:00
xiaotianxt
f920b2001d performance: introduce lazy-loading for ChatList
Reduce the first load JS bundle size using next/dynamic.
2023-04-06 02:41:27 +08:00
Yidadaa
c2b37f811b feat: close #469 support reset session and do not send memory 2023-04-06 02:37:12 +08:00
Yidadaa
0e77177a60 fix: #439 context prompt input with textarea 2023-04-06 02:05:44 +08:00
Yifei Zhang
9aaceda40d Merge pull request #482 from zyqq/main
fix(utils): 修复复制问题
2023-04-06 01:48:20 +08:00
Yifei Zhang
eb1573de4f Update issue templates 2023-04-06 01:44:02 +08:00
xiaotianxt
301cbbfdfb feat(dnd): add drag and drop feature 2023-04-06 01:43:10 +08:00
xiaotianxt
3490c294dc chore(deps): introduce dnd 2023-04-06 01:43:10 +08:00
Yifei Zhang
dbdb9baf5f Update issue templates 2023-04-06 01:35:53 +08:00
Yidadaa
7da0cc6551 doc: update screenshots 2023-04-06 01:22:29 +08:00
Yidadaa
de78fd07c5 doc: update document 2023-04-06 00:41:44 +08:00
Yifei Zhang
2ae62d9c0e Merge pull request #497 from waltcow/main
fix 头像无法正常显示
2023-04-05 22:07:17 +08:00
Yifei Zhang
ee4564d926 Merge pull request #500 from yorunning/fix
fix: header title overflow
2023-04-05 20:02:26 +08:00
Yorun
909e2ab60f fix: header title overflow 2023-04-05 11:32:45 +00:00
Yifei Zhang
17d6f3d715 Merge pull request #496 from quark-zju/m/memo-markdown
perf: memorize markdown rendering
2023-04-05 18:00:57 +08:00
waltcow
f4fc682fa3 add getEmojiUrl as util function 2023-04-05 15:48:44 +08:00
waltcow
80904ac2bb fix import 2023-04-05 15:31:46 +08:00
waltcow
91322f33eb emoji resouce CDN format override 2023-04-05 15:29:25 +08:00
Jun Wu
962f434e17 perf: memorize markdown rendering
Markdown rendering can take time. Use `React.memo` for better performance.

The improvement is especially visible if there are complex elements. For
example, a `<Chat />` with an output of `如何推导三次方程求根方程?` (which
uses latex) now renders in about 5ms, down from ~140ms.

Related: #302
2023-04-05 00:16:10 -07:00
Yifei Zhang
4542c18385 Merge pull request #491 from quark-zju/patch-1
chore: removed redundant checkUsage
2023-04-05 15:06:49 +08:00
Jun Wu
2625c1246b chore: removed redundant checkUsage
`checkUsage()` was already called by another `useEffect`.
There is no need to call it twice.
2023-04-04 22:41:33 -07:00
yiqiuzheng
0af55366f1 fix(utils): 修复复制问题 2023-04-05 02:49:44 +08:00
Yifei Zhang
bd7567fa85 Merge pull request #434 from gaogao1030/optimize
[opt]  Optimize the display of exported chat history content.
2023-04-05 02:08:38 +08:00
Yifei Zhang
9d328e489b Merge pull request #476 from ClarenceYk/main
Update settings.tsx
2023-04-05 01:21:03 +08:00
MaYuKe
8ba1dedcea Update settings.tsx
Update parameter `step` of historyMessageCount to 1.
2023-04-04 23:32:19 +08:00
Yifei Zhang
bf8d136936 Merge pull request #456 from KakaWanYifan/linkTarget={_blank}
linkTarget={'_blank'},这个对于对话框中的链接,会在新的tab页打开,体验更好。
2023-04-04 17:07:41 +08:00
kakawanyifan
0508b0080f linkTarget={'_blank'},这个对于对话框中的链接,会在新的tab页打开,体验更好。 2023-04-04 16:33:36 +08:00
Yifei Zhang
1d2a95361b Merge pull request #447 from KakaWanYifan/main
访问改成授权
2023-04-04 15:00:43 +08:00
kakawanyifan
47f055872e 访问改成授权 2023-04-04 13:27:50 +08:00
Yifei Zhang
15429cd67f Merge pull request #435 from yorunning/fix-1
fix: add media query to theme-color, fix auto theme not updating theme-color
2023-04-04 11:16:34 +08:00
ガオガオ
be4144fab3 [opt] Optimize the display of exported chat history content. 2023-04-04 05:55:06 +08:00
Yifei Zhang
7d7f3716be Update README.md 2023-04-04 01:06:00 +08:00
yidadaa
4e644cfca7 fix: #418 valid model config 2023-04-04 01:05:33 +08:00
Yorun
30ff915e9d fix: add media query to theme-color, fix auto theme not updating theme-color 2023-04-03 16:24:59 +00:00
Yifei Zhang
7572c99f4d Merge pull request #420 from ZyXianzi/main
feat: optimize usage display
2023-04-03 22:52:08 +08:00
Feifan Zheng
bb30fdfa17 feat: optimize usage display 2023-04-03 12:18:04 +00:00
Yifei Zhang
a4743034a9 Merge pull request #415 from RugerMcCarthy/opt/back_to_autoscroll
opt: back to autoscroll when hit bottom
2023-04-03 17:57:40 +08:00
RugerMc
c2a79e8a11 opt: back to auto scroll 2023-04-03 17:51:35 +08:00
Ruger
ec88785b6d Merge branch 'Yidadaa:main' into main 2023-04-03 16:37:44 +08:00
Yifei Zhang
d923869dc3 Merge pull request #404 from cyhhao/pr2
optimize: scrolling experience
2023-04-03 15:33:01 +08:00
cyhhao
ae8ceb8dca rm input mouseover 2023-04-03 15:21:03 +08:00
cyhhao
8db26bbd5f rm msg body mouseover 2023-04-03 15:20:16 +08:00
cyhhao
efd3310711 fix conflict 2023-04-03 14:58:34 +08:00
cyhhao
a6890c0f58 optimize: scrolling experience 2023-04-03 14:56:13 +08:00
Yifei Zhang
b5a69b66b2 Merge pull request #401 from cyhhao/pr2
fix: context message should exclude all error tips
2023-04-03 14:26:32 +08:00
Yifei Zhang
54447ba677 Merge pull request #400 from yorunning/main
ci(sync): add push permission
2023-04-03 14:24:08 +08:00
GH Action - Upstream Sync
13a86aead7 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-04-03 06:23:04 +00:00
cyhhao
73f4ea38c6 session message should exclude all error tips 2023-04-03 14:13:57 +08:00
Yorun
82c025a34a ci(sync): add push permission 2023-04-03 06:03:36 +00:00
Yifei Zhang
fb127f7857 Merge pull request #377 from ilario92/main
Update: locales in Italian
2023-04-03 13:44:40 +08:00
Yifei Zhang
6f82140ba5 Update README.md 2023-04-03 13:37:55 +08:00
github-actions[bot]
6a9751efce Merge pull request #3 from Yidadaa/main
Fork Sync
2023-04-03 05:29:44 +00:00
Yidadaa
5c75b6c784 fix: #397 #373 Array.prototype.at polyfill errors 2023-04-03 13:29:37 +08:00
Ruger
34ea20b109 Merge pull request #2 from Yidadaa/main
Fork Sync
2023-04-03 13:28:42 +08:00
Yifei Zhang
6e6d2a3310 Update README.md 2023-04-03 11:43:38 +08:00
Yifei Zhang
56ba8a65e0 fix: improve scroll 2023-04-03 03:35:24 +00:00
Yifei Zhang
0e784c50ad fix: #384 improve scroll 2023-04-03 03:27:42 +00:00
Yifei Zhang
73865651a0 fix: #366 use fallback copy 2023-04-03 03:27:42 +00:00
Yifei Zhang
f863a562a0 Merge pull request #392 from yorunning/main
ci(sync): skip main repo execution
2023-04-03 10:53:01 +08:00
Yifei Zhang
0d3bd42780 Update app.ts 2023-04-03 10:50:29 +08:00
Yorun
62f8675199 ci(sync): skip main repo execution 2023-04-03 02:11:40 +00:00
Yorun
8a1c2f89be ci(sync): shorten the timer interval 2023-04-03 02:10:48 +00:00
Yifei Zhang
5843303076 Update README.md 2023-04-03 03:26:30 +08:00
Yifei Zhang
e2c1475857 revert: fix mobile scroll 2023-04-02 19:24:29 +00:00
Yifei Zhang
8d34b0f454 chore: fix mobile scroll 2023-04-02 19:20:57 +00:00
Yifei Zhang
b44caeeefb fix: #289 #367 #353 #369 provide more error message info 2023-04-02 19:14:53 +00:00
Ilario Scandurra
502d22bd20 Merge branch 'Yidadaa:main' into main 2023-04-02 21:14:04 +02:00
Yifei Zhang
8d60a414f0 chore: fix usage display 2023-04-02 18:55:08 +00:00
Yifei Zhang
e8d71c815e chore: fix preview bubble 2023-04-02 18:51:37 +00:00
Yifei Zhang
1afca0b28a fix: mobile scroll problem 2023-04-02 18:41:25 +00:00
ilarioscandurra
f9f29afba9 Update: locales in Italian 2023-04-02 20:32:43 +02:00
Yifei Zhang
f52bcc2a37 chore: update readme 2023-04-02 18:29:07 +00:00
Yifei Zhang
2647bdb4ed fixup: ux improve 2023-04-02 18:28:07 +00:00
Ilario Scandurra
c98517fc48 Merge branch 'main' into main 2023-04-02 20:13:58 +02:00
Yifei Zhang
e8dd391ccf Merge pull request #378 from Yidadaa/prompt-edit
feat: add context prompt edit
2023-04-03 02:03:54 +08:00
Yifei Zhang
e3c3cd3d18 fixup: translation context 2023-04-02 18:02:03 +00:00
Yifei Zhang
b05b96e3a4 Merge pull request #376 from cyhhao/patch-2
translate SendPreviewBubble
2023-04-03 01:50:48 +08:00
Yifei Zhang
b85245e317 feat: #138 add context prompt, close #330 #321 2023-04-02 17:48:43 +00:00
ilarioscandurra
f606a61e1c Update: locales in Italian 2023-04-02 19:23:50 +02:00
Call White
a10f4d8abc translate SendPreviewBubble 2023-04-03 01:07:06 +08:00
Yifei Zhang
c978de2c10 fix: #253 #356 auto scroll ux 2023-04-02 15:19:36 +00:00
Yifei Zhang
6c1862797b refactor: split homt.tsx components 2023-04-02 15:05:54 +00:00
Yifei Zhang
4f0108b0ea fix: #289 use highlight.js instead of prism 2023-04-02 14:48:18 +00:00
Yifei Zhang
7b5af271d5 fix: #367 failed to fetch account usage 2023-04-02 14:22:06 +00:00
Yifei Zhang
37587f6f71 fix: #244 optimize polyfill 2023-04-02 13:56:34 +00:00
Yifei Zhang
328a903c24 Merge branch 'main' into prompt-edit 2023-04-02 13:50:50 +00:00
Yifei Zhang
fdbdd33e77 Merge pull request #355 from cesaryuan/patch-1
fix: fix history message count
2023-04-02 21:42:46 +08:00
Cesaryuan
a356ee857c Merge branch 'main' into patch-1 2023-04-02 21:39:03 +08:00
Yifei Zhang
cf6f09b7b8 Merge pull request #360 from MapleGu/main
🐞 fix(locales): Fix the missing SendPreviewBubble in ES configuration
2023-04-02 21:22:02 +08:00
MapleUncle
a90e646381 🐞 fix(locales): Fix the missing SendPreviewBubble in ES configuration 2023-04-02 20:38:14 +08:00
Yifei Zhang
16028795f9 fix: #203 pwa installation problem 2023-04-02 12:28:18 +00:00
Cesaryuan
12f342f015 fix: historyMessageCount 2023-04-02 20:23:56 +08:00
Yifei Zhang
e248e9196a Merge pull request #271 from RugerMcCarthy/feat/send_preview_option
feat: add switch of send preview bubble
2023-04-02 20:08:35 +08:00
Cesaryuan
fea4f561b4 fix: fix history message count
Bug: The length of `new Array(20).slice(20 - 24) ` is 4 which should be 24.
2023-04-02 19:43:11 +08:00
Yifei Zhang
d226090926 Merge pull request #346 from AprilNEA/reset
The Clear Data button on the Settings page is only clear for all dialog data.
2023-04-02 18:36:51 +08:00
Yifei Zhang
2d534bfdf4 Merge pull request #347 from SadPencil/patch-1
Fix typos
2023-04-02 18:34:16 +08:00
Sad Pencil
ed5cd11d6a Fix typos 2023-04-02 14:23:12 +08:00
AprilNEA
0a60a87c9f Merge branch 'main' into reset
# Conflicts:
#	app/components/settings.tsx
2023-04-02 13:45:34 +08:00
AprilNEA
506cdbc83c feat: clear session only 2023-04-02 13:42:47 +08:00
Yifei Zhang
a64c4384b1 Merge pull request #322 from quark-zju/wexin-compat
fix: 微信 Android 内置浏览器兼容性
2023-04-02 13:17:56 +08:00
Yifei Zhang
d54c983187 Merge pull request #333 from qirong77/main
fix:修复中文输入法下enter错误发送消息问题
2023-04-02 13:12:42 +08:00
Jun Wu
cd5f8f7407 app: polyfill Array.at
This fixes compatibility issue with older browsers like WeChat webview.
The summary feature now works as expected.
2023-04-01 11:38:52 -07:00
linqirong
00a282214e fix:修复中文输入法下enter错误发送消息问题 2023-04-02 00:12:31 +08:00
Yifei Zhang
44145f11db Merge pull request #324 from yorunning/main
ci: update sync action
2023-04-01 23:25:59 +08:00
Yorun
ad63b10aea ci: update sync action 2023-04-01 11:52:10 +00:00
Jun Wu
327ac765df utils: simplify trimTopic
Also avoid using Array.prototype.at, which does not seem to exist
in the Wexin builtin webview (Android Wexin 8.0.30).
2023-04-01 03:37:28 -07:00
Jun Wu
83cea2adb8 api: set Content-Type to json
This avoids issues in browsers like WeChat where the encoding is
incorrect and the summary feature does not work if it contains
zh-CN characters.
2023-04-01 03:37:09 -07:00
Yifei Zhang
0385f6ede9 fix: #305 disable double click to copy on pc 2023-04-01 15:46:34 +08:00
Yifei Zhang
45bf2c3d25 fix: remove scroll anchor height 2023-04-01 15:39:30 +08:00
Yifei Zhang
e6b64b0f2c Merge pull request #303 from leedom92/add-confirm-when-remove-chatitem
feat: add confirm tips when deleting conversation on pc
2023-04-01 13:08:06 +08:00
leedom
4dc1e025e1 feat: add confirm tips when deleting conversation on pc 2023-04-01 10:24:06 +08:00
Yifei Zhang
ba08b10de1 Merge pull request #285 from Dogtiti/main
修复在移动端高度被搜索栏占用导致无法完整显示一屏问题
2023-04-01 02:02:05 +08:00
Yifei Zhang
de35862cc5 Merge pull request #294 from hibobmaster/fix-deploy
Update docker.yml
2023-04-01 01:53:07 +08:00
hibobmaster
407c9fc9c3 Update docker.yml 2023-03-31 23:03:57 +08:00
Dogtiti
536358cb3c Merge branch 'Yidadaa:main' into main 2023-03-31 19:21:46 +08:00
Dogtiti
5f7a264e52 fix: 修复在手机浏览器高度样式问题 2023-03-31 19:21:11 +08:00
Yifei Zhang
c70c311989 Merge pull request #283 from Yidadaa/fix-credit-cache
fix: #277 no cache for credit query
2023-03-31 18:41:21 +08:00
Yifei Zhang
e5aa72af76 fix: #277 no cache for credit query 2023-03-31 18:33:26 +08:00
github-actions[bot]
4dbc984351 Merge pull request #1 from Yidadaa/main
Fork Sync
2023-03-31 08:52:24 +00:00
Yifei Zhang
eb586ba361 Merge pull request #259 from DanielRondonGarcia/main
Update: locales in spanish
2023-03-31 16:45:29 +08:00
RugerMc
1db210097c feat: add switch of send preview bubble 2023-03-31 13:16:12 +08:00
Daniel Gerardo Rondón García
7f16698f01 Update: language options to "Language".
- Update Name option in language files to display 'Language' consistently
- Fix locale issues in 'tw' and 'cn' files that were mistakenly changed
2023-03-30 23:32:56 -05:00
Yifei Zhang
61eb356fd9 Update README.md 2023-03-31 12:02:14 +08:00
Yifei Zhang
35a402c67e Merge pull request #262 from XiaoMiku01/main
add auto sync fork action daily
2023-03-31 11:19:50 +08:00
Yifei Zhang
5a910e0f29 Update README.md 2023-03-31 10:37:33 +08:00
XiaoMiku01
be8a35063c add auto sync fork action 2023-03-31 10:05:58 +08:00
Daniel Gerardo Rondón García
df75b9973a Update: locales in spanish 2023-03-30 19:48:57 -05:00
Yifei Zhang
2f2e0b6762 fix: commit id as version id 2023-03-30 18:15:49 +00:00
Yifei Zhang
83862eae44 Update docker.yml 2023-03-31 01:56:42 +08:00
Yifei Zhang
6e6faec398 Merge pull request #255 from Yidadaa/docker-fix
fix: docker build
2023-03-31 01:42:37 +08:00
Yifei Zhang
e7e39ba56e fix: docker build 2023-03-30 17:41:43 +00:00
Yifei Zhang
fe9dd88c3f Merge pull request #254 from Yidadaa/bugfix-0330
Bugfix 0330
2023-03-31 01:10:36 +08:00
Yifei Zhang
b3fdf3efec fix: #182 prompt cannot be selected 2023-03-30 17:08:50 +00:00
Yifei Zhang
802ea20ec4 fix: auto scroll on enter 2023-03-30 17:04:32 +00:00
Yifei Zhang
52a217883d Merge pull request #234 from GOWxx/feat_set_history_to_zero
feat: support history message count to zero
2023-03-31 00:56:57 +08:00
Yifei Zhang
7783545bff feat: use tag as version number 2023-03-30 16:46:17 +00:00
Yifei Zhang
1b140a1ed3 fix: tight border on mobile style 2023-03-30 16:20:47 +00:00
Yifei Zhang
bf50ebac94 fix: #229 disable light code theme 2023-03-30 15:50:08 +00:00
Yifei Zhang
7599ae385b fix: #244 better scroll ux 2023-03-30 15:42:54 +00:00
Yifei Zhang
7827b40f17 fix: #185 input and select align center 2023-03-30 15:25:38 +00:00
Yifei Zhang
dea3d26335 fix: crash caused by filter config 2023-03-30 15:20:19 +00:00
Yifei Zhang
9eb77207fb Merge pull request #226 from jack0pan/fix/deps
fix: update yarn.lock file
2023-03-30 19:01:04 +08:00
Yifei Zhang
164d3fb4fe Merge pull request #228 from yanglyu902/fix-title
fix: remove English punctuation in generated title
2023-03-30 19:00:05 +08:00
GOWxx
0c9add7988 feat: support history message count to zero 2023-03-30 18:50:58 +08:00
Jack
166329abee fix: update yarn.lock file 2023-03-30 09:55:41 +00:00
Leotrinos
7d7abca2c4 fix trimTopic 2023-03-30 02:55:19 -07:00
Yifei Zhang
dab69c7507 Merge pull request #215 from hibobmaster/multi-arch-docker-build
CI: Multi Arch docker builds
2023-03-30 16:06:22 +08:00
hibobmaster
852f8b8aa5 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web into multi-arch-docker-build 2023-03-30 15:53:44 +08:00
Yifei Zhang
fe858621f2 Merge pull request #213 from leedom92/copy-content-in-mobile
feat: setUserInput with onDoubleClickCapture in mobile phone
2023-03-30 15:38:54 +08:00
Leedom
6e4e804af8 Update home.tsx 2023-03-30 15:32:03 +08:00
leedom
e68aaf24f1 feat: setUserInput with onDoubleClickCapture in mobile phone 2023-03-30 15:22:10 +08:00
Yifei Zhang
29c20a3d5c Merge pull request #202 from RugerMcCarthy/feat/display_line_break
feat: import remark-breaks plugin
2023-03-30 14:56:21 +08:00
hibobmaster
6ed61f533a Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web into multi-arch-docker-build 2023-03-30 13:48:06 +08:00
hibobmaster
ad7a365f32 Support multi-arch docker build 2023-03-30 13:19:32 +08:00
RugerMc
2c5420ab9e feat: import ramarkBreaks plugin 2023-03-30 12:48:38 +08:00
Yifei Zhang
8d6d6bbf5d Update README.md 2023-03-30 11:22:27 +08:00
Yifei Zhang
d5235c81d0 Update README.md 2023-03-30 11:21:44 +08:00
Yifei Zhang
9e5b119e92 Merge pull request #197 from angular-moon/patch-1
onUserSubmit hide promptHints
2023-03-30 10:53:54 +08:00
angular-moon
d9fc9cd198 onUserSubmit hide promptHints 2023-03-30 10:16:00 +08:00
Yifei Zhang
8b4db412d8 Update home.tsx 2023-03-30 02:30:38 +08:00
Yifei Zhang
9c6f3ebb54 Merge pull request #179 from Yidadaa/prompt
fix: middleware match error
2023-03-30 02:11:11 +08:00
Yifei Zhang
53e30e20db fix: middleware match error 2023-03-29 18:09:15 +00:00
Yifei Zhang
d908099798 Merge pull request #172 from Yidadaa/prompt
feat: improve prompts ux
2023-03-30 02:01:55 +08:00
Yifei Zhang
469c8e9b00 fixup 2023-03-29 17:53:46 +00:00
Yifei Zhang
cd9799588d fixup 2023-03-29 17:53:36 +00:00
Yifei Zhang
447dec9444 feat: close #2 add check account balance 2023-03-29 17:45:26 +00:00
Yifei Zhang
45088a3e06 feat: #112 add edit chat title 2023-03-29 16:02:50 +00:00
Yifei Zhang
08f3c7026d feat: #170 auto scroll after retrying 2023-03-29 15:40:37 +00:00
Yifei Zhang
e606810581 refactor: #121 trigger auto-cmp with / prefix 2023-03-29 15:31:55 +00:00
Yifei Zhang
73b2ede53a Merge pull request #162 from HimiCos/main
fix: dialog height
2023-03-29 22:12:05 +08:00
Yifei Zhang
f1b2f895b4 Merge pull request #163 from GOWxx/bugfix_send_button_cover_text
fix: send button covering the text in the textarea
2023-03-29 22:11:34 +08:00
Yifei Zhang
9724308008 Merge pull request #164 from gtang8/patch-2
Update README.md
2023-03-29 22:05:22 +08:00
gtang8
be4706d02d Update README.md 2023-03-29 21:18:31 +08:00
HimiCos
752c083905 fix: dialog height 2023-03-29 20:53:23 +08:00
GOWxx
c47e90004a fix: send button covering the text in the textarea 2023-03-29 20:37:21 +08:00
Yifei Zhang
380f818285 fix: #159 temperature should range 0 - 2 2023-03-29 20:00:23 +08:00
Yifei Zhang
f893f53b1c Merge pull request #158 from HimiCos/main
chore: standardize input box punctuation
2023-03-29 19:45:22 +08:00
HimiCos
cac604aee3 chore: standardize input box punctuation 2023-03-29 19:31:34 +08:00
Yifei Zhang
525b5b8ee6 Merge pull request #148 from RugerMcCarthy/dev
supports the display of line breaks in Markdown
2023-03-29 18:05:52 +08:00
RugerMc
974c455bf9 feat: display line break hints in enter mode 2023-03-29 17:53:52 +08:00
RugerMc
b94607f636 feat: supports the display of line breaks in Markdown 2023-03-29 17:19:30 +08:00
Yifei Zhang
8a05f84838 Merge pull request #147 from Yidadaa/Yidadaa-patch-1
fix: #140 crash while hydrating store from old version
2023-03-29 16:09:06 +08:00
Yifei Zhang
ca0082856a Update settings.tsx 2023-03-29 16:06:22 +08:00
Yifei Zhang
e12238ba8b Merge pull request #146 from iSource/fix-mobile-autofocus
fix: mobile textarea autofocus ui error
2023-03-29 16:00:21 +08:00
iSource
71f119c9e8 fix: mobile textarea autofocus ui error 2023-03-29 15:45:31 +08:00
Yifei Zhang
067121d968 Merge pull request #89 from iFwu/main
feat: add font size config
2023-03-29 14:49:05 +08:00
Yifei Zhang
f5d775c055 Merge pull request #127 from stonega/main
fix: message top action style issue
2023-03-29 14:48:20 +08:00
伏晓
f979822508 feat: add font size setting 2023-03-29 13:15:32 +08:00
Yifei Zhang
cfbe6d77b5 Merge pull request #129 from HimiCos/main
Update app/locales/tw.ts
2023-03-29 12:36:30 +08:00
HimiCos
dee7950601 Update app/locales/tw.ts 2023-03-29 12:27:32 +08:00
Yifei Zhang
18d8eb4767 Merge pull request #125 from basefas/main
chore: move some dependencies to devDependencies
2023-03-29 11:04:28 +08:00
basefas
f2d019ff97 chore: move some dependencies to devDependencies 2023-03-29 10:53:13 +08:00
Yifei Zhang
3a170d50cb Update README.md 2023-03-29 02:26:14 +08:00
Yifei Zhang
17ee2ee135 Update home.tsx 2023-03-29 02:19:20 +08:00
Yifei Zhang
07956486b5 Merge pull request #74 from Yidadaa/prompt
feat: #2 add prompt list
2023-03-29 02:13:02 +08:00
Yifei Zhang
e648a59b1f feat: add lint-staged 2023-03-28 18:09:05 +00:00
Yifei Zhang
83400093a2 fixup: i18n for prompts 2023-03-28 17:45:23 +00:00
Yifei Zhang
6782e65fdf feat: #2 add prompt hints 2023-03-28 17:45:23 +00:00
Yifei Zhang
7d5e742ea6 feat: #2 add prompt list 2023-03-28 17:45:23 +00:00
Yifei Zhang
19b511e3f8 Update README.md 2023-03-28 23:13:30 +08:00
Yifei Zhang
cde3cbed21 Merge pull request #109 from HimiCos/main
feat: language support traditional chinese
2023-03-28 23:00:16 +08:00
HimiCos
307be405ac feat: language support traditional chinese 2023-03-28 21:29:30 +08:00
石门
1c017b8ee9 fix: minor fix 2023-03-28 20:14:44 +08:00
石门
48dc2c2295 fix: minor fix 2023-03-28 19:38:18 +08:00
石门
3a3999d73a fix: hide actions when loading 2023-03-28 18:59:03 +08:00
Yifei Zhang
7f3cbaa064 Merge pull request #100 from iSource/fix-docker-access-code
fix: docker access code setting missing
2023-03-28 18:52:46 +08:00
石门
7a5c35baf3 fix: hide actions when loading 2023-03-28 18:48:03 +08:00
iSource
eb72c83b7e fix: docker access code setting missing 2023-03-28 17:55:03 +08:00
Yifei Zhang
e93ea0fa97 Update README.md 2023-03-28 17:21:27 +08:00
Yifei Zhang
3b6f93afdf feat: add one-key setup script 2023-03-28 16:51:23 +08:00
Yifei Zhang
4597a2286a Update README.md 2023-03-28 14:53:07 +08:00
Yifei Zhang
780968979d Update README.md 2023-03-28 13:36:08 +08:00
Yifei Zhang
adc0db4c74 Merge pull request #76 from xiaotianxt/main
feat: CmdEnter config for submitkey, bug fix for auto scrolling
2023-03-28 13:35:36 +08:00
Yifei Zhang
f0dd95a2a3 Merge pull request #87 from Yidadaa/bugfix-0328
fix: light theme code highlight
2023-03-28 13:34:39 +08:00
Yifei Zhang
6155a190ac fix: light theme code highlight 2023-03-28 05:24:51 +00:00
xiaotianxt
493aa8c591 refactor: refocus textarea after btn clicked 2023-03-28 13:02:49 +08:00
xiaotianxt
6c82f804ae fix: Enter key bug 2023-03-28 12:56:36 +08:00
xiaotianxt
a2807c9815 fix(scroll): scroll after click submit button
The behavior of SubmitKey and SubmitButton should be the same
2023-03-28 12:56:36 +08:00
xiaotianxt
d822f333c2 feat(SubmitKey): add MetaEnter option
Add another option for MacOS user who prefer Cmd+Enter
or Linux user who prefer Meta+Enter.
2023-03-28 12:56:36 +08:00
Yifei Zhang
8f498075b9 Merge pull request #81 from iSource/fix-code-copy-button
fix: code copy button position
2023-03-28 11:57:03 +08:00
Yifei Zhang
c4bf6a6383 Merge pull request #84 from iSource/add-robots-txt
feat: add robots.txt
2023-03-28 11:56:47 +08:00
Yifei Zhang
939402b2d9 Merge pull request #85 from AprilNEA/fix-mobile
fix: fix #82, close sidebar after new session
2023-03-28 11:56:29 +08:00
AprilNEA
684a3c41ef fix: fix #82, close sidebar after new session 2023-03-28 11:51:36 +08:00
iSource
306f0850e9 feat: add robots.txt 2023-03-28 11:25:47 +08:00
iSource
55f37248f7 fix: code copy button position 2023-03-28 10:50:50 +08:00
Yifei Zhang
c93a46a02f Update README.md 2023-03-28 02:15:52 +08:00
Yifei Zhang
77a3fdea5f Update README.md 2023-03-27 23:02:39 +08:00
Yifei Zhang
cc1a1d4f3c feat: #27 add docker image publish actions 2023-03-27 14:46:30 +00:00
Yifei Zhang
0463b350d8 feat: #24 docker publish actions 2023-03-27 22:44:09 +08:00
Yifei Zhang
8f87a68f72 Merge pull request #39 from AprilNEA/docker
feat: add docker deployment support
2023-03-27 22:41:20 +08:00
Yifei Zhang
60f27fdfbb Merge pull request #68 from AprilNEA/main
fix(#65): fix unknown git commit id
2023-03-27 22:01:19 +08:00
Yifei Zhang
d17706636b Merge pull request #66 from iFwu/main
fix: resolve hydration error
2023-03-27 21:59:19 +08:00
Yifei Zhang
9570691d5b Delete userRole.tsx 2023-03-27 21:52:50 +08:00
AprilNEA
efe4fcc188 Merge branch 'Yidadaa:main' into main 2023-03-27 18:31:47 +08:00
AprilNEA
efaf6590ef fix(#65): fix unknown git commit id 2023-03-27 18:31:04 +08:00
伏晓
fb06fb8c38 fix: resolve hydration error 2023-03-27 18:22:55 +08:00
Yifei Zhang
f188841188 Merge pull request #67 from iSource/pwa-support
feat: add PWA support
2023-03-27 18:19:49 +08:00
jianjian.ma
5593c067c4 feat: add PWA support 2023-03-27 18:02:35 +08:00
jianjian.ma
689b7bab26 feat: add PWA support 2023-03-27 17:53:32 +08:00
Yifei Zhang
a81e7394f0 Update README.md 2023-03-27 17:45:36 +08:00
Yifei Zhang
492fed6802 Merge pull request #64 from AprilNEA/fix-theme
Fix broswer tasklist and support safari webapp #54
2023-03-27 17:44:21 +08:00
jianjian.ma
bdf17fafff feat: add PWA support 2023-03-27 17:41:44 +08:00
AprilNEA
58baa23199 Merge branch 'fix-theme' of https://github.com/AprilNEA/ChatGPT-Next-Web into fix-theme 2023-03-27 17:05:15 +08:00
AprilNEA
dd80c4563d feat: adding iOS Webapp support
- fix media query about background-color
- Use colors from CSS files instead of fixed values
2023-03-27 17:04:11 +08:00
AprilNEA
785372ad73 fix: fix the different colors on mobile 2023-03-27 15:47:46 +08:00
AprilNEA
d8e4808316 Merge branch 'Yidadaa:main' into fix-theme 2023-03-27 15:10:10 +08:00
AprilNEA
6446692db0 feat: support safari appleWebApp 2023-03-27 15:02:12 +08:00
AprilNEA
cd73c3a7cb fix: taskbar color follow(#54) 2023-03-27 15:01:35 +08:00
AprilNEA
d0eee767fa fix: resolve conflict 2023-03-27 14:01:03 +08:00
AprilNEA
e880df6db9 docs: add instructions for docker deployment 2023-03-27 13:59:23 +08:00
AprilNEA
96c4f5bbd9 Merge branch 'Yidadaa:main' into docker 2023-03-27 13:53:46 +08:00
AprilNEA
2645540721 perf: build in stages to reduce container size 2023-03-27 13:49:26 +08:00
Yifei Zhang
b1f27aaf93 Merge pull request #61 from Yidadaa/bugfix-0327
fix: #7 disable light code theme
2023-03-27 11:07:07 +08:00
Yifei Zhang
fb2d281aac fix: #7 disable light code theme 2023-03-27 03:02:25 +00:00
Yifei Zhang
84d73fa1f2 Merge pull request #48 from Yidadaa/custom-token
v1.4 Custom Api Key & Copy Code Button
2023-03-26 20:36:45 +08:00
Yifei Zhang
f858407f9a fixup 2023-03-26 12:35:15 +00:00
Yifei Zhang
b57663bf02 feat: now support gpt-4 model 2023-03-26 12:32:22 +00:00
Yifei Zhang
e57bd51809 feat: #9 add copy code button 2023-03-26 12:29:02 +00:00
Yifei Zhang
df66eef919 feat: support using user api key 2023-03-26 11:58:25 +00:00
Yifei Zhang
f1b6641f19 Update README.md 2023-03-26 19:28:30 +08:00
Yifei Zhang
bb45c62a81 Merge pull request #45 from Yidadaa/bugfix-0326
v1.3 Stop and Retry Button
2023-03-26 19:16:02 +08:00
Yifei Zhang
1e89fe14ac fix: #34 only auto scroll when textbox is focused 2023-03-26 11:10:34 +00:00
Yifei Zhang
86507fa569 feat: #2 #8 add stop and retry button 2023-03-26 10:59:09 +00:00
Yifei Zhang
4180363f58 Update README.md 2023-03-26 17:51:46 +08:00
Yifei Zhang
a5ec15236a fix: #38 high resolution favicon 2023-03-26 09:32:47 +00:00
AprilNEA
8d0d08725d feat: add Dockerfile for docker deployment support 2023-03-26 16:56:05 +08:00
Yifei Zhang
43b6835564 Update README.md 2023-03-26 16:52:00 +08:00
Yifei Zhang
3f865ffa1e Update README.md 2023-03-26 15:18:34 +08:00
Yifei Zhang
5cdcaac35b Merge pull request #29 from Yidadaa/bugfix-0325
v1.2 bug fix, ui improvement and access control by code
2023-03-26 15:12:46 +08:00
Yifei Zhang
d0d1673ccc fixup: disable access control when CODE is empty 2023-03-26 07:06:06 +00:00
Yifei Zhang
3136d6d3fd fix: #10 replace export icon 2023-03-26 06:57:13 +00:00
Yifei Zhang
2c899cf00e feat: #2 add access control by
access code
2023-03-26 06:53:40 +00:00
Yifei Zhang
15e49e8b46 Update README.md 2023-03-26 00:25:10 +08:00
Yidadaa
a5b3998304 fix: #23 errors when dev on windows 2023-03-25 22:24:52 +08:00
Yifei Zhang
9681fd8e14 Update README.md 2023-03-25 22:18:48 +08:00
Yifei Zhang
8399677350 Update README.md 2023-03-25 14:54:42 +08:00
Yifei Zhang
b1670b3558 chore: fix typos 2023-03-24 18:44:16 +08:00
93 changed files with 5746 additions and 1883 deletions

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
public/serviceWorker.js

View File

@@ -1,3 +1,4 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"plugins": ["prettier"]
}

43
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,43 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Deployment**
- [ ] Docker
- [ ] Vercel
- [ ] Server
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional Logs**
Add any logs about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

20
.github/ISSUE_TEMPLATE/功能建议.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: 功能建议
about: 请告诉我们你的灵光一闪
title: "[Feature] "
labels: ''
assignees: ''
---
**这个功能与现有的问题有关吗?**
如果有关,请在此列出链接或者描述问题。
**你想要什么功能或者有什么建议?**
尽管告诉我们。
**有没有可以参考的同类竞品?**
可以给出参考产品的链接或者截图。
**其他信息**
可以说说你的其他考虑。

23
.github/ISSUE_TEMPLATE/反馈问题.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: 反馈问题
about: 请告诉我们你遇到的问题
title: "[Bug] "
labels: ''
assignees: ''
---
**描述问题**
请在此描述你遇到了什么问题。
**如何复现**
请告诉我们你是通过什么操作触发的该问题。
**截图**
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
**一些必要的信息**
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
- 浏览器: [比如 chrome, safari]
- 版本: [填写设置页面的版本号]
- 部署方式:[比如 vercel、docker 或者服务器部署]

52
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Publish Docker image
on:
workflow_dispatch:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v3
-
name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: yidadaa/chatgpt-next-web
tags: |
type=raw,value=latest
type=ref,event=tag
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

33
.github/workflows/sync.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: "0 */6 * * *" # every 6 hours
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v3
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Set test_mode true to run tests instead of the true action!!
test_mode: false

7
.gitignore vendored
View File

@@ -34,4 +34,9 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
dev
dev
public/prompts.json
.vscode
.idea

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

6
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
"eslint --fix",
"prettier --write"
]
}

10
.prettierrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'always',
};

View File

@@ -1,4 +0,0 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
flynn.zhang@foxmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
FROM base AS builder
RUN apk update && apk add --no-cache git
ENV OPENAI_API_KEY=""
ENV CODE=""
ARG DOCKER=true
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
ENV OPENAI_API_KEY=""
ENV CODE=""
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
CMD ["node","server.js"]

177
README.md
View File

@@ -1,84 +1,187 @@
<div align="center">
<img src="./static/icon.svg" alt="预览"/>
<img src="./docs/images/icon.svg" alt="icon"/>
<h1 align="center">ChatGPT Next Web</h1>
One-Click to deploy well-designed ChatGPT web UI on Vercel.
一键免费部署你的私人 ChatGPT 网页应用。
One-Click to deploy your own ChatGPT web UI.
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join 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)
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
[![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)
![主界面](./static/cover.png)
![cover](./docs/images/cover.png)
</div>
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
- 精心设计的 UI响应式设计支持深色模式
- 极快的首屏加载速度(~85kb
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 一键导出聊天记录,完整的 Markdown 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## Features
- **Deploy for free with one-click** on Vercel in under 1 minute
- Responsive design, and dark mode
- Fast first screen loading speed (~85kb)
- Responsive design, dark mode and PWA
- Fast first screen loading speed (~100kb)
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
- Automatically compresses chat history to support long conversations while also saving your tokens
- One-click export all chat history with full Markdown support
- I18n supported
## 使用
## Roadmap
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [ ] User Prompt: user can edit and save custom prompts to prompt list
- [ ] Desktop App with tauri
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
- [ ] Plugins: support network search, caculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### Not in Plan
- User login, accounts, cloud sync
- UI text customize
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
- 精心设计的 UI响应式设计支持深色模式支持 PWA
- 极快的首屏加载速度(~100kb
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 一键导出聊天记录,完整的 Markdown 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [ ] 允许用户自行编辑内置 Prompt 列表
- [ ] 使用 tauri 打包桌面应用
- [ ] 支持自部署的大语言模型
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
- 界面文字自定义
- 用户登录、账号管理、消息云同步
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. 点击右侧按钮开始部署:
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
## Get Started
> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. Click
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web);
[![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), remember that `CODE` is your page password;
3. Enjoy :)
## 保持更新 Keep Updated
## FAQ
[简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md)
本项目会持续更新,如果你想让代码库总是保持更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步,建议定期进行同步操作以获得新功能。
## Keep Updated
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
This project will be continuously maintained. If you want to keep the code repository up to date, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. It is recommended to perform synchronization operations regularly.
We recommend that you follow the steps below to re-deploy:
- Delete the original repo;
- Fork this project;
- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy;
- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process.
This project will be continuously updated, and after forking the project, the upstream code will be automatically synchronized every day without additional operations.
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time.
## 开发 Development
## Access Password
> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
点击下方按钮,开始二次开发:
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
```
code1,code2,code3
```
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
## Environment Variables
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
Your openai api key.
### `CODE` (optional)
Access passsword, separated by comma.
### `BASE_URL` (optional)
> Default: `api.openai.com`
Override openai api request base url.
### `PROTOCOL` (optional)
> Default: `https`
> Values: `http` | `https`
Override openai api request protocol.
## 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` 文件,里面填入环境变量:
Before development, you must create a new `.env.local` file at project root, and place your api key into it:
Before starting development, you must create a new `.env.local` file at project root, and place your api key into it:
```
OPENAI_API_KEY=<your api key here>
```
## 截图 Screenshots
### Local Development
![设置 Settings](./static/settings.png)
```shell
# 1. install nodejs and yarn first
# 2. config local env vars in `.env.local`
# 3. run
yarn install
yarn dev
```
![更多展示 More](./static/more.png)
## Deployment
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
### Docker (Recommended)
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
```
### Shell
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## Screenshots
![Settings](./docs/images/settings.png)
![More](./docs/images/more.png)
## Special Thanks
### Sponsor
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
### Contributor
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## LICENSE
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

144
README_CN.md Normal file
View File

@@ -0,0 +1,144 @@
<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)
[![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)
![主界面](./static/cover.png)
</div>
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
- 精心设计的 UI响应式设计支持深色模式
- 极快的首屏加载速度(~100kb
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 一键导出聊天记录,完整的 Markdown 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开始使用
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. 点击右侧按钮开始部署:
[![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),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
## 保持更新
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
推荐你按照下列步骤重新部署:
- 删除掉原先的 repo
- fork 本项目;
- 前往 vercel 控制台,删除掉原先的 project然后新建 project选择你刚刚 fork 出来的项目重新进行部署即可;
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
本项目会持续更新,当你 Fork 项目之后,默认会每天自动同步上游代码,无需额外操作。
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
## 配置页面访问密码
> 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。
本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码:
```
code1,code2,code3
```
增加或修改该环境变量后,请**重新部署**项目使改动生效。
## 环境变量
> 本项目大多数配置项都通过环境变量来设置。
### `OPENAI_API_KEY` (必填项)
OpanAI 密钥,你在 openai 账户页面申请的 api key。
### `CODE` (可选)
访问密码,可选,可以使用逗号隔开多个密码。
**警告**:如果不填写此项,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。
### `BASE_URL` (可选)
> Default: `api.openai.com`
OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填写此选项。
### `PROTOCOL` (可选)
> Default: `https`
> Values: `http` | `https`
OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。
## 开发
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
点击下方按钮,开始二次开发:
[![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=<your api key here>
```
### 本地开发
1. 安装 nodejs 和 yarn具体细节请询问 ChatGPT
2. 执行 `yarn install && yarn dev` 即可。
## 部署
### 容器部署 (推荐)
> 注意docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
```
### 本地部署
在控制台运行下方命令:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## 鸣谢
### 捐赠者
> 仅列出了部分大额打赏,小额打赏(< 100RMB人数太多在此不再列出敬请谅解。
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
### 贡献者
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## 开源协议
> 反对 996从我开始。
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

17
app/api/access.ts Normal file
View File

@@ -0,0 +1,17 @@
import md5 from "spark-md5";
export function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
}
}
export const ACCESS_CODES = getAccessCodes();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@@ -1,21 +1,21 @@
import type { ChatRequest } from "../chat/typing";
import { createParser } from "eventsource-parser";
import { NextRequest } from "next/server";
import { requestOpenai } from "../common";
const apiKey = process.env.OPENAI_API_KEY;
async function createStream(payload: ReadableStream<Uint8Array>) {
async function createStream(req: NextRequest) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const res = await fetch("https://api.openai.com/v1/chat/completions", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
method: "POST",
body: payload,
});
const res = await requestOpenai(req);
const contentType = res.headers.get("Content-Type") ?? "";
if (!contentType.includes("stream")) {
const content = await (
await res.text()
).replace(/provided:.*. You/, "provided: ***. You");
console.log("[Stream] error ", content);
return "```json\n" + content + "```";
}
const stream = new ReadableStream({
async start(controller) {
@@ -49,10 +49,13 @@ async function createStream(payload: ReadableStream<Uint8Array>) {
export async function POST(req: NextRequest) {
try {
const stream = await createStream(req.body!);
const stream = await createStream(req);
return new Response(stream);
} catch (error) {
console.error("[Chat Stream]", error);
return new Response(
["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""),
);
}
}

View File

@@ -1 +0,0 @@
config.ts

View File

@@ -1,26 +0,0 @@
import { OpenAIApi, Configuration } from "openai";
import { ChatRequest } from "./typing";
const apiKey = process.env.OPENAI_API_KEY;
const openai = new OpenAIApi(
new Configuration({
apiKey,
})
);
export async function POST(req: Request) {
try {
const requestBody = (await req.json()) as ChatRequest;
const completion = await openai!.createChatCompletion(
{
...requestBody,
}
);
return new Response(JSON.stringify(completion.data));
} catch (e) {
console.error("[Chat] ", e);
return new Response(JSON.stringify(e));
}
}

22
app/api/common.ts Normal file
View File

@@ -0,0 +1,22 @@
import { NextRequest } from "next/server";
const OPENAI_URL = "api.openai.com";
const DEFAULT_PROTOCOL = "https";
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
export async function requestOpenai(req: NextRequest) {
const apiKey = req.headers.get("token");
const openaiPath = req.headers.get("path");
console.log("[Proxy] ", openaiPath);
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
method: req.method,
body: req.body,
});
}

31
app/api/openai/route.ts Normal file
View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { requestOpenai } from "../common";
async function makeRequest(req: NextRequest) {
try {
const api = await requestOpenai(req);
const res = new NextResponse(api.body);
res.headers.set("Content-Type", "application/json");
res.headers.set("Cache-Control", "no-cache");
return res;
} catch (e) {
console.error("[OpenAI] ", req.body, e);
return NextResponse.json(
{
error: true,
msg: JSON.stringify(e),
},
{
status: 500,
}
);
}
}
export async function POST(req: NextRequest) {
return makeRequest(req);
}
export async function GET(req: NextRequest) {
return makeRequest(req);
}

View File

@@ -6,11 +6,22 @@
justify-content: center;
padding: 10px;
box-shadow: var(--card-shadow);
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
user-select: none;
outline: none;
border: none;
color: var(--black);
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
}
.shadow {
box-shadow: var(--card-shadow);
}
.border {
@@ -18,7 +29,6 @@
}
.icon-button:hover {
filter: brightness(0.9);
border-color: var(--primary);
}
@@ -36,25 +46,7 @@
}
}
@mixin dark-button {
div:not(:global(.no-dark))>.icon-button-icon {
filter: invert(0.5);
}
.icon-button:hover {
filter: brightness(1.2);
}
}
:global(.dark) {
@include dark-button;
}
@media (prefers-color-scheme: dark) {
@include dark-button;
}
.icon-button-text {
margin-left: 5px;
font-size: 12px;
}
}

View File

@@ -7,22 +7,33 @@ export function IconButton(props: {
icon: JSX.Element;
text?: string;
bordered?: boolean;
shadow?: boolean;
noDark?: boolean;
className?: string;
title?: string;
disabled?: boolean;
}) {
return (
<div
<button
className={
styles["icon-button"] +
` ${props.bordered && styles.border} ${props.className ?? ""}`
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
props.className ?? ""
} clickable`
}
onClick={props.onClick}
title={props.title}
disabled={props.disabled}
role="button"
>
<div className={styles["icon-button-icon"]}>{props.icon}</div>
<div
className={styles["icon-button-icon"] + ` ${props.noDark && "no-dark"}`}
>
{props.icon}
</div>
{props.text && (
<div className={styles["icon-button-text"]}>{props.text}</div>
)}
</div>
</button>
);
}

View File

@@ -0,0 +1,108 @@
import DeleteIcon from "../icons/delete.svg";
import styles from "./home.module.scss";
import {
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { useChatStore } from "../store";
import Locale from "../locales";
import { isMobileScreen } from "../utils";
export function ChatItem(props: {
onClick?: () => void;
onDelete?: () => void;
title: string;
count: number;
time: string;
selected: boolean;
id: number;
index: number;
}) {
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div>
)}
</Draggable>
);
}
export function ChatList() {
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
useChatStore((state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.removeSession,
state.moveSession,
]);
const chatStore = useChatStore();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
moveSession(source.index, destination.index);
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chat-list">
{(provided) => (
<div
className={styles["chat-list"]}
ref={provided.innerRef}
{...provided.droppableProps}
>
{sessions.map((item, i) => (
<ChatItem
title={item.topic}
time={item.lastUpdate}
count={item.messages.length}
key={item.id}
id={item.id}
index={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={chatStore.deleteSession}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}

View File

@@ -0,0 +1,83 @@
@import "../styles/animation.scss";
.prompt-toast {
position: absolute;
bottom: -50px;
z-index: 999;
display: flex;
justify-content: center;
width: calc(100% - 40px);
.prompt-toast-inner {
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
background-color: var(--white);
color: var(--black);
border: var(--border-in-light);
box-shadow: var(--card-shadow);
padding: 10px 20px;
border-radius: 100px;
animation: slide-in-from-top ease 0.3s;
.prompt-toast-content {
margin-left: 10px;
}
}
}
.context-prompt {
.context-prompt-row {
display: flex;
justify-content: center;
width: 100%;
margin-bottom: 10px;
.context-role {
margin-right: 10px;
}
.context-content {
flex: 1;
max-width: 100%;
text-align: left;
}
.context-delete-button {
margin-left: 10px;
}
}
.context-prompt-button {
flex: 1;
}
}
.memory-prompt {
margin-top: 20px;
.memory-prompt-title {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.memory-prompt-action {
display: flex;
align-items: center;
}
}
.memory-prompt-content {
background-color: var(--gray);
border-radius: 6px;
padding: 10px;
font-size: 12px;
user-select: text;
}
}

685
app/components/chat.tsx Normal file
View File

@@ -0,0 +1,685 @@
import { useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
import ExportIcon from "../icons/export.svg";
import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import {
Message,
SubmitKey,
useChatStore,
BOT_HELLO,
ROLES,
createMessage,
} from "../store";
import {
copyToClipboard,
downloadAs,
getEmojiUrl,
isMobileScreen,
selectOrCopy,
} from "../utils";
import dynamic from "next/dynamic";
import { ControllerPool } from "../requests";
import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales";
import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
import { Input, Modal, showModal, showToast } from "./ui-lib";
const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown),
{
loading: () => <LoadingIcon />,
},
);
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
loading: () => <LoadingIcon />,
});
export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role !== "user") {
return <BotIcon className={styles["user-avtar"]} />;
}
return (
<div className={styles["user-avtar"]}>
<Emoji unified={config.avatar} size={18} getEmojiUrl={getEmojiUrl} />
</div>
);
}
function exportMessages(messages: Message[], topic: string) {
const mdText =
`# ${topic}\n\n` +
messages
.map((m) => {
return m.role === "user"
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
})
.join("\n\n");
const filename = `${topic}.md`;
showModal({
title: Locale.Export.Title,
children: (
<div className="markdown-body">
<pre className={styles["export-content"]}>{mdText}</pre>
</div>
),
actions: [
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Export.Copy}
onClick={() => copyToClipboard(mdText)}
/>,
<IconButton
key="download"
icon={<DownloadIcon />}
bordered
text={Locale.Export.Download}
onClick={() => downloadAs(mdText, filename)}
/>,
],
});
}
function PromptToast(props: {
showToast?: boolean;
showModal?: boolean;
setShowModal: (_: boolean) => void;
}) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const context = session.context;
const addContextPrompt = (prompt: Message) => {
chatStore.updateCurrentSession((session) => {
session.context.push(prompt);
});
};
const removeContextPrompt = (i: number) => {
chatStore.updateCurrentSession((session) => {
session.context.splice(i, 1);
});
};
const updateContextPrompt = (i: number, prompt: Message) => {
chatStore.updateCurrentSession((session) => {
session.context[i] = prompt;
});
};
return (
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
{props.showToast && (
<div
className={chatStyle["prompt-toast-inner"] + " clickable"}
role="button"
onClick={() => props.setShowModal(true)}
>
<BrainIcon />
<span className={chatStyle["prompt-toast-content"]}>
{Locale.Context.Toast(context.length)}
</span>
</div>
)}
{props.showModal && (
<div className="modal-mask">
<Modal
title={Locale.Context.Edit}
onClose={() => props.setShowModal(false)}
actions={[
<IconButton
key="reset"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Reset}
onClick={() =>
confirm(Locale.Memory.ResetConfirm) &&
chatStore.resetSession()
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Copy}
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
]}
>
<>
<div className={chatStyle["context-prompt"]}>
{context.map((c, i) => (
<div className={chatStyle["context-prompt-row"]} key={i}>
<select
value={c.role}
className={chatStyle["context-role"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
<Input
value={c.content}
type="text"
className={chatStyle["context-content"]}
rows={1}
onInput={(e) =>
updateContextPrompt(i, {
...c,
content: e.currentTarget.value as any,
})
}
/>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => removeContextPrompt(i)}
bordered
/>
</div>
))}
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "system",
content: "",
date: "",
})
}
/>
</div>
</div>
<div className={chatStyle["memory-prompt"]}>
<div className={chatStyle["memory-prompt-title"]}>
<span>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
</span>
<label className={chatStyle["memory-prompt-action"]}>
{Locale.Memory.Send}
<input
type="checkbox"
checked={session.sendMemory}
onChange={() =>
chatStore.updateCurrentSession(
(session) =>
(session.sendMemory = !session.sendMemory),
)
}
></input>
</label>
</div>
<div className={chatStyle["memory-prompt-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</div>
</div>
</>
</Modal>
</div>
)}
</div>
);
}
function useSubmitHandler() {
const config = useChatStore((state) => state.config);
const submitKey = config.submitKey;
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Enter") return false;
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
return (
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
(config.submitKey === SubmitKey.Enter &&
!e.altKey &&
!e.ctrlKey &&
!e.shiftKey &&
!e.metaKey)
);
};
return {
submitKey,
shouldSubmit,
};
}
export function PromptHints(props: {
prompts: Prompt[];
onPromptSelect: (prompt: Prompt) => void;
}) {
if (props.prompts.length === 0) return null;
return (
<div className={styles["prompt-hints"]}>
{props.prompts.map((prompt, i) => (
<div
className={styles["prompt-hint"]}
key={prompt.title + i.toString()}
onClick={() => props.onPromptSelect(prompt)}
>
<div className={styles["hint-title"]}>{prompt.title}</div>
<div className={styles["hint-content"]}>{prompt.content}</div>
</div>
))}
</div>
);
}
function useScrollToBottom() {
// for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
// auto scroll
useLayoutEffect(() => {
const dom = scrollRef.current;
if (dom && autoScroll) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
}
});
return {
scrollRef,
autoScroll,
setAutoScroll,
};
}
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
}) {
type RenderMessage = Message & { preview?: boolean };
const chatStore = useChatStore();
const [session, sessionIndex] = useChatStore((state) => [
state.currentSession(),
state.currentSessionIndex,
]);
const fontSize = useChatStore((state) => state.config.fontSize);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const { scrollRef, setAutoScroll } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(false);
const onChatBodyScroll = (e: HTMLElement) => {
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
setHitBottom(isTouchBottom);
};
// prompt hints
const promptStore = usePromptStore();
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
const onSearch = useDebouncedCallback(
(text: string) => {
setPromptHints(promptStore.search(text));
},
100,
{ leading: true, trailing: true },
);
const onPromptSelect = (prompt: Prompt) => {
setUserInput(prompt.content);
setPromptHints([]);
inputRef.current?.focus();
};
const scrollInput = () => {
const dom = inputRef.current;
if (!dom) return;
const paddingBottomNum: number = parseInt(
window.getComputedStyle(dom).paddingBottom,
10,
);
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
};
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
scrollInput();
setUserInput(text);
const n = text.trim().length;
// clear search results
if (n === 0) {
setPromptHints([]);
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion
if (text.startsWith("/")) {
let searchText = text.slice(1);
if (searchText.length === 0) {
searchText = " ";
}
onSearch(searchText);
}
}
};
// submit user input
const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
setUserInput("");
setPromptHints([]);
if (!isMobileScreen()) inputRef.current?.focus();
setAutoScroll(true);
};
// stop response
const onUserStop = (messageId: number) => {
ControllerPool.stop(sessionIndex, messageId);
};
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
}
};
const onRightClick = (e: any, message: Message) => {
// auto fill user input
if (message.role === "user") {
setUserInput(message.content);
}
// copy to clipboard
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
}
};
const onResend = (botIndex: number) => {
// find last user input message and resend
for (let i = botIndex; i >= 0; i -= 1) {
if (messages[i].role === "user") {
setIsLoading(true);
chatStore
.onUserInput(messages[i].content)
.then(() => setIsLoading(false));
chatStore.updateCurrentSession((session) =>
session.messages.splice(i, 2),
);
inputRef.current?.focus();
return;
}
}
};
const config = useChatStore((state) => state.config);
const context: RenderMessage[] = session.context.slice();
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
context.push(BOT_HELLO);
}
// preview messages
const messages = context
.concat(session.messages as RenderMessage[])
.concat(
isLoading
? [
{
...createMessage({
role: "assistant",
content: "……",
}),
preview: true,
},
]
: [],
)
.concat(
userInput.length > 0 && config.sendPreviewBubble
? [
{
...createMessage({
role: "user",
content: userInput,
}),
preview: true,
},
]
: [],
);
const [showPromptModal, setShowPromptModal] = useState(false);
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
inputRef.current?.focus();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}>
<div className={styles["window-header-title"]}>
<div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
}
}}
>
{session.topic}
</div>
<div className={styles["window-header-sub-title"]}>
{Locale.Chat.SubTitle(session.messages.length)}
</div>
</div>
<div className={styles["window-actions"]}>
<div className={styles["window-action-button"] + " " + styles.mobile}>
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar}
/>
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<BrainIcon />}
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
setShowPromptModal(true);
}}
/>
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<ExportIcon />}
bordered
title={Locale.Chat.Actions.Export}
onClick={() => {
exportMessages(
session.messages.filter((msg) => !msg.isError),
session.topic,
);
}}
/>
</div>
</div>
<PromptToast
showToast={!hitBottom}
showModal={showPromptModal}
setShowModal={setShowPromptModal}
/>
</div>
<div
className={styles["chat-body"]}
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
onTouchStart={() => {
inputRef.current?.blur();
setAutoScroll(false);
}}
>
{messages.map((message, i) => {
const isUser = message.role === "user";
return (
<div
key={i}
className={
isUser ? styles["chat-message-user"] : styles["chat-message"]
}
>
<div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}>
<Avatar role={message.role} />
</div>
{(message.preview || message.streaming) && (
<div className={styles["chat-message-status"]}>
{Locale.Chat.Typing}
</div>
)}
<div className={styles["chat-message-item"]}>
{!isUser &&
!(message.preview || message.content.length === 0) && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => onUserStop(message.id ?? i)}
>
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
)}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</div>
</div>
)}
{(message.preview || message.content.length === 0) &&
!isUser ? (
<LoadingIcon />
) : (
<div
className="markdown-body"
style={{ fontSize: `${fontSize}px` }}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen()) return;
setUserInput(message.content);
}}
>
<Markdown content={message.content} />
</div>
)}
</div>
{!isUser && !message.preview && (
<div className={styles["chat-message-actions"]}>
<div className={styles["chat-message-action-date"]}>
{message.date.toLocaleString()}
</div>
</div>
)}
</div>
</div>
);
})}
</div>
<div className={styles["chat-input-panel"]}>
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
<div className={styles["chat-input-panel-inner"]}>
<textarea
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={2}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
onFocus={() => setAutoScroll(true)}
onBlur={() => {
setAutoScroll(false);
setTimeout(() => setPromptHints([]), 500);
}}
autoFocus={!props?.sideBarShowing}
/>
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
noDark
onClick={onUserSubmit}
/>
</div>
</div>
</div>
);
}

47
app/components/error.tsx Normal file
View File

@@ -0,0 +1,47 @@
import React from "react";
import { IconButton } from "./button";
import GithubIcon from "../icons/github.svg";
import { ISSUE_URL } from "../constant";
interface IErrorBoundaryState {
hasError: boolean;
error: Error | null;
info: React.ErrorInfo | null;
}
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null, info: null };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// Update state with error details
this.setState({ hasError: true, error, info });
}
render() {
if (this.state.hasError) {
// Render error message
return (
<div className="error">
<h2>Oops, something went wrong!</h2>
<pre>
<code>{this.state.error?.toString()}</code>
<code>{this.state.info?.componentStack}</code>
</pre>
<a href={ISSUE_URL} className="report">
<IconButton
text="Report This Error"
icon={<GithubIcon />}
bordered
/>
</a>
</div>
);
}
// if no error occurred, render children
return this.props.children;
}
}

View File

@@ -1,4 +1,5 @@
@import "./window.scss";
@import "../styles/animation.scss";
@mixin container {
background-color: var(--white);
@@ -26,13 +27,13 @@
@media only screen and (min-width: 600px) {
.tight-container {
--window-width: 100vw;
--window-height: 100vh;
--window-height: var(--full-height);
--window-content-width: calc(100% - var(--sidebar-width));
@include container();
max-width: 100vw;
max-height: 100vh;
max-height: var(--full-height);
border-radius: 0;
}
@@ -73,8 +74,8 @@
.sidebar {
position: absolute;
left: -100%;
z-index: 999;
height: 100vh;
z-index: 1000;
height: var(--full-height);
transition: all ease 0.3s;
box-shadow: none;
}
@@ -124,7 +125,7 @@
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
transition: background-color 0.3s ease;
cursor: pointer;
user-select: none;
border: 2px solid transparent;
@@ -132,18 +133,6 @@
overflow: hidden;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0px);
}
}
.chat-item:hover {
background-color: var(--hover-color);
}
@@ -168,6 +157,7 @@
right: -20px;
transition: all ease 0.3s;
opacity: 0;
cursor: pointer;
}
.chat-item:hover > .chat-item-delete {
@@ -218,7 +208,15 @@
flex: 1;
overflow: auto;
padding: 20px;
margin-bottom: 100px;
position: relative;
}
.chat-body-title {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.chat-message {
@@ -292,6 +290,7 @@
position: absolute;
right: 20px;
top: -26px;
left: 100px;
transition: all ease 0.3s;
opacity: 0;
pointer-events: none;
@@ -302,6 +301,7 @@
.chat-message-top-action {
opacity: 0.5;
color: var(--black);
white-space: nowrap;
cursor: pointer;
&:hover {
@@ -332,12 +332,64 @@
}
.chat-input-panel {
position: absolute;
bottom: 20px;
display: flex;
width: 100%;
padding: 20px;
padding-top: 5px;
box-sizing: border-box;
flex-direction: column;
}
@mixin single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.prompt-hints {
min-height: 20px;
width: 100%;
max-height: 50vh;
overflow: auto;
display: flex;
flex-direction: column-reverse;
background-color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--shadow);
.prompt-hint {
color: var(--black);
padding: 6px 10px;
animation: slide-in ease 0.3s;
cursor: pointer;
transition: all ease 0.3s;
border: transparent 1px solid;
margin: 4px;
border-radius: 8px;
&:not(:last-child) {
margin-top: 0;
}
.hint-title {
font-size: 12px;
font-weight: bolder;
@include single-line();
}
.hint-content {
font-size: 12px;
@include single-line();
}
&-selected,
&:hover {
border-color: var(--primary);
}
}
}
.chat-input-panel-inner {
@@ -354,7 +406,7 @@
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 14px;
padding: 10px 14px 50px;
resize: none;
outline: none;
}
@@ -375,7 +427,7 @@
position: absolute;
right: 30px;
bottom: 10px;
bottom: 30px;
}
.export-content {

View File

@@ -1,6 +1,8 @@
"use client";
import { useState, useRef, useEffect, useLayoutEffect } from "react";
require("../polyfill");
import { useState, useEffect } from "react";
import { IconButton } from "./button";
import styles from "./home.module.scss";
@@ -8,25 +10,20 @@ import styles from "./home.module.scss";
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
import ExportIcon from "../icons/export.svg";
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import LoadingIcon from "../icons/three-dots.svg";
import MenuIcon from "../icons/menu.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
import { showModal, showToast } from "./ui-lib";
import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils";
import { useChatStore } from "../store";
import { isMobileScreen } from "../utils";
import Locale from "../locales";
import { Chat } from "./chat";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -37,404 +34,67 @@ export function Loading(props: { noLogo?: boolean }) {
);
}
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
});
const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />,
});
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
loading: () => <LoadingIcon />,
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => <Loading noLogo />,
});
export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role === "assistant") {
return <BotIcon className={styles["user-avtar"]} />;
}
return (
<div className={styles["user-avtar"]}>
<Emoji unified={config.avatar} size={18} />
</div>
);
}
export function ChatItem(props: {
onClick?: () => void;
onDelete?: () => void;
title: string;
count: number;
time: string;
selected: boolean;
}) {
return (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div>
);
}
export function ChatList() {
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
(state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.removeSession,
]
);
return (
<div className={styles["chat-list"]}>
{sessions.map((item, i) => (
<ChatItem
title={item.topic}
time={item.lastUpdate}
count={item.messages.length}
key={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={() => removeSession(i)}
/>
))}
</div>
);
}
function useSubmitHandler() {
const config = useChatStore((state) => state.config);
const submitKey = config.submitKey;
const shouldSubmit = (e: KeyboardEvent) => {
if (e.key !== "Enter") return false;
return (
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
(config.submitKey === SubmitKey.Enter &&
!e.altKey &&
!e.ctrlKey &&
!e.shiftKey)
);
};
return {
submitKey,
shouldSubmit,
};
}
export function Chat(props: { showSideBar?: () => void }) {
type RenderMessage = Message & { preview?: boolean };
const session = useChatStore((state) => state.currentSession());
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const onUserInput = useChatStore((state) => state.onUserInput);
const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true);
onUserInput(userInput).then(() => setIsLoading(false));
setUserInput("");
};
const onInputKeyDown = (e: KeyboardEvent) => {
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
}
};
const latestMessageRef = useRef<HTMLDivElement>(null);
const [hoveringMessage, setHoveringMessage] = useState(false);
const messages = (session.messages as RenderMessage[])
.concat(
isLoading
? [
{
role: "assistant",
content: "……",
date: new Date().toLocaleString(),
preview: true,
},
]
: []
)
.concat(
userInput.length > 0
? [
{
role: "user",
content: userInput,
date: new Date().toLocaleString(),
preview: true,
},
]
: []
);
useLayoutEffect(() => {
setTimeout(() => {
const dom = latestMessageRef.current;
if (dom && !isIOS() && !hoveringMessage) {
dom.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
}, 500);
});
return (
<div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}>
<div
className={styles["window-header-title"]}
onClick={props?.showSideBar}
>
<div className={styles["window-header-main-title"]}>
{session.topic}
</div>
<div className={styles["window-header-sub-title"]}>
{Locale.Chat.SubTitle(session.messages.length)}
</div>
</div>
<div className={styles["window-actions"]}>
<div className={styles["window-action-button"] + " " + styles.mobile}>
<IconButton
icon={<MenuIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar}
/>
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<BrainIcon />}
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
showMemoryPrompt(session);
}}
/>
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<ExportIcon />}
bordered
title={Locale.Chat.Actions.Export}
onClick={() => {
exportMessages(session.messages, session.topic);
}}
/>
</div>
</div>
</div>
<div
className={styles["chat-body"]}
onMouseOver={() => {
setHoveringMessage(true);
}}
onMouseOut={() => {
setHoveringMessage(false);
}}
>
{messages.map((message, i) => {
const isUser = message.role === "user";
return (
<div
key={i}
className={
isUser ? styles["chat-message-user"] : styles["chat-message"]
}
>
<div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}>
<Avatar role={message.role} />
</div>
{(message.preview || message.streaming) && (
<div className={styles["chat-message-status"]}>
{Locale.Chat.Typing}
</div>
)}
<div className={styles["chat-message-item"]}>
{!isUser && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming && (
<div
className={styles["chat-message-top-action"]}
onClick={() => showToast(Locale.WIP)}
>
{Locale.Chat.Actions.Stop}
</div>
)}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</div>
</div>
)}
{(message.preview || message.content.length === 0) &&
!isUser ? (
<LoadingIcon />
) : (
<div
className="markdown-body"
onContextMenu={(e) => {
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
}
}}
>
<Markdown content={message.content} />
</div>
)}
</div>
{!isUser && !message.preview && (
<div className={styles["chat-message-actions"]}>
<div className={styles["chat-message-action-date"]}>
{message.date.toLocaleString()}
</div>
</div>
)}
</div>
</div>
);
})}
<div ref={latestMessageRef} style={{ opacity: 0, height: "2em" }}>
-
</div>
</div>
<div className={styles["chat-input-panel"]}>
<div className={styles["chat-input-panel-inner"]}>
<textarea
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={3}
onInput={(e) => setUserInput(e.currentTarget.value)}
value={userInput}
onKeyDown={(e) => onInputKeyDown(e as any)}
/>
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"] + " no-dark"}
onClick={onUserSubmit}
/>
</div>
</div>
</div>
);
}
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
}
const metaDescriptionDark = document.querySelector(
'meta[name="theme-color"][media]',
);
const metaDescriptionLight = document.querySelector(
'meta[name="theme-color"]:not([media])',
);
if (config.theme === "auto") {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
} else {
const themeColor = getComputedStyle(document.body)
.getPropertyValue("--theme-color")
.trim();
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
}
}, [config.theme]);
}
function exportMessages(messages: Message[], topic: string) {
const mdText =
`# ${topic}\n\n` +
messages
.map((m) => {
return m.role === "user" ? `## ${m.content}` : m.content.trim();
})
.join("\n\n");
const filename = `${topic}.md`;
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
showModal({
title: Locale.Export.Title,
children: (
<div className="markdown-body">
<pre className={styles["export-content"]}>{mdText}</pre>
</div>
),
actions: [
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Export.Copy}
onClick={() => copyToClipboard(mdText)}
/>,
<IconButton
key="download"
icon={<DownloadIcon />}
bordered
text={Locale.Export.Download}
onClick={() => downloadAs(mdText, filename)}
/>,
],
});
}
useEffect(() => {
setHasHydrated(true);
}, []);
function showMemoryPrompt(session: ChatSession) {
showModal({
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
children: (
<div className="markdown-body">
<pre className={styles["export-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</pre>
</div>
),
actions: [
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Copy}
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
],
});
}
return hasHydrated;
};
export function Home() {
function _Home() {
const [createNewSession, currentIndex, removeSession] = useChatStore(
(state) => [
state.newSession,
state.currentSessionIndex,
state.removeSession,
]
],
);
const loading = !useChatStore?.persist?.hasHydrated();
const chatStore = useChatStore();
const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
// setting
@@ -450,7 +110,9 @@ export function Home() {
return (
<div
className={`${
config.tightBorder ? styles["tight-container"] : styles.container
config.tightBorder && !isMobileScreen()
? styles["tight-container"]
: styles.container
}`}
>
<div
@@ -481,11 +143,7 @@ export function Home() {
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
removeSession(currentIndex);
}
}}
onClick={chatStore.deleteSession}
/>
</div>
<div className={styles["sidebar-action"]}>
@@ -495,11 +153,12 @@ export function Home() {
setOpenSettings(true);
setShowSideBar(false);
}}
shadow
/>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank">
<IconButton icon={<GithubIcon />} />
<IconButton icon={<GithubIcon />} shadow />
</a>
</div>
</div>
@@ -507,7 +166,11 @@ export function Home() {
<IconButton
icon={<AddIcon />}
text={Locale.Home.NewChat}
onClick={createNewSession}
onClick={() => {
createNewSession();
setShowSideBar(false);
}}
shadow
/>
</div>
</div>
@@ -522,9 +185,21 @@ export function Home() {
}}
/>
) : (
<Chat key="chat" showSideBar={() => setShowSideBar(true)} />
<Chat
key="chat"
showSideBar={() => setShowSideBar(true)}
sideBarShowing={showSideBar}
/>
)}
</div>
</div>
);
}
export function Home() {
return (
<ErrorBoundary>
<_Home></_Home>
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,7 @@
.input-range {
border: var(--border-in-light);
border-radius: 10px;
padding: 5px 15px 5px 10px;
font-size: 12px;
display: flex;
}

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import styles from "./input-range.module.scss";
interface InputRangeProps {
onChange: React.ChangeEventHandler<HTMLInputElement>;
title?: string;
value: number | string;
className?: string;
min: string;
max: string;
step: string;
}
export function InputRange({
onChange,
title,
value,
className,
min,
max,
step,
}: InputRangeProps) {
return (
<div className={styles["input-range"] + ` ${className ?? ""}`}>
{title || value}
<input
type="range"
title={title}
value={value}
min={min}
max={max}
step={step}
onChange={onChange}
></input>
</div>
);
}

View File

@@ -1,18 +1,73 @@
import ReactMarkdown from "react-markdown";
import "katex/dist/katex.min.css";
import RemarkMath from "remark-math";
import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import RehypePrsim from "rehype-prism-plus";
import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
return (
<pre ref={ref}>
<span
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
);
}
const useLazyLoad = (ref: RefObject<Element>): boolean => {
const [isIntersecting, setIntersecting] = useState<boolean>(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIntersecting(true);
observer.disconnect();
}
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, [ref]);
return isIntersecting;
};
export function Markdown(props: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm]}
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[RehypePrsim, { ignoreMissing: true }],
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
}}
linkTarget={'_blank'}
>
{props.content}
</ReactMarkdown>

View File

@@ -18,3 +18,12 @@
.avatar {
cursor: pointer;
}
.password-input {
display: flex;
justify-content: flex-end;
.password-eye {
margin-right: 4px;
}
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo, HTMLProps } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
@@ -7,8 +7,11 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import { List, ListItem, Popover } from "./ui-lib";
import { List, ListItem, Popover, showToast } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -17,13 +20,19 @@ import {
Theme,
ALL_MODELS,
useUpdateStore,
useAccessStore,
ModalConfigValidator,
} from "../store";
import { Avatar } from "./home";
import { Avatar } from "./chat";
import Locale, { changeLang, getLang } from "../locales";
import { getCurrentCommitId } from "../utils";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getCurrentVersion, getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
function SettingItem(props: {
title: string;
@@ -38,25 +47,44 @@ function SettingItem(props: {
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
)}
</div>
<div>{props.children}</div>
{props.children}
</ListItem>
);
}
function PasswordInput(props: HTMLProps<HTMLInputElement>) {
const [visible, setVisible] = useState(false);
function changeVisibility() {
setVisible(!visible);
}
return (
<div className={styles["password-input"]}>
<IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={styles["password-eye"]}
/>
<input {...props} type={visible ? "text" : "password"} />
</div>
);
}
export function Settings(props: { closeSettings: () => void }) {
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig, resetConfig, clearAllData] = useChatStore(
(state) => [
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
useChatStore((state) => [
state.config,
state.updateConfig,
state.resetConfig,
state.clearAllData,
]
);
state.clearSessions,
]);
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentId = getCurrentCommitId();
const currentId = getCurrentVersion();
const remoteId = updateStore.remoteId;
const hasNewVersion = currentId !== remoteId;
@@ -67,12 +95,41 @@ export function Settings(props: { closeSettings: () => void }) {
});
}
const [usage, setUsage] = useState<{
used?: number;
subscription?: number;
}>();
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
requestUsage()
.then((res) => setUsage(res))
.finally(() => {
setLoadingUsage(false);
});
}
const accessStore = useAccessStore();
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const promptStore = usePromptStore();
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
const showUsage = !!accessStore.token || !!accessStore.accessCode;
useEffect(() => {
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<ErrorBoundary>
<div className={styles["window-header"]}>
<div className={styles["window-header-title"]}>
<div className={styles["window-header-main-title"]}>
@@ -86,7 +143,7 @@ export function Settings(props: { closeSettings: () => void }) {
<div className={styles["window-action-button"]}>
<IconButton
icon={<ClearIcon />}
onClick={clearAllData}
onClick={clearSessions}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
@@ -118,6 +175,7 @@ export function Settings(props: { closeSettings: () => void }) {
<EmojiPicker
lazyLoadEmojis
theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl}
onEmojiClick={(e) => {
updateConfig((config) => (config.avatar = e.unified));
setShowEmojiPicker(false);
@@ -166,7 +224,7 @@ export function Settings(props: { closeSettings: () => void }) {
onChange={(e) => {
updateConfig(
(config) =>
(config.submitKey = e.target.value as any as SubmitKey)
(config.submitKey = e.target.value as any as SubmitKey),
);
}}
>
@@ -186,7 +244,7 @@ export function Settings(props: { closeSettings: () => void }) {
value={config.theme}
onChange={(e) => {
updateConfig(
(config) => (config.theme = e.target.value as any as Theme)
(config) => (config.theme = e.target.value as any as Theme),
);
}}
>
@@ -199,57 +257,169 @@ export function Settings(props: { closeSettings: () => void }) {
</ListItem>
<SettingItem title={Locale.Settings.Lang.Name}>
<div className="">
<select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
}}
>
<option value="en" key="en">
{Locale.Settings.Lang.Options.en}
<select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
}}
>
{AllLangs.map((lang) => (
<option value={lang} key={lang}>
{Locale.Settings.Lang.Options[lang]}
</option>
<option value="cn" key="cn">
{Locale.Settings.Lang.Options.cn}
</option>
</select>
</div>
))}
</select>
</SettingItem>
<div className="no-mobile">
<SettingItem title={Locale.Settings.TightBorder}>
<input
type="checkbox"
checked={config.tightBorder}
onChange={(e) =>
updateConfig(
(config) => (config.tightBorder = e.currentTarget.checked)
)
}
></input>
</SettingItem>
</div>
<SettingItem
title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle}
>
<InputRange
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
max="18"
step="1"
onChange={(e) =>
updateConfig(
(config) =>
(config.fontSize = Number.parseInt(e.currentTarget.value)),
)
}
></InputRange>
</SettingItem>
<SettingItem title={Locale.Settings.TightBorder}>
<input
type="checkbox"
checked={config.tightBorder}
onChange={(e) =>
updateConfig(
(config) => (config.tightBorder = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem title={Locale.Settings.SendPreviewBubble}>
<input
type="checkbox"
checked={config.sendPreviewBubble}
onChange={(e) =>
updateConfig(
(config) =>
(config.sendPreviewBubble = e.currentTarget.checked),
)
}
></input>
</SettingItem>
</List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
/>
</SettingItem>
</List>
<List>
{enabledAccessControl ? (
<SettingItem
title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle}
>
<PasswordInput
value={accessStore.accessCode}
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
onChange={(e) => {
accessStore.updateCode(e.currentTarget.value);
}}
/>
</SettingItem>
) : (
<></>
)}
<SettingItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<PasswordInput
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
}}
/>
</SettingItem>
<SettingItem
title={Locale.Settings.Usage.Title}
subTitle={
showUsage
? loadingUsage
? Locale.Settings.Usage.IsChecking
: Locale.Settings.Usage.SubTitle(
usage?.used ?? "[?]",
usage?.subscription ?? "[?]",
)
: Locale.Settings.Usage.NoAccess
}
>
{!showUsage || loadingUsage ? (
<div />
) : (
<IconButton
icon={<ResetIcon></ResetIcon>}
text={Locale.Settings.Usage.Check}
onClick={checkUsage}
/>
)}
</SettingItem>
<SettingItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<input
type="range"
<InputRange
title={config.historyMessageCount.toString()}
value={config.historyMessageCount}
min="2"
min="0"
max="25"
step="2"
step="1"
onChange={(e) =>
updateConfig(
(config) =>
(config.historyMessageCount = e.target.valueAsNumber)
(config.historyMessageCount = e.target.valueAsNumber),
)
}
></input>
></InputRange>
</SettingItem>
<SettingItem
@@ -265,7 +435,7 @@ export function Settings(props: { closeSettings: () => void }) {
updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber)
e.currentTarget.valueAsNumber),
)
}
></input>
@@ -278,7 +448,10 @@ export function Settings(props: { closeSettings: () => void }) {
value={config.modelConfig.model}
onChange={(e) => {
updateConfig(
(config) => (config.modelConfig.model = e.currentTarget.value)
(config) =>
(config.modelConfig.model = ModalConfigValidator.model(
e.currentTarget.value,
)),
);
}}
>
@@ -293,20 +466,21 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<input
type="range"
value={config.modelConfig.temperature.toFixed(1)}
<InputRange
value={config.modelConfig.temperature?.toFixed(1)}
min="0"
max="1"
max="2"
step="0.1"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.temperature =
e.currentTarget.valueAsNumber)
ModalConfigValidator.temperature(
e.currentTarget.valueAsNumber,
)),
);
}}
></input>
></InputRange>
</SettingItem>
<SettingItem
title={Locale.Settings.MaxTokens.Title}
@@ -315,13 +489,15 @@ export function Settings(props: { closeSettings: () => void }) {
<input
type="number"
min={100}
max={4000}
max={32000}
value={config.modelConfig.max_tokens}
onChange={(e) =>
updateConfig(
(config) =>
(config.modelConfig.max_tokens =
e.currentTarget.valueAsNumber)
ModalConfigValidator.max_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
@@ -330,9 +506,8 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<input
type="range"
value={config.modelConfig.presence_penalty.toFixed(1)}
<InputRange
value={config.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.5"
@@ -340,13 +515,15 @@ export function Settings(props: { closeSettings: () => void }) {
updateConfig(
(config) =>
(config.modelConfig.presence_penalty =
e.currentTarget.valueAsNumber)
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></input>
></InputRange>
</SettingItem>
</List>
</div>
</>
</ErrorBoundary>
);
}

View File

@@ -1,3 +1,5 @@
@import "../styles/animation.scss";
.card {
background-color: var(--white);
border-radius: 10px;
@@ -24,18 +26,6 @@
height: 100vh;
}
@keyframes slide-in {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.list-item {
display: flex;
justify-content: space-between;
@@ -138,17 +128,46 @@
justify-content: center;
.toast-content {
max-width: 80vw;
word-break: break-all;
font-size: 14px;
background-color: var(--white);
box-shadow: var(--card-shadow);
border: var(--border-in-light);
color: var(--black);
padding: 10px 30px;
padding: 10px 20px;
border-radius: 50px;
margin-bottom: 20px;
display: flex;
align-items: center;
.toast-action {
padding-left: 20px;
color: var(--primary);
opacity: 0.8;
border: 0;
background: none;
cursor: pointer;
font-family: inherit;
&:hover {
opacity: 1;
}
}
}
}
.input {
border: var(--border-in-light);
border-radius: 10px;
padding: 10px;
font-family: inherit;
background-color: var(--white);
color: var(--black);
resize: none;
min-width: 50px;
}
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;

View File

@@ -2,6 +2,7 @@ import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import { createRoot } from "react-dom/client";
import React from "react";
export function Popover(props: {
children: JSX.Element;
@@ -36,7 +37,7 @@ export function ListItem(props: { children: JSX.Element[] }) {
return <div className={styles["list-item"]}>{props.children}</div>;
}
export function List(props: { children: JSX.Element[] }) {
export function List(props: { children: JSX.Element[] | JSX.Element }) {
return <div className={styles.list}>{props.children}</div>;
}
@@ -109,17 +110,37 @@ export function showModal(props: ModalProps) {
root.render(<Modal {...props} onClose={closeModal}></Modal>);
}
export type ToastProps = { content: string };
export type ToastProps = {
content: string;
action?: {
text: string;
onClick: () => void;
};
};
export function Toast(props: ToastProps) {
return (
<div className={styles["toast-container"]}>
<div className={styles["toast-content"]}>{props.content}</div>
<div className={styles["toast-content"]}>
<span>{props.content}</span>
{props.action && (
<button
onClick={props.action.onClick}
className={styles["toast-action"]}
>
{props.action.text}
</button>
)}
</div>
</div>
);
}
export function showToast(content: string, delay = 3000) {
export function showToast(
content: string,
action?: ToastProps["action"],
delay = 3000,
) {
const div = document.createElement("div");
div.className = styles.show;
document.body.appendChild(div);
@@ -138,5 +159,19 @@ export function showToast(content: string, delay = 3000) {
close();
}, delay);
root.render(<Toast content={content} />);
root.render(<Toast content={content} action={action} />);
}
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
autoHeight?: boolean;
rows?: number;
};
export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
></textarea>
);
}

View File

@@ -1,6 +1,7 @@
.window-header {
padding: 14px 20px;
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
position: relative;
display: flex;
justify-content: space-between;
@@ -9,6 +10,7 @@
.window-header-title {
max-width: calc(100% - 100px);
overflow: hidden;
.window-header-main-title {
font-size: 20px;
@@ -32,4 +34,4 @@
.window-action-button {
margin-left: 10px;
}
}

View File

@@ -1,5 +1,7 @@
export const OWNER = "Yidadaa";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
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`;

1
app/icons/edit.svg Normal file
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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(10.5 11) rotate(0 1.4166666666666665 1.8333333333333333)" d="M2.83,0L2.83,3C2.83,3.37 2.53,3.67 2.17,3.67L0,3.67 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2.6666666666666665 1.3333333333333333) rotate(0 5.333333333333333 6.666666666666666)" d="M10.67,4L10.67,0.67C10.67,0.3 10.37,0 10,0L0.67,0C0.3,0 0,0.3 0,0.67L0,12.67C0,13.03 0.3,13.33 0.67,13.33L2.67,13.33 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 5.333333333333333) rotate(0 2.333333333333333 0)" d="M0,0L4.67,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7.666666666666666 7.666666666666666) rotate(0 2.833333333333333 3.5)" d="M0,7L5.67,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 8) rotate(0 1.3333333333333333 0)" d="M0,0L2.67,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,24 +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">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 9) rotate(0 6 2.6666666666666665)"
d="M12,0C12,2 10.67,5.33 6,5.33C1.33,5.33 0,2 0,0 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(8.0026 1.7001966666666668) rotate(0 0 4.649918333333334)"
d="M0,0L0,9.3 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(4 1.6666666666666665) rotate(0 4 2)" d="M0,4L4,0L8,4 " />
</g>
</g>
</svg>
<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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.2400716519614834 2.3333321805983163) rotate(0 6.785117896431597 4.552683909700841)" d="M12.27,9.11C13.36,8.34 13.83,6.94 13.43,5.67C13.02,4.39 11.78,3.69 10.44,3.69L9.67,3.69C9.16,1.72 7.5,0.27 5.47,0.03C3.45,-0.2 1.5,0.84 0.56,2.64C-0.38,4.45 -0.11,6.64 1.23,8.17 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 7.666666666666666) rotate(0 0.00140000000000029 3)" d="M0,6L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.8786 11.5454) rotate(0 2.1213333333333333 1.0606666666666662)" d="M4.24,0L2.12,2.12L0,0 " /></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

4
app/icons/eye-off.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7071 5.70711C20.0976 5.31658 20.0976 4.68342 19.7071 4.29289C19.3166 3.90237 18.6834 3.90237 18.2929 4.29289L14.032 8.55382C13.4365 8.20193 12.7418 8 12 8C9.79086 8 8 9.79086 8 12C8 12.7418 8.20193 13.4365 8.55382 14.032L4.29289 18.2929C3.90237 18.6834 3.90237 19.3166 4.29289 19.7071C4.68342 20.0976 5.31658 20.0976 5.70711 19.7071L9.96803 15.4462C10.5635 15.7981 11.2582 16 12 16C14.2091 16 16 14.2091 16 12C16 11.2582 15.7981 10.5635 15.4462 9.96803L19.7071 5.70711ZM12.518 10.0677C12.3528 10.0236 12.1792 10 12 10C10.8954 10 10 10.8954 10 12C10 12.1792 10.0236 12.3528 10.0677 12.518L12.518 10.0677ZM11.482 13.9323L13.9323 11.482C13.9764 11.6472 14 11.8208 14 12C14 13.1046 13.1046 14 12 14C11.8208 14 11.6472 13.9764 11.482 13.9323ZM15.7651 4.8207C14.6287 4.32049 13.3675 4 12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C1.92276 13.7326 2.86706 15.0637 4.21194 16.3739L5.62626 14.9596C4.4555 13.8229 3.61144 12.6531 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C12.7719 6 13.5135 6.13385 14.2193 6.36658L15.7651 4.8207ZM12 18C11.2282 18 10.4866 17.8661 9.78083 17.6334L8.23496 19.1793C9.37136 19.6795 10.6326 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C22.0773 10.2674 21.133 8.93627 19.7881 7.62611L18.3738 9.04043C19.5446 10.1771 20.3887 11.3469 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

4
app/icons/eye.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.30147 15.5771C4.77832 14.2684 3.6904 12.7726 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C14.1843 6 16.1261 7.07185 17.6986 8.42294C19.2218 9.73158 20.3097 11.2274 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18C9.81574 18 7.87402 16.9282 6.30147 15.5771ZM12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C2.00757 13.8624 3.23268 15.5772 4.99812 17.0941C6.75717 18.6054 9.14754 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C21.9925 10.1376 20.7674 8.42276 19.002 6.90595C17.2429 5.39462 14.8525 4 12 4ZM10 12C10 10.8954 10.8955 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8955 14 10 13.1046 10 12ZM12 8C9.7909 8 8.00004 9.79086 8.00004 12C8.00004 14.2091 9.7909 16 12 16C14.2092 16 16 14.2091 16 12C16 9.79086 14.2092 8 12 8Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

21
app/icons/return.svg Normal file
View File

@@ -0,0 +1,21 @@
<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">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2.6666666666666665) rotate(0 1.1666333333333334 2.1666666666666665)"
d="M2.33,0L0,2L2.33,4.33 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 4.666666666666666) rotate(0 6.000006859869576 4.333333333333333)"
d="M0,0L7.66,0C9.96,0 11.91,1.87 12,4.17C12.09,6.59 10.09,8.67 7.66,8.67L2,8.67 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1013 B

View File

@@ -1,13 +1,45 @@
/* eslint-disable @next/next/no-page-custom-font */
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/prism.scss";
import "./styles/highlight.scss";
import process from "child_process";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
let COMMIT_ID: string | undefined;
try {
COMMIT_ID = process
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch (e) {
console.error("No git or not from git repo.");
}
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot."
description: "Your personal ChatGPT Chat Bot.",
appleWebApp: {
title: "ChatGPT Next Web",
statusBarStyle: "default",
},
themeColor: "#fafafa",
};
const COMMIT_ID = process.env.COMMIT_ID
function Meta() {
const metas = {
version: COMMIT_ID ?? "unknown",
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
export default function RootLayout({
children,
@@ -21,7 +53,12 @@ export default function RootLayout({
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta name="version" content={COMMIT_ID} />
<meta
name="theme-color"
content="#151515"
media="(prefers-color-scheme: dark)"
/>
<Meta />
<link rel="manifest" href="/site.webmanifest"></link>
<link rel="preconnect" href="https://fonts.googleapis.com"></link>
<link rel="preconnect" href="https://fonts.gstatic.com"></link>
@@ -29,6 +66,7 @@ export default function RootLayout({
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
rel="stylesheet"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>{children}</body>
</html>

View File

@@ -1,5 +1,10 @@
import { SubmitKey } from "../store/app";
const cn = {
WIP: "该功能仍在开发中……",
Error: {
Unauthorized: "现在是未授权状态,请在设置页输入访问密码。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
},
@@ -11,24 +16,39 @@ const cn = {
Export: "导出聊天记录",
Copy: "复制",
Stop: "停止",
Retry: "重试",
},
Rename: "重命名对话",
Typing: "正在输入…",
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
Input: (submitKey: string) => {
var inputHints = `输入消息,${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行";
}
return inputHints;
},
Send: "发送",
},
Export: {
Title: "导出聊天记录为 Markdown",
Copy: "全部复制",
Download: "下载文件",
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
},
Memory: {
Title: "上下文记忆 Prompt",
Title: "历史记忆",
EmptyContent: "尚未记忆",
Copy: "全部复制",
Send: "发送记忆",
Copy: "复制记忆",
Reset: "重置对话",
ResetConfirm: "重置后将清空当前对话记录以及历史记忆,确认重置?",
},
Home: {
NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?",
DeleteToast: "已删除会话",
Revert: "撤销",
},
Settings: {
Title: "设置",
@@ -41,11 +61,19 @@ const cn = {
Lang: {
Name: "Language",
Options: {
cn: "中文",
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
},
},
Avatar: "头像",
FontSize: {
Title: "字体大小",
SubTitle: "聊天内容的字体大小",
},
Update: {
Version: (x: string) => `当前版本:${x}`,
IsLatest: "已是最新版本",
@@ -57,6 +85,17 @@ const cn = {
SendKey: "发送键",
Theme: "主题",
TightBorder: "紧凑边框",
SendPreviewBubble: "发送预览气泡",
Prompt: {
Disable: {
Title: "禁用提示词自动补全",
SubTitle: "在输入框开头输入 / 即可触发自动补全",
},
List: "自定义提示词列表",
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom}`,
Edit: "编辑",
},
HistoryCount: {
Title: "附带历史消息数",
SubTitle: "每次请求携带的历史消息数",
@@ -65,6 +104,25 @@ const cn = {
Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "余额查询",
SubTitle(used: any, total: any) {
return `本月已使用 $${used},订阅总额 $${total}`;
},
IsChecking: "正在检查…",
Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额",
},
AccessCode: {
Title: "访问密码",
SubTitle: "现在是未授权访问状态",
Placeholder: "请输入访问密码",
},
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",
@@ -87,9 +145,9 @@ const cn = {
History: (content: string) =>
"这是 ai 和用户的历史聊天总结作为前情提要:" + content,
Topic:
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
Summarize:
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 50 字以内",
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 200 字以内",
},
ConfirmClearAll: "确认清除所有聊天、设置数据?",
},
@@ -97,6 +155,11 @@ const cn = {
Success: "已写入剪切板",
Failed: "复制失败,请赋予剪切板权限",
},
Context: {
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
Edit: "前置上下文和历史记忆",
Add: "新增一条",
},
};
export type LocaleType = typeof cn;

View File

@@ -1,7 +1,12 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const en: LocaleType = {
WIP: "WIP...",
Error: {
Unauthorized:
"Unauthorized access, please enter access code in settings page.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} messages`,
},
@@ -13,25 +18,40 @@ const en: LocaleType = {
Export: "Export All Messages as Markdown",
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
},
Rename: "Rename Chat",
Typing: "Typing…",
Input: (submitKey: string) =>
`Type something and press ${submitKey} to send`,
Input: (submitKey: string) => {
var inputHints = `Type something and press ${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", press Shift + Enter to newline";
}
return inputHints;
},
Send: "Send",
},
Export: {
Title: "All Messages",
Copy: "Copy All",
Download: "Download",
MessageFromYou: "Message From You",
MessageFromChatGPT: "Message From ChatGPT",
},
Memory: {
Title: "Memory Prompt",
EmptyContent: "Nothing yet.",
Copy: "Copy All",
Send: "Send Memory",
Copy: "Copy Memory",
Reset: "Reset Session",
ResetConfirm:
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
},
Home: {
NewChat: "New Chat",
DeleteChat: "Confirm to delete the selected conversation?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
},
Settings: {
Title: "Settings",
@@ -42,13 +62,20 @@ const en: LocaleType = {
Close: "Close",
},
Lang: {
Name: "语言",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Options: {
cn: "中文",
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Font Size",
SubTitle: "Adjust font size of chat content",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Latest version",
@@ -60,6 +87,17 @@ const en: LocaleType = {
SendKey: "Send Key",
Theme: "Theme",
TightBorder: "Tight Border",
SendPreviewBubble: "Send Preview Bubble",
Prompt: {
Disable: {
Title: "Disable auto-completion",
SubTitle: "Input / to trigger auto-completion",
},
List: "Prompt List",
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
},
HistoryCount: {
Title: "Attached Messages Count",
SubTitle: "Number of sent messages attached per request",
@@ -69,6 +107,25 @@ const en: LocaleType = {
SubTitle:
"Will compress if uncompressed messages length exceeds the value",
},
Token: {
Title: "API Key",
SubTitle: "Use your key to ignore access code limit",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Account Balance",
SubTitle(used: any, total: any) {
return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
Check: "Check Again",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {
Title: "Access Code",
SubTitle: "Access control enabled",
Placeholder: "Need Access Code",
},
Model: "Model",
Temperature: {
Title: "Temperature",
@@ -93,9 +150,9 @@ const en: LocaleType = {
"This is a summary of the chat history between the AI and the user as a recap: " +
content,
Topic:
"Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.",
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
Summarize:
"Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
"Summarize our discussion briefly in 200 words or less to use as a prompt for future context.",
},
ConfirmClearAll: "Confirm to clear all chat and setting data?",
},
@@ -103,6 +160,11 @@ const en: LocaleType = {
Success: "Copied to clipboard",
Failed: "Copy failed, please grant permission to access clipboard",
},
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Contextual and Memory Prompts",
Add: "Add One",
},
};
export default en;

172
app/locales/es.ts Normal file
View File

@@ -0,0 +1,172 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const es: LocaleType = {
WIP: "En construcción...",
Error: {
Unauthorized:
"Acceso no autorizado, por favor ingrese el código de acceso en la página de configuración.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} mensajes`,
},
Chat: {
SubTitle: (count: number) => `${count} mensajes con ChatGPT`,
Actions: {
ChatList: "Ir a la lista de chats",
CompressedHistory: "Historial de memoria comprimido",
Export: "Exportar todos los mensajes como Markdown",
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
Input: (submitKey: string) => {
var inputHints = `Escribe algo y presiona ${submitKey} para enviar`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", presiona Shift + Enter para nueva línea";
}
return inputHints;
},
Send: "Enviar",
},
Export: {
Title: "Todos los mensajes",
Copy: "Copiar todo",
Download: "Descargar",
MessageFromYou: "Mensaje de ti",
MessageFromChatGPT: "Mensaje de ChatGPT",
},
Memory: {
Title: "Historial de memoria",
EmptyContent: "Aún no hay nada.",
Copy: "Copiar todo",
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
},
Home: {
NewChat: "Nuevo chat",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
},
Settings: {
Title: "Configuración",
SubTitle: "Todas las configuraciones",
Actions: {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
},
Lang: {
Name: "Language",
Options: {
cn: "简体中文",
en: "Inglés",
tw: "繁體中文",
es: "Español",
it: "Italiano",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Tamaño de fuente",
SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
},
Update: {
Version: (x: string) => `Versión: ${x}`,
IsLatest: "Última versión",
CheckUpdate: "Buscar actualizaciones",
IsChecking: "Buscando actualizaciones...",
FoundUpdate: (x: string) => `Se encontró una nueva versión: ${x}`,
GoToUpdate: "Actualizar",
},
SendKey: "Tecla de envío",
Theme: "Tema",
TightBorder: "Borde ajustado",
SendPreviewBubble: "Enviar burbuja de vista previa",
Prompt: {
Disable: {
Title: "Desactivar autocompletado",
SubTitle: "Escribe / para activar el autocompletado",
},
List: "Lista de autocompletado",
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",
SubTitle: "Número de mensajes enviados adjuntos por solicitud",
},
CompressThreshold: {
Title: "Umbral de compresión de historial",
SubTitle:
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
},
Token: {
Title: "Clave de API",
SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso",
Placeholder: "Clave de la API de OpenAI",
},
Usage: {
Title: "Saldo de la cuenta",
SubTitle(used: any, total: any) {
return `Usado $${used}, subscription $${total}`;
},
IsChecking: "Comprobando...",
Check: "Comprobar de nuevo",
NoAccess: "Introduzca la clave API para comprobar el saldo",
},
AccessCode: {
Title: "Código de acceso",
SubTitle: "Control de acceso habilitado",
Placeholder: "Necesita código de acceso",
},
Model: "Modelo",
Temperature: {
Title: "Temperatura",
SubTitle: "Un valor mayor genera una salida más aleatoria",
},
MaxTokens: {
Title: "Máximo de tokens",
SubTitle: "Longitud máxima de tokens de entrada y tokens generados",
},
PresencePenlty: {
Title: "Penalización de presencia",
SubTitle:
"Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas",
},
},
Store: {
DefaultTopic: "Nueva conversación",
BotHello: "¡Hola! ¿Cómo puedo ayudarte hoy?",
Error: "Algo salió mal, por favor intenta nuevamente más tarde.",
Prompt: {
History: (content: string) =>
"Este es un resumen del historial del chat entre la IA y el usuario como recapitulación: " +
content,
Topic:
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
Summarize:
"Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
},
ConfirmClearAll:
"¿Confirmar para borrar todos los datos de chat y configuración?",
},
Copy: {
Success: "Copiado al portapapeles",
Failed:
"La copia falló, por favor concede permiso para acceder al portapapeles",
},
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Contextual and Memory Prompts",
Add: "Add One",
},
};
export default es;

View File

@@ -1,53 +1,63 @@
import CN from './cn'
import EN from './en'
import CN from "./cn";
import EN from "./en";
import TW from "./tw";
import ES from "./es";
import IT from "./it";
export type { LocaleType } from './cn'
export type { LocaleType } from "./cn";
type Lang = 'en' | 'cn'
export const AllLangs = ["en", "cn", "tw", "es", "it"] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = 'lang'
const LANG_KEY = "lang";
function getItem(key: string) {
try {
return localStorage.getItem(key)
} catch {
return null
}
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function setItem(key: string, value: string) {
try {
localStorage.setItem(key, value)
} catch { }
try {
localStorage.setItem(key, value);
} catch {}
}
function getLanguage() {
try {
return navigator.language.toLowerCase()
} catch {
return 'cn'
}
try {
return navigator.language.toLowerCase();
} catch {
return "cn";
}
}
export function getLang(): Lang {
const savedLang = getItem(LANG_KEY)
const savedLang = getItem(LANG_KEY);
if (['en', 'cn'].includes(savedLang ?? '')) {
return savedLang as Lang
}
if (AllLangs.includes((savedLang ?? "") as Lang)) {
return savedLang as Lang;
}
const lang = getLanguage()
const lang = getLanguage();
if (lang.includes('zh') || lang.includes('cn')) {
return 'cn'
} else {
return 'en'
}
if (lang.includes("zh") || lang.includes("cn")) {
return "cn";
} else if (lang.includes("tw")) {
return "tw";
} else if (lang.includes("es")) {
return "es";
} else if (lang.includes("it")) {
return "it";
} else {
return "en";
}
}
export function changeLang(lang: Lang) {
setItem(LANG_KEY, lang)
location.reload()
setItem(LANG_KEY, lang);
location.reload();
}
export default { en: EN, cn: CN }[getLang()]
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT }[getLang()];

173
app/locales/it.ts Normal file
View File

@@ -0,0 +1,173 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const it: LocaleType = {
WIP: "Work in progress...",
Error: {
Unauthorized:
"Accesso non autorizzato, inserire il codice di accesso nella pagina delle impostazioni.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} messaggi`,
},
Chat: {
SubTitle: (count: number) => `${count} messaggi con ChatGPT`,
Actions: {
ChatList: "Vai alla Chat List",
CompressedHistory: "Prompt di memoria della cronologia compressa",
Export: "Esportazione di tutti i messaggi come Markdown",
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
},
Rename: "Rinomina Chat",
Typing: "Typing…",
Input: (submitKey: string) => {
var inputHints = `Scrivi qualcosa e premi ${submitKey} per inviare`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", premi Shift + Enter per andare a capo";
}
return inputHints;
},
Send: "Invia",
},
Export: {
Title: "Tutti i messaggi",
Copy: "Copia tutto",
Download: "Scarica",
MessageFromYou: "Messaggio da te",
MessageFromChatGPT: "Messaggio da ChatGPT",
},
Memory: {
Title: "Prompt di memoria",
EmptyContent: "Vuoto.",
Copy: "Copia tutto",
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
},
Settings: {
Title: "Impostazioni",
SubTitle: "Tutte le impostazioni",
Actions: {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
},
Lang: {
Name: "Lingue",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Dimensione carattere",
SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
},
Update: {
Version: (x: string) => `Versione: ${x}`,
IsLatest: "Ultima versione",
CheckUpdate: "Controlla aggiornamenti",
IsChecking: "Sto controllando gli aggiornamenti...",
FoundUpdate: (x: string) => `Trovata nuova versione: ${x}`,
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
Theme: "tema",
TightBorder: "Bordi stretti",
SendPreviewBubble: "Invia l'anteprima della bolla",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
SubTitle: "Input / per attivare il completamento automatico",
},
List: "Elenco dei suggerimenti",
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Modifica",
},
HistoryCount: {
Title: "Conteggio dei messaggi allegati",
SubTitle: "Numero di messaggi inviati allegati per richiesta",
},
CompressThreshold: {
Title: "Soglia di compressione della cronologia",
SubTitle:
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
Title: "Chiave API",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
return `Usato in questo mese $${used}, subscription $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",
NoAccess: "Inserire la chiave API per controllare il saldo",
},
AccessCode: {
Title: "Codice d'accesso",
SubTitle: "Controllo d'accesso abilitato",
Placeholder: "Inserisci il codice d'accesso",
},
Model: "Modello GPT",
Temperature: {
Title: "Temperature",
SubTitle: "Un valore maggiore rende l'output più casuale",
},
MaxTokens: {
Title: "Token massimi",
SubTitle: "Lunghezza massima dei token in ingresso e dei token generati",
},
PresencePenlty: {
Title: "Penalità di presenza",
SubTitle:
"Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti",
},
},
Store: {
DefaultTopic: "Nuova conversazione",
BotHello: "Ciao, come posso aiutarti oggi?",
Error: "Qualcosa è andato storto, riprova più tardi.",
Prompt: {
History: (content: string) =>
"Questo è un riassunto della cronologia delle chat tra l'IA e l'utente:" +
content,
Topic:
"Si prega di generare un titolo di quattro o cinque parole che riassuma la nostra conversazione senza alcuna traccia, punteggiatura, virgolette, punti, simboli o testo aggiuntivo. Rimuovere le virgolette",
Summarize:
"Riassumi brevemente la nostra discussione in 200 caratteri o meno per usarla come spunto per una futura conversazione.",
},
ConfirmClearAll:
"Confermi la cancellazione di tutti i dati della chat e delle impostazioni?",
},
Copy: {
Success: "Copiato sugli appunti",
Failed:
"Copia fallita, concedere l'autorizzazione all'accesso agli appunti",
},
Context: {
Toast: (x: any) => `Con ${x} prompts contestuali`,
Edit: "Prompt contestuali e di memoria",
Add: "Aggiungi altro",
},
};
export default it;

164
app/locales/tw.ts Normal file
View File

@@ -0,0 +1,164 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const tw: LocaleType = {
WIP: "該功能仍在開發中……",
Error: {
Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 條對話`,
},
Chat: {
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
Actions: {
ChatList: "查看消息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
Retry: "重試",
},
Rename: "重命名對話",
Typing: "正在輸入…",
Input: (submitKey: string) => {
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 鍵換行";
}
return inputHints;
},
Send: "發送",
},
Export: {
Title: "匯出聊天記錄為 Markdown",
Copy: "複製全部",
Download: "下載檔案",
MessageFromYou: "來自你的訊息",
MessageFromChatGPT: "來自 ChatGPT 的訊息",
},
Memory: {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
Send: "發送記憶",
Reset: "重置對話",
ResetConfirm: "重置後將清空當前對話記錄以及歷史記憶,確認重置?",
},
Home: {
NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?",
DeleteToast: "已刪除對話",
Revert: "撤銷",
},
Settings: {
Title: "設定",
SubTitle: "設定選項",
Actions: {
ClearAll: "清除所有數據",
ResetAll: "重置所有設定",
Close: "關閉",
},
Lang: {
Name: "Language",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
},
},
Avatar: "大頭貼",
FontSize: {
Title: "字型大小",
SubTitle: "聊天內容的字型大小",
},
Update: {
Version: (x: string) => `當前版本:${x}`,
IsLatest: "已是最新版本",
CheckUpdate: "檢查更新",
IsChecking: "正在檢查更新...",
FoundUpdate: (x: string) => `發現新版本:${x}`,
GoToUpdate: "前往更新",
},
SendKey: "發送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
SendPreviewBubble: "發送預覽氣泡",
Prompt: {
Disable: {
Title: "停用提示詞自動補全",
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全",
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom}`,
Edit: "編輯",
},
HistoryCount: {
Title: "附帶歷史訊息數",
SubTitle: "每次請求附帶的歷史訊息數",
},
CompressThreshold: {
Title: "歷史訊息長度壓縮閾值",
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可規避授權訪問限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "帳戶餘額",
SubTitle(used: any, total: any) {
return `本月已使用 $${used},订阅总额 $${total}`;
},
IsChecking: "正在檢查…",
Check: "重新檢查",
NoAccess: "輸入API Key查看餘額",
},
AccessCode: {
Title: "授權碼",
SubTitle: "現在是未授權訪問狀態",
Placeholder: "請輸入授權碼",
},
Model: "模型 (model)",
Temperature: {
Title: "隨機性 (temperature)",
SubTitle: "值越大,回復越隨機",
},
MaxTokens: {
Title: "單次回復限制 (max_tokens)",
SubTitle: "單次交互所用的最大 Token 數",
},
PresencePenlty: {
Title: "話題新穎度 (presence_penalty)",
SubTitle: "值越大,越有可能擴展到新話題",
},
},
Store: {
DefaultTopic: "新的對話",
BotHello: "請問需要我的協助嗎?",
Error: "出錯了,請稍後再嘗試",
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Summarize:
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt且字數控制在 200 字以內",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
Copy: {
Success: "已複製到剪貼簿中",
Failed: "複製失敗,請賦予剪貼簿權限",
},
Context: {
Toast: (x: any) => `已設置 ${x} 條前置上下文`,
Edit: "前置上下文和歷史記憶",
Add: "新增壹條",
},
};
export default tw;

View File

@@ -1,5 +1,6 @@
import { Analytics } from "@vercel/analytics/react";
import { Home } from './components/home'
import { Home } from "./components/home";
export default function App() {
return (

27
app/polyfill.ts Normal file
View File

@@ -0,0 +1,27 @@
declare global {
interface Array<T> {
at(index: number): T | undefined;
}
}
if (!Array.prototype.at) {
Array.prototype.at = function (index: number) {
// Get the length of the array
const length = this.length;
// Convert negative index to a positive index
if (index < 0) {
index = length + index;
}
// Return undefined if the index is out of range
if (index < 0 || index >= length) {
return undefined;
}
// Use Array.prototype.slice method to get value at the specified index
return Array.prototype.slice.call(this, index, index + 1)[0];
};
}
export {};

View File

@@ -1,5 +1,6 @@
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
import { filterConfig, Message, ModelConfig } from "./store";
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
import { showToast } from "./components/ui-lib";
const TIME_OUT_MS = 30000;
@@ -19,25 +20,100 @@ const makeRequestParam = (
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
}
const modelConfig = useChatStore.getState().config.modelConfig;
return {
model: "gpt-3.5-turbo",
messages: sendMessages,
stream: options?.stream,
...modelConfig,
};
};
function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {};
if (accessStore.enabledAccessControl()) {
headers["access-code"] = accessStore.accessCode;
}
if (accessStore.token && accessStore.token.length > 0) {
headers["token"] = accessStore.token;
}
return headers;
}
export function requestOpenaiClient(path: string) {
return (body: any, method = "POST") =>
fetch("/api/openai?_vercel_no_cache=1", {
method,
headers: {
"Content-Type": "application/json",
path,
...getHeaders(),
},
body: body && JSON.stringify(body),
});
}
export async function requestChat(messages: Message[]) {
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
const res = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(req),
});
const res = await requestOpenaiClient("v1/chat/completions")(req);
return (await res.json()) as ChatReponse;
try {
const response = (await res.json()) as ChatReponse;
return response;
} catch (error) {
console.error("[Request Chat] ", error, res.body);
}
}
export async function requestUsage() {
const formatDate = (d: Date) =>
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
.getDate()
.toString()
.padStart(2, "0")}`;
const ONE_DAY = 2 * 24 * 60 * 60 * 1000;
const now = new Date(Date.now() + ONE_DAY);
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth);
const endDate = formatDate(now);
const [used, subs] = await Promise.all([
requestOpenaiClient(
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
)(null, "GET"),
requestOpenaiClient("dashboard/billing/subscription")(null, "GET"),
]);
const response = (await used.json()) as {
total_usage?: number;
error?: {
type: string;
message: string;
};
};
const total = (await subs.json()) as {
hard_limit_usd?: number;
};
if (response.error && response.error.type) {
showToast(response.error.message);
return;
}
if (response.total_usage) {
response.total_usage = Math.round(response.total_usage) / 100;
}
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
};
}
export async function requestChatStream(
@@ -46,7 +122,8 @@ export async function requestChatStream(
filterBot?: boolean;
modelConfig?: ModelConfig;
onMessage: (message: string, done: boolean) => void;
onError: (error: Error) => void;
onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void;
}
) {
const req = makeRequestParam(messages, {
@@ -54,11 +131,6 @@ export async function requestChatStream(
filterBot: options?.filterBot,
});
// valid and assign model config
if (options?.modelConfig) {
Object.assign(req, filterConfig(options.modelConfig));
}
console.log("[Request] ", req);
const controller = new AbortController();
@@ -69,6 +141,8 @@ export async function requestChatStream(
method: "POST",
headers: {
"Content-Type": "application/json",
path: "v1/chat/completions",
...getHeaders(),
},
body: JSON.stringify(req),
signal: controller.signal,
@@ -86,6 +160,8 @@ export async function requestChatStream(
const reader = res.body?.getReader();
const decoder = new TextDecoder();
options?.onController?.(controller);
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
@@ -103,13 +179,16 @@ export async function requestChatStream(
}
finish();
} else if (res.status === 401) {
console.error("Anauthorized");
options?.onError(new Error("Anauthorized"), res.status);
} else {
console.error("Stream Error");
options?.onError(new Error("Stream Error"));
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
}
} catch (err) {
console.error("NetWork Error");
options?.onError(new Error("NetWork Error"));
console.error("NetWork Error", err);
options?.onError(err as Error);
}
}
@@ -124,5 +203,35 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
const res = await requestChat(messages);
return res.choices.at(0)?.message?.content ?? "";
return res?.choices?.at(0)?.message?.content ?? "";
}
// To store message streaming controller
export const ControllerPool = {
controllers: {} as Record<string, AbortController>,
addController(
sessionIndex: number,
messageId: number,
controller: AbortController
) {
const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
const controller = this.controllers[key];
controller?.abort();
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
},
key(sessionIndex: number, messageIndex: number) {
return `${sessionIndex},${messageIndex}`;
},
};

36
app/store/access.ts Normal file
View File

@@ -0,0 +1,36 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
export interface AccessControlStore {
accessCode: string;
token: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
}
export const ACCESS_KEY = "access-control";
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
enabledAccessControl() {
return queryMeta("access") === "enabled";
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
},
updateToken(token: string) {
set((state) => ({ token }));
},
}),
{
name: ACCESS_KEY,
version: 1,
}
)
);

View File

@@ -2,21 +2,39 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import { requestChatStream, requestWithPrompt } from "../requests";
import { trimTopic } from "../utils";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales";
import { showToast } from "../components/ui-lib";
export type Message = ChatCompletionResponseMessage & {
date: string;
streaming?: boolean;
isError?: boolean;
id?: number;
};
export function createMessage(override: Partial<Message>): Message {
return {
id: Date.now(),
date: new Date().toLocaleString(),
role: "user",
content: "",
...override,
};
}
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
@@ -26,14 +44,17 @@ export enum Theme {
}
export interface ChatConfig {
maxToken?: number;
historyMessageCount: number; // -1 means all
compressMessageLengthThreshold: number;
sendBotMessages: boolean; // send bot's message or not
submitKey: SubmitKey;
avatar: string;
fontSize: number;
theme: Theme;
tightBorder: boolean;
sendPreviewBubble: boolean;
disablePromptHint: boolean;
modelConfig: {
model: string;
@@ -45,22 +66,26 @@ export interface ChatConfig {
export type ModelConfig = ChatConfig["modelConfig"];
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
{
name: "gpt-4",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: false,
available: ENABLE_GPT4,
},
{
name: "gpt-3.5-turbo",
@@ -72,41 +97,39 @@ export const ALL_MODELS = [
},
];
export function isValidModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available);
export function limitNumber(
x: number,
min: number,
max: number,
defaultValue: number,
) {
if (typeof x !== "number" || isNaN(x)) {
return defaultValue;
}
return Math.min(max, Math.max(min, x));
}
export function isValidNumber(x: number, min: number, max: number) {
return typeof x === "number" && x <= max && x >= min;
export function limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available)
? name
: ALL_MODELS[4].name;
}
export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
const validator: {
[k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean;
} = {
model(x) {
return isValidModel(x as string);
},
max_tokens(x) {
return isValidNumber(x as number, 100, 4000);
},
presence_penalty(x) {
return isValidNumber(x as number, -2, 2);
},
temperature(x) {
return isValidNumber(x as number, 0, 1);
},
};
Object.keys(validator).forEach((k) => {
const key = k as keyof ModelConfig;
if (!validator[key](config[key])) {
delete config[key];
}
});
return config;
}
export const ModalConfigValidator = {
model(x: string) {
return limitModel(x);
},
max_tokens(x: number) {
return limitNumber(x, 0, 32000, 2000);
},
presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0);
},
temperature(x: number) {
return limitNumber(x, 0, 2, 1);
},
};
const DEFAULT_CONFIG: ChatConfig = {
historyMessageCount: 4,
@@ -114,8 +137,12 @@ const DEFAULT_CONFIG: ChatConfig = {
sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
disablePromptHint: false,
modelConfig: {
model: "gpt-3.5-turbo",
@@ -134,7 +161,9 @@ export interface ChatStat {
export interface ChatSession {
id: number;
topic: string;
sendMemory: boolean;
memoryPrompt: string;
context: Message[];
messages: Message[];
stat: ChatStat;
lastUpdate: string;
@@ -142,6 +171,10 @@ export interface ChatSession {
}
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = createMessage({
role: "assistant",
content: Locale.Store.BotHello,
});
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
@@ -149,14 +182,10 @@ function createEmptySession(): ChatSession {
return {
id: Date.now(),
topic: DEFAULT_TOPIC,
sendMemory: true,
memoryPrompt: "",
messages: [
{
role: "assistant",
content: Locale.Store.BotHello,
date: createDate,
},
],
context: [],
messages: [],
stat: {
tokenCount: 0,
wordCount: 0,
@@ -171,9 +200,12 @@ interface ChatStore {
config: ChatConfig;
sessions: ChatSession[];
currentSessionIndex: number;
clearSessions: () => void;
removeSession: (index: number) => void;
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
deleteSession: () => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
@@ -183,8 +215,9 @@ interface ChatStore {
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
updater: (message?: Message) => void,
) => void;
resetSession: () => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
@@ -194,6 +227,10 @@ interface ChatStore {
clearAllData: () => void;
}
function countMessages(msgs: Message[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
}
const LOCAL_KEY = "chat-next-web-store";
export const useChatStore = create<ChatStore>()(
@@ -205,6 +242,13 @@ export const useChatStore = create<ChatStore>()(
...DEFAULT_CONFIG,
},
clearSessions() {
set(() => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
}));
},
resetConfig() {
set(() => ({ config: { ...DEFAULT_CONFIG } }));
},
@@ -250,6 +294,31 @@ export const useChatStore = create<ChatStore>()(
});
},
moveSession(from: number, to: number) {
set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state;
// move the session
const newSessions = [...sessions];
const session = newSessions[from];
newSessions.splice(from, 1);
newSessions.splice(to, 0, session);
// modify current session id
let newIndex = oldIndex === from ? to : oldIndex;
if (oldIndex > from && oldIndex <= to) {
newIndex -= 1;
} else if (oldIndex < from && oldIndex >= to) {
newIndex += 1;
}
return {
currentSessionIndex: newIndex,
sessions: newSessions,
};
});
},
newSession() {
set((state) => ({
currentSessionIndex: 0,
@@ -257,6 +326,26 @@ export const useChatStore = create<ChatStore>()(
}));
},
deleteSession() {
const deletedSession = get().currentSession();
const index = get().currentSessionIndex;
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
}
showToast(Locale.Home.DeleteToast, {
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(state.sessions.slice(index + Number(isLastSession))),
}));
},
});
},
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
@@ -280,22 +369,21 @@ export const useChatStore = create<ChatStore>()(
},
async onUserInput(content) {
const userMessage: Message = {
const userMessage: Message = createMessage({
role: "user",
content,
date: new Date().toLocaleString(),
};
});
const botMessage: Message = {
content: "",
const botMessage: Message = createMessage({
role: "assistant",
date: new Date().toLocaleString(),
streaming: true,
};
});
// get recent messages
const recentMessages = get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage);
const sessionIndex = get().currentSessionIndex;
const messageIndex = get().currentSession().messages.length + 1;
// save user's and bot's message
get().updateCurrentSession((session) => {
@@ -303,21 +391,43 @@ export const useChatStore = create<ChatStore>()(
session.messages.push(botMessage);
});
// make request
console.log("[User Input] ", sendMessages);
requestChatStream(sendMessages, {
onMessage(content, done) {
// stream response
if (done) {
botMessage.streaming = false;
botMessage.content = content;
get().onNewMessage(botMessage);
ControllerPool.remove(
sessionIndex,
botMessage.id ?? messageIndex,
);
} else {
botMessage.content = content;
set(() => ({}));
}
},
onError(error) {
botMessage.content += "\n\n" + Locale.Store.Error;
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
userMessage.isError = true;
botMessage.isError = true;
set(() => ({}));
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
botMessage.id ?? messageIndex,
controller,
);
},
filterBot: !get().config.sendBotMessages,
modelConfig: get().config.modelConfig,
@@ -337,24 +447,31 @@ export const useChatStore = create<ChatStore>()(
getMessagesWithMemory() {
const session = get().currentSession();
const config = get().config;
const n = session.messages.length;
const recentMessages = session.messages.slice(
n - config.historyMessageCount
);
const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length;
const memoryPrompt = get().getMemoryPrompt();
const context = session.context.slice();
if (session.memoryPrompt) {
recentMessages.unshift(memoryPrompt);
if (
session.sendMemory &&
session.memoryPrompt &&
session.memoryPrompt.length > 0
) {
const memoryPrompt = get().getMemoryPrompt();
context.push(memoryPrompt);
}
const recentMessages = context.concat(
messages.slice(Math.max(0, n - config.historyMessageCount)),
);
return recentMessages;
},
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
updater: (message?: Message) => void,
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
@@ -363,32 +480,43 @@ export const useChatStore = create<ChatStore>()(
set(() => ({ sessions }));
},
resetSession() {
get().updateCurrentSession((session) => {
session.messages = [];
session.memoryPrompt = "";
});
},
summarizeSession() {
const session = get().currentSession();
if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) {
// should summarize topic
// should summarize topic after chating more than 50 words
const SUMMARIZE_MIN_LEN = 50;
if (
session.topic === DEFAULT_TOPIC &&
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
) {
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
(res) => {
get().updateCurrentSession(
(session) => (session.topic = trimTopic(res))
(session) =>
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
);
}
},
);
}
const config = get().config;
let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex
);
const historyMsgLength = toBeSummarizedMsgs.reduce(
(pre, cur) => pre + cur.content.length,
0
session.lastSummarizeIndex,
);
if (historyMsgLength > 4000) {
const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
-config.historyMessageCount
Math.max(0, n - config.historyMessageCount),
);
}
@@ -401,7 +529,7 @@ export const useChatStore = create<ChatStore>()(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
config.compressMessageLengthThreshold
config.compressMessageLengthThreshold,
);
if (historyMsgLength > config.compressMessageLengthThreshold) {
@@ -423,7 +551,7 @@ export const useChatStore = create<ChatStore>()(
onError(error) {
console.error("[Summarize] ", error);
},
}
},
);
}
},
@@ -451,7 +579,20 @@ export const useChatStore = create<ChatStore>()(
}),
{
name: LOCAL_KEY,
version: 1,
}
)
version: 1.2,
migrate(persistedState, version) {
const state = persistedState as ChatStore;
if (version === 1) {
state.sessions.forEach((s) => (s.context = []));
}
if (version < 1.2) {
state.sessions.forEach((s) => (s.sendMemory = true));
}
return state;
},
},
),
);

View File

@@ -1,2 +1,3 @@
export * from "./app";
export * from "./update";
export * from "./access";

117
app/store/prompt.ts Normal file
View File

@@ -0,0 +1,117 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
export interface Prompt {
id?: number;
title: string;
content: string;
}
export interface PromptStore {
latestId: number;
prompts: Map<number, Prompt>;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
engine: new Fuse<Prompt>([], { keys: ["title"] }),
count: {
builtin: 0,
},
init(prompts: Prompt[]) {
if (this.ready) {
return;
}
this.engine.setCollection(prompts);
this.ready = true;
},
remove(id: number) {
this.engine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
this.engine.add(prompt);
},
search(text: string) {
const results = this.engine.search(text);
return results.map((v) => v.item);
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
latestId: 0,
prompts: new Map(),
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
prompts.set(prompt.id, prompt);
set(() => ({
latestId: prompt.id!,
prompts: prompts,
}));
return prompt.id!;
},
remove(id) {
const prompts = get().prompts;
prompts.delete(id);
SearchService.remove(id);
set(() => ({
prompts,
}));
},
search(text) {
return SearchService.search(text) as Prompt[];
},
}),
{
name: PROMPT_KEY,
version: 1,
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>;
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
const builtinPrompts = [res.en, res.cn]
.map((promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
title,
content,
} as Prompt),
);
})
.concat([...(state?.prompts?.values() ?? [])]);
const allPromptsForSearch = builtinPrompts.reduce(
(pre, cur) => pre.concat(cur),
[],
);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch);
});
},
},
),
);

View File

@@ -1,7 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL } from "../constant";
import { getCurrentCommitId } from "../utils";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { getCurrentVersion } from "../utils";
export interface UpdateStore {
lastUpdate: number;
@@ -19,16 +19,17 @@ export const useUpdateStore = create<UpdateStore>()(
remoteId: "",
async getLatestCommitId(force = false) {
const overOneHour = Date.now() - get().lastUpdate > 3600 * 1000;
const shouldFetch = force || overOneHour;
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins;
if (!shouldFetch) {
return getCurrentCommitId();
return getCurrentVersion();
}
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
// const remoteId = data[0].name as string;
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const sha = data[0].sha as string;
const remoteId = sha.substring(0, 7);
const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({
lastUpdate: Date.now(),
remoteId,
@@ -37,13 +38,13 @@ export const useUpdateStore = create<UpdateStore>()(
return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
return getCurrentCommitId();
return getCurrentVersion();
}
},
}),
{
name: UPDATE_KEY,
version: 1,
}
)
},
),
);

23
app/styles/animation.scss Normal file
View File

@@ -0,0 +1,23 @@
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0px);
}
}
@keyframes slide-in-from-top {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0px);
}
}

View File

@@ -7,6 +7,7 @@
--second: rgb(231, 248, 255);
--hover-color: #f3f3f3;
--bar-color: rgba(0, 0, 0, 0.1);
--theme-color: var(--gray);
/* shadow */
--shadow: 50px 50px 100px 10px rgb(0, 0, 0, 0.1);
@@ -28,6 +29,8 @@
--bar-color: rgba(255, 255, 255, 0.1);
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
}
.light {
@@ -50,12 +53,13 @@
--sidebar-width: 300px;
--window-content-width: calc(100% - var(--sidebar-width));
--message-max-width: 80%;
--full-height: 100%;
}
@media only screen and (max-width: 600px) {
:root {
--window-width: 100vw;
--window-height: 100vh;
--window-height: var(--full-height);
--sidebar-width: 100vw;
--window-content-width: var(--window-width);
--message-max-width: 100%;
@@ -71,13 +75,16 @@
@include dark;
}
}
html {
height: var(--full-height);
}
body {
background-color: var(--gray);
color: var(--black);
margin: 0;
padding: 0;
height: 100vh;
height: var(--full-height);
width: 100vw;
display: flex;
justify-content: center;
@@ -85,6 +92,10 @@ body {
user-select: none;
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
@media only screen and (max-width: 600px) {
background-color: var(--second);
}
}
::-webkit-scrollbar {
@@ -106,12 +117,21 @@ body {
select {
border: var(--border-in-light);
padding: 8px 10px;
padding: 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;
background-color: var(--white);
color: var(--black);
text-align: center;
}
label {
cursor: pointer;
}
input {
text-align: center;
}
input[type="checkbox"] {
@@ -139,19 +159,11 @@ input[type="checkbox"]:checked::after {
input[type="range"] {
appearance: none;
border: var(--border-in-light);
border-radius: 10px;
padding: 5px 15px 5px 10px;
background-color: var(--white);
color: var(--black);
&::before {
content: attr(value);
font-size: 12px;
}
}
input[type="range"]::-webkit-slider-thumb {
@mixin thumb() {
appearance: none;
height: 8px;
width: 20px;
@@ -160,22 +172,50 @@ input[type="range"]::-webkit-slider-thumb {
cursor: pointer;
transition: all ease 0.3s;
margin-left: 5px;
border: none;
}
input[type="range"]::-webkit-slider-thumb:hover {
input[type="range"]::-webkit-slider-thumb {
@include thumb();
}
input[type="range"]::-moz-range-thumb {
@include thumb();
}
input[type="range"]::-ms-thumb {
@include thumb();
}
@mixin thumbHover() {
transform: scaleY(1.2);
width: 24px;
}
input[type="number"] {
input[type="range"]::-webkit-slider-thumb:hover {
@include thumbHover();
}
input[type="range"]::-moz-range-thumb:hover {
@include thumbHover();
}
input[type="range"]::-ms-thumb:hover {
@include thumbHover();
}
input[type="number"],
input[type="text"],
input[type="password"] {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
height: 32px;
min-height: 36px;
box-sizing: border-box;
background: var(--white);
color: var(--black);
padding: 0 10px;
max-width: 50%;
}
div.math {
@@ -187,7 +227,7 @@ div.math {
position: fixed;
top: 0;
left: 0;
height: 100vh;
height: var(--full-height);
width: 100vw;
background-color: rgba($color: #000000, $alpha: 0.5);
display: flex;
@@ -204,3 +244,64 @@ div.math {
text-decoration: underline;
}
}
pre {
position: relative;
&:hover .copy-code-button {
pointer-events: all;
transform: translateX(0px);
opacity: 0.5;
}
.copy-code-button {
position: absolute;
right: 10px;
top: 1em;
cursor: pointer;
padding: 0px 5px;
background-color: var(--black);
color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
transform: translateX(10px);
pointer-events: none;
opacity: 0;
transition: all ease 0.3s;
&:after {
content: "copy";
}
&:hover {
opacity: 1;
}
}
}
.clickable {
cursor: pointer;
div:not(.no-dark) > svg {
filter: invert(0.5);
}
&:hover {
filter: brightness(0.9);
}
}
.error {
width: 80%;
border-radius: 20px;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
padding: 20px;
overflow: auto;
background-color: var(--white);
color: var(--black);
pre {
overflow: auto;
}
}

115
app/styles/highlight.scss Normal file
View File

@@ -0,0 +1,115 @@
.markdown-body {
pre {
padding: 0;
}
pre,
code {
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
}
pre code {
display: block;
overflow-x: auto;
padding: 1em;
}
code {
padding: 3px 5px;
}
.hljs,
pre {
background: #1a1b26;
color: #cbd2ea;
}
/*!
Theme: Tokyo-night-Dark
origin: https://github.com/enkia/tokyo-night-vscode-theme
Description: Original highlight.js style
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
License: see project LICENSE
Touched: 2022
*/
.hljs-comment,
.hljs-meta {
color: #565f89;
}
.hljs-deletion,
.hljs-doctag,
.hljs-regexp,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id,
.hljs-selector-pseudo,
.hljs-tag,
.hljs-template-tag,
.hljs-variable.language_ {
color: #f7768e;
}
.hljs-link,
.hljs-literal,
.hljs-number,
.hljs-params,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #ff9e64;
}
.hljs-attribute,
.hljs-built_in {
color: #e0af68;
}
.hljs-keyword,
.hljs-property,
.hljs-subst,
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
color: #7dcfff;
}
.hljs-selector-tag {
color: #73daca;
}
.hljs-addition,
.hljs-bullet,
.hljs-quote,
.hljs-string,
.hljs-symbol {
color: #9ece6a;
}
.hljs-code,
.hljs-formula,
.hljs-section {
color: #7aa2f7;
}
.hljs-attr,
.hljs-char.escape_,
.hljs-keyword,
.hljs-name,
.hljs-operator {
color: #bb9af7;
}
.hljs-punctuation {
color: #c0caf5;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}

View File

@@ -839,21 +839,20 @@
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
padding: 16px 16px 8px 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
display: inline-block;
max-width: 100%;
padding: 0;
margin: 0;
overflow: visible;
overflow-x: scroll;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
@@ -1117,4 +1116,4 @@
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
}

View File

@@ -1,147 +0,0 @@
.markdown-body {
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #282a36;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6272a4;
}
.token.punctuation {
color: #f8f8f2;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ff79c6;
}
.token.boolean,
.token.number {
color: #bd93f9;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #50fa7b;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #f1fa8c;
}
.token.keyword {
color: #8be9fd;
}
.token.regex,
.token.important {
color: #ffb86c;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
}
@mixin light {
.markdown-body pre[class*="language-"] {
filter: invert(1) hue-rotate(50deg) brightness(1.3);
}
}
@mixin dark {
.markdown-body pre[class*="language-"] {
filter: none;
}
}
:root {
@include light();
}
.light {
@include light();
}
.dark {
@include dark();
}
@media (prefers-color-scheme: dark) {
:root {
@include dark();
}
}

View File

@@ -1,34 +1,36 @@
import { EmojiStyle } from "emoji-picker-react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
export function trimTopic(topic: string) {
const s = topic.split("");
let lastChar = s.at(-1); // 获取 s 的最后一个字符
let pattern = /[,。!?、]/; // 定义匹配中文标点符号的正则表达式
while (lastChar && pattern.test(lastChar!)) {
s.pop();
lastChar = s.at(-1);
}
return s.join("");
return topic.replace(/[,。!?”“"、,.!?]*$/, "");
}
export function copyToClipboard(text: string) {
navigator.clipboard
.writeText(text)
.then((res) => {
export async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
showToast(Locale.Copy.Success);
} catch (error) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
showToast(Locale.Copy.Success);
})
.catch((err) => {
} catch (error) {
showToast(Locale.Copy.Failed);
});
}
document.body.removeChild(textArea);
}
}
export function downloadAs(text: string, filename: string) {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
element.setAttribute("download", filename);
@@ -45,6 +47,10 @@ export function isIOS() {
return /iphone|ipad|ipod/.test(userAgent);
}
export function isMobileScreen() {
return window.innerWidth <= 600;
}
export function selectOrCopy(el: HTMLElement, content: string) {
const currentSelection = window.getSelection();
@@ -57,20 +63,31 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true;
}
export function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
let currentId: string;
export function getCurrentCommitId() {
export function getCurrentVersion() {
if (currentId) {
return currentId;
}
if (document) {
const meta = document.head.querySelector(
"meta[name='version']"
) as HTMLMetaElement;
currentId = meta?.content ?? "";
} else {
currentId = process.env.COMMIT_ID ?? "";
}
currentId = queryMeta("version");
return currentId;
}
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}

138
docs/faq-cn.md Normal file
View File

@@ -0,0 +1,138 @@
# 常见问题
> We are sorry that there is currently no English version of the FAQ. English users can use translation tools to access this document. We look forward to receiving your PR for an English version of the documentation.
## 如何快速获得帮助?
1. 询问ChatGPT / Bing / 百度 / Google等。
2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。
# 部署相关问题
## 为什么 Docker 部署版本一直提示更新
Docker 版本相当于稳定版latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。
## 如何部署在Vercel上
1. 注册Github账号fork该项目
2. 注册Vercel需手机验证可以用中国号码连接你的Github账户
3. Vercel上新建项目选择你在Github fork的项目按需填写环境变量开始部署。部署之后你可以在有梯子的条件下通过vercel提供的域名访问你的项目。
4. 如果需要在国内无墙访问在你的域名管理网站添加一条域名的CNAME记录指向cname.vercel-dns.com。之后在Vercel上设置你的域名访问。
## 如何修改 Vercel 环境变量
- 进入 vercel 的控制台页面;
- 选中你的 chatgpt next web 项目;
- 点击页面头部的 Settings 选项;
- 找到侧边栏的 Environment Variables 选项;
- 修改对应的值即可。
## 环境变量CODE是什么必须设置吗
这是你自定义的访问密码,你可以选择:
1. 不设置,删除该环境变量即可。谨慎:此时任何人可以访问你的项目。
2. 部署项目时设置环境变量CODE支持多个密码逗号分隔。设置访问密码后用户需要在设置界面输入访问密码才可以使用。参见[相关说明](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
## 为什么我部署的版本没有流式响应
> 相关讨论:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
如果你使用 ngnix 反向代理,需要在配置文件中增加下列代码:
```
# 不缓存,支持流式输出
proxy_cache off; # 关闭缓存
proxy_buffering off; # 关闭代理缓冲
chunked_transfer_encoding on; # 开启分块传输编码
tcp_nopush on; # 开启TCP NOPUSH选项禁止Nagle算法
tcp_nodelay on; # 开启TCP NODELAY选项禁止延迟ACK算法
keepalive_timeout 300; # 设定keep-alive超时时间为65秒
```
如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。
## 我部署好了,但是无法访问
请检查排除以下问题:
- 服务启动了吗?
- 端口正确映射了吗?
- 防火墙开放端口了吗?
- 到服务器的路由通吗?
- 域名正确解析了吗?
# 使用相关问题
## 为什么会一直提示“出错了,稍后重试吧”
原因可能有很多,请依次排查:
- 请先检查你的代码版本是否为最新版本,更新到最新版本后重试;
- 请检查 api key 是否设置正确,环境变量名称必须为全大写加下划线;
- 请检查 api key 是否可用;
- 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue并附上 vercel 的 runtime log 或者 docker 运行时的 log。
## 为什么 ChatGPT 的回复会乱码
设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1那么就有可能造成回复乱码将其调回 1 以内即可。
## 使用时提示“现在是未授权状态,请在设置页输入访问密码”?
项目通过环境变量CODE设置了访问密码。第一次使用时需要到设置中输入访问码才可以使用。
## 使用时提示"You exceeded your current quota, ..."
API KEY有问题。余额不足。
## 什么是代理,如何使用?
由于OpenAI的IP限制中国和其他一些国家/地区无法直接连接OpenAI API需要通过代理。你可以使用代理服务器正向代理或者已经设置好的OpenAI API反向代理。
- 正向代理例子科学上网梯子。docker部署的情况下设置环境变量HTTP_PROXY为你的代理地址http://地址:端口)。
- 反向代理例子可以用别人搭建的代理地址或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
## 国内服务器可以部署吗?
可以但需要解决的问题:
- 需要代理才能连接github和openAI等网站
- 国内服务器要设置域名解析的话需要备案;
- 国内政策限制代理访问外网/ChatGPT相关应用可能被封。
# 网络服务相关问题
## Cloudflare是什么
CloudflareCF是一个提供CDN域名管理静态页面托管边缘计算函数部署等的网络服务供应商。常见的用途购买和/或托管你的域名解析、动态域名等给你的服务器套上CDN可以隐藏ip免被墙部署网站CF Pages。CF免费提供大多数服务。
## Vercel是什么
Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多Web应用可以一键免费部署在Vercel上。无需懂代码无需懂linux无需服务器无需付费无需设置OpenAI API代理。缺点是需要绑定域名才可以在国内无墙访问。
## 如何获得一个域名?
1. 自己去域名供应商处注册国外有Namesilo支持支付宝, Cloudflare等等国内有万网等等
2. 免费的域名供应商eu.org(二级域名)等;
3. 问朋友要一个免费的二级域名。
## 如何获得一台服务器
- 国外服务器供应商举例亚马逊云谷歌云VultrBandwagonHostdare等等
国外服务器事项服务器线路影响国内访问速度推荐CN2 GIA和CN2线路的服务器。若服务器在国内访问困难丢包严重等可以尝试套CDNCloudflare等供应商
- 国内服务器供应商:阿里云,腾讯等;
国内服务器事项解析域名需要备案国内服务器带宽较贵访问国外网站Github, openAI等需要代理。
# OpenAI相关问题
## 如何注册OpenAI账号
去chat.openai.com注册。你需要
- 一个良好的梯子OpenAI支持地区原生IP地址
- 一个支持的邮箱例如Gmail或者公司/学校邮箱非Outlook或qq邮箱
- 接收短信认证的方式例如SMS-activate网站
## 怎么开通OpenAI API? 怎么查询API余额
官网地址需梯子https://platform.openai.com/account/usage
有网友搭建了无需梯子的余额查询代理请询问网友获取。请鉴别来源是否可靠以免API Key泄露。
## 我新注册的OpenAI账号怎么没有API余额
4月6日更新新注册账号通常会在24小时后显示API余额。当前新注册账号赠送5美元余额。
## 如何给OpenAI API充值
OpenAI只接受指定地区的信用卡中国信用卡无法使用。一些途径举例
1. Depay虚拟信用卡
2. 申请国外信用卡
3. 网上找人代充
## 如何使用GPT-4的API访问
4月6日更新GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist准备好你的OpenAI组织IDhttps://openai.com/waitlist/gpt-4-api
之后等待邮件消息。
## 如何使用 Azure OpenAI 接口
请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
## 为什么我的 Token 消耗得这么快?
> 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
- 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀;
- 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查:
- 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。**
- 如果你的密码设置很短,比如 5 位以内的字母,那么爆破成本是非常低的,建议你搜索一下 docker 的日志记录确认是否有人大量尝试了密码组合关键字got access code
- 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
- 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
- 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。

136
docs/faq.en.md Normal file
View File

@@ -0,0 +1,136 @@
# Frequently Asked Questions
## How to get help quickly?
1. Ask ChatGPT / Bing / Baidu / Google, etc.
2. Ask online friends. Please provide background information and a detailed description of the problem. High-quality questions are more likely to get useful answers.
# Deployment Related Questions
## Why does the Docker deployment version always prompt for updates
The Docker version is equivalent to the stable version, and the latest Docker is always consistent with the latest release version. Currently, our release frequency is once every one to two days, so the Docker version will always be one to two days behind the latest commit, which is expected.
## How to deploy on Vercel
1. Register a Github account and fork this project.
2. Register Vercel (mobile phone verification required, Chinese number can be used), and connect your Github account.
3. Create a new project on Vercel, select the project you forked on Github, fill in the required environment variables, and start deploying. After deployment, you can access your project through the domain provided by Vercel. (Requires proxy in mainland China)
* If you need to access it directly in China: At your DNS provider, add a CNAME record for the domain name, pointing to cname.vercel-dns.com. Then set up your domain access on Vercel.
## How to modify Vercel environment variables
- Enter the Vercel console page;
- Select your chatgpt-next-web project;
- Click on the Settings option at the top of the page;
- Find the Environment Variables option in the sidebar;
- Modify the corresponding values as needed.
## What is the environment variable CODE? Is it necessary to set it?
This is your custom access password, you can choose:
1. Do not set it, delete the environment variable. Be cautious: anyone can access your project at this time.
2. When deploying the project, set the environment variable CODE (supports multiple passwords, separated by commas). After setting the access password, users need to enter the access password in the settings page to use it. See [related instructions](https://github.com/Yidadaa/ChatGPT-Next-Web#access-password)
## Why doesn't the version I deployed have streaming response
> Related discussion: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
If you use nginx reverse proxy, you need to add the following code to the configuration file:
```
# No caching, support streaming output
proxy_cache off; # Turn off caching
proxy_buffering off; # Turn off proxy buffering
chunked_transfer_encoding on; # Turn on chunked transfer encoding
tcp_nopush on; # Turn on TCP NOPUSH option, disable Nagle algorithm
tcp_nodelay on; # Turn on TCP NODELAY option, disable delay ACK algorithm
keepalive_timeout 300; # Set keep-alive timeout to 65 seconds
```
If you are deploying on netlify, this issue is still waiting to be resolved, please be patient.
## I've deployed, but it's not accessible
Please check and troubleshoot the following issues:
- Is the service started?
- Is the port correctly mapped?
- Is the firewall port open?
- Is the route to the server okay?
- Is the domain name resolved correctly?
# Usage Related Questions
## Why does it always prompt "An error occurred, please try again later"
There could be many reasons, please check the following in order:
- First, check if your code version is the latest version, update to the latest version and try again;
- Check if the api key is set correctly, the environment variable name must be uppercase with underscores;
- Check if the api key is available;
- If you still cannot determine the problem after going through the above steps, please submit a new issue in the issue area and attach the runtime log of vercel or the log of docker runtime.
## Why does ChatGPT's reply get garbled
In the settings page - model settings, there is an item called `temperature`. If this value is greater than 1, it may cause garbled replies. Adjust it back to within 1.
## It prompts "Now it's unauthorized, please enter the access password on the settings page" when using?
The project has set an access password through the environment variable CODE. When using it for the first time, you need to go to settings and enter the access code to use.
## It prompts "You exceeded your current quota, ..." when using?
The API KEY is problematic. Insufficient balance.
## What is a proxy and how to use it?
Due to IP restrictions of OpenAI, China and some other countries/regions cannot directly connect to OpenAI API and need to go through a proxy. You can use a proxy server (forward proxy) or a pre-configured OpenAI API reverse proxy.
- Forward proxy example: VPN ladder. In the case of docker deployment, set the environment variable HTTP_PROXY to your proxy address (http://address:port).
- Reverse proxy example: You can use someone else's proxy address or set it up for free through Cloudflare. Set the project environment variable BASE_URL to your proxy address.
## Can I deploy it on a server in China?
It is possible but there are issues to be addressed:
- Proxy is required to connect to websites such as Github and OpenAI;
- Domain name resolution requires filing for servers in China;
- Chinese policy restricts proxy access to foreign websites/ChatGPT-related applications, which may be blocked.
# Network Service Related Questions
## What is Cloudflare?
Cloudflare (CF) is a network service provider offering CDN, domain management, static page hosting, edge computing function deployment, and more. Common use cases: purchase and/or host your domain (resolution, dynamic domain, etc.), apply CDN to your server (can hide IP to avoid being blocked), deploy websites (CF Pages). CF offers most services for free.
## What is Vercel?
Vercel is a global cloud platform designed to help developers build and deploy modern web applications more quickly. This project and many web applications can be deployed on Vercel with a single click for free. No need to understand code, Linux, have a server, pay, or set up an OpenAI API proxy. The downside is that you need to bind a domain name to access it without restrictions in China.
## How to obtain a domain name?
1. Register with a domain provider, such as Namesilo (supports Alipay) or Cloudflare for international providers, and Wanwang for domestic providers in China.
2. Free domain name providers: eu.org (second-level domain), etc.
3. Ask friends for a free second-level domain.
## How to obtain a server
- Examples of international server providers: Amazon Web Services, Google Cloud, Vultr, Bandwagon, Hostdare, etc.
International server considerations: Server lines affect access speed in China; CN2 GIA and CN2 lines are recommended. If the server has difficulty accessing in China (serious packet loss, etc.), you can try using a CDN (from providers like Cloudflare).
- Domestic server providers: Alibaba Cloud, Tencent, etc.
Domestic server considerations: Domain name resolution requires filing; domestic server bandwidth is relatively expensive; accessing foreign websites (Github, OpenAI, etc.) requires a proxy.
# OpenAI-related Questions
## How to register an OpenAI account?
Go to chat.openai.com to register. You will need:
- A good VPN (OpenAI only allows native IP addresses of supported regions)
- A supported email (e.g., Gmail or a company/school email, not Outlook or QQ email)
- A way to receive SMS verification (e.g., SMS-activate website)
## How to activate OpenAI API? How to check API balance?
Official website (requires VPN): https://platform.openai.com/account/usage
Some users have set up a proxy to check the balance without a VPN; ask online friends for access. Please verify the source is reliable to avoid API Key leakage.
## Why doesn't my new OpenAI account have an API balance?
(Updated April 6th) Newly registered accounts usually display API balance within 24 hours. New accounts are currently given a $5 balance.
## How to recharge OpenAI API?
OpenAI only accepts credit cards from designated regions (Chinese credit cards cannot be used). If the credit cards from your region is not supported, some options include:
1. Depay virtual credit card
2. Apply for a foreign credit card
3. Find someone online to top up
## How to access the GPT-4 API?
(Updated April 6th) Access to the GPT-4 API requires a separate application. Go to the following address and enter your information to join the waitlist (prepare your OpenAI organization ID): https://openai.com/waitlist/gpt-4-api
Wait for email updates afterwards.
## How to use the Azure OpenAI interface
Please refer to: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
## Why is my Token consumed so fast?
> Related discussion: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
- If you have GPT-4 access and use GPT-4 API regularly, your bill will increase rapidly since GPT-4 pricing is about 15 times higher than GPT-3.5;
- If you are using GPT-3.5 and not using it frequently, but still find your bill increasing fast, please troubleshoot immediately using these steps:
- Check your API key consumption record on the OpenAI website; if your token is consumed every hour and each time consumes tens of thousands of tokens, your key must have been leaked. Please delete it and regenerate it immediately. **Do not check your balance on random websites.**
- If your password is short, such as 5 characters or fewer, the cost of brute-forcing is very low. It is recommended to search docker logs to confirm whether someone has tried a large number of password combinations. Keyword: got access code
- By following these two methods, you can locate the reason for your token's rapid consumption:
- If the OpenAI consumption record is abnormal but the Docker log has no issues, it means your API key has been leaked;
- If the Docker log shows a large number of got access code brute-force attempts, your password has been cracked.

BIN
docs/images/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
docs/images/more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

BIN
docs/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

57
middleware.ts Normal file
View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import { ACCESS_CODES } from "./app/api/access";
import md5 from "spark-md5";
export const config = {
matcher: ["/api/openai", "/api/chat-stream"],
};
export function middleware(req: NextRequest) {
const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
return NextResponse.json(
{
error: true,
needAccessCode: true,
msg: "Please go settings page and fill your access code.",
},
{
status: 401,
},
);
}
// inject api key
if (!token) {
const apiKey = process.env.OPENAI_API_KEY;
if (apiKey) {
console.log("[Auth] set system token");
req.headers.set("token", apiKey);
} else {
return NextResponse.json(
{
error: true,
msg: "Empty Api Key",
},
{
status: 401,
},
);
}
} else {
console.log("[Auth] set user token");
}
return NextResponse.next({
request: {
headers: req.headers,
},
});
}

View File

@@ -14,4 +14,8 @@ const nextConfig = {
}
};
if (process.env.DOCKER) {
nextConfig.output = 'standalone'
}
module.exports = nextConfig;

View File

@@ -1,37 +1,53 @@
{
"name": "chatgpt-next-web",
"version": "1.1",
"version": "1.9.3",
"private": false,
"license": "Anti 996",
"scripts": {
"dev": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next dev",
"build": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next build",
"start": "cross-env COMMIT_ID=$(git rev-parse --short HEAD) next start",
"lint": "next lint"
"dev": "yarn fetch && next dev",
"build": "yarn fetch && next build",
"start": "next start",
"lint": "next lint",
"fetch": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install"
},
"dependencies": {
"@hello-pangea/dnd": "^16.2.0",
"@svgr/webpack": "^6.5.1",
"@types/node": "^18.14.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-katex": "^3.0.0",
"@vercel/analytics": "^0.1.11",
"cross-env": "^7.0.3",
"emoji-picker-react": "^4.4.7",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
"eventsource-parser": "^0.1.0",
"git-rev-sync": "^3.0.2",
"fuse.js": "^6.6.2",
"next": "^13.2.3",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.5",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2",
"rehype-prism-plus": "^1.5.1",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.59.2",
"typescript": "4.9.5",
"spark-md5": "^3.0.2",
"use-debounce": "^9.0.3",
"zustand": "^4.3.6"
},
"devDependencies": {
"@types/node": "^18.14.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"cross-env": "^7.0.3",
"eslint": "^8.36.0",
"eslint-config-next": "13.2.3",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"typescript": "4.9.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Disallow: /
User-agent: vitals.vercel-insights.com
Allow: /

15
public/serviceWorker.js Normal file
View File

@@ -0,0 +1,15 @@
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
self.addEventListener("activate", function (event) {
console.log("ServiceWorker activated.");
});
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
return cache.addAll([]);
}),
);
});
self.addEventListener("fetch", (e) => {});

View File

@@ -0,0 +1,9 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function (err) {
console.error('ServiceWorker registration failed: ', err);
});
});
}

View File

@@ -1 +1,21 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "ChatGPT Next Web",
"short_name": "ChatGPT",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

53
scripts/fetch-prompts.mjs Normal file
View File

@@ -0,0 +1,53 @@
import fetch from "node-fetch";
import fs from "fs/promises";
const RAW_CN_URL =
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
const CN_URL =
"https://cdn.jsdelivr.net/gh/PlexPt/awesome-chatgpt-prompts-zh@main/prompts-zh.json";
const RAW_EN_URL =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
const EN_URL =
"https://cdn.jsdelivr.net/gh/f/awesome-chatgpt-prompts@main/prompts.csv";
const FILE = "./public/prompts.json";
async function fetchCN() {
console.log("[Fetch] fetching cn prompts...");
try {
const raw = await (await fetch(CN_URL)).json();
return raw.map((v) => [v.act, v.prompt]);
} catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error);
return [];
}
}
async function fetchEN() {
console.log("[Fetch] fetching en prompts...");
try {
const raw = await (await fetch(EN_URL)).text();
return raw
.split("\n")
.slice(1)
.map((v) => v.split('","').map((v) => v.replace('"', "")));
} catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error);
return [];
}
}
async function main() {
Promise.all([fetchCN(), fetchEN()])
.then(([cn, en]) => {
fs.writeFile(FILE, JSON.stringify({ cn, en }));
})
.catch((e) => {
console.error("[Fetch] failed to fetch prompts");
fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] }));
})
.finally(() => {
console.log("[Fetch] saved to " + FILE);
});
}
main();

65
scripts/setup.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Check if running on a supported system
case "$(uname -s)" in
Linux)
if [[ -f "/etc/lsb-release" ]]; then
. /etc/lsb-release
if [[ "$DISTRIB_ID" != "Ubuntu" ]]; then
echo "This script only works on Ubuntu, not $DISTRIB_ID."
exit 1
fi
else
if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")$ ]]; then
echo "Unsupported Linux distribution."
exit 1
fi
fi
;;
Darwin)
echo "Running on MacOS."
;;
*)
echo "Unsupported operating system."
exit 1
;;
esac
# Check if needed dependencies are installed and install if necessary
if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then
case "$(uname -s)" in
Linux)
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"ubuntu\"" ]]; then
sudo apt-get update
sudo apt-get -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"centos\"" ]]; then
sudo yum -y install epel-release
sudo yum -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then
sudo pacman -Syu -y
sudo pacman -S -y nodejs git yarn
else
echo "Unsupported Linux distribution"
exit 1
fi
;;
Darwin)
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node git yarn
;;
esac
fi
# Clone the repository and install dependencies
git clone https://github.com/Yidadaa/ChatGPT-Next-Web
cd ChatGPT-Next-Web
yarn install
# Prompt user for environment variables
read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY
read -p "Enter CODE: " CODE
read -p "Enter PORT: " PORT
# Build and run the project using the environment variables
OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build
OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

2203
yarn.lock

File diff suppressed because it is too large Load Diff