Compare commits

..

454 Commits
v1.9.1 ... v2.4

Author SHA1 Message Date
Yifei Zhang
b5b8593e7f Merge pull request #1492 from Yidadaa/bugfix-0514
fix: #1444 async load google fonts
2023-05-14 23:29:01 +08:00
Yidadaa
03163d6a61 fix: #1444 async load google fonts 2023-05-14 23:25:22 +08:00
Yifei Zhang
cada0aa70b Merge pull request #1491 from Yidadaa/bugfix-0514
fix: #1423 should not scroll right when dragging side bar items
2023-05-14 22:57:01 +08:00
Yifei Zhang
75488e4bd5 Merge pull request #1475 from PaRaD1SE98/main
remove error messages in toBeSummarizedMsgs
2023-05-14 14:18:27 +08:00
Yidadaa
6da3aab046 fix: #1423 should not scroll right when dragging side bar items 2023-05-14 02:21:35 +08:00
PaRaD1SE98
ff2589c97f remove error messages for chat title summary 2023-05-14 02:34:32 +09:00
PaRaD1SE98
a9f000e7ef remove error messages in toBeSummarizedMsgs 2023-05-14 01:24:20 +09:00
Yifei Zhang
d9be63e6cb Merge pull request #1452 from PaRaD1SE98/main
fix: typo upater -> updater
2023-05-13 14:52:12 +08:00
Yifei Zhang
1a626a68f0 Merge pull request #1450 from yorunning/fix-1
fix: the theme-color selector
2023-05-13 14:50:54 +08:00
Yifei Zhang
330504b91e Merge pull request #1446 from wsw2000/feat/switchWindowScrollIntoView
feat: scrolling effect when switching chat windows
2023-05-13 14:50:42 +08:00
Yifei Zhang
6bb0166055 Merge pull request #1454 from yanCode/fix/i18n
fix: show Vietnamese in its own language
2023-05-13 14:48:06 +08:00
Yifei Zhang
b22988e6b8 Merge pull request #1459 from InitialXKO/patch-1
增加文生图面具
2023-05-13 14:45:12 +08:00
Yifei Zhang
f7edac961f Merge pull request #1456 from yanCode/fix/doc
docs: add supported languages to README.md
2023-05-13 14:43:52 +08:00
InitialXKO
5b9b120fa6 增加文生图面具 2023-05-13 09:36:04 +08:00
ShengYan, Zhang
f07e4fc87f docs: add supported languages to README.md 2023-05-13 09:02:15 +08:00
ShengYan, Zhang
a0e192b6e4 fix: show Vitenamese in it's own language 2023-05-13 07:23:14 +08:00
PaRaD1SE98
dc3fa6c780 Update settings.tsx
fix: typo upater -> updater
2023-05-12 22:46:22 +09:00
Yorun
dd5604f5d9 style: move nextjs supported meta tags to metadata 2023-05-12 19:23:49 +08:00
Yorun
170936a96e fix: the theme-color selector 2023-05-12 18:47:41 +08:00
wsw
93c9974019 feat: scrolling effect when switching chat windows 2023-05-12 17:54:40 +08:00
Yifei Zhang
377579e802 Update README.md 2023-05-12 17:19:40 +08:00
Yifei Zhang
c49dbab127 Merge pull request #1431 from Yidadaa/bugfix-0511
fix: #1401 try to disable zoom
2023-05-12 01:48:37 +08:00
Yidadaa
36adfe87fb fix: #1401 try to disable zoom 2023-05-11 23:21:16 +08:00
Yifei Zhang
cdfcf0f068 Merge pull request #1421 from zxdclyz/hide-hints
Fix: click the prompt button to hide hints when it's already shown
2023-05-11 18:53:59 +08:00
Yifei Zhang
c3676091ee Merge pull request #1417 from yanCode/fix/hotkey-clashes
Fix bug #1413 hotkey clashes
2023-05-11 18:53:44 +08:00
liyuze
ec19b86ade fix: click the prompt button to hide hints when it's already shown 2023-05-11 18:29:25 +08:00
ShengYan, Zhang
7bf74c6a5d fix: bug #1413 cmd/alt/ctrl should be checked for arrowUp events 2023-05-11 16:08:34 +08:00
ShengYan, Zhang
cbb50c14e1 fix: bug #1413 input '/' when clicking icon to open prompt modal 2023-05-11 15:42:32 +08:00
Yifei Zhang
d42622a5c1 Merge pull request #1383 from PaRaD1SE98/main
给所有select元素添加一个右侧下拉按钮
2023-05-11 11:48:59 +08:00
Yifei Zhang
f9bee1485b Merge pull request #1396 from pBrambi/main
Add 🇨🇿 Czech translation
2023-05-11 11:08:49 +08:00
Petr Branberger
c87eee1fda Fixed typo syntax error 2023-05-10 16:33:22 +02:00
PaRaD1SE98
960aa90c32 优化Select component 2023-05-10 14:09:30 +00:00
Petr Branberger
c4210be3c7 Czech language update 2023-05-10 15:48:36 +02:00
Petr Branberger
28a49827ff Merge branch 'main' of https://github.com/pBrambi/ChatGPT-Next-Web 2023-05-10 15:12:31 +02:00
Petr Branberger
736869454b Fix /cs.ts to include the option 2023-05-10 12:50:12 +02:00
Petr Branberger
db9084b0dc Fix redefined "DE" in /index.ts 2023-05-10 12:39:33 +02:00
Yifei Zhang
c39d75b448 Merge pull request #1381 from crim50n/main
Adding Russian translation
2023-05-10 15:57:07 +08:00
crim50n
71a546dd44 Adding Russian translation
Adding a Russian translation and fixing other language files to make them look the same.
2023-05-10 10:16:40 +03:00
PaRaD1SE98
96e3d3a22c 给所有select元素添加一个右侧下拉按钮 2023-05-10 07:14:49 +00:00
Yifei Zhang
0ad91101a4 Merge pull request #1371 from yanCode/fix/textarea-line
fix: row count logic
2023-05-10 10:47:06 +08:00
Yifei Zhang
e9df58709e Merge pull request #1368 from darth-pika-hu/main
添加Cloudflare Pages的部署说明
2023-05-10 10:38:34 +08:00
ShengYan, Zhang
a80dcaa1c3 fix: row count logic 2023-05-10 09:27:33 +08:00
Darth Pika
89551774db Re-format the Chinese version and added an English version
Finally found a computer.
2023-05-09 17:45:01 -07:00
Darth Pika
be58d3afb6 Update cloudflare-pages-cn.md 2023-05-09 17:31:19 -07:00
Darth Pika
652d803739 Update cloudflare-pages-cn.md 2023-05-09 17:25:00 -07:00
Darth Pika
2688914125 Create cloudflare-pages-cn.md
添加Cloudflare Pages的部署说明。这里临时说明,Next-on-Pages有一个bug将很快修复,修复后,Build命令会修改。
2023-05-09 16:47:41 -07:00
Yifei Zhang
1d489cfcea Merge pull request #1364 from Yidadaa/client-side
feat: add model and time info to prompts
2023-05-10 00:52:44 +08:00
Yidadaa
cb55ce084c feat: add model and time info to prompts 2023-05-10 00:51:43 +08:00
Yifei Zhang
dae7da0e4e Merge pull request #1361 from Yidadaa/bugfix-0509
feat: #1055, #444 and fix #1359 hot key to switch chat and allow to disable gpt-4
2023-05-09 23:39:53 +08:00
Yidadaa
e4630e6a0f fixup 2023-05-09 23:38:34 +08:00
Yidadaa
6d9abf11b8 fix: #1363 session index after deleting 2023-05-09 23:36:30 +08:00
Yidadaa
7e8def50aa feat: close #444 use env var to disable gpt-4 2023-05-09 23:20:03 +08:00
Yidadaa
2b7f72deec feat: close #1055 cmd/alt/ctrl + arrow up/down to switch window 2023-05-09 23:01:17 +08:00
Yidadaa
9b1f25140e fix: #1359 empty line wrap count 2023-05-09 22:46:06 +08:00
Yifei Zhang
f4caa0029e Update README.md 2023-05-09 16:51:27 +08:00
Yifei Zhang
15e046b3ce Update README.md 2023-05-09 11:27:57 +08:00
Yifei Zhang
eeef92b068 Merge pull request #1335 from yzy1996/patch-1
改为一天同步一次 Update sync.yml
2023-05-09 01:22:20 +08:00
Crazyang
a1418fe33c Update sync.yml
因为 Vercel 频繁更新会失败,遂建议改为一天同步一次。
2023-05-09 01:03:51 +08:00
Yifei Zhang
8f21e736dd Merge pull request #1333 from Yidadaa/0508-bugfix
feat: #1303 and fix #1294 1307, new modal style
2023-05-09 00:44:53 +08:00
Yidadaa
222301307f feat: close #1301 support message actions 2023-05-09 00:39:00 +08:00
Yidadaa
1b19fdfe11 feat: #1303 improve long text input ux and mobile modal 2023-05-08 22:49:51 +08:00
Yidadaa
1f2ef1cdb7 fix: #1307 empty messages 2023-05-08 22:21:06 +08:00
Yidadaa
c394b21423 fix: #1294 fallback while mermaid render fails 2023-05-08 22:18:19 +08:00
Yifei Zhang
696e84ea88 Merge pull request #1315 from binh234/vi_translation
Add Vietnamese translation
2023-05-08 11:51:28 +08:00
Yifei Zhang
4a17dca7b9 Merge pull request #1323 from yanCode/fix/prompt-list-style
fix: styles on .user-prompt-buttons
2023-05-08 10:43:59 +08:00
ShengYan, Zhang
9bcfe6461d chore: remove an unused css name 2023-05-08 08:48:47 +08:00
ShengYan, Zhang
4a82a91f2d fix: styles on .user-prompt-buttons 2023-05-08 08:27:10 +08:00
Yifei Zhang
24cd905911 Merge pull request #1319 from zhongmeizhi/main
feat: mobile chat overscroll-behavior none
2023-05-07 20:36:46 +08:00
Mokou
6d62ab4257 feat: mobile chat overscroll-behavior none 2023-05-07 18:33:40 +08:00
Binh Le
f6ff32f339 Add Vietnamese translation 2023-05-07 14:54:09 +07:00
Yifei Zhang
bc523d302b Merge pull request #1312 from LaChimere/user/lachimere/fix-typo
Fix typo
2023-05-07 15:38:14 +08:00
LaChimere
1facbb2906 fix typo 2023-05-07 13:52:26 +08:00
Yifei Zhang
8265436437 Merge pull request #1297 from yanCode/enhancement/loading-icon
fix: show Loading Icon when checking repo update
2023-05-07 00:36:40 +08:00
ShengYan, Zhang
96545bd523 fix: show Loading Icon when checking repo update 2023-05-06 20:12:26 +08:00
Yifei Zhang
e773ed2912 Update README.md 2023-05-05 23:43:26 +08:00
Yifei Zhang
9949fc4646 Merge pull request #1276 from Yidadaa/bugfix-0505
feat: #951 mermaid support
2023-05-05 23:38:27 +08:00
Yidadaa
d88da1f6ab feat: close #951 support mermaid 2023-05-05 23:32:35 +08:00
Yidadaa
fe8e3f2bcf fix: #1273 overlap detecting 2023-05-05 22:59:21 +08:00
Yidadaa
4b9d753254 fix: #1251 use google fonts mirror 2023-05-05 22:49:41 +08:00
Yifei Zhang
5ba385b74d Update README.md 2023-05-05 15:42:27 +08:00
Yifei Zhang
ec655f5182 Merge pull request #1248 from wsw2000/main
refactor: determine userInput by trimming
2023-05-04 23:26:02 +08:00
Yifei Zhang
093df395c7 Merge pull request #1235 from xesrc/main
resolve problem about basic auth when the app is behind a proxy server
2023-05-04 23:25:49 +08:00
Yifei Zhang
92d2373d77 Merge pull request #1247 from Yidadaa/proxy-api
fix: #1237 #1233 mask bug and proxy bug
2023-05-04 23:09:24 +08:00
wsw
319959ad6e refactor: determine userInput by trimming 2023-05-04 22:53:22 +08:00
Yidadaa
de1e4b4c6c Merge branch 'main' into proxy-api 2023-05-04 22:50:26 +08:00
Yidadaa
c2e79d22d2 fix: #1233 detect api key with custom prefix 2023-05-04 22:50:07 +08:00
Yidadaa
596c9b1d27 feat: close #887 import masks 2023-05-04 22:33:13 +08:00
Yidadaa
40223e6b3f fix: #1237 can not delete cloned mask 2023-05-04 22:31:10 +08:00
Yidadaa
eec1dd6448 fix: proxy api request 2023-05-04 22:18:03 +08:00
Yifei Zhang
6c1261d28d Merge pull request #1241 from yanCode/main
fix: bug #1240
2023-05-04 19:08:53 +08:00
Yifei Zhang
4697ea1c1a Update README.md 2023-05-04 19:08:10 +08:00
ShengYan, Zhang
6e20031dce fix: bug #1240 2023-05-04 18:52:28 +08:00
xesrc
a5fe9bc6d6 resolve problem about basic auth when the app is behind a proxy server 2023-05-04 16:11:44 +08:00
Yifei Zhang
637660df2a Merge pull request #1232 from sanding0/main
chore: update registry mirror
2023-05-04 14:41:01 +08:00
roller
e1549d109e chore: update registry mirror 2023-05-04 11:26:14 +08:00
Yifei Zhang
5f8fc3d155 Merge pull request #1222 from Yidadaa/proxy-api
refactor: merge /api/chat-stream to /api/openai
2023-05-04 00:13:25 +08:00
Yidadaa
fce3b3ce7b feat: use commit time as version id 2023-05-04 00:12:00 +08:00
Yidadaa
074bd9f045 feat: close #663 allow disable user api key input 2023-05-03 23:49:33 +08:00
Yidadaa
b1ea26467d refactor: extract client side openai url 2023-05-03 23:25:17 +08:00
Yidadaa
48ebd74859 refactor: merge token and access code 2023-05-03 23:08:37 +08:00
Yidadaa
ef5b7ce853 refactor: merge /api/chat-stream to /api/openai 2023-05-03 17:07:09 +08:00
Yifei Zhang
e0053d57f7 Merge pull request #1220 from Yidadaa/bugfix-0503
feat: #782 select prompt with arrow down / up
2023-05-03 16:26:27 +08:00
Yidadaa
06268543d0 fixup 2023-05-03 16:24:25 +08:00
Yidadaa
58eadd6d7b feat: close #782 select prompt with arrow down / up 2023-05-03 16:22:37 +08:00
Yidadaa
f250594e97 Merge branch 'main' into bugfix-0503 2023-05-03 15:56:02 +08:00
Yidadaa
c1b6828ed4 fix: #1201 wont close prompt list when blur 2023-05-03 15:55:46 +08:00
Yidadaa
328ecd1cfb fix: #1210 change default lang to en 2023-05-03 15:22:44 +08:00
Yifei Zhang
435e82c824 Merge pull request #1196 from ClarenceDan/main
fix: Resolve markdown link issue
2023-05-02 20:19:47 +08:00
Clarence Dan
aeda7520fe fix: Resolve markdown link issue
Resolved Markdown Issue
This pull request also resolves an issue where internal links were not redirecting properly in markdown, and optimizes the behavior for external links to open in a new window.
2023-05-02 11:18:18 +08:00
Yifei Zhang
56ef8e3ebf Merge pull request #1192 from Yidadaa/bugfix-0501
fixup
2023-05-02 03:12:11 +08:00
Yidadaa
717c123b82 feat: improve mask ui 2023-05-02 03:10:13 +08:00
Yidadaa
132f6c8420 feat: improve mask ui 2023-05-02 03:01:42 +08:00
Yidadaa
116e16e30d fixup 2023-05-02 02:52:25 +08:00
Yifei Zhang
5dbb6afc60 Merge pull request #1191 from Yidadaa/bugfix-0501
fixup
2023-05-02 02:46:50 +08:00
Yidadaa
ae8050a3f7 fixup 2023-05-02 02:45:15 +08:00
Yifei Zhang
8870e966a6 Merge pull request #1190 from Yidadaa/bugfix-0501
Bugfix 0501
2023-05-02 02:40:32 +08:00
Yidadaa
f5a5cffdec fixup 2023-05-02 02:38:30 +08:00
Yidadaa
220c622f8f fixup 2023-05-02 02:37:15 +08:00
Yidadaa
e509749421 perf: improve prompt list performance 2023-05-02 02:26:43 +08:00
Yidadaa
a69cec89fb perf: close #909 reduce message items render time 2023-05-02 00:31:44 +08:00
Yidadaa
8f5c289818 fix: #751 do not cache request 2023-05-01 23:48:23 +08:00
Yifei Zhang
5e544891aa Merge pull request #1189 from Yidadaa/bugfix-0501
Bugfix 0501
2023-05-01 23:44:32 +08:00
Yidadaa
1aaf4ae5bc fix: #1126 can not select prompt 2023-05-01 23:39:54 +08:00
Yidadaa
9f3188fe45 fix: #1124 mask model config does not works 2023-05-01 23:37:02 +08:00
Yidadaa
b2fc7d476a fix: #1147 edit mask after creating a new mask 2023-05-01 23:23:39 +08:00
Yidadaa
c37885e743 fix: #1130 #1131 delete right session 2023-05-01 23:21:28 +08:00
Yidadaa
0209ace221 fix: #1154 wrong date range when query usage 2023-05-01 22:53:33 +08:00
Yifei Zhang
b0cd8579f1 Merge pull request #1144 from 0x9be00ff1/fix-mask-download
fix: mask download not working #1119
2023-04-28 20:43:52 +08:00
Zhenyu Zhu
ba0753c418 fix: mask download not working 2023-04-28 19:25:03 +08:00
Yidadaa
6419ce345f fix: hot fix for data migration 2023-04-28 01:54:57 +08:00
Yifei Zhang
f78a27491a Merge pull request #993 from Yidadaa/session-config
v2.0 Prompt Templates
2023-04-28 01:23:43 +08:00
Yidadaa
7ef20f22c2 doc: update docs 2023-04-28 01:21:30 +08:00
Yidadaa
ace3f7f532 Merge branch 'main' into session-config 2023-04-28 00:35:04 +08:00
Yidadaa
fb32770486 feat: i18n refactor and style adjustment 2023-04-28 00:34:37 +08:00
Yidadaa
6c1144f6f4 fixup 2023-04-27 02:14:38 +08:00
Yidadaa
59edcc3e2e feat: add side bar mask entry 2023-04-27 02:12:09 +08:00
Yidadaa
30040a0366 feat: migrate state from v1 to v2 2023-04-27 02:00:22 +08:00
Yifei Zhang
a604644da7 Merge pull request #1102 from BaiMeow/main
fix: use innerHTML may leads unexpected script execution
2023-04-27 01:21:36 +08:00
Yifei Zhang
64af3ccf0c Merge pull request #1103 from shih-liang/patch-1
修复在聊天很短没有覆盖整个区域时显示回到底部按钮的bug
2023-04-27 01:21:03 +08:00
Yidadaa
401c1364be fix: merge conflict 2023-04-27 01:18:09 +08:00
Yidadaa
c7c58ef031 feat: add i18n for mask 2023-04-27 01:16:21 +08:00
Shi Liang
0f9fe92fa3 修复在聊天很短没有覆盖整个区域时显示回到底部按钮的bug 2023-04-26 21:42:43 +08:00
柏喵Sakura
bbf3d044cb Merge branch 'Yidadaa:main' into main 2023-04-26 21:25:21 +08:00
BaiMeow
7ed8517771 fix: innerHTML may leads to script execution 2023-04-26 21:24:14 +08:00
Yifei Zhang
2deb5cbc9e fix: #1094 try to disable zoom on ios safari 2023-04-26 14:43:43 +08:00
Yifei Zhang
fbacfba92a Update README.md 2023-04-26 11:32:07 +08:00
Yifei Zhang
fcce9968e9 Update README.md 2023-04-26 11:30:17 +08:00
Yidadaa
3cda44e05b fixup 2023-04-26 02:05:35 +08:00
Yidadaa
a7a8aad9bc feat: add mask crud 2023-04-26 02:02:46 +08:00
Yifei Zhang
2e01a93a4a Update issue templates 2023-04-25 22:54:21 +08:00
Yifei Zhang
c4ca05865d Update README_CN.md 2023-04-25 20:12:00 +08:00
Yidadaa
ffa7302571 feat: add mask page 2023-04-25 00:49:27 +08:00
Yifei Zhang
6b36b255ef Merge pull request #1016 from PeterDaveHello/improve-tw-locale
Improve tw locale
2023-04-24 15:26:36 +08:00
Peter Dave Hello
e5e2f6c2e1 Improve tw locale 2023-04-24 04:11:12 +08:00
Yidadaa
708c6829f7 fixup 2023-04-24 01:17:28 +08:00
Yidadaa
aeb986243c feat: add mask screen 2023-04-24 01:15:44 +08:00
Yidadaa
e654cee3c8 fixup: request params 2023-04-23 21:54:18 +08:00
Yifei Zhang
71aa9d05ed Merge pull request #1019 from yunwuu/main
fix: raw.split is not a function
2023-04-23 10:55:58 +08:00
雲霧
725054c7d5 fix: raw.split is not a function 2023-04-23 10:51:34 +08:00
Yifei Zhang
2efba6ea6f Merge pull request #1015 from yunwuu/main
fix typo & add timeout to stuck request
2023-04-23 01:57:30 +08:00
Yifei Zhang
e7796f902a Merge pull request #1011 from eltociear/patch-1
Update README.md
2023-04-23 01:55:50 +08:00
Yidadaa
b23adf9d5d fixup 2023-04-23 01:37:47 +08:00
Yidadaa
7345639af3 feat: add session config modal 2023-04-23 01:27:15 +08:00
雲霧
818629e58b chore: add timeout to prompt download request 2023-04-23 00:17:00 +08:00
雲霧
1761289716 fix: typo 2023-04-22 23:53:58 +08:00
Ikko Eltociear Ashimine
5d2fb8791c Update README.md
Github -> GitHub
2023-04-23 00:07:16 +09:00
Yifei Zhang
8d0a420cc8 Merge pull request #991 from yunwuu/main
fix: typo
2023-04-22 01:18:38 +08:00
Yidadaa
2e3aa940fb Merge branch 'main' into bugfix-0421 2023-04-22 01:17:03 +08:00
Yidadaa
4cdb2f0fa3 feat: session-level model config 2023-04-22 01:13:23 +08:00
雲霧
79f58f5c6a fix: typo 2023-04-22 00:47:15 +08:00
Yifei Zhang
56d098c317 Merge pull request #980 from 0x9be00ff1/fix-presence-penalty-step
[Fix] adjust presence_penalty step 0.1
2023-04-22 00:43:44 +08:00
Yifei Zhang
4ca0ed3779 Merge pull request #987 from Yidadaa/bugfix-0421
Bugfix 0421
2023-04-22 00:42:10 +08:00
Yidadaa
a3ca8ea5c4 feat: new chat-item avatar 2023-04-22 00:35:50 +08:00
Yidadaa
ae479f4a92 fix: #963 config not work 2023-04-22 00:12:07 +08:00
Yidadaa
ab826363ea fix: #965 improve loading animation 2023-04-21 23:37:25 +08:00
Yidadaa
e1ce1f2f40 feat: close #976 esc to close modal 2023-04-21 23:28:53 +08:00
Yidadaa
209a727fe9 feat: close #928 summarize with gpt3.5 2023-04-21 23:22:02 +08:00
Zhenyu Zhu
4d45c07bf2 fix: adjust presence_penalty step 0.1 2023-04-21 18:52:32 +08:00
Yifei Zhang
d053bee8d3 Merge pull request #979 from jzjwonderful/bug-978
[Bug] Unsupported Linux distribution when running setup.sh on Ubuntu OS
2023-04-21 18:15:55 +08:00
jzjwonderful
596a46846a fix bug 978 2023-04-21 17:26:40 +08:00
Yifei Zhang
0b36daf6ac Merge pull request #968 from shih-liang/patch-2
修复 Edge Runtime 设置失效问题
2023-04-21 14:55:01 +08:00
Shi Liang
ffb0fa25f1 Merge pull request #1 from shih-liang/patch-1
chat-stream: runtime = "experimental-edge";
2023-04-21 13:08:01 +08:00
Shi Liang
8966fd3b23 openai runtime = "experimental-edge"; 2023-04-21 13:03:38 +08:00
Shi Liang
b6a7104b60 chat-stream: runtime = "experimental-edge"; 2023-04-21 13:03:02 +08:00
Yifei Zhang
a62bca442e Merge pull request #959 from Yidadaa/0420-mask
refactor: close #643 use react router
2023-04-21 02:54:56 +08:00
Yidadaa
82ad0573be feat: close #380 collapse side bar 2023-04-21 02:52:53 +08:00
pBrambi
6a74d62e98 Czech 2023-04-20 19:26:31 +02:00
Yidadaa
5185166e3b fixup 2023-04-21 01:18:49 +08:00
Yidadaa
693dcf12d6 refactor: close #643 use react router 2023-04-21 01:12:39 +08:00
Yifei Zhang
ee0f847827 Merge pull request #955 from Yidadaa/bugfix-0420
Bugfix 0420
2023-04-20 23:31:43 +08:00
Yidadaa
55281ed548 feat: reactive isMobileScreen 2023-04-20 23:20:25 +08:00
Yidadaa
2390da1165 fix: #930 wont show delete for first message 2023-04-20 23:09:42 +08:00
Yidadaa
06d503152b feat: close #928 summarize with gpt-3.5 2023-04-20 23:04:58 +08:00
Yidadaa
2e9e69d66c fixup 2023-04-20 22:58:19 +08:00
Yidadaa
e3d2dd7279 feat: close #427 add OPENAI_ORG_ID 2023-04-20 22:55:14 +08:00
Yidadaa
7e8973c9ff feat: close #291 gpt-4 model uses black icon 2023-04-20 22:52:14 +08:00
pBrambi
35ccfb14c2 Create cs.ts
Czech language translation
2023-04-20 01:02:43 +02:00
Yifei Zhang
b2c711a5e8 Merge pull request #933 from yinm0591/main
Added support for organization ID
2023-04-19 22:23:12 +08:00
Yin Min
21da781350 Update common.ts
Add OrgID
2023-04-19 19:28:33 +08:00
Yifei Zhang
beb04d8181 Update README.md 2023-04-19 15:36:10 +08:00
Yifei Zhang
072a35b4ee fix: #915 allow send 0 history messages 2023-04-19 11:20:07 +08:00
Yifei Zhang
e68f17b463 Merge pull request #902 from ClarenceYk/for_test
Update app/components/chat.tsx
2023-04-18 19:10:30 +08:00
Ma Yuke
dfcae92ae0 Update app/components/chat.tsx
fix: chat-body flickers when click-and-hold the scroll bar using mouse
2023-04-18 18:01:09 +08:00
Yifei Zhang
1ec228e46c Merge pull request #900 from aooyoo/main
fix user prompt translation
2023-04-18 16:59:03 +08:00
aooyoo
d1653ded9a fix user prompt translation 2023-04-18 08:13:32 +00:00
Yifei Zhang
8da581695f Merge pull request #893 from L1468999760/add_alpha
fix:delete user.svg
2023-04-18 11:55:28 +08:00
Yifei Zhang
9e46ca31dd Merge pull request #892 from Yidadaa/improve-memory
feat: close #864 improve long term history
2023-04-18 11:54:24 +08:00
Yidadaa
ad1c8ffe21 fixup 2023-04-18 11:44:15 +08:00
Yifei Zhang
6b30e2beb0 Merge pull request #891 from jtung4/pr
Optimize topic and summary prompts in tw.ts
2023-04-18 11:42:57 +08:00
Yifei Zhang
d75b7d49b8 feat: close #864 improve long term history 2023-04-18 11:42:08 +08:00
jtung4
53173d9053 Optimize topic and summary prompts in tw.ts 2023-04-18 11:38:26 +08:00
L1468999760
e68207be2c fix:delete user.svg 2023-04-18 11:34:33 +08:00
Yifei Zhang
146ef1bf49 Merge pull request #885 from Chandler-Lu/patch-1
fix: typo
2023-04-18 10:41:31 +08:00
yesRabbit
bd85d9a36a fix: typo 2023-04-18 08:55:24 +08:00
Yifei Zhang
c03b4adec0 Update README.md 2023-04-18 01:48:07 +08:00
Yifei Zhang
b7b16aa33b Merge pull request #882 from Yidadaa/bugfix-0417
feat: user prompts
2023-04-18 01:40:38 +08:00
Yidadaa
789a779775 feat: user prompts 2023-04-18 01:34:12 +08:00
Yidadaa
fdc8278b90 feat: check usage throttle 2023-04-17 23:12:27 +08:00
Yidadaa
525a2ff9a7 fix: #866 remove unused retry messages 2023-04-17 22:51:14 +08:00
Yifei Zhang
78c9e66c1f Merge pull request #870 from jaw52/fix/windows_error
fix: error in windows
2023-04-17 16:38:55 +08:00
jaw
3038dfdb27 fix: error in windows 2023-04-17 15:37:03 +08:00
Yifei Zhang
7cd9f644ee Merge pull request #861 from ClarenceYk/for_test
Update app/requests.ts
2023-04-17 13:00:06 +08:00
Ma Yuke
64e78329ec Update app/requests.ts
fix: displaying the number of subscription beyond two decimal places.
2023-04-17 11:53:09 +08:00
Yifei Zhang
e49acda806 Merge pull request #859 from Yidadaa/Yidadaa-patch-1
fix: #853 fetch duplex errors
2023-04-17 11:41:47 +08:00
Yifei Zhang
b79845fcaa fixup 2023-04-17 11:36:32 +08:00
Yifei Zhang
76ef5ef9a9 fixup 2023-04-17 11:34:33 +08:00
Yifei Zhang
cc053b148d fix: #853 fetch duplex errors 2023-04-17 11:27:31 +08:00
Yifei Zhang
2979df1d82 Update README.md 2023-04-17 11:17:05 +08:00
Yifei Zhang
c305ba3c92 Merge pull request #839 from Yidadaa/bugfix-0416
feat: close #813 log user ip time
2023-04-16 18:57:59 +08:00
Yidadaa
12d4081311 feat: close #539 add delete message button 2023-04-16 18:55:29 +08:00
Yidadaa
124938ecc9 fix: #832 update nextjs version to 13.3.0 2023-04-16 18:17:46 +08:00
Yidadaa
ea3e8a7459 fix: #829 filter empty prompt 2023-04-16 18:11:09 +08:00
Yidadaa
dc3883ed1a feat: close #118 add stop all button 2023-04-16 18:07:43 +08:00
Yidadaa
bd69c8f5dd feat: close #813 log user ip time 2023-04-16 17:20:33 +08:00
Yifei Zhang
b751861bfb Merge pull request #838 from tscherrie/add-german-translations
some final language quality improvements
2023-04-16 16:57:14 +08:00
tscherrie
d035523f30 Merge branch 'Yidadaa:main' into add-german-translations 2023-04-16 10:44:22 +03:00
tscherrie tscherru
f9906aba7f found typo 2023-04-16 07:43:38 +00:00
tscherrie tscherru
410695d823 some final language quality improvements 2023-04-16 07:11:36 +00:00
Yifei Zhang
cd198ee1fa Merge pull request #836 from tscherrie/add-german-translations
Add german language translation de.ts
2023-04-16 14:51:07 +08:00
tscherrie tscherru
2c35c26749 fixed typo 2023-04-16 05:32:55 +00:00
tscherrie tscherru
f042d07ee7 added de to index and other files 2023-04-16 05:07:54 +00:00
tscherrie tscherru
4ce5f89c66 fixed german language translations 2023-04-16 04:16:04 +00:00
tscherrie tscherru
e0fa0d1936 Add german language translation de.tss 2023-04-16 04:00:31 +00:00
Yifei Zhang
96d65f7d39 Merge pull request #830 from ilario92/main
fix IT translation
2023-04-16 00:39:59 +08:00
ilarioscandurra
98fd08d9e5 fix IT translation 2023-04-15 16:33:04 +02:00
Yifei Zhang
e0f6c801d5 Merge pull request #812 from Yidadaa/bugfix-0415
refactor: remove protocol env vars
2023-04-15 02:52:53 +08:00
Yidadaa
ad274b7cad fixup 2023-04-15 02:51:15 +08:00
Yidadaa
0a79df3670 refactor: remove protocol env vars 2023-04-15 02:50:04 +08:00
Yifei Zhang
f5a05da792 Merge pull request #811 from Yidadaa/bugfix-0415
fix: #804 disable auto scroll in textarea
2023-04-15 02:38:30 +08:00
Yidadaa
47a2911ee2 feat: edit session title button 2023-04-15 02:37:24 +08:00
Yidadaa
fb14785cad fix: #804 disable auto scroll in textarea 2023-04-15 02:28:30 +08:00
Yifei Zhang
b0f7aeece5 Merge pull request #807 from jtung4/pr
feat: close #796 improve tw summarize title prompt
2023-04-15 02:03:45 +08:00
jtung4
73216513dd Update tw.ts 2023-04-14 19:49:48 +08:00
Yifei Zhang
563eb96282 Update README.md 2023-04-13 22:47:40 +08:00
Yifei Zhang
480ffa0ab3 Merge pull request #770 from JessySnow/dev/fix_docker_config
fix docker proxy config
2023-04-13 21:57:48 +08:00
JessySnow
4baadefa1d fix docker proxy config 2023-04-13 18:36:50 +08:00
Yifei Zhang
9fef4cab73 Merge pull request #755 from peanut996/patch-2
Fix wrong spelling
2023-04-13 14:45:47 +08:00
Peanuts
7fc43b4745 Fix wrong spelling 2023-04-13 11:07:42 +08:00
Yifei Zhang
86fb36163c Update README_CN.md 2023-04-13 11:05:36 +08:00
Yifei Zhang
5ad8a3e214 Update README.md 2023-04-13 11:04:48 +08:00
Yifei Zhang
f6f07abf1d Update markdown.tsx 2023-04-13 02:16:21 +08:00
Yifei Zhang
233e7bf406 Merge pull request #746 from Yidadaa/bugfix-0412
feat: close #680 lazy load markdown dom
2023-04-13 02:11:05 +08:00
Yifei Zhang
3f34731adf Merge pull request #747 from yorunning/ci-prompt
ci(sync): add an error prompt
2023-04-13 02:09:05 +08:00
Yidadaa
8363cdd9fa feat: close #680 lazy rendering markdown 2023-04-13 02:07:24 +08:00
Yorun
e12ea69b6a ci(sync): add an error prompt 2023-04-12 15:31:40 +00:00
Yidadaa
d790b0b372 feat: close #680 lazy load markdown dom 2023-04-12 23:27:28 +08:00
Yidadaa
9146b98285 fix: *.scss *.svg types 2023-04-12 23:04:45 +08:00
Yifei Zhang
2fb67f52a1 Merge pull request #742 from Yidadaa/revert-715-bugfix-scrollthumb
Revert "fix: autoscroll conflict"
2023-04-12 19:48:59 +08:00
Yifei Zhang
c1bb53c4e3 Revert "fix: autoscroll conflict" 2023-04-12 19:48:12 +08:00
Yifei Zhang
2b921093e1 Update README.md 2023-04-12 17:03:42 +08:00
Yifei Zhang
7c9248a764 Update README.md 2023-04-12 14:58:11 +08:00
Yifei Zhang
5fc0ae1377 Update README.md 2023-04-12 12:13:52 +08:00
Yifei Zhang
3ee99cae75 docs: add vercel instructions 2023-04-12 12:11:22 +08:00
Yifei Zhang
a15bd3ce33 Merge pull request #715 from betgo/bugfix-scrollthumb
fix: autoscroll conflict
2023-04-12 11:05:53 +08:00
Yifei Zhang
88fff82e3a Merge pull request #721 from helloworld-xdy/main
fix:修复正常响应结果后额外追加‘出错了,请稍后再试’bug
2023-04-11 22:56:10 +08:00
Yifei Zhang
3f2a8cc0bb Merge pull request #723 from latorc/patch-1
docs: Update faq-cn.md
2023-04-11 22:53:17 +08:00
latorc
65aca5c38a Update faq-cn.md 2023-04-11 22:17:36 +08:00
helloworld_xy
d8ef4a1031 fix:修复正常响应结果后额外追加‘出错了,请稍后再试’bug 2023-04-11 19:03:09 +08:00
Yifei Zhang
8d2abe36a9 Update README.md 2023-04-11 16:04:36 +08:00
Yifei Zhang
5a56bc3465 Merge pull request #688 from aooyoo/main
add Japanese
2023-04-11 15:37:23 +08:00
Yifei Zhang
b4cea45bfe Merge pull request #708 from peanut996/patch-1
[refactor] rename ChatResponse.
2023-04-11 15:36:50 +08:00
betgo
4269775665 fix: autoscroll conflict 2023-04-11 14:27:34 +08:00
peanut996
a1c709bb58 [refactor] rename ChatResponse. 2023-04-11 12:06:12 +08:00
Yifei Zhang
9f92968b96 Update issue templates 2023-04-11 11:21:05 +08:00
Yifei Zhang
f43d936ef0 Update faq-cn.md 2023-04-11 11:15:24 +08:00
Yifei Zhang
0f739f442e Merge pull request #697 from Yidadaa/bugfix-0410
fix: runtime config and proxy fix
2023-04-11 02:59:51 +08:00
Yidadaa
6841846613 fixup 2023-04-11 02:56:48 +08:00
Yidadaa
d6e6dd09f0 feat: dynamic config 2023-04-11 02:54:31 +08:00
Yidadaa
9b61cb1335 refactor: build/runtime/client configs 2023-04-11 01:21:34 +08:00
Yidadaa
7aee53ea05 fix: #507 break cjk chars in stream mode 2023-04-10 23:13:20 +08:00
Yidadaa
8df8ee8936 fix: #676 docker override old proxy files 2023-04-10 22:46:58 +08:00
aooyoo
042e989ebb fix spelling errors 2023-04-10 14:12:31 +00:00
aooyoo
7670b8b738 add new language, Japanese 2023-04-10 14:01:54 +00:00
Yifei Zhang
ec985f6a1d Merge pull request #679 from muhammetdemirel/main
Added new language, Turkish.
2023-04-10 18:03:39 +08:00
Yifei Zhang
2ec99bbb70 Update docker.yml 2023-04-10 17:39:21 +08:00
Muhammet Demirel
d7edcadec7 Added new language, Turkish. 2023-04-10 12:04:30 +03:00
Yifei Zhang
150735b001 Update access.ts 2023-04-10 10:57:16 +08:00
Yidadaa
3c1e81897a fixup: wont show fullscreen icon on mobile 2023-04-10 01:13:33 +08:00
Yifei Zhang
601e72b56c Merge pull request #659 from Yidadaa/bugfix-0409
fix: many UI bugs and resizable side bar
2023-04-10 01:04:38 +08:00
Yidadaa
09fd743e2e feat: wider app body 2023-04-10 00:56:44 +08:00
Yidadaa
6ae61c5357 fix: #522 resizable side bar 2023-04-10 00:54:17 +08:00
Yidadaa
eae5a8a2e6 feat: #577 maximum / minimium icon 2023-04-10 00:00:36 +08:00
Yidadaa
0e05733bbb fix: #589 improve unauthorized tips 2023-04-09 23:51:12 +08:00
Yidadaa
4a492264a1 fix: #641 delete wrong chat list 2023-04-09 23:41:16 +08:00
Yidadaa
174c745279 fix: #648 password input style 2023-04-09 23:35:45 +08:00
Yifei Zhang
3cfec63a95 Merge pull request #654 from Yidadaa/docker
feat: #120 docker proxy
2023-04-09 20:46:37 +08:00
yidadaa
0c92d49f89 chore: update readme 2023-04-09 20:43:34 +08:00
yidadaa
1bb7b4a653 feat: add proxy for docker 2023-04-09 20:35:42 +08:00
Yifei Zhang
72aa2bcad8 Update cn.ts 2023-04-09 13:31:33 +08:00
Yidadaa
c8be5e4267 feat: add docker proxy 2023-04-09 00:56:56 +08:00
Yifei Zhang
40b8b225f9 Create vercel.json 2023-04-08 22:27:51 +08:00
Yifei Zhang
2666241df7 Merge pull request #618 from ilario92/main
Update: Confirmation on reset chats and settings
2023-04-08 16:58:20 +08:00
GH Action - Upstream Sync
037d4638ea Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-04-08 07:07:44 +00:00
Yifei Zhang
c4f1376faf Merge pull request #621 from ClarenceYk/main
Update ui-lib.module.scss
2023-04-08 14:43:05 +08:00
MaYuKe
7d2e850c42 Update ui-lib.module.scss
Fix: icon-button can not be completely masked by popover.
2023-04-08 12:19:14 +08:00
GH Action - Upstream Sync
70b6507299 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-04-07 21:07:51 +00:00
Ilario Scandurra
00ba47c4de Update sync.yml 2023-04-07 22:19:21 +02:00
Ilario Scandurra
5b9fd1b101 Update sync.yml 2023-04-07 22:19:06 +02:00
Yifei Zhang
913305190a Update README_CN.md 2023-04-08 03:30:11 +08:00
Yifei Zhang
3c6f2962cc Merge pull request #617 from Yidadaa/bugfix-0408
fix: auto grow textarea
2023-04-08 03:13:14 +08:00
Yidadaa
45c8de42b9 fix: #528 wont send max_tokens 2023-04-08 03:03:07 +08:00
Yidadaa
637cda5b4c fix: #613 show all prompts when input / 2023-04-08 02:53:35 +08:00
ilarioscandurra
d935de3e57 Update: Confirmation on reset chats and settings 2023-04-07 20:50:06 +02:00
Yidadaa
13035ecb0d fix: auto grow textarea 2023-04-08 02:36:02 +08:00
Yifei Zhang
d6b2cf8bcb Merge pull request #318 from leedom92/textarea-height-optimize
feat: textarea with adaptive height
2023-04-08 01:01:52 +08:00
Yifei Zhang
9afed21efd Merge pull request #595 from yorunning/fix
fix: hide toast when confirmation box is cancelled
2023-04-08 00:59:09 +08:00
Yorun
71d9fbc367 fix: hide toast on cancel session deletion on mobile 2023-04-07 14:51:08 +08:00
Yifei Zhang
f83859113e Update README.md 2023-04-07 13:58:56 +08:00
leedom
00d45e7cc4 fix: solve navigator undefined && merge from main 2023-04-07 13:47:34 +08:00
leedom
406ed8a02a Merge branch 'main' into textarea-height-optimize 2023-04-07 13:33:37 +08:00
Yifei Zhang
e793b0c49d Merge pull request #587 from zhongmeizhi/main
feat: 添加对热键的支持 Escape to close settings, Up Arrow to get last input
2023-04-07 13:09:09 +08:00
leedom
de740ec57f feat: add calcTextareaHeight.ts 2023-04-07 12:42:47 +08:00
leedom
e5b4cb28fe fix: useEffect hooks 2023-04-07 12:39:27 +08:00
leedom
cb210d82e1 update: resizeTextarea 2023-04-07 12:33:09 +08:00
Mokou
58b956f7cc feat: Hot keys: Escape to close settings, Up Arrow to get last input 2023-04-07 12:26:22 +08:00
leedom
620b98fe6a Merge branch 'main' into textarea-height-optimize 2023-04-07 12:20:53 +08:00
leedom
3656c8458f feat: textarea with adaptive height 2023-04-07 12:17:37 +08:00
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
leedom
a8a8becf96 merge 2023-04-07 07:29:17 +08:00
leedom
b8a605f07d merge 2023-04-07 07:18:53 +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
Ilario Scandurra
502d22bd20 Merge branch 'Yidadaa:main' into main 2023-04-02 21:14:04 +02:00
ilarioscandurra
f9f29afba9 Update: locales in Italian 2023-04-02 20:32:43 +02:00
Ilario Scandurra
c98517fc48 Merge branch 'main' into main 2023-04-02 20:13:58 +02:00
ilarioscandurra
f606a61e1c Update: locales in Italian 2023-04-02 19:23:50 +02:00
leedom
3825b3c2c2 Merge branch 'textarea-height-optimize' of github.com:leedom92/ChatGPT-Next-Web into textarea-height-optimize 2023-04-02 11:16:30 +08:00
Leedom
4369b26e22 Update calcTextareaHeight.js 2023-04-02 09:47:55 +08:00
leedom
6f7c2916ef Merge remote-tracking branch 'upstream/main' into textarea-height-optimize 2023-04-02 03:20:06 +08:00
Leedom
b419e7d918 Update home.tsx 2023-04-02 02:56:45 +08:00
Leedom
ad8e09d188 Update home.tsx 2023-04-02 02:54:46 +08:00
leedom
bce020fc8e feat: add calcTextareaHeight.js from element-ui 2023-04-02 02:42:00 +08:00
leedom
a811637176 refactor: use the method of element-ui 2023-04-02 02:40:00 +08:00
Leedom
9ba59351c5 Update home.tsx 2023-04-01 17:48:05 +08:00
leedom
2a79d35667 feat: add optimize textarea height when inputing 2023-04-01 17:34:38 +08:00
github-actions[bot]
4dbc984351 Merge pull request #1 from Yidadaa/main
Fork Sync
2023-03-31 08:52:24 +00:00
132 changed files with 9387 additions and 2204 deletions

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: ''
---
**这个功能与现有的问题有关吗?**
如果有关,请在此列出链接或者描述问题。
**你想要什么功能或者有什么建议?**
尽管告诉我们。
**有没有可以参考的同类竞品?**
可以给出参考产品的链接或者截图。
**其他信息**
可以说说你的其他考虑。

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

@@ -0,0 +1,32 @@
---
name: 反馈问题
about: 请告诉我们你遇到的问题
title: "[Bug] "
labels: ''
assignees: ''
---
**反馈须知**
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。
请在下方中括号内输入 x 来表示你已经知晓相关内容。
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
**描述问题**
请在此描述你遇到了什么问题。
**如何复现**
请告诉我们你是通过什么操作触发的该问题。
**截图**
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
**一些必要的信息**
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
- 浏览器: [比如 chrome, safari]
- 版本: [填写设置页面的版本号]
- 部署方式:[比如 vercel、docker 或者服务器部署]

View File

@@ -43,7 +43,7 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,29 +1,40 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: '0 */12 * * *' # every 12 hours
workflow_dispatch: # on button click
- cron: "0 0 * * *" # every day
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, provided by github
- name: Checkout target repo
uses: actions/checkout@v3
# 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
# 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
- name: Sync check
if: failure()
run: |
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
exit 1

5
.gitignore vendored
View File

@@ -36,4 +36,7 @@ yarn-error.log*
next-env.d.ts
dev
public/prompts.json
public/prompts.json
.vscode
.idea

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.

View File

@@ -8,6 +8,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn config set registry 'https://registry.npmmirror.com/'
RUN yarn install
FROM base AS builder
@@ -16,7 +17,6 @@ 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
@@ -27,6 +27,9 @@ RUN yarn build
FROM base AS runner
WORKDIR /app
RUN apk add proxychains-ng
ENV PROXY_URL=""
ENV OPENAI_API_KEY=""
ENV CODE=""
@@ -37,4 +40,20 @@ COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
CMD ["node","server.js"]
CMD if [ -n "$PROXY_URL" ]; then \
protocol=$(echo $PROXY_URL | cut -d: -f1); \
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
port=$(echo $PROXY_URL | cut -d: -f3); \
conf=/etc/proxychains.conf; \
echo "strict_chain" > $conf; \
echo "proxy_dns" >> $conf; \
echo "remote_dns_subnet 224" >> $conf; \
echo "tcp_read_time_out 15000" >> $conf; \
echo "tcp_connect_time_out 8000" >> $conf; \
echo "[ProxyList]" >> $conf; \
echo "$protocol $host $port" >> $conf; \
cat /etc/proxychains.conf; \
proxychains -f $conf node server.js; \
else \
node server.js; \
fi

251
README.md
View File

@@ -1,105 +1,136 @@
<div align="center">
<img src="./static/icon.svg" alt="预览"/>
<img src="./docs/images/icon.svg" alt="icon"/>
<h1 align="center">ChatGPT Next Web</h1>
English / [简体中文](./README_CN.md)
One-Click to deploy well-designed ChatGPT web UI on Vercel.
一键免费部署你的私人 ChatGPT 网页应用。
One-Click to deploy your own ChatGPT web UI.
[Demo](https://chatgpt.nextweb.fun/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
[演示 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)
[演示](https://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/236402186-fa76e930-64f5-47ae-b967-b0f04b1fbf56.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
[![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
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 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)
- Privacy first, all data stored locally in the browser
- Markdown support: LaTex, mermaid, code highlight, etc.
- Responsive design, dark mode and PWA
- Fast first screen loading speed (~100kb), support streaming response
- New in v2: create, share and debug your chat tools with prompt templates (mask)
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
- Automatically compresses chat history to support long conversations while also saving your tokens
- One-click export all chat history with full Markdown support
- I18n supported
- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
## 开发计划 Roadmap
## Roadmap
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [x] User Prompt: user can edit and save custom prompts to prompt list
- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
- [ ] Share as image, share to ShareGPT
- [ ] Desktop App with tauri
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能 Not in Plan
### Not in Plan
- User login, accounts, cloud sync 用户登录、账号管理、消息云同步
- UI text customize 界面文字自定义
- User login, accounts, cloud sync
- UI text customize
## 开始使用
## What's New
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 在某些区域被污染了,绑定自定义域名即可直连。
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
- 完整的 Markdown 支持LaTex 公式、Mermaid 流程图、代码高亮等等
- 精心设计的 UI响应式设计支持深色模式支持 PWA
- 极快的首屏加载速度(~100kb支持流式响应
- 隐私安全,所有数据保存在用户浏览器本地
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [x] 允许用户自行编辑内置 Prompt 列表
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
- [ ] 分享为图片,分享到 ShareGPT
- [ ] 使用 tauri 打包桌面应用
- [ ] 支持自部署的大语言模型
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
- 界面文字自定义
- 用户登录、账号管理、消息云同步
## 最新动态
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
## 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&env=CODE&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
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
推荐你按照下列步骤重新部署:
[简体中文 > 常见问题](./docs/faq-cn.md)
- 删除掉原先的 repo
- fork 本项目;
- 前往 vercel 控制台,删除掉原先的 project然后新建 project选择你刚刚 fork 出来的项目重新进行部署即可;
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
[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
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
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.
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.
- Delete the original repository;
- Use the fork button in the upper right corner of the page to fork this project;
- Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md).
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.
### Enable Automatic Updates
> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
![Automatic Updates](./docs/images/enable-actions.jpg)
![Enable Automatic Updates](./docs/images/enable-actions-sync.jpg)
### Manually Updating Code
If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time.
## 配置密码 Password
## Access Password
本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码
```
code1,code2,code3
```
增加或修改该环境变量后,请**重新部署**项目使改动生效。
> [简体中文 > 如何增加访问密码](./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:
@@ -109,90 +140,134 @@ code1,code2,code3
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
## 环境变量 Environment Variables
## Environment Variables
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
OpanAI 密钥。
Your openai api key.
### `CODE` (optional)
访问密码,可选,可以使用逗号隔开多个密码。
Access passsword, separated by comma.
### `BASE_URL` (optional)
> Default: `api.openai.com`
> Default: `https://api.openai.com`
OpenAI 接口代理 URL。
> Examples: `http://your-openai-proxy.com`
Override openai api request base url.
### `PROTOCOL` (optional)
### `OPENAI_ORG_ID` (optional)
> Default: `https`
Specify OpenAI organization ID.
> Values: `http` | `https`
### `HIDE_USER_API_KEY` (optional)
OpenAI 接口协议。
> Default: Empty
Override openai api request protocol.
If you do not want users to input their own API key, set this value to 1.
## 开发 Development
### `DISABLE_GPT4` (optional)
点击下方按钮,开始二次开发:
> Default: Empty
If you do not want users to use GPT-4, set this value to 1.
## 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 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>
```
### 本地开发 Local Development
### Local Development
> 如果你是中国大陆用户,不建议在本地进行开发,除非你能够独立解决 OpenAI API 本地代理问题。
```shell
# 1. install nodejs and yarn first
# 2. config local env vars in `.env.local`
# 3. run
yarn install
yarn dev
```
1. 安装 nodejs 和 yarn具体细节请询问 ChatGPT
2. 执行 `yarn install && yarn dev` 即可。
## Deployment
### 本地部署 Local Deployment
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
### Docker (Recommended)
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="your-password" \
yidadaa/chatgpt-next-web
```
You can start service behind a proxy:
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="your-password" \
-e PROXY_URL="http://localhost:7890" \
yidadaa/chatgpt-next-web
```
### Shell
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
### 容器部署 Docker Deployment
## Screenshots
```shell
docker pull yidadaa/chatgpt-next-web
![Settings](./docs/images/settings.png)
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
```
![More](./docs/images/more.png)
## 截图 Screenshots
## Donation
![设置 Settings](./static/settings.png)
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
![更多展示 More](./static/more.png)
## Special Thanks
### Sponsor
## 鸣谢 Special Thanks
### 捐赠者 Sponsor
> 仅列出捐赠金额 >= 100RMB 的用户。
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
[@relativequantum](https://github.com/relativequantum)
[@desenmeng](https://github.com/desenmeng)
[@webees](https://github.com/webees)
[@chazzhou](https://github.com/chazzhou)
[@hauy](https://github.com/hauy)
[@Corwin006](https://github.com/Corwin006)
[@yankunsong](https://github.com/yankunsong)
[@ypwhs](https://github.com/ypwhs)
[@fxxxchao](https://github.com/fxxxchao)
[@hotic](https://github.com/hotic)
[@WingCH](https://github.com/WingCH)
[@jtung4](https://github.com/jtung4)
[@micozhu](https://github.com/micozhu)
[@jhansion](https://github.com/jhansion)
[@Sha1rholder](https://github.com/Sha1rholder)
[@AnsonHyq](https://github.com/AnsonHyq)
[@synwith](https://github.com/synwith)
### 贡献者 Contributor
### Contributor
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)

175
README_CN.md Normal file
View File

@@ -0,0 +1,175 @@
<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)
![主界面](./docs/images/cover.png)
</div>
## 开始使用
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 和[页面访问密码](#配置页面访问密码) CODE
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
## 保持更新
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
推荐你按照下列步骤重新部署:
- 删除掉原先的仓库;
- 使用页面右上角的 fork 按钮fork 本项目;
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
### 打开自动更新
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新
![自动更新](./docs/images/enable-actions.jpg)
![启用自动更新](./docs/images/enable-actions-sync.jpg)
### 手动更新代码
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
## 配置页面访问密码
> 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。
> **警告**:请务必将密码的位数设置得足够长,最好 7 位以上,否则[会被爆破](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码:
```
code1,code2,code3
```
增加或修改该环境变量后,请**重新部署**项目使改动生效。
## 环境变量
> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。
### `OPENAI_API_KEY` (必填项)
OpanAI 密钥,你在 openai 账户页面申请的 api key。
### `CODE` (可选)
访问密码,可选,可以使用逗号隔开多个密码。
**警告**:如果不填写此项,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。
### `BASE_URL` (可选)
> Default: `https://api.openai.com`
> Examples: `http://your-openai-proxy.com`
OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填写此选项。
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
### `OPENAI_ORG_ID` (可选)
指定 OpenAI 中的组织 ID。
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
### `DISABLE_GPT4` (可选)
如果你不想让用户使用 GPT-4将此环境变量设置为 1 即可。
## 开发
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 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 18 和 yarn具体细节请询问 ChatGPT
2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
## 部署
### 容器部署 (推荐)
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
> ⚠️ 注意docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
yidadaa/chatgpt-next-web
```
你也可以指定 proxy
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
--net=host \
-e PROXY_URL="http://127.0.0.1:7890" \
yidadaa/chatgpt-next-web
```
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
### 本地部署
在控制台运行下方命令:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
## 鸣谢
### 捐赠者
> 见英文版。
### 贡献者
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## 开源协议
> 反对 996从我开始。
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

View File

@@ -1,17 +0,0 @@
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;

71
app/api/auth.ts Normal file
View File

@@ -0,0 +1,71 @@
import { NextRequest } from "next/server";
import { getServerSideConfig } from "../config/server";
import md5 from "spark-md5";
import { ACCESS_CODE_PREFIX } from "../constant";
const serverConfig = getServerSideConfig();
function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip");
const forwardedFor = req.headers.get("x-forwarded-for");
if (!ip && forwardedFor) {
ip = forwardedFor.split(",").at(0) ?? "";
}
return ip;
}
function parseApiKey(bearToken: string) {
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
return {
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isOpenAiKey ? token : "",
};
}
export function auth(req: NextRequest) {
const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token
const { accessCode, apiKey: token } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return {
error: true,
needAccessCode: true,
msg: "Please go settings page and fill your access code.",
};
}
// if user does not provide an api key, inject system api key
if (!token) {
const apiKey = serverConfig.apiKey;
if (apiKey) {
console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${apiKey}`);
} else {
console.log("[Auth] admin did not provide an api key");
return {
error: true,
msg: "Empty Api Key",
};
}
} else {
console.log("[Auth] use user api key");
}
return {
error: false,
};
}

View File

@@ -1,61 +0,0 @@
import { createParser } from "eventsource-parser";
import { NextRequest } from "next/server";
import { requestOpenai } from "../common";
async function createStream(req: NextRequest) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const res = await requestOpenai(req);
const contentType = res.headers.get("Content-Type") ?? "";
if (!contentType.includes("stream")) {
const content = await (
await res.text()
).replace(/provided:.*. You/, "provided: ***. You");
console.log("[Stream] error ", content);
return "```json\n" + content + "```";
}
const stream = new ReadableStream({
async start(controller) {
function onParse(event: any) {
if (event.type === "event") {
const data = event.data;
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
if (data === "[DONE]") {
controller.close();
return;
}
try {
const json = JSON.parse(data);
const text = json.choices[0].delta.content;
const queue = encoder.encode(text);
controller.enqueue(queue);
} catch (e) {
controller.error(e);
}
}
}
const parser = createParser(onParse);
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk));
}
},
});
return stream;
}
export async function POST(req: NextRequest) {
try {
const stream = await createStream(req);
return new Response(stream);
} catch (error) {
console.error("[Chat Stream]", error);
}
}
export const config = {
runtime: "edge",
};

View File

@@ -6,16 +6,38 @@ 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");
const authValue = req.headers.get("Authorization") ?? "";
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
let baseUrl = BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`;
}
console.log("[Proxy] ", openaiPath);
console.log("[Base Url]", baseUrl);
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
if (process.env.OPENAI_ORG_ID) {
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
}
if (!authValue || !authValue.startsWith("Bearer sk-")) {
console.error("[OpenAI Request] invalid api key provided", authValue);
}
return fetch(`${baseUrl}/${openaiPath}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
Authorization: authValue,
...(process.env.OPENAI_ORG_ID && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
}),
},
cache: "no-store",
method: req.method,
body: req.body,
});

26
app/api/config/route.ts Normal file
View File

@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,
hideUserApiKey: serverConfig.hideUserApiKey,
enableGPT4: serverConfig.enableGPT4,
};
declare global {
type DangerConfig = typeof DANGER_CONFIG;
}
async function handle() {
return NextResponse.json(DANGER_CONFIG);
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";

View File

@@ -0,0 +1,101 @@
import { createParser } from "eventsource-parser";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
import { requestOpenai } from "../../common";
async function createStream(res: Response) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
async start(controller) {
function onParse(event: any) {
if (event.type === "event") {
const data = event.data;
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
if (data === "[DONE]") {
controller.close();
return;
}
try {
const json = JSON.parse(data);
const text = json.choices[0].delta.content;
const queue = encoder.encode(text);
controller.enqueue(queue);
} catch (e) {
controller.error(e);
}
}
}
const parser = createParser(onParse);
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk, { stream: true }));
}
},
});
return stream;
}
function formatResponse(msg: any) {
const jsonMsg = ["```json\n", JSON.stringify(msg, null, " "), "\n```"].join(
"",
);
return new Response(jsonMsg);
}
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[OpenAI Route] params ", params);
const authResult = auth(req);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const api = await requestOpenai(req);
const contentType = api.headers.get("Content-Type") ?? "";
// streaming response
if (contentType.includes("stream")) {
const stream = await createStream(api);
const res = new Response(stream);
res.headers.set("Content-Type", contentType);
return res;
}
// try to parse error msg
try {
const mayBeErrorBody = await api.json();
if (mayBeErrorBody.error) {
console.error("[OpenAI Response] ", mayBeErrorBody);
return formatResponse(mayBeErrorBody);
} else {
const res = new Response(JSON.stringify(mayBeErrorBody));
res.headers.set("Content-Type", "application/json");
res.headers.set("Cache-Control", "no-cache");
return res;
}
} catch (e) {
console.error("[OpenAI Parse] ", e);
return formatResponse({
msg: "invalid response from openai server",
error: e,
});
}
} catch (e) {
console.error("[OpenAI] ", e);
return formatResponse(e);
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";

View File

@@ -1,30 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { requestOpenai } from "../common";
async function makeRequest(req: NextRequest) {
try {
const api = await requestOpenai(req);
const res = new NextResponse(api.body);
res.headers.set("Content-Type", "application/json");
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

@@ -4,4 +4,6 @@ import type {
} from "openai";
export type ChatRequest = CreateChatCompletionRequest;
export type ChatReponse = CreateChatCompletionResponse;
export type ChatResponse = CreateChatCompletionResponse;
export type Updater<T> = (updater: (value: T) => void) => void;

28
app/command.ts Normal file
View File

@@ -0,0 +1,28 @@
import { useSearchParams } from "react-router-dom";
type Command = (param: string) => void;
interface Commands {
fill?: Command;
submit?: Command;
mask?: Command;
}
export function useCommand(commands: Commands = {}) {
const [searchParams, setSearchParams] = useSearchParams();
if (commands === undefined) return;
let shouldUpdate = false;
searchParams.forEach((param, name) => {
const commandName = name as keyof Commands;
if (typeof commands[commandName] === "function") {
commands[commandName]!(param);
searchParams.delete(name);
shouldUpdate = true;
}
});
if (shouldUpdate) {
setSearchParams(searchParams);
}
}

View File

@@ -10,6 +10,23 @@
transition: all 0.3s ease;
overflow: hidden;
user-select: none;
outline: none;
border: none;
color: var(--black);
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
&.primary {
background-color: var(--primary);
color: white;
path {
fill: white !important;
}
}
}
.shadow {
@@ -41,4 +58,7 @@
.icon-button-text {
margin-left: 5px;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -4,34 +4,42 @@ import styles from "./button.module.scss";
export function IconButton(props: {
onClick?: () => void;
icon: JSX.Element;
icon?: JSX.Element;
type?: "primary" | "danger";
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.shadow && styles.shadow} ${
props.className ?? ""
} clickable`
} clickable ${styles[props.type ?? ""]}`
}
onClick={props.onClick}
title={props.title}
disabled={props.disabled}
role="button"
>
<div
className={styles["icon-button-icon"] + ` ${props.noDark && "no-dark"}`}
>
{props.icon}
</div>
{props.icon && (
<div
className={
styles["icon-button-icon"] +
` ${props.type === "primary" && "no-dark"}`
}
>
{props.icon}
</div>
)}
{props.text && (
<div className={styles["icon-button-text"]}>{props.text}</div>
)}
</div>
</button>
);
}

View File

@@ -1,17 +1,22 @@
import { useState, useRef, useEffect, useLayoutEffect } from "react";
import DeleteIcon from "../icons/delete.svg";
import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg";
import styles from "./home.module.scss";
import {
Message,
SubmitKey,
useChatStore,
ChatSession,
BOT_HELLO,
} from "../store";
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { useChatStore } from "../store";
import Locale from "../locales";
import { isMobileScreen } from "../utils";
import { Link, useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask";
import { useRef, useEffect } from "react";
export function ChatItem(props: {
onClick?: () => void;
@@ -20,54 +25,135 @@ export function ChatItem(props: {
count: number;
time: string;
selected: boolean;
id: number;
index: number;
narrow?: boolean;
mask: Mask;
}) {
const draggableRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.selected && draggableRef.current) {
draggableRef.current?.scrollIntoView({
block: "center",
});
}
}, [props.selected]);
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)}
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={(ele) => {
draggableRef.current = ele;
provided.innerRef(ele);
}}
{...provided.draggableProps}
{...provided.dragHandleProps}
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
props.count,
)}`}
>
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}>
<MaskAvatar mask={props.mask} />
</div>
<div className={styles["chat-item-narrow-count"]}>
{props.count}
</div>
</div>
) : (
<>
<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"]}>
{new Date(props.time).toLocaleString()}
</div>
</div>
</>
)}
<div
className={styles["chat-item-delete"]}
onClickCapture={props.onDelete}
>
<DeleteIcon />
</div>
</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] = useChatStore(
export function ChatList(props: { narrow?: boolean }) {
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
(state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.removeSession,
state.moveSession,
],
);
const chatStore = useChatStore();
const navigate = useNavigate();
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 (
<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={() =>
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
removeSession(i)
}
/>
))}
</div>
<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={new Date(item.lastUpdate).toLocaleString()}
count={item.messages.length}
key={item.id}
id={item.id}
index={i}
selected={i === selectedIndex}
onClick={() => {
navigate(Path.Chat);
selectSession(i);
}}
onDelete={() => {
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(i);
}
}}
narrow={props.narrow}
mask={item.mask}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}

View File

@@ -1,5 +1,29 @@
@import "../styles/animation.scss";
.chat-input-actions {
display: flex;
flex-wrap: wrap;
.chat-input-action {
display: inline-flex;
border-radius: 20px;
font-size: 12px;
background-color: var(--white);
color: var(--black);
border: var(--border-in-light);
padding: 4px 10px;
animation: slide-in ease 0.3s;
box-shadow: var(--card-shadow);
transition: all ease 0.3s;
margin-bottom: 10px;
align-items: center;
&:not(:last-child) {
margin-right: 5px;
}
}
}
.prompt-toast {
position: absolute;
bottom: -50px;
@@ -29,6 +53,20 @@
}
}
.section-title {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.section-title-action {
display: flex;
align-items: center;
}
}
.context-prompt {
.context-prompt-row {
display: flex;
@@ -57,17 +95,13 @@
}
.memory-prompt {
margin-top: 20px;
.memory-prompt-title {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
}
margin: 20px 0;
.memory-prompt-content {
background-color: var(--gray);
border-radius: 6px;
background-color: var(--white);
color: var(--black);
border: var(--border-in-light);
border-radius: 10px;
padding: 10px;
font-size: 12px;
user-select: text;

View File

@@ -3,29 +3,42 @@ import { 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 MenuIcon from "../icons/menu.svg";
import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.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 PromptIcon from "../icons/prompt.svg";
import MaskIcon from "../icons/mask.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import ResetIcon from "../icons/reload.svg";
import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg";
import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg";
import {
Message,
SubmitKey,
useChatStore,
ChatSession,
BOT_HELLO,
ROLES,
createMessage,
useAccessStore,
Theme,
useAppConfig,
DEFAULT_TOPIC,
} from "../store";
import {
copyToClipboard,
downloadAs,
isMobileScreen,
selectOrCopy,
autoGrowTextArea,
useMobileScreen,
} from "../utils";
import dynamic from "next/dynamic";
@@ -38,36 +51,26 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
import { Modal, showModal, showToast } from "./ui-lib";
import { ListItem, Modal, showModal } from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path } from "../constant";
import { Avatar } from "./emoji";
import { MaskAvatar, MaskConfig } from "./mask";
import { useMaskStore } from "../store/mask";
import { useCommand } from "../command";
const Markdown = dynamic(async () => (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} />
</div>
);
}
function exportMessages(messages: Message[], topic: string) {
const mdText =
`# ${topic}\n\n` +
messages
.map((m) => {
return m.role === "user" ? `## ${m.content}` : m.content.trim();
return m.role === "user"
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
})
.join("\n\n");
const filename = `${topic}.md`;
@@ -98,6 +101,64 @@ function exportMessages(messages: Message[], topic: string) {
});
}
export function SessionConfigModel(props: { onClose: () => void }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const maskStore = useMaskStore();
const navigate = useNavigate();
return (
<div className="modal-mask">
<Modal
title={Locale.Context.Edit}
onClose={() => props.onClose()}
actions={[
<IconButton
key="reset"
icon={<ResetIcon />}
bordered
text={Locale.Chat.Config.Reset}
onClick={() =>
confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Chat.Config.SaveAs}
onClick={() => {
navigate(Path.Masks);
setTimeout(() => {
maskStore.create(session.mask);
}, 500);
}}
/>,
]}
>
<MaskConfig
mask={session.mask}
updateMask={(updater) => {
const mask = { ...session.mask };
updater(mask);
chatStore.updateCurrentSession((session) => (session.mask = mask));
}}
extraListItems={
session.mask.modelConfig.sendMemory ? (
<ListItem
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
></ListItem>
) : (
<></>
)
}
></MaskConfig>
</Modal>
</div>
);
}
function PromptToast(props: {
showToast?: boolean;
showModal?: boolean;
@@ -105,25 +166,7 @@ function PromptToast(props: {
}) {
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;
});
};
const context = session.mask.context;
return (
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
@@ -140,96 +183,14 @@ function PromptToast(props: {
</div>
)}
{props.showModal && (
<div className="modal-mask">
<Modal
title={Locale.Context.Edit}
onClose={() => props.setShowModal(false)}
actions={[
<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"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
content: e.target.value as any,
})
}
></input>
<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"]}>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
</div>
<div className={chatStyle["memory-prompt-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</div>
</div>
</>
</Modal>
</div>
<SessionConfigModel onClose={() => props.setShowModal(false)} />
)}
</div>
);
}
function useSubmitHandler() {
const config = useChatStore((state) => state.config);
const config = useAppConfig();
const submitKey = config.submitKey;
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -258,15 +219,65 @@ export function PromptHints(props: {
prompts: Prompt[];
onPromptSelect: (prompt: Prompt) => void;
}) {
if (props.prompts.length === 0) return null;
const noPrompts = props.prompts.length === 0;
const [selectIndex, setSelectIndex] = useState(0);
const selectedRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectIndex(0);
}, [props.prompts.length]);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (noPrompts) return;
if (e.metaKey || e.altKey || e.ctrlKey) {
return;
}
// arrow up / down to select prompt
const changeIndex = (delta: number) => {
e.stopPropagation();
e.preventDefault();
const nextIndex = Math.max(
0,
Math.min(props.prompts.length - 1, selectIndex + delta),
);
setSelectIndex(nextIndex);
selectedRef.current?.scrollIntoView({
block: "center",
});
};
if (e.key === "ArrowUp") {
changeIndex(1);
} else if (e.key === "ArrowDown") {
changeIndex(-1);
} else if (e.key === "Enter") {
const selectedPrompt = props.prompts.at(selectIndex);
if (selectedPrompt) {
props.onPromptSelect(selectedPrompt);
}
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [noPrompts, selectIndex]);
if (noPrompts) return null;
return (
<div className={styles["prompt-hints"]}>
{props.prompts.map((prompt, i) => (
<div
className={styles["prompt-hint"]}
ref={i === selectIndex ? selectedRef : null}
className={
styles["prompt-hint"] +
` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
}
key={prompt.title + i.toString()}
onClick={() => props.onPromptSelect(prompt)}
onMouseEnter={() => setSelectIndex(i)}
>
<div className={styles["hint-title"]}>{prompt.title}</div>
<div className={styles["hint-content"]}>{prompt.content}</div>
@@ -280,26 +291,109 @@ function useScrollToBottom() {
// for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const scrollToBottom = () => {
const dom = scrollRef.current;
if (dom) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
}
};
// auto scroll
useLayoutEffect(() => {
const dom = scrollRef.current;
if (dom && autoScroll) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
}
autoScroll && scrollToBottom();
});
return {
scrollRef,
autoScroll,
setAutoScroll,
scrollToBottom,
};
}
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
export function ChatActions(props: {
showPromptModal: () => void;
scrollToBottom: () => void;
showPromptHints: () => void;
hitBottom: boolean;
}) {
const config = useAppConfig();
const navigate = useNavigate();
// switch themes
const theme = config.theme;
function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme);
const nextIndex = (themeIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
config.update((config) => (config.theme = nextTheme));
}
// stop all responses
const couldStop = ControllerPool.hasPending();
const stopAll = () => ControllerPool.stopAll();
return (
<div className={chatStyle["chat-input-actions"]}>
{couldStop && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={stopAll}
>
<StopIcon />
</div>
)}
{!props.hitBottom && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.scrollToBottom}
>
<BottomIcon />
</div>
)}
{props.hitBottom && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptModal}
>
<BrainIcon />
</div>
)}
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={nextTheme}
>
{theme === Theme.Auto ? (
<AutoIcon />
) : theme === Theme.Light ? (
<LightIcon />
) : theme === Theme.Dark ? (
<DarkIcon />
) : null}
</div>
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptHints}
>
<PromptIcon />
</div>
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={() => {
navigate(Path.Masks);
}}
>
<MaskIcon />
</div>
</div>
);
}
export function Chat() {
type RenderMessage = Message & { preview?: boolean };
const chatStore = useChatStore();
@@ -307,17 +401,20 @@ export function Chat(props: {
state.currentSession(),
state.currentSessionIndex,
]);
const fontSize = useChatStore((state) => state.config.fontSize);
const config = useAppConfig();
const fontSize = 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 { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(true);
const isMobileScreen = useMobileScreen();
const navigate = useNavigate();
const onChatBodyScroll = (e: HTMLElement) => {
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100;
setHitBottom(isTouchBottom);
};
@@ -333,100 +430,143 @@ export function Chat(props: {
);
const onPromptSelect = (prompt: Prompt) => {
setUserInput(prompt.content);
setPromptHints([]);
inputRef.current?.focus();
setTimeout(() => setUserInput(prompt.content), 60);
};
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;
};
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
() => {
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
const inputRows = Math.min(
20,
Math.max(2 + Number(!isMobileScreen), rows),
);
setInputRows(inputRows);
},
100,
{
leading: true,
trailing: true,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]);
// 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) {
} else if (!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;
const doSubmit = (userInput: string) => {
if (userInput.trim() === "") return;
setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
localStorage.setItem(LAST_INPUT_KEY, userInput);
setUserInput("");
setPromptHints([]);
inputRef.current?.focus();
if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true);
};
// stop response
const onUserStop = (messageIndex: number) => {
ControllerPool.stop(sessionIndex, messageIndex);
const onUserStop = (messageId: number) => {
ControllerPool.stop(sessionIndex, messageId);
};
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// if ArrowUp and no userInput, fill with last input
if (
e.key === "ArrowUp" &&
userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey)
) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
e.preventDefault();
return;
}
if (shouldSubmit(e)) {
onUserSubmit();
doSubmit(userInput);
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) => {
const findLastUserIndex = (messageId: 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));
inputRef.current?.focus();
return;
let lastUserMessageIndex: number | null = null;
for (let i = 0; i < session.messages.length; i += 1) {
const message = session.messages[i];
if (message.id === messageId) {
break;
}
if (message.role === "user") {
lastUserMessageIndex = i;
}
}
return lastUserMessageIndex;
};
const config = useChatStore((state) => state.config);
const deleteMessage = (userIndex: number) => {
chatStore.updateCurrentSession((session) =>
session.messages.splice(userIndex, 2),
);
};
const context: RenderMessage[] = session.context.slice();
const onDelete = (botMessageId: number) => {
const userIndex = findLastUserIndex(botMessageId);
if (userIndex === null) return;
deleteMessage(userIndex);
};
const onResend = (botMessageId: number) => {
// find last user input message and resend
const userIndex = findLastUserIndex(botMessageId);
if (userIndex === null) return;
setIsLoading(true);
const content = session.messages[userIndex].content;
deleteMessage(userIndex);
chatStore.onUserInput(content).then(() => setIsLoading(false));
inputRef.current?.focus();
};
const context: RenderMessage[] = session.mask.context.slice();
const accessStore = useAccessStore();
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
context.push(BOT_HELLO);
const copiedHello = Object.assign({}, BOT_HELLO);
if (!accessStore.isAuthorized()) {
copiedHello.content = Locale.Error.Unauthorized;
}
context.push(copiedHello);
}
// preview messages
@@ -436,9 +576,10 @@ export function Chat(props: {
isLoading
? [
{
role: "assistant",
content: "……",
date: new Date().toLocaleString(),
...createMessage({
role: "assistant",
content: "……",
}),
preview: true,
},
]
@@ -448,9 +589,10 @@ export function Chat(props: {
userInput.length > 0 && config.sendPreviewBubble
? [
{
role: "user",
content: userInput,
date: new Date().toLocaleString(),
...createMessage({
role: "user",
content: userInput,
}),
preview: true,
},
]
@@ -459,64 +601,80 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
// Auto focus
useEffect(() => {
inputRef.current?.focus();
}, []);
const renameSession = () => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
}
};
const location = useLocation();
const isChat = location.pathname === Path.Chat;
const autoFocus = !isMobileScreen || isChat; // only focus in chat page
useCommand({
fill: setUserInput,
submit: (text) => {
doSubmit(text);
},
});
return (
<div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}>
<div
className={styles["window-header-title"]}
onClick={props?.showSideBar}
>
<div className="window-header">
<div className="window-header-title">
<div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClick={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
}
}}
className={`window-header-main-title " ${styles["chat-body-title"]}`}
onClickCapture={renameSession}
>
{session.topic}
{!session.topic ? DEFAULT_TOPIC : session.topic}
</div>
<div className={styles["window-header-sub-title"]}>
<div className="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}>
<div className="window-actions">
<div className={"window-action-button" + " " + styles.mobile}>
<IconButton
icon={<MenuIcon />}
icon={<ReturnIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar}
onClick={() => navigate(Path.Home)}
/>
</div>
<div className={styles["window-action-button"]}>
<div className="window-action-button">
<IconButton
icon={<BrainIcon />}
icon={<RenameIcon />}
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
setShowPromptModal(true);
}}
onClick={renameSession}
/>
</div>
<div className={styles["window-action-button"]}>
<div className="window-action-button">
<IconButton
icon={<ExportIcon />}
bordered
title={Locale.Chat.Actions.Export}
onClick={() => {
exportMessages(session.messages, session.topic);
exportMessages(
session.messages.filter((msg) => !msg.isError),
session.topic,
);
}}
/>
</div>
{!isMobileScreen && (
<div className="window-action-button">
<IconButton
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {
config.update(
(config) => (config.tightBorder = !config.tightBorder),
);
}}
/>
</div>
)}
</div>
<PromptToast
@@ -530,11 +688,20 @@ export function Chat(props: {
className={styles["chat-body"]}
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseOver={() => inputRef.current?.blur()}
onTouchStart={() => inputRef.current?.blur()}
onMouseDown={() => inputRef.current?.blur()}
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
onTouchStart={() => {
inputRef.current?.blur();
setAutoScroll(false);
}}
>
{messages.map((message, i) => {
const isUser = message.role === "user";
const showActions =
!isUser &&
i > 0 &&
!(message.preview || message.content.length === 0);
const showTyping = message.preview || message.streaming;
return (
<div
@@ -545,57 +712,67 @@ export function Chat(props: {
>
<div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}>
<Avatar role={message.role} />
{message.role === "user" ? (
<Avatar avatar={config.avatar} />
) : (
<MaskAvatar mask={session.mask} />
)}
</div>
{(message.preview || message.streaming) && (
{showTyping && (
<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 ? (
{showActions && (
<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={() => onUserStop(i)}
onClick={() => onDelete(message.id ?? i)}
>
{Locale.Chat.Actions.Stop}
{Locale.Chat.Actions.Delete}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
onClick={() => onResend(message.id ?? i)}
>
{Locale.Chat.Actions.Retry}
</div>
)}
</>
)}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</div>
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</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>
)}
<Markdown
content={message.content}
loading={
(message.preview || message.content.length === 0) &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput(message.content);
}}
fontSize={fontSize}
parentRef={scrollRef}
defaultShow={i >= messages.length - 10}
/>
</div>
{!isUser && !message.preview && (
<div className={styles["chat-message-actions"]}>
@@ -612,28 +789,42 @@ export function Chat(props: {
<div className={styles["chat-input-panel"]}>
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
<ChatActions
showPromptModal={() => setShowPromptModal(true)}
scrollToBottom={scrollToBottom}
hitBottom={hitBottom}
showPromptHints={() => {
// Click again to close
if (promptHints.length > 0) {
setPromptHints([]);
return;
}
inputRef.current?.focus();
setUserInput("/");
onSearch("");
}}
/>
<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}
onBlur={() => setAutoScroll(false)}
rows={inputRows}
autoFocus={autoFocus}
/>
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
noDark
onClick={onUserSubmit}
type="primary"
onClick={() => doSubmit(userInput)}
/>
</div>
</div>

59
app/components/emoji.tsx Normal file
View File

@@ -0,0 +1,59 @@
import EmojiPicker, {
Emoji,
EmojiStyle,
Theme as EmojiTheme,
} from "emoji-picker-react";
import { ModelType } from "../store";
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}
export function AvatarPicker(props: {
onEmojiClick: (emojiId: string) => void;
}) {
return (
<EmojiPicker
lazyLoadEmojis
theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl}
onEmojiClick={(e) => {
props.onEmojiClick(e.unified);
}}
/>
);
}
export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) {
return (
<div className="no-dark">
{props.model?.startsWith("gpt-4") ? (
<BlackBotIcon className="user-avatar" />
) : (
<BotIcon className="user-avatar" />
)}
</div>
);
}
return (
<div className="user-avatar">
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
</div>
);
}
export function EmojiAvatar(props: { avatar: string; size?: number }) {
return (
<Emoji
unified={props.avatar}
size={props.size ?? 18}
getEmojiUrl={getEmojiUrl}
/>
);
}

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

@@ -0,0 +1,73 @@
import React from "react";
import { IconButton } from "./button";
import GithubIcon from "../icons/github.svg";
import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { downloadAs } from "../utils";
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 });
}
clearAndSaveData() {
try {
downloadAs(
JSON.stringify(localStorage),
"chatgpt-next-web-snapshot.json",
);
} finally {
localStorage.clear();
location.reload();
}
}
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>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<a href={ISSUE_URL} className="report">
<IconButton
text="Report This Error"
icon={<GithubIcon />}
bordered
/>
</a>
<IconButton
icon={<ResetIcon />}
text="Clear All Data"
onClick={() =>
confirm(Locale.Settings.Actions.ConfirmClearAll) &&
this.clearAndSaveData()
}
bordered
/>
</div>
</div>
);
}
// if no error occurred, render children
return this.props.children;
}
}

View File

@@ -1,6 +1,3 @@
@import "./window.scss";
@import "../styles/animation.scss";
@mixin container {
background-color: var(--white);
border: var(--border-in-light);
@@ -10,7 +7,7 @@
background-color: var(--white);
min-width: 600px;
min-height: 480px;
max-width: 900px;
max-width: 1200px;
display: flex;
overflow: hidden;
@@ -36,6 +33,7 @@
max-height: var(--full-height);
border-radius: 0;
border: 0;
}
}
@@ -48,6 +46,40 @@
display: flex;
flex-direction: column;
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
position: relative;
transition: width ease 0.05s;
.sidebar-header-bar {
display: flex;
margin-bottom: 20px;
.sidebar-bar-button {
flex-grow: 1;
&:not(:last-child) {
margin-right: 10px;
}
}
}
}
.sidebar-drag {
$width: 10px;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: $width;
background-color: var(--black);
cursor: ew-resize;
opacity: 0;
transition: all ease 0.3s;
&:hover,
&:active {
opacity: 0.2;
}
}
.window-content {
@@ -104,19 +136,19 @@
.sidebar-title {
font-size: 20px;
font-weight: bold;
animation: slide-in ease 0.3s;
}
.sidebar-sub-title {
font-size: 12px;
font-weight: 400px;
animation: slide-in ease 0.3s;
}
.sidebar-body {
flex: 1;
overflow: auto;
}
.chat-list {
overflow-x: hidden;
}
.chat-item {
@@ -125,12 +157,11 @@
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;
position: relative;
overflow: hidden;
}
.chat-item:hover {
@@ -149,19 +180,21 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
animation: slide-in ease 0.3s;
}
.chat-item-delete {
position: absolute;
top: 10px;
right: -20px;
right: 0;
transition: all ease 0.3s;
opacity: 0;
cursor: pointer;
}
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
right: 10px;
transform: translateX(-10px);
}
.chat-item:hover > .chat-item-delete:hover {
@@ -174,12 +207,105 @@
color: rgb(166, 166, 166);
font-size: 12px;
margin-top: 8px;
animation: slide-in ease 0.3s;
}
.chat-item-count {
}
.chat-item-count,
.chat-item-date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.narrow-sidebar {
.sidebar-title,
.sidebar-sub-title {
display: none;
}
.sidebar-logo {
position: relative;
display: flex;
justify-content: center;
}
.sidebar-header-bar {
flex-direction: column;
.sidebar-bar-button {
&:not(:last-child) {
margin-right: 0;
margin-bottom: 10px;
}
}
}
.chat-item {
padding: 0;
min-height: 50px;
display: flex;
justify-content: center;
align-items: center;
transition: all ease 0.3s;
overflow: hidden;
&:hover {
.chat-item-narrow {
transform: scale(0.7) translateX(-50%);
}
}
}
.chat-item-narrow {
line-height: 0;
font-weight: lighter;
color: var(--black);
transform: translateX(0);
transition: all ease 0.3s;
padding: 4px;
display: flex;
flex-direction: column;
justify-content: center;
.chat-item-avatar {
display: flex;
justify-content: center;
opacity: 0.2;
position: absolute;
transform: scale(4);
}
.chat-item-narrow-count {
font-size: 24px;
font-weight: bolder;
text-align: center;
color: var(--primary);
opacity: 0.6;
}
}
.chat-item-delete {
top: 15px;
}
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
right: 5px;
}
.sidebar-tail {
flex-direction: column-reverse;
align-items: center;
.sidebar-actions {
flex-direction: column-reverse;
align-items: center;
.sidebar-action {
margin-right: 0;
margin-top: 15px;
}
}
}
}
.sidebar-tail {
@@ -207,7 +333,9 @@
flex: 1;
overflow: auto;
padding: 20px;
padding-bottom: 40px;
position: relative;
overscroll-behavior: none;
}
.chat-body-title {
@@ -221,6 +349,10 @@
.chat-message {
display: flex;
flex-direction: row;
&:last-child {
animation: slide-in ease 0.3s;
}
}
.chat-message-user {
@@ -233,7 +365,6 @@
display: flex;
flex-direction: column;
align-items: flex-start;
animation: slide-in ease 0.3s;
&:hover {
.chat-message-top-actions {
@@ -259,17 +390,6 @@
margin-top: 5px;
}
.user-avtar {
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
border-radius: 10px;
}
.chat-message-item {
box-sizing: border-box;
max-width: 100%;
@@ -331,11 +451,16 @@
}
.chat-input-panel {
position: relative;
width: 100%;
padding: 20px;
padding-top: 5px;
padding-top: 10px;
box-sizing: border-box;
flex-direction: column;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top: var(--border-in-light);
box-shadow: var(--card-shadow);
}
@mixin single-line {
@@ -405,17 +530,11 @@
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 14px 50px;
padding: 10px 90px 10px 14px;
resize: none;
outline: none;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
}
.chat-input:focus {
border: 1px solid var(--primary);
}
@@ -426,11 +545,22 @@
position: absolute;
right: 30px;
bottom: 30px;
bottom: 32px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
.chat-input-send {
bottom: 30px;
}
}
.export-content {
white-space: break-spaces;
padding: 10px !important;
}
.loading-content {

View File

@@ -1,42 +1,32 @@
"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";
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import {
Message,
SubmitKey,
useChatStore,
ChatSession,
BOT_HELLO,
} from "../store";
import {
copyToClipboard,
downloadAs,
isMobileScreen,
selectOrCopy,
} from "../utils";
import Locale from "../locales";
import { ChatList } from "./chat-list";
import { Chat } from "./chat";
import { getCSSVar, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { Path, SlotID } from "../constant";
import { ErrorBoundary } from "./error";
import {
HashRouter as Router,
Routes,
Route,
useLocation,
} from "react-router-dom";
import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
export function Loading(props: { noLogo?: boolean }) {
return (
<div className={styles["loading-content"]}>
<div className={styles["loading-content"] + " no-dark"}>
{!props.noLogo && <BotIcon />}
<LoadingIcon />
</div>
@@ -47,8 +37,20 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />,
});
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
const Chat = dynamic(async () => (await import("./chat")).Chat, {
loading: () => <Loading noLogo />,
});
const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, {
loading: () => <Loading noLogo />,
});
const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />,
});
export function useSwitchTheme() {
const config = useAppConfig();
useEffect(() => {
document.body.classList.remove("light");
@@ -60,11 +62,21 @@ function useSwitchTheme() {
document.body.classList.add("light");
}
const themeColor = getComputedStyle(document.body)
.getPropertyValue("--theme-color")
.trim();
const metaDescription = document.querySelector('meta[name="theme-color"]');
metaDescription?.setAttribute("content", themeColor);
const metaDescriptionDark = document.querySelector(
'meta[name="theme-color"][media*="dark"]',
);
const metaDescriptionLight = document.querySelector(
'meta[name="theme-color"][media*="light"]',
);
if (config.theme === "auto") {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
} else {
const themeColor = getCSSVar("--theme-color");
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
}
}, [config.theme]);
}
@@ -78,116 +90,62 @@ const useHasHydrated = () => {
return hasHydrated;
};
export function Home() {
const [createNewSession, currentIndex, removeSession] = useChatStore(
(state) => [
state.newSession,
state.currentSessionIndex,
state.removeSession,
],
);
const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
const loadAsyncGoogleFont = () => {
const linkEl = document.createElement("link");
linkEl.rel = "stylesheet";
linkEl.href =
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
document.head.appendChild(linkEl);
};
// setting
const [openSettings, setOpenSettings] = useState(false);
const config = useChatStore((state) => state.config);
function Screen() {
const config = useAppConfig();
const location = useLocation();
const isHome = location.pathname === Path.Home;
const isMobileScreen = useMobileScreen();
useSwitchTheme();
if (loading) {
return <Loading />;
}
useEffect(() => {
loadAsyncGoogleFont();
}, []);
return (
<div
className={`${
config.tightBorder && !isMobileScreen()
? styles["tight-container"]
: styles.container
}`}
className={
styles.container +
` ${
config.tightBorder && !isMobileScreen
? styles["tight-container"]
: styles.container
}`
}
>
<div
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
>
<div className={styles["sidebar-header"]}>
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
<div className={styles["sidebar-sub-title"]}>
Build your own AI assistant.
</div>
<div className={styles["sidebar-logo"]}>
<ChatGptIcon />
</div>
</div>
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<div
className={styles["sidebar-body"]}
onClick={() => {
setOpenSettings(false);
setShowSideBar(false);
}}
>
<ChatList />
</div>
<div className={styles["sidebar-tail"]}>
<div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
removeSession(currentIndex);
}
}}
/>
</div>
<div className={styles["sidebar-action"]}>
<IconButton
icon={<SettingsIcon />}
onClick={() => {
setOpenSettings(true);
setShowSideBar(false);
}}
shadow
/>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank">
<IconButton icon={<GithubIcon />} shadow />
</a>
</div>
</div>
<div>
<IconButton
icon={<AddIcon />}
text={Locale.Home.NewChat}
onClick={() => {
createNewSession();
setShowSideBar(false);
}}
shadow
/>
</div>
</div>
</div>
<div className={styles["window-content"]}>
{openSettings ? (
<Settings
closeSettings={() => {
setOpenSettings(false);
setShowSideBar(true);
}}
/>
) : (
<Chat
key="chat"
showSideBar={() => setShowSideBar(true)}
sideBarShowing={showSideBar}
/>
)}
<div className={styles["window-content"]} id={SlotID.AppBody}>
<Routes>
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>
</div>
</div>
);
}
export function Home() {
useSwitchTheme();
if (!useHasHydrated()) {
return <Loading />;
}
return (
<ErrorBoundary>
<Router>
<Screen />
</Router>
</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

@@ -7,9 +7,67 @@ import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
export function Mermaid(props: { code: string; onError: () => void }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (props.code && ref.current) {
mermaid
.run({
nodes: [ref.current],
})
.catch((e) => {
props.onError();
console.error("[Mermaid] ", e.message);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.code]);
function viewSvgInNewWindow() {
const svg = ref.current?.querySelector("svg");
if (!svg) return;
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const win = window.open(url);
if (win) {
win.onload = () => URL.revokeObjectURL(url);
}
}
return (
<div
className="no-dark"
style={{ cursor: "pointer", overflow: "auto" }}
ref={ref}
onClick={() => viewSvgInNewWindow()}
>
{props.code}
</div>
);
}
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
const [mermaidCode, setMermaidCode] = useState("");
useEffect(() => {
if (!ref.current) return;
const mermaidDom = ref.current.querySelector("code.language-mermaid");
if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
}, [props.children]);
if (mermaidCode) {
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
}
return (
<pre ref={ref}>
@@ -27,30 +85,7 @@ export function PreCode(props: { children: any }) {
);
}
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 }) {
function _MarkDownContent(props: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
@@ -66,9 +101,80 @@ export function Markdown(props: { content: string }) {
]}
components={{
pre: PreCode,
a: (aProps) => {
const href = aProps.href || "";
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />;
},
}}
>
{props.content}
</ReactMarkdown>
);
}
export const MarkdownContent = React.memo(_MarkDownContent);
export function Markdown(
props: {
content: string;
loading?: boolean;
fontSize?: number;
parentRef: RefObject<HTMLDivElement>;
defaultShow?: boolean;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0);
const inView = useRef(!!props.defaultShow);
const parent = props.parentRef.current;
const md = mdRef.current;
const checkInView = () => {
if (parent && md) {
const parentBounds = parent.getBoundingClientRect();
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
const mdBounds = md.getBoundingClientRect();
const parentTop = parentBounds.top - twoScreenHeight;
const parentBottom = parentBounds.bottom + twoScreenHeight;
const isOverlap =
Math.max(parentTop, mdBounds.top) <=
Math.min(parentBottom, mdBounds.bottom);
inView.current = isOverlap;
}
if (inView.current && md) {
renderedHeight.current = Math.max(
renderedHeight.current,
md.getBoundingClientRect().height,
);
}
};
setTimeout(() => checkInView(), 1);
return (
<div
className="markdown-body"
style={{
fontSize: `${props.fontSize ?? 14}px`,
height:
!inView.current && renderedHeight.current > 0
? renderedHeight.current
: "auto",
}}
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
>
{inView.current &&
(props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
))}
</div>
);
}

View File

@@ -0,0 +1,108 @@
@import "../styles/animation.scss";
.mask-page {
height: 100%;
display: flex;
flex-direction: column;
.mask-page-body {
padding: 20px;
overflow-y: auto;
.mask-filter {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
animation: slide-in ease 0.3s;
height: 40px;
display: flex;
.search-bar {
flex-grow: 1;
max-width: 100%;
min-width: 0;
}
.mask-filter-lang {
height: 100%;
margin-left: 10px;
}
.mask-create {
height: 100%;
margin-left: 10px;
box-sizing: border-box;
min-width: 80px;
}
}
.mask-item {
display: flex;
justify-content: space-between;
padding: 20px;
border: var(--border-in-light);
animation: slide-in ease 0.3s;
&:not(:last-child) {
border-bottom: 0;
}
&:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
&:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.mask-header {
display: flex;
align-items: center;
.mask-icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.mask-title {
.mask-name {
font-size: 14px;
font-weight: bold;
}
.mask-info {
font-size: 12px;
}
}
}
.mask-actions {
display: flex;
flex-wrap: nowrap;
transition: all ease 0.3s;
}
@media screen and (max-width: 600px) {
display: flex;
flex-direction: column;
padding-bottom: 10px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: var(--card-shadow);
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.mask-actions {
width: 100%;
justify-content: space-between;
padding-top: 10px;
}
}
}
}
}

443
app/components/mask.tsx Normal file
View File

@@ -0,0 +1,443 @@
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useState } from "react";
import { downloadAs, readFromFile } from "../utils";
import { Updater } from "../api/openai/typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.mask.avatar} />
) : (
<Avatar model={props.mask.modelConfig.model} />
);
}
export function MaskConfig(props: {
mask: Mask;
updateMask: Updater<Mask>;
extraListItems?: JSX.Element;
readonly?: boolean;
}) {
const [showPicker, setShowPicker] = useState(false);
const updateConfig = (updater: (config: ModelConfig) => void) => {
if (props.readonly) return;
const config = { ...props.mask.modelConfig };
updater(config);
props.updateMask((mask) => (mask.modelConfig = config));
};
return (
<>
<ContextPrompts
context={props.mask.context}
updateContext={(updater) => {
const context = props.mask.context.slice();
updater(context);
props.updateMask((mask) => (mask.context = context));
}}
/>
<List>
<ListItem title={Locale.Mask.Config.Avatar}>
<Popover
content={
<AvatarPicker
onEmojiClick={(emoji) => {
props.updateMask((mask) => (mask.avatar = emoji));
setShowPicker(false);
}}
></AvatarPicker>
}
open={showPicker}
onClose={() => setShowPicker(false)}
>
<div
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
<MaskAvatar mask={props.mask} />
</div>
</Popover>
</ListItem>
<ListItem title={Locale.Mask.Config.Name}>
<input
type="text"
value={props.mask.name}
onInput={(e) =>
props.updateMask((mask) => (mask.name = e.currentTarget.value))
}
></input>
</ListItem>
</List>
<List>
<ModelConfigList
modelConfig={{ ...props.mask.modelConfig }}
updateConfig={updateConfig}
/>
{props.extraListItems}
</List>
</>
);
}
function ContextPromptItem(props: {
prompt: Message;
update: (prompt: Message) => void;
remove: () => void;
}) {
const [focusingInput, setFocusingInput] = useState(false);
return (
<div className={chatStyle["context-prompt-row"]}>
{!focusingInput && (
<Select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
props.update({
...props.prompt,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</Select>
)}
<Input
value={props.prompt.content}
type="text"
className={chatStyle["context-content"]}
rows={focusingInput ? 5 : 1}
onFocus={() => setFocusingInput(true)}
onBlur={() => setFocusingInput(false)}
onInput={(e) =>
props.update({
...props.prompt,
content: e.currentTarget.value as any,
})
}
/>
{!focusingInput && (
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => props.remove()}
bordered
/>
)}
</div>
);
}
export function ContextPrompts(props: {
context: Message[];
updateContext: (updater: (context: Message[]) => void) => void;
}) {
const context = props.context;
const addContextPrompt = (prompt: Message) => {
props.updateContext((context) => context.push(prompt));
};
const removeContextPrompt = (i: number) => {
props.updateContext((context) => context.splice(i, 1));
};
const updateContextPrompt = (i: number, prompt: Message) => {
props.updateContext((context) => (context[i] = prompt));
};
return (
<>
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
{context.map((c, i) => (
<ContextPromptItem
key={i}
prompt={c}
update={(prompt) => updateContextPrompt(i, prompt)}
remove={() => removeContextPrompt(i)}
/>
))}
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "user",
content: "",
date: "",
})
}
/>
</div>
</div>
</>
);
}
export function MaskPage() {
const navigate = useNavigate();
const maskStore = useMaskStore();
const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang>();
const allMasks = maskStore
.getAll()
.filter((m) => !filterLang || m.lang === filterLang);
const [searchMasks, setSearchMasks] = useState<Mask[]>([]);
const [searchText, setSearchText] = useState("");
const masks = searchText.length > 0 ? searchMasks : allMasks;
// simple search, will refactor later
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) => m.name.includes(text));
setSearchMasks(result);
} else {
setSearchMasks(allMasks);
}
};
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
const editingMask =
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const closeMaskModal = () => setEditingMaskId(undefined);
const downloadAll = () => {
downloadAs(JSON.stringify(masks), FileName.Masks);
};
const importFromFile = () => {
readFromFile().then((content) => {
try {
const importMasks = JSON.parse(content);
if (Array.isArray(importMasks)) {
for (const mask of importMasks) {
if (mask.name) {
maskStore.create(mask);
}
}
}
} catch {}
});
};
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.Mask.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.Mask.Page.SubTitle(allMasks.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<DownloadIcon />}
bordered
onClick={downloadAll}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<UploadIcon />}
bordered
onClick={() => importFromFile()}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.Mask.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<Select
className={styles["mask-filter-lang"]}
value={filterLang ?? Locale.Settings.Lang.All}
onChange={(e) => {
const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined);
} else {
setFilterLang(value as Lang);
}
}}
>
<option key="all" value={Locale.Settings.Lang.All}>
{Locale.Settings.Lang.All}
</option>
{AllLangs.map((lang) => (
<option value={lang} key={lang}>
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</Select>
<IconButton
className={styles["mask-create"]}
icon={<AddIcon />}
text={Locale.Mask.Page.Create}
bordered
onClick={() => {
const createdMask = maskStore.create();
setEditingMaskId(createdMask.id);
}}
/>
</div>
<div>
{masks.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}>
<MaskAvatar mask={m} />
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
<div className={styles["mask-info"] + " one-line"}>
{`${Locale.Mask.Item.Info(m.context.length)} / ${
Locale.Settings.Lang.Options[m.lang]
} / ${m.modelConfig.model}`}
</div>
</div>
</div>
<div className={styles["mask-actions"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Mask.Item.Chat}
onClick={() => {
chatStore.newSession(m);
navigate(Path.Chat);
}}
/>
{m.builtin ? (
<IconButton
icon={<EyeIcon />}
text={Locale.Mask.Item.View}
onClick={() => setEditingMaskId(m.id)}
/>
) : (
<IconButton
icon={<EditIcon />}
text={Locale.Mask.Item.Edit}
onClick={() => setEditingMaskId(m.id)}
/>
)}
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
text={Locale.Mask.Item.Delete}
onClick={() => {
if (confirm(Locale.Mask.Item.DeleteConfirm)) {
maskStore.delete(m.id);
}
}}
/>
)}
</div>
</div>
))}
</div>
</div>
</div>
{editingMask && (
<div className="modal-mask">
<Modal
title={Locale.Mask.EditModal.Title(editingMask?.builtin)}
onClose={closeMaskModal}
actions={[
<IconButton
icon={<DownloadIcon />}
text={Locale.Mask.EditModal.Download}
key="export"
bordered
onClick={() =>
downloadAs(
JSON.stringify(editingMask),
`${editingMask.name}.json`,
)
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Mask.EditModal.Clone}
onClick={() => {
navigate(Path.Masks);
maskStore.create(editingMask);
setEditingMaskId(undefined);
}}
/>,
]}
>
<MaskConfig
mask={editingMask}
updateMask={(updater) =>
maskStore.update(editingMaskId!, updater)
}
readonly={editingMask.builtin}
/>
</Modal>
</div>
)}
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,140 @@
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";
import { InputRange } from "./input-range";
import { List, ListItem, Select } from "./ui-lib";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
return (
<>
<ListItem title={Locale.Settings.Model}>
<Select
value={props.modelConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.model = ModalConfigValidator.model(
e.currentTarget.value,
)),
);
}}
>
{ALL_MODELS.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{v.name}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<InputRange
value={props.modelConfig.temperature?.toFixed(1)}
min="0"
max="1" // lets limit it to 0-1
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.temperature = ModalConfigValidator.temperature(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={32000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.max_tokens = ModalConfigValidator.max_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<InputRange
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
max="32"
step="1"
onChange={(e) =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
type="checkbox"
checked={props.modelConfig.sendMemory}
onChange={(e) =>
props.updateConfig(
(config) => (config.sendMemory = e.currentTarget.checked),
)
}
></input>
</ListItem>
</>
);
}

View File

@@ -0,0 +1,115 @@
@import "../styles/animation.scss";
.new-chat {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.mask-header {
display: flex;
justify-content: space-between;
width: 100%;
padding: 10px;
box-sizing: border-box;
animation: slide-in-from-top ease 0.3s;
}
.mask-cards {
display: flex;
margin-top: 5vh;
margin-bottom: 20px;
animation: slide-in ease 0.3s;
.mask-card {
padding: 20px 10px;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
border-radius: 14px;
background-color: var(--white);
transform: scale(1);
&:first-child {
transform: rotate(-15deg) translateY(5px);
}
&:last-child {
transform: rotate(15deg) translateY(5px);
}
}
}
.title {
font-size: 32px;
font-weight: bolder;
margin-bottom: 1vh;
animation: slide-in ease 0.35s;
}
.sub-title {
animation: slide-in ease 0.4s;
}
.actions {
margin-top: 5vh;
margin-bottom: 5vh;
animation: slide-in ease 0.45s;
display: flex;
justify-content: center;
.more {
font-size: 12px;
margin-left: 10px;
}
}
.masks {
flex-grow: 1;
width: 100%;
overflow: hidden;
align-items: center;
padding-top: 20px;
animation: slide-in ease 0.5s;
.mask-row {
margin-bottom: 10px;
display: flex;
justify-content: center;
@for $i from 1 to 10 {
&:nth-child(#{$i * 2}) {
margin-left: 50px;
}
}
.mask {
display: flex;
align-items: center;
padding: 10px 14px;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 10px;
margin-right: 10px;
max-width: 8em;
transform: scale(1);
cursor: pointer;
transition: all ease 0.3s;
&:hover {
transform: translateY(-5px) scale(1.1);
z-index: 999;
border-color: var(--primary);
}
.mask-name {
margin-left: 10px;
font-size: 14px;
}
}
}
}
}

197
app/components/new-chat.tsx Normal file
View File

@@ -0,0 +1,197 @@
import { useEffect, useRef, useState } from "react";
import { Path, SlotID } from "../constant";
import { IconButton } from "./button";
import { EmojiAvatar } from "./emoji";
import styles from "./new-chat.module.scss";
import LeftIcon from "../icons/left.svg";
import LightningIcon from "../icons/lightning.svg";
import EyeIcon from "../icons/eye.svg";
import { useLocation, useNavigate } from "react-router-dom";
import { Mask, useMaskStore } from "../store/mask";
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x);
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
const ymin = Math.max(aRect.y, bRect.y);
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
const width = xmax - xmin;
const height = ymax - ymin;
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
return intersectionArea;
}
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const changeOpacity = () => {
const dom = domRef.current;
const parent = document.getElementById(SlotID.AppBody);
if (!parent || !dom) return;
const domRect = dom.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
const intersectionArea = getIntersectionArea(domRect, parentRect);
const domArea = domRect.width * domRect.height;
const ratio = intersectionArea / domArea;
const opacity = ratio > 0.9 ? 1 : 0.4;
dom.style.opacity = opacity.toString();
};
setTimeout(changeOpacity, 30);
window.addEventListener("resize", changeOpacity);
return () => window.removeEventListener("resize", changeOpacity);
}, [domRef]);
return (
<div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
<MaskAvatar mask={props.mask} />
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
</div>
);
}
function useMaskGroup(masks: Mask[]) {
const [groups, setGroups] = useState<Mask[][]>([]);
useEffect(() => {
const appBody = document.getElementById(SlotID.AppBody);
if (!appBody || masks.length === 0) return;
const rect = appBody.getBoundingClientRect();
const maxWidth = rect.width;
const maxHeight = rect.height * 0.6;
const maskItemWidth = 120;
const maskItemHeight = 50;
const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
let maskIndex = 0;
const nextMask = () => masks[maskIndex++ % masks.length];
const rows = Math.ceil(maxHeight / maskItemHeight);
const cols = Math.ceil(maxWidth / maskItemWidth);
const newGroups = new Array(rows)
.fill(0)
.map((_, _i) =>
new Array(cols)
.fill(0)
.map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
);
setGroups(newGroups);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return groups;
}
export function NewChat() {
const chatStore = useChatStore();
const maskStore = useMaskStore();
const masks = maskStore.getAll();
const groups = useMaskGroup(masks);
const navigate = useNavigate();
const config = useAppConfig();
const { state } = useLocation();
const startChat = (mask?: Mask) => {
chatStore.newSession(mask);
setTimeout(() => navigate(Path.Chat), 1);
};
useCommand({
mask: (id) => {
try {
const mask = maskStore.get(parseInt(id));
startChat(mask ?? undefined);
} catch {
console.error("[New Chat] failed to create chat from mask id=", id);
}
},
});
return (
<div className={styles["new-chat"]}>
<div className={styles["mask-header"]}>
<IconButton
icon={<LeftIcon />}
text={Locale.NewChat.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
{!state?.fromHome && (
<IconButton
text={Locale.NewChat.NotShow}
onClick={() => {
if (confirm(Locale.NewChat.ConfirmNoShow)) {
startChat();
config.update(
(config) => (config.dontShowMaskSplashScreen = true),
);
}
}}
></IconButton>
)}
</div>
<div className={styles["mask-cards"]}>
<div className={styles["mask-card"]}>
<EmojiAvatar avatar="1f606" size={24} />
</div>
<div className={styles["mask-card"]}>
<EmojiAvatar avatar="1f916" size={24} />
</div>
<div className={styles["mask-card"]}>
<EmojiAvatar avatar="1f479" size={24} />
</div>
</div>
<div className={styles["title"]}>{Locale.NewChat.Title}</div>
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
<div className={styles["actions"]}>
<IconButton
text={Locale.NewChat.Skip}
onClick={() => startChat()}
icon={<LightningIcon />}
type="primary"
shadow
/>
<IconButton
className={styles["more"]}
text={Locale.NewChat.More}
onClick={() => navigate(Path.Masks)}
icon={<EyeIcon />}
bordered
shadow
/>
</div>
<div className={styles["masks"]}>
{groups.map((masks, i) => (
<div key={i} className={styles["mask-row"]}>
{masks.map((mask, index) => (
<MaskItem
key={index}
mask={mask}
onClick={() => startChat(mask)}
/>
))}
</div>
))}
</div>
</div>
);
}

View File

@@ -1,20 +1,72 @@
@import "./window.scss";
.settings {
padding: 20px;
overflow: auto;
}
.settings-title {
font-size: 14px;
font-weight: bolder;
}
.settings-sub-title {
font-size: 12px;
font-weight: normal;
}
.avatar {
cursor: pointer;
}
.edit-prompt-modal {
display: flex;
flex-direction: column;
.edit-prompt-title {
max-width: unset;
margin-bottom: 20px;
text-align: left;
}
.edit-prompt-content {
max-width: unset;
}
}
.user-prompt-modal {
min-height: 40vh;
.user-prompt-search {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
background-color: var(--gray);
}
.user-prompt-list {
border: var(--border-in-light);
border-radius: 10px;
.user-prompt-item {
display: flex;
justify-content: space-between;
padding: 10px;
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.user-prompt-header {
max-width: calc(100% - 100px);
.user-prompt-title {
font-size: 14px;
line-height: 2;
font-weight: bold;
}
.user-prompt-content {
font-size: 12px;
}
}
.user-prompt-buttons {
display: flex;
align-items: center;
column-gap: 2px;
.user-prompt-button {
//height: 100%;
padding: 7px;
}
}
}
}
}

View File

@@ -1,140 +1,325 @@
import { useState, useEffect, useRef, useMemo } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import LoadingIcon from "../icons/three-dots.svg";
import EditIcon from "../icons/edit.svg";
import { List, ListItem, Popover, showToast } from "./ui-lib";
import EyeIcon from "../icons/eye.svg";
import {
Input,
List,
ListItem,
Modal,
PasswordInput,
Popover,
Select,
} from "./ui-lib";
import { ModelConfigList } from "./model-config";
import { IconButton } from "./button";
import {
SubmitKey,
useChatStore,
Theme,
ALL_MODELS,
useUpdateStore,
useAccessStore,
useAppConfig,
} from "../store";
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getCurrentVersion } from "../utils";
import { copyToClipboard } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests";
import { Path, UPDATE_URL } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji";
function EditPromptModal(props: { id: number; onClose: () => void }) {
const promptStore = usePromptStore();
const prompt = promptStore.get(props.id);
return prompt ? (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.EditModal.Title}
onClose={props.onClose}
actions={[
<IconButton
key=""
onClick={props.onClose}
text={Locale.UI.Confirm}
bordered
/>,
]}
>
<div className={styles["edit-prompt-modal"]}>
<input
type="text"
value={prompt.title}
readOnly={!prompt.isUser}
className={styles["edit-prompt-title"]}
onInput={(e) =>
promptStore.update(
props.id,
(prompt) => (prompt.title = e.currentTarget.value),
)
}
></input>
<Input
value={prompt.content}
readOnly={!prompt.isUser}
className={styles["edit-prompt-content"]}
rows={10}
onInput={(e) =>
promptStore.update(
props.id,
(prompt) => (prompt.content = e.currentTarget.value),
)
}
></Input>
</div>
</Modal>
</div>
) : null;
}
function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts();
const builtinPrompts = SearchService.builtinPrompts;
const allPrompts = userPrompts.concat(builtinPrompts);
const [searchInput, setSearchInput] = useState("");
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
const [editingPromptId, setEditingPromptId] = useState<number>();
useEffect(() => {
if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput);
setSearchPrompts(searchResult);
} else {
setSearchPrompts([]);
}
}, [searchInput]);
function SettingItem(props: {
title: string;
subTitle?: string;
children: JSX.Element;
}) {
return (
<ListItem>
<div className={styles["settings-title"]}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
)}
</div>
{props.children}
</ListItem>
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<IconButton
key="add"
onClick={() =>
promptStore.add({
title: "Empty Prompt",
content: "Empty Prompt Content",
})
}
icon={<AddIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
/>,
]}
>
<div className={styles["user-prompt-modal"]}>
<input
type="text"
className={styles["user-prompt-search"]}
placeholder={Locale.Settings.Prompt.Modal.Search}
value={searchInput}
onInput={(e) => setSearchInput(e.currentTarget.value)}
></input>
<div className={styles["user-prompt-list"]}>
{prompts.map((v, _) => (
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
<div className={styles["user-prompt-header"]}>
<div className={styles["user-prompt-title"]}>{v.title}</div>
<div className={styles["user-prompt-content"] + " one-line"}>
{v.content}
</div>
</div>
<div className={styles["user-prompt-buttons"]}>
{v.isUser && (
<IconButton
icon={<ClearIcon />}
className={styles["user-prompt-button"]}
onClick={() => promptStore.remove(v.id!)}
/>
)}
{v.isUser ? (
<IconButton
icon={<EditIcon />}
className={styles["user-prompt-button"]}
onClick={() => setEditingPromptId(v.id)}
/>
) : (
<IconButton
icon={<EyeIcon />}
className={styles["user-prompt-button"]}
onClick={() => setEditingPromptId(v.id)}
/>
)}
<IconButton
icon={<CopyIcon />}
className={styles["user-prompt-button"]}
onClick={() => copyToClipboard(v.content)}
/>
</div>
</div>
))}
</div>
</div>
</Modal>
{editingPromptId !== undefined && (
<EditPromptModal
id={editingPromptId!}
onClose={() => setEditingPromptId(undefined)}
/>
)}
</div>
);
}
export function Settings(props: { closeSettings: () => void }) {
function formatVersionDate(t: string) {
const d = new Date(+t);
const year = d.getUTCFullYear();
const month = d.getUTCMonth() + 1;
const day = d.getUTCDate();
return [
year.toString(),
month.toString().padStart(2, "0"),
day.toString().padStart(2, "0"),
].join("");
}
export function Settings() {
const navigate = useNavigate();
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
useChatStore((state) => [
state.config,
state.updateConfig,
state.resetConfig,
state.clearAllData,
state.clearSessions,
]);
const config = useAppConfig();
const updateConfig = config.update;
const resetConfig = config.reset;
const chatStore = useChatStore();
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentId = getCurrentVersion();
const remoteId = updateStore.remoteId;
const hasNewVersion = currentId !== remoteId;
const currentVersion = formatVersionDate(updateStore.version);
const remoteId = formatVersionDate(updateStore.remoteVersion);
const hasNewVersion = currentVersion !== remoteId;
function checkUpdate(force = false) {
setCheckingUpdate(true);
updateStore.getLatestCommitId(force).then(() => {
updateStore.getLatestVersion(force).then(() => {
setCheckingUpdate(false);
});
console.log(
"[Update] local version ",
new Date(+updateStore.version).toLocaleString(),
);
console.log(
"[Update] remote version ",
new Date(+updateStore.remoteVersion).toLocaleString(),
);
}
const [usage, setUsage] = useState<{
used?: number;
}>();
const usage = {
used: updateStore.used,
subscription: updateStore.subscription,
};
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
function checkUsage(force = false) {
setLoadingUsage(true);
requestUsage()
.then((res) =>
setUsage({
used: res,
}),
)
.finally(() => {
setLoadingUsage(false);
});
updateStore.updateUsage(force).finally(() => {
setLoadingUsage(false);
});
}
useEffect(() => {
checkUpdate();
checkUsage();
}, []);
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 customCount = promptStore.getUserPrompts().length ?? 0;
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
const showUsage = accessStore.isAuthorized();
useEffect(() => {
// checks per minutes
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const keydownEvent = (e: KeyboardEvent) => {
if (e.key === "Escape") {
navigate(Path.Home);
}
};
document.addEventListener("keydown", keydownEvent);
return () => {
document.removeEventListener("keydown", keydownEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className={styles["window-header"]}>
<div className={styles["window-header-title"]}>
<div className={styles["window-header-main-title"]}>
<ErrorBoundary>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.Settings.Title}
</div>
<div className={styles["window-header-sub-title"]}>
<div className="window-header-sub-title">
{Locale.Settings.SubTitle}
</div>
</div>
<div className={styles["window-actions"]}>
<div className={styles["window-action-button"]}>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<ClearIcon />}
onClick={clearSessions}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
chatStore.clearAllData();
}
}}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
</div>
<div className={styles["window-action-button"]}>
<div className="window-action-button">
<IconButton
icon={<ResetIcon />}
onClick={resetConfig}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
resetConfig();
}
}}
bordered
title={Locale.Settings.Actions.ResetAll}
/>
</div>
<div className={styles["window-action-button"]}>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
onClick={props.closeSettings}
onClick={() => navigate(Path.Home)}
bordered
title={Locale.Settings.Actions.Close}
/>
@@ -143,15 +328,13 @@ export function Settings(props: { closeSettings: () => void }) {
</div>
<div className={styles["settings"]}>
<List>
<SettingItem title={Locale.Settings.Avatar}>
<ListItem title={Locale.Settings.Avatar}>
<Popover
onClose={() => setShowEmojiPicker(false)}
content={
<EmojiPicker
lazyLoadEmojis
theme={EmojiTheme.AUTO}
onEmojiClick={(e) => {
updateConfig((config) => (config.avatar = e.unified));
<AvatarPicker
onEmojiClick={(avatar: string) => {
updateConfig((config) => (config.avatar = avatar));
setShowEmojiPicker(false);
}}
/>
@@ -162,13 +345,13 @@ export function Settings(props: { closeSettings: () => void }) {
className={styles.avatar}
onClick={() => setShowEmojiPicker(true)}
>
<Avatar role="user" />
<Avatar avatar={config.avatar} />
</div>
</Popover>
</SettingItem>
</ListItem>
<SettingItem
title={Locale.Settings.Update.Version(currentId)}
<ListItem
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={
checkingUpdate
? Locale.Settings.Update.IsChecking
@@ -178,7 +361,7 @@ export function Settings(props: { closeSettings: () => void }) {
}
>
{checkingUpdate ? (
<div />
<LoadingIcon />
) : hasNewVersion ? (
<Link href={UPDATE_URL} target="_blank" className="link">
{Locale.Settings.Update.GoToUpdate}
@@ -190,10 +373,10 @@ export function Settings(props: { closeSettings: () => void }) {
onClick={() => checkUpdate(true)}
/>
)}
</SettingItem>
</ListItem>
<SettingItem title={Locale.Settings.SendKey}>
<select
<ListItem title={Locale.Settings.SendKey}>
<Select
value={config.submitKey}
onChange={(e) => {
updateConfig(
@@ -207,14 +390,11 @@ export function Settings(props: { closeSettings: () => void }) {
{v}
</option>
))}
</select>
</SettingItem>
</Select>
</ListItem>
<ListItem>
<div className={styles["settings-title"]}>
{Locale.Settings.Theme}
</div>
<select
<ListItem title={Locale.Settings.Theme}>
<Select
value={config.theme}
onChange={(e) => {
updateConfig(
@@ -227,11 +407,11 @@ export function Settings(props: { closeSettings: () => void }) {
{v}
</option>
))}
</select>
</Select>
</ListItem>
<SettingItem title={Locale.Settings.Lang.Name}>
<select
<ListItem title={Locale.Settings.Lang.Name}>
<Select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
@@ -242,15 +422,14 @@ export function Settings(props: { closeSettings: () => void }) {
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</select>
</SettingItem>
</Select>
</ListItem>
<SettingItem
<ListItem
title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle}
>
<input
type="range"
<InputRange
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
@@ -262,22 +441,13 @@ export function Settings(props: { closeSettings: () => void }) {
(config.fontSize = Number.parseInt(e.currentTarget.value)),
)
}
></input>
</SettingItem>
></InputRange>
</ListItem>
<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}>
<ListItem
title={Locale.Settings.SendPreviewBubble.Title}
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
>
<input
type="checkbox"
checked={config.sendPreviewBubble}
@@ -288,10 +458,88 @@ export function Settings(props: { closeSettings: () => void }) {
)
}
></input>
</SettingItem>
</ListItem>
<ListItem
title={Locale.Settings.Mask.Title}
subTitle={Locale.Settings.Mask.SubTitle}
>
<input
type="checkbox"
checked={!config.dontShowMaskSplashScreen}
onChange={(e) =>
updateConfig(
(config) =>
(config.dontShowMaskSplashScreen =
!e.currentTarget.checked),
)
}
></input>
</ListItem>
</List>
<List>
<SettingItem
{enabledAccessControl ? (
<ListItem
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);
}}
/>
</ListItem>
) : (
<></>
)}
{!accessStore.hideUserApiKey ? (
<ListItem
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);
}}
/>
</ListItem>
) : null}
<ListItem
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(true)}
/>
)}
</ListItem>
</List>
<List>
<ListItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
@@ -305,9 +553,9 @@ export function Settings(props: { closeSettings: () => void }) {
)
}
></input>
</SettingItem>
</ListItem>
<SettingItem
<ListItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
@@ -317,178 +565,26 @@ export function Settings(props: { closeSettings: () => void }) {
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
onClick={() => setShowPromptModal(true)}
/>
</SettingItem>
</List>
<List>
{enabledAccessControl ? (
<SettingItem
title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle}
>
<input
value={accessStore.accessCode}
type="text"
placeholder={Locale.Settings.AccessCode.Placeholder}
onChange={(e) => {
accessStore.updateCode(e.currentTarget.value);
}}
></input>
</SettingItem>
) : (
<></>
)}
<SettingItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<input
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
}}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Usage.Title}
subTitle={
loadingUsage
? Locale.Settings.Usage.IsChecking
: Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]")
}
>
{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"
title={config.historyMessageCount.toString()}
value={config.historyMessageCount}
min="0"
max="25"
step="2"
onChange={(e) =>
updateConfig(
(config) =>
(config.historyMessageCount = e.target.valueAsNumber),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={config.compressMessageLengthThreshold}
onChange={(e) =>
updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</SettingItem>
</ListItem>
</List>
<List>
<SettingItem title={Locale.Settings.Model}>
<select
value={config.modelConfig.model}
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.model = e.currentTarget.value),
);
}}
>
{ALL_MODELS.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{v.name}
</option>
))}
</select>
</SettingItem>
<SettingItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<input
type="range"
value={config.modelConfig.temperature.toFixed(1)}
min="0"
max="2"
step="0.1"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.temperature =
e.currentTarget.valueAsNumber),
);
}}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={4096}
value={config.modelConfig.max_tokens}
onChange={(e) =>
updateConfig(
(config) =>
(config.modelConfig.max_tokens =
e.currentTarget.valueAsNumber),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<input
type="range"
value={config.modelConfig.presence_penalty.toFixed(1)}
min="-2"
max="2"
step="0.5"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.presence_penalty =
e.currentTarget.valueAsNumber),
);
}}
></input>
</SettingItem>
<ModelConfigList
modelConfig={config.modelConfig}
updateConfig={(updater) => {
const modelConfig = { ...config.modelConfig };
updater(modelConfig);
config.update((config) => (config.modelConfig = modelConfig));
}}
/>
</List>
{shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} />
)}
</div>
</>
</ErrorBoundary>
);
}

205
app/components/sidebar.tsx Normal file
View File

@@ -0,0 +1,205 @@
import { useEffect, useRef } from "react";
import styles from "./home.module.scss";
import { IconButton } from "./button";
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import MaskIcon from "../icons/mask.svg";
import PluginIcon from "../icons/plugin.svg";
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import {
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH,
Path,
REPO_URL,
} from "../constant";
import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { showToast } from "./ui-lib";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null,
});
function useHotKey() {
const chatStore = useChatStore();
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.metaKey || e.altKey || e.ctrlKey) {
const n = chatStore.sessions.length;
const limit = (x: number) => (x + n) % n;
const i = chatStore.currentSessionIndex;
if (e.key === "ArrowUp") {
chatStore.selectSession(limit(i - 1));
} else if (e.key === "ArrowDown") {
chatStore.selectSession(limit(i + 1));
}
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
});
}
function useDragSideBar() {
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
const config = useAppConfig();
const startX = useRef(0);
const startDragWidth = useRef(config.sidebarWidth ?? 300);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 50) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current);
});
const onDragMouseDown = (e: MouseEvent) => {
startX.current = e.clientX;
window.addEventListener("mousemove", handleMouseMove.current);
window.addEventListener("mouseup", handleMouseUp.current);
};
const isMobileScreen = useMobileScreen();
const shouldNarrow =
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
useEffect(() => {
const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH
: limit(config.sidebarWidth ?? 300);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return {
onDragMouseDown,
shouldNarrow,
};
}
export function SideBar(props: { className?: string }) {
const chatStore = useChatStore();
// drag side bar
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
const navigate = useNavigate();
const config = useAppConfig();
useHotKey();
return (
<div
className={`${styles.sidebar} ${props.className} ${
shouldNarrow && styles["narrow-sidebar"]
}`}
>
<div className={styles["sidebar-header"]}>
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
<div className={styles["sidebar-sub-title"]}>
Build your own AI assistant.
</div>
<div className={styles["sidebar-logo"] + " no-dark"}>
<ChatGptIcon />
</div>
</div>
<div className={styles["sidebar-header-bar"]}>
<IconButton
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
shadow
/>
<IconButton
icon={<PluginIcon />}
text={shouldNarrow ? undefined : Locale.Plugin.Name}
className={styles["sidebar-bar-button"]}
onClick={() => showToast(Locale.WIP)}
shadow
/>
</div>
<div
className={styles["sidebar-body"]}
onClick={(e) => {
if (e.target === e.currentTarget) {
navigate(Path.Home);
}
}}
>
<ChatList narrow={shouldNarrow} />
</div>
<div className={styles["sidebar-tail"]}>
<div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex);
}
}}
/>
</div>
<div className={styles["sidebar-action"]}>
<Link to={Path.Settings}>
<IconButton icon={<SettingsIcon />} shadow />
</Link>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank">
<IconButton icon={<GithubIcon />} shadow />
</a>
</div>
</div>
<div>
<IconButton
icon={<AddIcon />}
text={shouldNarrow ? undefined : Locale.Home.NewChat}
onClick={() => {
if (config.dontShowMaskSplashScreen) {
chatStore.newSession();
navigate(Path.Chat);
} else {
navigate(Path.NewChat);
}
}}
shadow
/>
</div>
</div>
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
</div>
);
}

View File

@@ -9,6 +9,7 @@
.popover {
position: relative;
z-index: 2;
}
.popover-content {
@@ -34,6 +35,25 @@
border-bottom: var(--border-in-light);
padding: 10px 20px;
animation: slide-in ease 0.6s;
.list-header {
display: flex;
align-items: center;
.list-icon {
margin-right: 10px;
}
.list-item-title {
font-size: 14px;
font-weight: bolder;
}
.list-item-sub-title {
font-size: 12px;
font-weight: normal;
}
}
}
.list {
@@ -52,7 +72,7 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
width: 50vw;
width: 60vw;
animation: slide-in ease 0.3s;
--modal-padding: 20px;
@@ -88,6 +108,8 @@
padding: var(--modal-padding);
display: flex;
justify-content: flex-end;
border-top: var(--border-in-light);
box-shadow: var(--shadow);
.modal-actions {
display: flex;
@@ -102,6 +124,18 @@
}
}
@media screen and (max-width: 600px) {
.modal-container {
width: 100vw;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.modal-content {
max-height: 50vh;
}
}
}
.show {
opacity: 1;
transition: all ease 0.3s;
@@ -121,11 +155,12 @@
.toast-container {
position: fixed;
bottom: 0;
bottom: 5vh;
left: 0;
width: 100vw;
display: flex;
justify-content: center;
pointer-events: none;
.toast-content {
max-width: 80vw;
@@ -135,18 +170,61 @@
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;
pointer-events: all;
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;
.toast-action {
padding-left: 20px;
color: var(--primary);
opacity: 0.8;
border: 0;
background: none;
cursor: pointer;
font-family: inherit;
.modal-content {
max-height: 50vh;
&: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;
}
.select-with-icon {
position: relative;
max-width: fit-content;
.select-with-icon-select {
height: 100%;
border: var(--border-in-light);
padding: 10px 25px 10px 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;
background-color: var(--white);
color: var(--black);
text-align: center;
}
.select-with-icon-icon {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
}
}

View File

@@ -1,7 +1,13 @@
import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg";
import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react";
import { IconButton } from "./button";
export function Popover(props: {
children: JSX.Element;
@@ -28,15 +34,38 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
);
}
export function ListItem(props: { children: JSX.Element[] }) {
if (props.children.length > 2) {
throw Error("Only Support Two Children");
}
return <div className={styles["list-item"]}>{props.children}</div>;
export function ListItem(props: {
title: string;
subTitle?: string;
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;
className?: string;
}) {
return (
<div className={styles["list-item"] + ` ${props.className}`}>
<div className={styles["list-header"]}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
<div className={styles["list-item-title"]}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["list-item-sub-title"]}>
{props.subTitle}
</div>
)}
</div>
</div>
{props.children}
</div>
);
}
export function List(props: { children: JSX.Element[] | JSX.Element }) {
export function List(props: {
children:
| Array<JSX.Element | null | undefined>
| JSX.Element
| null
| undefined;
}) {
return <div className={styles.list}>{props.children}</div>;
}
@@ -58,11 +87,26 @@ export function Loading() {
interface ModalProps {
title: string;
children?: JSX.Element;
children?: JSX.Element | JSX.Element[];
actions?: JSX.Element[];
onClose?: () => void;
}
export function Modal(props: ModalProps) {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
props.onClose?.();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles["modal-container"]}>
<div className={styles["modal-header"]}>
@@ -109,17 +153,41 @@ 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;
};
onClose?: () => 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?.();
props.onClose?.();
}}
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 +206,59 @@ export function showToast(content: string, delay = 3000) {
close();
}, delay);
root.render(<Toast content={content} />);
root.render(<Toast content={content} action={action} onClose={close} />);
}
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
autoHeight?: boolean;
rows?: number;
};
export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
></textarea>
);
}
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
const [visible, setVisible] = useState(false);
function changeVisibility() {
setVisible(!visible);
}
return (
<div className={"password-input-container"}>
<IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={"password-eye"}
/>
<input
{...props}
type={visible ? "text" : "password"}
className={"password-input"}
/>
</div>
);
}
export function Select(
props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
>,
) {
const { className, children, ...otherProps } = props;
return (
<div className={`${styles["select-with-icon"]} ${className}`}>
<select className={styles["select-with-icon-select"]} {...otherProps}>
{children}
</select>
<DownIcon className={styles["select-with-icon-icon"]} />
</div>
);
}

24
app/config/build.ts Normal file
View File

@@ -0,0 +1,24 @@
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return childProcess
.execSync('git log -1 --format="%at000" --date=unix')
.toString()
.trim();
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
commitId: COMMIT_ID,
};
};

46
app/config/server.ts Normal file
View File

@@ -0,0 +1,46 @@
import md5 from "spark-md5";
declare global {
namespace NodeJS {
interface ProcessEnv {
OPENAI_API_KEY?: string;
CODE?: string;
PROXY_URL?: string;
VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
}
}
}
const ACCESS_CODES = (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 getServerSideConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
apiKey: process.env.OPENAI_API_KEY,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
enableGPT4: !process.env.DISABLE_GPT4,
};
};

View File

@@ -1,6 +1,42 @@
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`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export enum Path {
Home = "/",
Chat = "/chat",
Settings = "/settings",
NewChat = "/new-chat",
Masks = "/masks",
}
export enum SlotID {
AppBody = "app-body",
}
export enum FileName {
Masks = "masks.json",
Prompts = "prompts.json",
}
export enum StoreKey {
Chat = "chat-next-web-store",
Access = "access-control",
Config = "app-config",
Mask = "mask-store",
Prompt = "prompt-store",
Update = "chat-update",
}
export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "ak-";
export const LAST_INPUT_KEY = "last-input";

11
app/global.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare module "*.jpg";
declare module "*.png";
declare module "*.woff2";
declare module "*.woff";
declare module "*.ttf";
declare module "*.scss" {
const content: Record<string, string>;
export default content;
}
declare module "*.svg";

1
app/icons/auto.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; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

28
app/icons/black-bot.svg Normal file
View File

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30"
height="30" viewBox="0 0 30 30" fill="none">
<defs>
<rect id="path_0" x="0" y="0" width="29.999999999999996" height="29.999999999999996" />
<rect id="path_1" x="0" y="0" width="20.45454545454545" height="20.45454545454545" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
<rect fill="#E7F8FF" opacity="1"
transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)" x="0" y="0"
width="29.999999999999996" height="29.999999999999996" rx="10" />
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<g opacity="1"
transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
<mask id="bg-mask-1" fill="white">
<use xlink:href="#path_1"></use>
</mask>
<g mask="url(#bg-mask-1)">
<path id="分组 1" fill-rule="evenodd" style="fill:#333333"
transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)" opacity="1"
d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z " />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

1
app/icons/bottom.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(4 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 736 B

1
app/icons/dark.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(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67C13.33,6.2 13.29,5.75 13.2,5.32C12.72,7.14 11.06,8.48 9.09,8.48C6.75,8.48 4.85,6.59 4.85,4.24C4.85,2.27 6.19,0.61 8.02,0.14C7.58,0.05 7.13,0 6.67,0Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 852 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

1
app/icons/down.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(-90 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(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 575 B

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

1
app/icons/left.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(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 573 B

1
app/icons/light.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(3.6666666666666665 3.6666666666666665) rotate(0 4.333333333333333 4.333333333333333)" d="M8.67,4.33C8.67,1.94 6.73,0 4.33,0C1.94,0 0,1.94 0,4.33C0,6.73 1.94,8.67 4.33,8.67C6.73,8.67 8.67,6.73 8.67,4.33Z " /><path id="路径 2" fill-rule="evenodd" style="fill:#333333" transform="translate(7.166666666666666 0.3333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 3" fill-rule="evenodd" style="fill:#333333" transform="translate(12 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 4" fill-rule="evenodd" style="fill:#333333" transform="translate(14 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 5" fill-rule="evenodd" style="fill:#333333" transform="translate(12 12) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 6" fill-rule="evenodd" style="fill:#333333" transform="translate(7.166666666666666 14) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(2.333333333333333 12) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 8" fill-rule="evenodd" style="fill:#333333" transform="translate(0.3333333333333333 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 9" fill-rule="evenodd" style="fill:#333333" transform="translate(2.333333333333333 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

1
app/icons/lightning.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"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M2.24783,8.85208c-0.03406,-0.0275 -0.06511,-0.05806 -0.09315,-0.09167c-0.02804,-0.03362 -0.05253,-0.06965 -0.07347,-0.10809c-0.02095,-0.03843 -0.03794,-0.07854 -0.05098,-0.12033c-0.01304,-0.04179 -0.02188,-0.08445 -0.02652,-0.12798c-0.00464,-0.04353 -0.00499,-0.08709 -0.00104,-0.13068c0.00394,-0.04359 0.0121,-0.08639 0.02447,-0.12838c0.01237,-0.04199 0.02872,-0.08236 0.04905,-0.12113l3.67,-7c0.02824,-0.05386 0.06319,-0.10294 0.10486,-0.14724c0.04167,-0.0443 0.08852,-0.08218 0.14056,-0.11365c0.05204,-0.03147 0.10735,-0.05538 0.16593,-0.07172c0.05857,-0.01634 0.11827,-0.02451 0.17909,-0.02451h5.99997c0.07473,0 0.1474,0.01221 0.218,0.03663c0.0706,0.02442 0.13527,0.05971 0.194,0.10586c0.0344,0.02705 0.06583,0.05719 0.0943,0.09044c0.02847,0.03324 0.05343,0.06894 0.0749,0.1071c0.02147,0.03816 0.03897,0.07805 0.0525,0.11966c0.0136,0.04161 0.02303,0.08414 0.0283,0.12761c0.0052,0.04346 0.0061,0.08701 0.0027,0.13066c-0.00333,0.04364 -0.01093,0.08654 -0.0228,0.12869c-0.0118,0.04215 -0.0276,0.08274 -0.0474,0.12177c-0.0198,0.03903 -0.04323,0.07576 -0.0703,0.11017l-2.8224,3.59141h3.6282c0.08387,0 0.16483,0.01527 0.2429,0.04581c0.07807,0.03055 0.1479,0.07427 0.2095,0.13116c0.03213,0.0297 0.06107,0.06225 0.0868,0.09766c0.0258,0.03541 0.04787,0.07298 0.0662,0.11273c0.01833,0.03975 0.0326,0.0809 0.0428,0.12346c0.01027,0.04256 0.01627,0.08571 0.018,0.12945c0.00173,0.04374 -0.00083,0.08723 -0.0077,0.13047c-0.0068,0.04324 -0.01777,0.08539 -0.0329,0.12646c-0.01513,0.04108 -0.03413,0.08028 -0.057,0.11761c-0.02287,0.03733 -0.04917,0.07208 -0.0789,0.10423l-7.99998,8.65995c-0.04107,0.04447 -0.08735,0.08263 -0.13882,0.1145c-0.05147,0.03187 -0.10626,0.05627 -0.16438,0.0732c-0.05812,0.01693 -0.11744,0.0258 -0.17797,0.0266c-0.06053,0.00073 -0.12006,-0.0066 -0.17859,-0.022c-0.08548,-0.02253 -0.16339,-0.0606 -0.23373,-0.1142c-0.07035,-0.05353 -0.12777,-0.1185 -0.17227,-0.1949c-0.04449,-0.0764 -0.07268,-0.1584 -0.08456,-0.246c-0.01187,-0.0876 -0.00653,-0.17413 0.01602,-0.2596l1.44925,-5.49326h-3.80464c-0.10945,0 -0.21263,-0.02541 -0.30956,-0.07623c-0.03877,-0.02033 -0.07519,-0.04424 -0.10924,-0.07173zM3.76889,7.66671h3.56774c0.04377,0 0.08713,0.00427 0.13006,0.01281c0.04293,0.00854 0.08461,0.02118 0.12506,0.03793c0.04044,0.01675 0.07886,0.03729 0.11526,0.06161c0.03639,0.02432 0.07007,0.05196 0.10102,0.08291c0.03095,0.03095 0.05859,0.06463 0.08291,0.10102c0.02432,0.0364 0.04485,0.07482 0.0616,0.11526c0.01675,0.04044 0.0294,0.08213 0.03794,0.12506c0.00854,0.04293 0.01281,0.08629 0.01281,0.13006c0,0.05765 -0.00735,0.11434 -0.02205,0.17007l-0.9867,3.73996l5.14866,-5.57336h-3.47657c-0.04378,0 -0.08713,-0.00427 -0.13006,-0.01281c-0.04293,-0.00854 -0.08462,-0.02119 -0.12507,-0.03794c-0.04044,-0.01675 -0.07886,-0.03729 -0.11525,-0.0616c-0.0364,-0.02432 -0.07008,-0.05196 -0.10103,-0.08291c-0.03095,-0.03095 -0.05859,-0.06463 -0.08291,-0.10103c-0.02432,-0.03639 -0.04485,-0.07481 -0.0616,-0.11525c-0.01675,-0.04045 -0.0294,-0.08214 -0.03794,-0.12507c-0.00854,-0.04293 -0.01281,-0.08629 -0.01281,-0.13006c0,-0.07469 0.01221,-0.14734 0.03663,-0.21794c0.02442,-0.07059 0.05971,-0.13526 0.10586,-0.19399l2.82235,-3.5914h-4.22496z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

1
app/icons/mask.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(2 3.333333333333333) rotate(0 6 5.666666666666666)" d="M6,0C2.69,0 0,2.54 0,5.67C0,8.8 2.69,11.33 6,11.33C9.31,11.33 12,8.8 12,5.67C12,2.54 9.31,0 6,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.64 6.715000010822796) rotate(14.999999999999998 1 1.3333333333333335)" d="M1,0C0.45,0 0,0.6 0,1.33C0,2.07 0.45,2.67 1,2.67C1.55,2.67 2,2.07 2,1.33C2,0.6 1.55,0 1,0Z " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.31 6.714999802665079) rotate(165.00000507213028 1.000000156118488 1.3333335414913166)" d="M1,0C0.45,0 0,0.6 0,1.33C0,2.07 0.45,2.67 1,2.67C1.55,2.67 2,2.07 2,1.33C2,0.6 1.55,0 1,0Z " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666599999999999 2.492504620561264) rotate(0 2.4176172657482775 2.2535810230527007)" d="M4,4.51C5.04,3.47 5.15,1.77 4.1,0.73C3.06,-0.32 1.04,-0.2 0,0.84 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.492667974925419 2.4926141635940393) rotate(0 2.4203326792039572 2.253609584869647)" d="M0.84,4.51C-0.2,3.47 -0.32,1.77 0.73,0.73C1.77,-0.32 3.8,-0.2 4.84,0.84 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.5 11.67) rotate(0 1.6666666666666665 0.33333029691911636)" d="M0,0C0.17,0.43 0.73,1.09 1.67,0.29C2.6,1.09 3.17,0.43 3.33,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

41
app/icons/max.svg Normal file
View File

@@ -0,0 +1,41 @@
<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) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 2) rotate(0 1.5 1.5)" d="M0,0L3,0L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 11) rotate(0 1.5 1.5)" d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 11) rotate(0 1.5 1.5)" d="M3,3L0,3L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2) rotate(0 1.5 1.5)" d="M0,3L0,0L3,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

45
app/icons/min.svg Normal file
View File

@@ -0,0 +1,45 @@
<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) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2.333333333333333) rotate(0 1.5 1.5)"
d="M0,0L0,3L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 2.333333333333333) rotate(0 1.5 1.5)"
d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 10.666666666666666) rotate(0 1.5 1.5)"
d="M3,3L3,0L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 10.666666666666666) rotate(0 1.4832500000000004 1.5)"
d="M0,3L0,0L2.97,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
app/icons/pause.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(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 6) rotate(0 0 2)" d="M0,0L0,4 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666666666666666 6) rotate(0 0 2)" d="M0,0L0,4 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
app/icons/plugin.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"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M6.94515,1.72485c0.26101,0.26101 0.39152,0.57606 0.39152,0.94515v3.33c0,0.36909 -0.13051,0.68414 -0.39152,0.94515c-0.26101,0.26101 -0.57606,0.39152 -0.94515,0.39152h-3.33c-0.36909,0 -0.68414,-0.13051 -0.94515,-0.39152c-0.26101,-0.26101 -0.39152,-0.57606 -0.39152,-0.94515v-3.33c0,-0.36909 0.13051,-0.68414 0.39152,-0.94515c0.26101,-0.26101 0.57606,-0.39152 0.94515,-0.39152h3.33c0.36909,0 0.68414,0.13051 0.94515,0.39152zM2.66667,6c0,0.00222 0.00111,0.00333 0.00333,0.00333h3.33c0.00222,0 0.00333,-0.00111 0.00333,-0.00333v-3.33c0,-0.00222 -0.00111,-0.00333 -0.00333,-0.00333h-3.33c-0.00222,0 -0.00333,0.00111 -0.00333,0.00333z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.94515,9.05822c0.26101,0.26101 0.39152,0.57607 0.39152,0.94518v3.33c0,0.36907 -0.13051,0.6841 -0.39152,0.9451c-0.26101,0.261 -0.57606,0.3915 -0.94515,0.3915h-3.33c-0.36909,0 -0.68414,-0.1305 -0.94515,-0.3915c-0.26101,-0.261 -0.39152,-0.57603 -0.39152,-0.9451v-3.33c0,-0.36911 0.13051,-0.68417 0.39152,-0.94518c0.26101,-0.26101 0.57606,-0.39151 0.94515,-0.39151h3.33c0.36909,0 0.68414,0.1305 0.94515,0.39151zM2.66667,13.3334c0,0.0022 0.00111,0.0033 0.00333,0.0033h3.33c0.00222,0 0.00333,-0.0011 0.00333,-0.0033v-3.33c0,-0.00227 -0.00111,-0.0034 -0.00333,-0.0034h-3.33c-0.00222,0 -0.00333,0.00113 -0.00333,0.0034z"></path><path id="路径 3" style="fill:#333333; opacity:1;" d="M13.7885,6.45515c-0.27727,0.27728 -0.59567,0.4921 -0.9552,0.64445c-0.37287,0.15805 -0.76283,0.23707 -1.1699,0.23707c-0.40727,0 -0.797,-0.07913 -1.1692,-0.23738c-0.35875,-0.15255 -0.67621,-0.36773 -0.95237,-0.64553c-0.27567,-0.2773 -0.48912,-0.59581 -0.64036,-0.95553c-0.15651,-0.37226 -0.23476,-0.76167 -0.23476,-1.16823c0,-0.40677 0.07836,-0.7959 0.23507,-1.1674c0.15143,-0.35898 0.36525,-0.67656 0.64144,-0.95275c0.27619,-0.27619 0.59379,-0.49001 0.95278,-0.64144c0.37147,-0.15672 0.7606,-0.23508 1.1674,-0.23508c0.40653,0 0.79593,0.07826 1.1682,0.23477c0.35973,0.15124 0.67823,0.36469 0.9555,0.64035c0.2778,0.27617 0.493,0.59363 0.6456,0.9524c0.1582,0.37217 0.2373,0.76189 0.2373,1.16915c0,0.40705 -0.079,0.79704 -0.237,1.16997c-0.1524,0.35951 -0.36723,0.67791 -0.6445,0.95518zM11.6634,2.66667c-0.46093,0 -0.8534,0.16199 -1.1774,0.48598c-0.324,0.32399 -0.486,0.71644 -0.486,1.17735c0,0.46231 0.16247,0.85689 0.4874,1.18374c0.32447,0.32639 0.71647,0.48959 1.176,0.48959c0.46087,0 0.85497,-0.16366 1.1823,-0.49098c0.32733,-0.32733 0.491,-0.72144 0.491,-1.18235c0,-0.4595 -0.1632,-0.85148 -0.4896,-1.17595c-0.32687,-0.32492 -0.72143,-0.48738 -1.1837,-0.48738z"></path><path id="路径 4" style="fill:#333333; opacity:1;" d="M14.2785,9.05822c0.261,0.26101 0.3915,0.57607 0.3915,0.94518v3.33c0,0.36907 -0.1305,0.6841 -0.3915,0.9451c-0.261,0.261 -0.57603,0.3915 -0.9451,0.3915h-3.33c-0.36911,0 -0.68417,-0.1305 -0.94518,-0.3915c-0.26101,-0.261 -0.39151,-0.57603 -0.39151,-0.9451v-3.33c0,-0.36911 0.1305,-0.68417 0.39151,-0.94518c0.26101,-0.26101 0.57607,-0.39151 0.94518,-0.39151h3.33c0.36907,0 0.6841,0.1305 0.9451,0.39151zM10,13.3334c0,0.0022 0.00113,0.0033 0.0034,0.0033h3.33c0.0022,0 0.0033,-0.0011 0.0033,-0.0033v-3.33c0,-0.00227 -0.0011,-0.0034 -0.0033,-0.0034h-3.33c-0.00227,0 -0.0034,0.00113 -0.0034,0.0034z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

1
app/icons/prompt.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.3; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 1.3333333333333333) rotate(0 4.666666666666666 4.666666666666666)" d="M1.36683 1.36683L2.77683 2.77683 M4.66667 0L4.66667 2 M4.66667 2L4.66667 0 M7.9623 1.36683L6.5523 2.77683 M6.5523 2.77683L7.9623 1.36683 M9.33333 4.66667L7.33333 4.66667 M7.33333 4.66667L9.33333 4.66667 M7.9623 7.9623L6.5523 6.5523 M6.5523 6.5523L7.9623 7.9623 M4.66667 9.33333L4.66667 7.33333 M4.66667 7.33333L4.66667 9.33333 M1.36683 7.9623L2.77683 6.5523 M2.77683 6.5523L1.36683 7.9623 M0 4.66667L2 4.66667 M2 4.66667L0 4.66667 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.847983333333333 6.1381) rotate(0 4.006941666666666 4.006933333333333)" d="M8.01,0L0,8.01 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
app/icons/rename.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(1.774903333333333 1.3006199999999999) rotate(0 6.599664999999999 6.599656666666666)" d="M2.83,13.2L13.2,2.83L10.37,0L0,10.37L0,13.2L2.83,13.2Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.317366666666667 4.129066666666667) rotate(0 1.4142166666666658 1.4142166666666665)" d="M0,0L2.83,2.83 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 911 B

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

17
app/icons/share.svg Normal file
View File

@@ -0,0 +1,17 @@
<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 1.3333333333333333) rotate(0 6.333333333333333 6.5)"
d="M6.67,3.67C1.67,3.67 0,7.33 0,13C0,13 2,8 6.67,8L6.67,11.67L12.67,6L6.67,0L6.67,3.67Z " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 740 B

1
app/icons/upload.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,34 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="38"
height="38" viewBox="0 0 38 38" fill="none">
<defs>
<filter id="filter_0" x="-4" y="-4" width="38" height="38" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="0" dy="2" />
<feGaussianBlur stdDeviation="2" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_Shadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_Shadow" result="shape" />
</filter>
<rect id="path_0" x="0" y="0" width="30" height="30" />
</defs>
<g opacity="1" transform="translate(4 2) rotate(0 15 15)">
<g id="undefined" filter="url(#filter_0)">
<rect stroke="#000000" stroke-width="1" stroke-opacity="0.05" />
<rect x="0" y="0" width="30" height="30" rx="10" />
</g>
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<g opacity="1" transform="translate(6 4.5) rotate(0 9 11)">
<text>
<tspan x="0" y="16.240000000000002" font-size="14" line-height="0" fill="#000000"
opacity="1" font-family="SourceHanSansCN-Regular" letter-spacing="0">🤣</tspan>
</text>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -2,45 +2,28 @@
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/highlight.scss";
import process from "child_process";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
import { getBuildConfig } from "./config/build";
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.");
}
const buildConfig = getBuildConfig();
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot.",
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
},
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
],
appleWebApp: {
title: "ChatGPT Next Web",
statusBarStyle: "black-translucent",
statusBarStyle: "default",
},
themeColor: "#fafafa",
};
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,
}: {
@@ -49,18 +32,8 @@ export default function RootLayout({
return (
<html lang="en">
<head>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<Meta />
<meta name="version" content={buildConfig.commitId} />
<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>
<link
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>

View File

@@ -1,9 +1,10 @@
import { SubmitKey } from "../store/app";
import { SubmitKey } from "../store/config";
const cn = {
WIP: "该功能仍在开发中……",
Error: {
Unauthorized: "现在是未授权状态,请在设置页填写授权码。",
Unauthorized:
"访问密码不正确或为空,请前往[设置](/#/settings)页输入正确的访问密码,或者填入你自己的 OpenAI API Key。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@@ -17,31 +18,43 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
Delete: "删除",
},
Rename: "重命名对话",
Typing: "正在输入…",
Input: (submitKey: string) => {
var inputHints = `输入消息,${submitKey} 发送`;
var inputHints = `${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行";
}
return inputHints;
return inputHints + "/ 触发补全";
},
Send: "发送",
Config: {
Reset: "重置默认",
SaveAs: "另存为面具",
},
},
Export: {
Title: "导出聊天记录为 Markdown",
Copy: "全部复制",
Download: "下载文件",
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
},
Memory: {
Title: "历史记忆",
EmptyContent: "尚未记忆",
Copy: "全部复制",
Title: "历史摘要",
EmptyContent: "对话内容过短,无需总结",
Send: "自动压缩聊天记录并作为上下文发送",
Copy: "复制摘要",
Reset: "重置对话",
ResetConfirm: "重置后将清空当前对话记录以及历史摘要,确认重置?",
},
Home: {
NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?",
DeleteToast: "已删除会话",
Revert: "撤销",
},
Settings: {
Title: "设置",
@@ -50,14 +63,24 @@ const cn = {
ClearAll: "清除所有数据",
ResetAll: "重置所有选项",
Close: "关闭",
ConfirmResetAll: "确认重置所有配置?",
ConfirmClearAll: "确认清除所有数据?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "头像",
@@ -76,8 +99,15 @@ const cn = {
},
SendKey: "发送键",
Theme: "主题",
TightBorder: "紧凑边框",
SendPreviewBubble: "发送预览气泡",
TightBorder: "无边框模式",
SendPreviewBubble: {
Title: "预览气泡",
SubTitle: "在预览气泡中预览 Markdown 内容",
},
Mask: {
Title: "面具启动页",
SubTitle: "新建聊天时,展示面具启动页",
},
Prompt: {
Disable: {
Title: "禁用提示词自动补全",
@@ -87,6 +117,14 @@ const cn = {
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom}`,
Edit: "编辑",
Modal: {
Title: "提示词列表",
Add: "新建",
Search: "搜索提示词",
},
EditModal: {
Title: "编辑提示词",
},
},
HistoryCount: {
Title: "附带历史消息数",
@@ -98,21 +136,23 @@ const cn = {
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过受控访问限制",
SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "账户余额",
SubTitle(used: any) {
return `本月已使用 $${used}`;
Title: "余额查询",
SubTitle(used: any, total: any) {
return `本月已使用 $${used},订阅总额 $${total}`;
},
IsChecking: "正在检查…",
Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额",
},
AccessCode: {
Title: "访问码",
SubTitle: "现在是受控访问状态",
Placeholder: "请输入访问码",
Title: "访问码",
SubTitle: "管理员已开启加密访问",
Placeholder: "请输入访问码",
},
Model: "模型 (model)",
Temperature: {
@@ -140,7 +180,6 @@ const cn = {
Summarize:
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 200 字以内",
},
ConfirmClearAll: "确认清除所有聊天、设置数据?",
},
Copy: {
Success: "已写入剪切板",
@@ -148,8 +187,55 @@ const cn = {
},
Context: {
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
Edit: "前置上下文和历史记忆",
Add: "新增一条",
Edit: "当前对话设置",
Add: "新增预设对话",
},
Plugin: {
Name: "插件",
},
Mask: {
Name: "面具",
Page: {
Title: "预设角色面具",
SubTitle: (count: number) => `${count} 个预设角色定义`,
Search: "搜索角色面具",
Create: "新建",
},
Item: {
Info: (count: number) => `包含 ${count} 条预设对话`,
Chat: "对话",
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
EditModal: {
Title: (readonly: boolean) =>
`编辑预设面具 ${readonly ? "(只读)" : ""}`,
Download: "下载预设",
Clone: "克隆预设",
},
Config: {
Avatar: "角色头像",
Name: "角色名称",
},
},
NewChat: {
Return: "返回",
Skip: "直接开始",
NotShow: "不再展示",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
Title: "挑选一个面具",
SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
More: "查看全部",
},
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
},
};

244
app/locales/cs.ts Normal file
View File

@@ -0,0 +1,244 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const cs: LocaleType = {
WIP: "V přípravě...",
Error: {
Unauthorized:
"Neoprávněný přístup, zadejte přístupový kód na stránce nastavení.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} zpráv`,
},
Chat: {
SubTitle: (count: number) => `${count} zpráv s ChatGPT`,
Actions: {
ChatList: "Přejít na seznam chatů",
CompressedHistory: "Pokyn z komprimované paměti historie",
Export: "Exportovat všechny zprávy jako Markdown",
Copy: "Kopírovat",
Stop: "Zastavit",
Retry: "Zopakovat",
Delete: "Smazat",
},
Rename: "Přejmenovat chat",
Typing: "Píše...",
Input: (submitKey: string) => {
var inputHints = `${submitKey} pro odeslání`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter pro řádkování";
}
return inputHints + ", / pro vyhledávání pokynů";
},
Send: "Odeslat",
Config: {
Reset: "Obnovit výchozí",
SaveAs: "Uložit jako Masku",
},
},
Export: {
Title: "Všechny zprávy",
Copy: "Kopírovat vše",
Download: "Stáhnout",
MessageFromYou: "Zpráva od vás",
MessageFromChatGPT: "Zpráva z ChatGPT",
},
Memory: {
Title: "Pokyn z paměti",
EmptyContent: "Zatím nic.",
Send: "Odeslat paměť",
Copy: "Kopírovat paměť",
Reset: "Obnovit relaci",
ResetConfirm:
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
},
Home: {
NewChat: "Nový chat",
DeleteChat: "Potvrzujete smazání vybrané konverzace?",
DeleteToast: "Chat smazán",
Revert: "Zvrátit",
},
Settings: {
Title: "Nastavení",
SubTitle: "Všechna nastavení",
Actions: {
ClearAll: "Vymazat všechna data",
ResetAll: "Obnovit veškeré nastavení",
Close: "Zavřít",
ConfirmResetAll: "Jste si jisti, že chcete obnovit všechna nastavení?",
ConfirmClearAll: "Jste si jisti, že chcete smazat všechna data?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Všechny jazyky",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Velikost písma",
SubTitle: "Nastavení velikosti písma obsahu chatu",
},
Update: {
Version: (x: string) => `Verze: ${x}`,
IsLatest: "Aktuální verze",
CheckUpdate: "Zkontrolovat aktualizace",
IsChecking: "Kontrola aktualizace...",
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
GoToUpdate: "Aktualizovat",
},
SendKey: "Odeslat klíč",
Theme: "Téma",
TightBorder: "Těsné ohraničení",
SendPreviewBubble: {
Title: "Odesílat chatovací bublinu s náhledem",
SubTitle: "Zobrazit v náhledu bubliny",
},
Mask: {
Title: "Úvodní obrazovka Masek",
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek",
},
Prompt: {
Disable: {
Title: "Deaktivovat automatické dokončování",
SubTitle: "Zadejte / pro spuštění automatického dokončování",
},
List: "Seznam pokynů",
ListCount: (builtin: number, custom: number) =>
`${builtin} vestavěných, ${custom} uživatelských`,
Edit: "Upravit",
Modal: {
Title: "Seznam pokynů",
Add: "Přidat pokyn",
Search: "Hledat pokyny",
},
EditModal: {
Title: "Editovat pokyn",
},
},
HistoryCount: {
Title: "Počet připojených zpráv",
SubTitle: "Počet odeslaných připojených zpráv na žádost",
},
CompressThreshold: {
Title: "Práh pro kompresi historie",
SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
},
Token: {
Title: "API klíč",
SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
Placeholder: "Klíč API OpenAI",
},
Usage: {
Title: "Stav účtu",
SubTitle(used: any, total: any) {
return `Použito tento měsíc $${used}, předplaceno $${total}`;
},
IsChecking: "Kontroluji...",
Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
},
AccessCode: {
Title: "Přístupový kód",
SubTitle: "Kontrola přístupu povolena",
Placeholder: "Potřebujete přístupový kód",
},
Model: "Model",
Temperature: {
Title: "Teplota",
SubTitle: "Větší hodnota činí výstup náhodnějším",
},
MaxTokens: {
Title: "Max. počet tokenů",
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů",
},
PresencePenlty: {
Title: "Přítomnostní korekce",
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
},
},
Store: {
DefaultTopic: "Nová konverzace",
BotHello: "Ahoj! Jak mohu dnes pomoci?",
Error: "Něco se pokazilo, zkuste to prosím později.",
Prompt: {
History: (content: string) =>
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " +
content,
Topic:
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.",
Summarize:
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.",
},
},
Copy: {
Success: "Zkopírováno do schránky",
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
},
Context: {
Toast: (x: any) => `Použití ${x} kontextových pokynů`,
Edit: "Kontextové a paměťové pokyny",
Add: "Přidat pokyn",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Maska",
Page: {
Title: "Šablona pokynu",
SubTitle: (count: number) => `${count} šablon pokynů`,
Search: "Hledat v šablonách",
Create: "Vytvořit",
},
Item: {
Info: (count: number) => `${count} pokynů`,
Chat: "Chat",
View: "Zobrazit",
Edit: "Upravit",
Delete: "Smazat",
DeleteConfirm: "Potvrdit smazání?",
},
EditModal: {
Title: (readonly: boolean) =>
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`,
Download: "Stáhnout",
Clone: "Duplikovat",
},
Config: {
Avatar: "Avatar Bota",
Name: "Jméno Bota",
},
},
NewChat: {
Return: "Zpět",
Skip: "Přeskočit",
Title: "Vyberte Masku",
SubTitle: "Chatovat s duší za Maskou",
More: "Najít více",
NotShow: "Nezobrazovat znovu",
ConfirmNoShow: "Potvrdit zakázáníMůžete jej povolit později v nastavení.",
},
UI: {
Confirm: "Potvrdit",
Cancel: "Zrušit",
Close: "Zavřít",
Create: "Vytvořit",
Edit: "Upravit",
},
};
export default cs;

249
app/locales/de.ts Normal file
View File

@@ -0,0 +1,249 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const de: LocaleType = {
WIP: "In Bearbeitung...",
Error: {
Unauthorized:
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`,
},
Chat: {
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
Actions: {
ChatList: "Zur Chat-Liste gehen",
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
Export: "Alle Nachrichten als Markdown exportieren",
Copy: "Kopieren",
Stop: "Stop",
Retry: "Wiederholen",
Delete: "Delete",
},
Rename: "Chat umbenennen",
Typing: "Tippen...",
Input: (submitKey: string) => {
var inputHints = `${submitKey} um zu Senden`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
}
return inputHints + ", / zum Durchsuchen von Prompts";
},
Send: "Senden",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
},
Export: {
Title: "Alle Nachrichten",
Copy: "Alles kopieren",
Download: "Herunterladen",
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
},
Memory: {
Title: "Gedächtnis-Prompt",
EmptyContent: "Noch nichts.",
Send: "Gedächtnis senden",
Copy: "Gedächtnis kopieren",
Reset: "Sitzung zurücksetzen",
ResetConfirm:
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
},
Home: {
NewChat: "Neuer Chat",
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
DeleteToast: "Chat gelöscht",
Revert: "Zurücksetzen",
},
Settings: {
Title: "Einstellungen",
SubTitle: "Alle Einstellungen",
Actions: {
ClearAll: "Alle Daten löschen",
ResetAll: "Alle Einstellungen zurücksetzen",
Close: "Schließen",
ConfirmResetAll:
"Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
ConfirmClearAll: "Möchten Sie wirklich alle Chats zurücksetzen?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Alle Sprachen",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Schriftgröße",
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Neueste Version",
CheckUpdate: "Update prüfen",
IsChecking: "Update wird geprüft...",
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Aktualisieren",
},
SendKey: "Senden-Taste",
Theme: "Erscheinungsbild",
TightBorder: "Enger Rahmen",
SendPreviewBubble: {
Title: "Vorschau-Bubble senden",
SubTitle: "Preview markdown in bubble",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
},
Prompt: {
Disable: {
Title: "Autovervollständigung deaktivieren",
SubTitle: "Autovervollständigung mit / starten",
},
List: "Prompt-Liste",
ListCount: (builtin: number, custom: number) =>
`${builtin} integriert, ${custom} benutzerdefiniert`,
Edit: "Bearbeiten",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
EditModal: {
Title: "Edit Prompt",
},
},
HistoryCount: {
Title: "Anzahl der angehängten Nachrichten",
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
},
CompressThreshold: {
Title: "Schwellenwert für Verlaufskomprimierung",
SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
},
Token: {
Title: "API-Schlüssel",
SubTitle:
"Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
Placeholder: "OpenAI API-Schlüssel",
},
Usage: {
Title: "Kontostand",
SubTitle(used: any, total: any) {
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
},
IsChecking: "Wird überprüft...",
Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
},
AccessCode: {
Title: "Zugangscode",
SubTitle: "Zugangskontrolle aktiviert",
Placeholder: "Zugangscode erforderlich",
},
Model: "Modell",
Temperature: {
Title: "Temperature", //Temperatur
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
},
MaxTokens: {
Title: "Max Tokens", //Maximale Token
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
},
PresencePenlty: {
Title: "Presence Penalty", //Anwesenheitsstrafe
SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
},
},
Store: {
DefaultTopic: "Neues Gespräch",
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
Error:
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
Prompt: {
History: (content: string) =>
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
content,
Topic:
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
Summarize:
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
},
},
Copy: {
Success: "In die Zwischenablage kopiert",
Failed:
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
},
Context: {
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
Edit: "Kontext- und Gedächtnis-Prompts",
Add: "Hinzufügen",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mask",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
};
export default de;

View File

@@ -1,8 +1,8 @@
import { SubmitKey } from "../store/app";
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const en: LocaleType = {
WIP: "WIP...",
WIP: "Coming Soon...",
Error: {
Unauthorized:
"Unauthorized access, please enter access code in settings page.",
@@ -19,31 +19,44 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
Delete: "Delete",
},
Rename: "Rename Chat",
Typing: "Typing…",
Input: (submitKey: string) => {
var inputHints = `Type something and press ${submitKey} to send`;
var inputHints = `${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", press Shift + Enter to newline";
inputHints += ", Shift + Enter to wrap";
}
return inputHints;
return inputHints + ", / to search prompts";
},
Send: "Send",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
},
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",
@@ -52,14 +65,24 @@ const en: LocaleType = {
ClearAll: "Clear All Data",
ResetAll: "Reset All Settings",
Close: "Close",
ConfirmResetAll: "Are you sure you want to reset all configurations?",
ConfirmClearAll: "Are you sure you want to reset all data?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "All Languages",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
@@ -78,7 +101,14 @@ const en: LocaleType = {
SendKey: "Send Key",
Theme: "Theme",
TightBorder: "Tight Border",
SendPreviewBubble: "Send Preview Bubble",
SendPreviewBubble: {
Title: "Send Preview Bubble",
SubTitle: "Preview markdown in bubble",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
},
Prompt: {
Disable: {
Title: "Disable auto-completion",
@@ -88,6 +118,14 @@ const en: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
EditModal: {
Title: "Edit Prompt",
},
},
HistoryCount: {
Title: "Attached Messages Count",
@@ -105,11 +143,12 @@ const en: LocaleType = {
},
Usage: {
Title: "Account Balance",
SubTitle(used: any) {
return `Used this month $${used}`;
SubTitle(used: any, total: any) {
return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
Check: "Check Again",
Check: "Check",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {
Title: "Access Code",
@@ -144,7 +183,6 @@ const en: LocaleType = {
Summarize:
"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?",
},
Copy: {
Success: "Copied to clipboard",
@@ -153,7 +191,54 @@ const en: LocaleType = {
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Contextual and Memory Prompts",
Add: "Add One",
Add: "Add a Prompt",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mask",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
};

View File

@@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app";
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const es: LocaleType = {
@@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
Delete: "Delete",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@@ -30,20 +31,32 @@ const es: LocaleType = {
return inputHints;
},
Send: "Enviar",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
},
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",
@@ -52,14 +65,24 @@ const es: LocaleType = {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
ConfirmResetAll: "Are you sure you want to reset all configurations?",
ConfirmClearAll: "Are you sure you want to reset all chat?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Todos los idiomas",
Options: {
cn: "简体中文",
en: "Inglés",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
@@ -78,7 +101,14 @@ const es: LocaleType = {
SendKey: "Tecla de envío",
Theme: "Tema",
TightBorder: "Borde ajustado",
SendPreviewBubble: "Enviar burbuja de vista previa",
SendPreviewBubble: {
Title: "Enviar burbuja de vista previa",
SubTitle: "Preview markdown in bubble",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
},
Prompt: {
Disable: {
Title: "Desactivar autocompletado",
@@ -88,6 +118,14 @@ const es: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
EditModal: {
Title: "Edit Prompt",
},
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",
@@ -105,11 +143,12 @@ const es: LocaleType = {
},
Usage: {
Title: "Saldo de la cuenta",
SubTitle(used: any) {
return `Usado $${used}`;
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",
@@ -144,8 +183,6 @@ const es: LocaleType = {
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",
@@ -157,6 +194,53 @@ const es: LocaleType = {
Edit: "Contextual and Memory Prompts",
Add: "Add One",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mask",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
};
export default es;

View File

@@ -2,13 +2,33 @@ import CN from "./cn";
import EN from "./en";
import TW from "./tw";
import ES from "./es";
import IT from "./it";
import TR from "./tr";
import JP from "./jp";
import DE from "./de";
import VI from "./vi";
import RU from "./ru";
import CS from "./cs";
export type { LocaleType } from "./cn";
export const AllLangs = ["cn", "tw", "en", "es"] as const;
type Lang = (typeof AllLangs)[number];
export const AllLangs = [
"en",
"cn",
"tw",
"es",
"it",
"tr",
"jp",
"de",
"vi",
"ru",
"cs",
] as const;
export type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang";
const DEFAULT_LANG = "en";
function getItem(key: string) {
try {
@@ -28,7 +48,8 @@ function getLanguage() {
try {
return navigator.language.toLowerCase();
} catch {
return "cn";
console.log("[Lang] failed to detect user lang.");
return DEFAULT_LANG;
}
}
@@ -41,15 +62,13 @@ export function getLang(): Lang {
const lang = getLanguage();
if (lang.includes("zh") || lang.includes("cn")) {
return "cn";
} else if (lang.includes("tw")) {
return "tw";
} else if (lang.includes("es")) {
return "es";
} else {
return "en";
for (const option of AllLangs) {
if (lang.includes(option)) {
return option;
}
}
return DEFAULT_LANG;
}
export function changeLang(lang: Lang) {
@@ -57,4 +76,16 @@ export function changeLang(lang: Lang) {
location.reload();
}
export default { en: EN, cn: CN, tw: TW, es: ES }[getLang()];
export default {
en: EN,
cn: CN,
tw: TW,
es: ES,
it: IT,
tr: TR,
jp: JP,
de: DE,
vi: VI,
ru: RU,
cs: CS,
}[getLang()] as typeof CN;

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

@@ -0,0 +1,247 @@
import { SubmitKey } from "../store/config";
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",
Delete: "Delete",
},
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",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
},
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:
"Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Cancellata",
Revert: "Revert",
},
Settings: {
Title: "Impostazioni",
SubTitle: "Tutte le impostazioni",
Actions: {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
ConfirmResetAll: "Sei sicuro vuoi cancellare tutte le impostazioni?",
ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tutte le lingue",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
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: "Schermo intero",
SendPreviewBubble: {
Title: "Anteprima di digitazione",
SubTitle: "Preview markdown in bubble",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
},
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",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
EditModal: {
Title: "Edit Prompt",
},
},
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: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
return `Attualmente usato in questo mese $${used}, soglia massima $${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.",
},
},
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",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mask",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
};
export default it;

245
app/locales/jp.ts Normal file
View File

@@ -0,0 +1,245 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const jp: LocaleType = {
WIP: "この機能は開発中です……",
Error: {
Unauthorized:
"現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードを入力してください。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 通のチャット`,
},
Chat: {
SubTitle: (count: number) => `ChatGPTとの ${count} 通のチャット`,
Actions: {
ChatList: "メッセージリストを表示",
CompressedHistory: "圧縮された履歴プロンプトを表示",
Export: "チャット履歴をエクスポート",
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
Delete: "Delete",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} で送信`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter で改行";
}
return inputHints + "/ で自動補完をトリガー";
},
Send: "送信",
Config: {
Reset: "重置默认",
SaveAs: "另存为面具",
},
},
Export: {
Title: "チャット履歴をMarkdown形式でエクスポート",
Copy: "すべてコピー",
Download: "ファイルをダウンロード",
MessageFromYou: "あなたからのメッセージ",
MessageFromChatGPT: "ChatGPTからのメッセージ",
},
Memory: {
Title: "履歴メモリ",
EmptyContent: "まだ記憶されていません",
Send: "メモリを送信",
Copy: "メモリをコピー",
Reset: "チャットをリセット",
ResetConfirm:
"リセット後、現在のチャット履歴と過去のメモリがクリアされます。リセットしてもよろしいですか?",
},
Home: {
NewChat: "新しいチャット",
DeleteChat: "選択したチャットを削除してもよろしいですか?",
DeleteToast: "チャットが削除されました",
Revert: "元に戻す",
},
Settings: {
Title: "設定",
SubTitle: "設定オプション",
Actions: {
ClearAll: "すべてのデータをクリア",
ResetAll: "すべてのオプションをリセット",
Close: "閉じる",
ConfirmResetAll: "すべての設定をリセットしてもよろしいですか?",
ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "アバター",
FontSize: {
Title: "フォントサイズ",
SubTitle: "チャット内容のフォントサイズ",
},
Update: {
Version: (x: string) => `現在のバージョン:${x}`,
IsLatest: "最新バージョンです",
CheckUpdate: "アップデートを確認",
IsChecking: "アップデートを確認しています...",
FoundUpdate: (x: string) => `新しいバージョンが見つかりました:${x}`,
GoToUpdate: "更新する",
},
SendKey: "送信キー",
Theme: "テーマ",
TightBorder: "ボーダーレスモード",
SendPreviewBubble: {
Title: "プレビューバブルの送信",
SubTitle: "在预览气泡中预览 Markdown 内容",
},
Mask: {
Title: "面具启动页",
SubTitle: "新建聊天时,展示面具启动页",
},
Prompt: {
Disable: {
Title: "プロンプトの自動補完を無効にする",
SubTitle:
"入力フィールドの先頭に / を入力すると、自動補完がトリガーされます。",
},
List: "カスタムプロンプトリスト",
ListCount: (builtin: number, custom: number) =>
`組み込み ${builtin} 件、ユーザー定義 ${custom}`,
Edit: "編集",
Modal: {
Title: "プロンプトリスト",
Add: "新規追加",
Search: "プロンプトワード検索",
},
EditModal: {
Title: "编辑提示词",
},
},
HistoryCount: {
Title: "履歴メッセージ数を添付",
SubTitle: "リクエストごとに添付する履歴メッセージ数",
},
CompressThreshold: {
Title: "履歴メッセージの長さ圧縮しきい値",
SubTitle:
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
},
Token: {
Title: "APIキー",
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
Placeholder: "OpenAI APIキー",
},
Usage: {
Title: "残高照会",
SubTitle(used: any, total: any) {
return `今月は $${used} を使用しました。総額は $${total} です。`;
},
IsChecking: "確認中...",
Check: "再確認",
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
},
AccessCode: {
Title: "アクセスパスワード",
SubTitle: "暗号化アクセスが有効になっています",
Placeholder: "アクセスパスワードを入力してください",
},
Model: "モデル (model)",
Temperature: {
Title: "ランダム性 (temperature)",
SubTitle:
"値が大きいほど、回答がランダムになります。1以上の値には文字化けが含まれる可能性があります。",
},
MaxTokens: {
Title: "シングルレスポンス制限 (max_tokens)",
SubTitle: "1回のインタラクションで使用される最大トークン数",
},
PresencePenlty: {
Title: "トピックの新鮮度 (presence_penalty)",
SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。",
},
},
Store: {
DefaultTopic: "新しいチャット",
BotHello: "何かお手伝いできることはありますか",
Error: "エラーが発生しました。しばらくしてからやり直してください。",
Prompt: {
History: (content: string) =>
"これは、AI とユーザの過去のチャットを要約した前提となるストーリーです:" +
content,
Topic:
"45文字でこの文章の簡潔な主題を返してください。説明、句読点、感嘆詞、余分なテキストは無しで。もし主題がない場合は、「おしゃべり」を返してください",
Summarize:
"あなたとユーザの会話を簡潔にまとめて、後続のコンテキストプロンプトとして使ってください。200字以内に抑えてください。",
},
},
Copy: {
Success: "クリップボードに書き込みました",
Failed: "コピーに失敗しました。クリップボード許可を与えてください。",
},
Context: {
Toast: (x: any) => `前置コンテキストが ${x} 件設定されました`,
Edit: "前置コンテキストと履歴メモリ",
Add: "新規追加",
},
Plugin: { Name: "插件" },
Mask: {
Name: "面具",
Page: {
Title: "预设角色面具",
SubTitle: (count: number) => `${count} 个预设角色定义`,
Search: "搜索角色面具",
Create: "新建",
},
Item: {
Info: (count: number) => `包含 ${count} 条预设对话`,
Chat: "对话",
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
EditModal: {
Title: (readonly: boolean) =>
`编辑预设面具 ${readonly ? "(只读)" : ""}`,
Download: "下载预设",
Clone: "克隆预设",
},
Config: {
Avatar: "角色头像",
Name: "角色名称",
},
},
NewChat: {
Return: "返回",
Skip: "跳过",
Title: "挑选一个面具",
SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
More: "搜索更多",
NotShow: "不再展示",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
},
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
},
};
export default jp;

250
app/locales/ru.ts Normal file
View File

@@ -0,0 +1,250 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const ru: LocaleType = {
WIP: "Скоро...",
Error: {
Unauthorized:
"Несанкционированный доступ. Пожалуйста, введите код доступа на странице настроек.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} сообщений`,
},
Chat: {
SubTitle: (count: number) => `${count} сообщений с ChatGPT`,
Actions: {
ChatList: "Перейти к списку чатов",
CompressedHistory: "Сжатая история памяти",
Export: "Экспортировать все сообщения в формате Markdown",
Copy: "Копировать",
Stop: "Остановить",
Retry: "Повторить",
Delete: "Удалить",
},
Rename: "Переименовать чат",
Typing: "Печатает…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} для отправки сообщения`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter для переноса строки";
}
return inputHints + ", / для поиска подсказок";
},
Send: "Отправить",
Config: {
Reset: "Сбросить настройки",
SaveAs: "Сохранить как маску",
},
},
Export: {
Title: "Все сообщения",
Copy: "Копировать все",
Download: "Скачать",
MessageFromYou: "Сообщение от вас",
MessageFromChatGPT: "Сообщение от ChatGPT",
},
Memory: {
Title: "Память",
EmptyContent: "Пусто.",
Send: "Отправить память",
Copy: "Копировать память",
Reset: "Сбросить сессию",
ResetConfirm:
"При сбросе текущая история переписки и историческая память будут удалены. Вы уверены, что хотите сбросить?",
},
Home: {
NewChat: "Новый чат",
DeleteChat: "Вы действительно хотите удалить выбранный разговор?",
DeleteToast: "Чат удален",
Revert: "Отмена",
},
Settings: {
Title: "Настройки",
SubTitle: "Все настройки",
Actions: {
ClearAll: "Очистить все данные",
ResetAll: "Сбросить все настройки",
Close: "Закрыть",
ConfirmResetAll: "Вы уверены, что хотите сбросить все настройки?",
ConfirmClearAll: "Вы уверены, что хотите очистить все данные?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Все языки",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Аватар",
FontSize: {
Title: "Размер шрифта",
SubTitle: "Настроить размер шрифта контента чата",
},
Update: {
Version: (x: string) => `Версия: ${x}`,
IsLatest: "Последняя версия",
CheckUpdate: "Проверить обновление",
IsChecking: "Проверка обновления...",
FoundUpdate: (x: string) => `Найдена новая версия: ${x}`,
GoToUpdate: "Обновить",
},
SendKey: "Клавиша отправки",
Theme: "Тема",
TightBorder: "Узкая граница",
SendPreviewBubble: {
Title: "Отправить предпросмотр",
SubTitle: "Предварительный просмотр markdown в пузыре",
},
Mask: {
Title: "Экран заставки маски",
SubTitle: "Показывать экран заставки маски перед началом нового чата",
},
Prompt: {
Disable: {
Title: "Отключить автозаполнение",
SubTitle: "Ввод / для запуска автозаполнения",
},
List: "Список подсказок",
ListCount: (builtin: number, custom: number) =>
`${builtin} встроенных, ${custom} пользовательских`,
Edit: "Редактировать",
Modal: {
Title: "Список подсказок",
Add: "Добавить",
Search: "Поиск подсказок",
},
EditModal: {
Title: "Редактировать подсказку",
},
},
HistoryCount: {
Title: "Количество прикрепляемых сообщений",
SubTitle:
"Количество отправляемых сообщений, прикрепляемых к каждому запросу",
},
CompressThreshold: {
Title: "Порог сжатия истории",
SubTitle:
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
},
Token: {
Title: "API ключ",
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
Placeholder: "API ключ OpenAI",
},
Usage: {
Title: "Баланс аккаунта",
SubTitle(used: any, total: any) {
return `Использовано в этом месяце $${used}, подписка $${total}`;
},
IsChecking: "Проверка...",
Check: "Проверить",
NoAccess: "Введите API ключ, чтобы проверить баланс",
},
AccessCode: {
Title: "Код доступа",
SubTitle: "Контроль доступа включен",
Placeholder: "Требуется код доступа",
},
Model: "Модель",
Temperature: {
Title: "Температура",
SubTitle: "Чем выше значение, тем более случайный вывод",
},
MaxTokens: {
Title: "Максимальное количество токенов",
SubTitle: "Максимальная длина вводных и генерируемых токенов",
},
PresencePenlty: {
Title: "Штраф за повторения",
SubTitle:
"Чем выше значение, тем больше вероятность общения на новые темы",
},
},
Store: {
DefaultTopic: "Новый разговор",
BotHello: "Здравствуйте! Как я могу вам помочь сегодня?",
Error: "Что-то пошло не так. Пожалуйста, попробуйте еще раз позже.",
Prompt: {
History: (content: string) =>
"Это краткое содержание истории чата между ИИ и пользователем: " +
content,
Topic:
"Пожалуйста, создайте заголовок из четырех или пяти слов, который кратко описывает нашу беседу, без введения, знаков пунктуации, кавычек, точек, символов или дополнительного текста. Удалите кавычки.",
Summarize:
"Кратко изложите нашу дискуссию в 200 словах или менее для использования в будущем контексте.",
},
},
Copy: {
Success: "Скопировано в буфер обмена",
Failed:
"Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена",
},
Context: {
Toast: (x: any) => `С ${x} контекстными подсказками`,
Edit: "Контекстные и памятные подсказки",
Add: "Добавить подсказку",
},
Plugin: {
Name: "Плагин",
},
Mask: {
Name: "Маска",
Page: {
Title: "Шаблон подсказки",
SubTitle: (count: number) => `${count} шаблонов подсказок`,
Search: "Поиск шаблонов",
Create: "Создать",
},
Item: {
Info: (count: number) => `${count} подсказок`,
Chat: "Чат",
View: "Просмотр",
Edit: "Редактировать",
Delete: "Удалить",
DeleteConfirm: "Подтвердить удаление?",
},
EditModal: {
Title: (readonly: boolean) =>
`Редактирование шаблона подсказки ${
readonly ? "(только для чтения)" : ""
}`,
Download: "Скачать",
Clone: "Клонировать",
},
Config: {
Avatar: "Аватар бота",
Name: "Имя бота",
},
},
NewChat: {
Return: "Вернуться",
Skip: "Пропустить",
Title: "Выберите маску",
SubTitle: "Общайтесь с душой за маской",
More: "Найти еще",
NotShow: "Не показывать снова",
ConfirmNoShow:
"Подтвердите отключение? Вы можете включить это позже в настройках.",
},
UI: {
Confirm: "Подтвердить",
Cancel: "Отмена",
Close: "Закрыть",
Create: "Создать",
Edit: "Редактировать",
},
};
export default ru;

247
app/locales/tr.ts Normal file
View File

@@ -0,0 +1,247 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const tr: LocaleType = {
WIP: "Çalışma devam ediyor...",
Error: {
Unauthorized:
"Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} mesaj`,
},
Chat: {
SubTitle: (count: number) => `ChatGPT tarafından ${count} mesaj`,
Actions: {
ChatList: "Sohbet Listesine Git",
CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu",
Export: "Tüm Mesajları Markdown Olarak Dışa Aktar",
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
Delete: "Delete",
},
Rename: "Sohbeti Yeniden Adlandır",
Typing: "Yazıyor…",
Input: (submitKey: string) => {
var inputHints = `Göndermek için ${submitKey}`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", kaydırmak için Shift + Enter";
}
return inputHints + ", komutları aramak için / (eğik çizgi)";
},
Send: "Gönder",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
},
Export: {
Title: "Tüm Mesajlar",
Copy: "Tümünü Kopyala",
Download: "İndir",
MessageFromYou: "Sizin Mesajınız",
MessageFromChatGPT: "ChatGPT'nin Mesajı",
},
Memory: {
Title: "Bellek Komutları",
EmptyContent: "Henüz değil.",
Send: "Belleği Gönder",
Copy: "Belleği Kopyala",
Reset: "Oturumu Sıfırla",
ResetConfirm:
"Sıfırlama, geçerli görüşme geçmişini ve geçmiş belleği siler. Sıfırlamak istediğinizden emin misiniz?",
},
Home: {
NewChat: "Yeni Sohbet",
DeleteChat: "Seçili sohbeti silmeyi onaylıyor musunuz?",
DeleteToast: "Sohbet Silindi",
Revert: "Geri Al",
},
Settings: {
Title: "Ayarlar",
SubTitle: "Tüm Ayarlar",
Actions: {
ClearAll: "Tüm Verileri Temizle",
ResetAll: "Tüm Ayarları Sıfırla",
Close: "Kapat",
ConfirmResetAll: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
ConfirmClearAll: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tüm Diller",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Yazı Boyutu",
SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
},
Update: {
Version: (x: string) => `Sürüm: ${x}`,
IsLatest: "En son sürüm",
CheckUpdate: "Güncellemeyi Kontrol Et",
IsChecking: "Güncelleme kontrol ediliyor...",
FoundUpdate: (x: string) => `Yeni sürüm bulundu: ${x}`,
GoToUpdate: "Güncelle",
},
SendKey: "Gönder Tuşu",
Theme: "Tema",
TightBorder: "Tam Ekran",
SendPreviewBubble: {
Title: "Mesaj Önizleme Balonu",
SubTitle: "Preview markdown in bubble",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
},
Prompt: {
Disable: {
Title: "Otomatik tamamlamayı devre dışı bırak",
SubTitle: "Otomatik tamamlamayı kullanmak için / (eğik çizgi) girin",
},
List: "Komut Listesi",
ListCount: (builtin: number, custom: number) =>
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
Edit: "Düzenle",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
EditModal: {
Title: "Edit Prompt",
},
},
HistoryCount: {
Title: "Ekli Mesaj Sayısı",
SubTitle: "İstek başına ekli gönderilen mesaj sayısı",
},
CompressThreshold: {
Title: "Geçmiş Sıkıştırma Eşiği",
SubTitle:
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
},
Token: {
Title: "API Anahtarı",
SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
Placeholder: "OpenAI API Anahtarı",
},
Usage: {
Title: "Hesap Bakiyesi",
SubTitle(used: any, total: any) {
return `Bu ay kullanılan $${used}, abonelik $${total}`;
},
IsChecking: "Kontrol ediliyor...",
Check: "Tekrar Kontrol Et",
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
},
AccessCode: {
Title: "Erişim Kodu",
SubTitle: "Erişim kontrolü etkinleştirme",
Placeholder: "Erişim Kodu Gerekiyor",
},
Model: "Model",
Temperature: {
Title: "Gerçeklik",
SubTitle:
"Daha büyük bir değer girildiğinde gerçeklik oranı düşer ve daha rastgele çıktılar üretir",
},
MaxTokens: {
Title: "Maksimum Belirteç",
SubTitle:
"Girdi belirteçlerinin ve oluşturulan belirteçlerin maksimum uzunluğu",
},
PresencePenlty: {
Title: "Varlık Cezası",
SubTitle:
"Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır",
},
},
Store: {
DefaultTopic: "Yeni Konuşma",
BotHello: "Merhaba! Size bugün nasıl yardımcı olabilirim?",
Error: "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyiniz.",
Prompt: {
History: (content: string) =>
"Bu, yapay zeka ile kullanıcı arasındaki sohbet geçmişinin bir özetidir: " +
content,
Topic:
"Lütfen herhangi bir giriş, noktalama işareti, tırnak işareti, nokta, sembol veya ek metin olmadan konuşmamızı özetleyen dört ila beş kelimelik bir başlık oluşturun. Çevreleyen tırnak işaretlerini kaldırın.",
Summarize:
"Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.",
},
},
Copy: {
Success: "Panoya kopyalandı",
Failed: "Kopyalama başarısız oldu, lütfen panoya erişim izni verin",
},
Context: {
Toast: (x: any) => `${x} bağlamsal bellek komutu`,
Edit: "Bağlamsal ve Bellek Komutları",
Add: "Yeni Ekle",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mask",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
},
};
export default tr;

View File

@@ -1,10 +1,10 @@
import { SubmitKey } from "../store/app";
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const tw: LocaleType = {
WIP: "該功能仍在開發中……",
Error: {
Unauthorized: "目前您的狀態是未授權,請前往設定頁面填寫授權碼。",
Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 條對話`,
@@ -12,12 +12,13 @@ const tw: LocaleType = {
Chat: {
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
Actions: {
ChatList: "查看息列表",
ChatList: "查看息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
Retry: "重試",
Delete: "刪除",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@@ -29,36 +30,57 @@ const tw: LocaleType = {
return inputHints;
},
Send: "發送",
Config: {
Reset: "重置默认",
SaveAs: "另存为面具",
},
},
Export: {
Title: "匯出聊天記錄為 Markdown",
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: "重所有設定",
ClearAll: "清除所有資料",
ResetAll: "重所有設定",
Close: "關閉",
ConfirmResetAll: "您確定要重設所有設定嗎?",
ConfirmClearAll: "您確定要清除所有数据嗎?",
},
Lang: {
Name: "Language",
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "大頭貼",
@@ -77,16 +99,31 @@ const tw: LocaleType = {
SendKey: "發送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
SendPreviewBubble: "發送預覽氣泡",
SendPreviewBubble: {
Title: "預覽氣泡",
SubTitle: "在预览气泡中预览 Markdown 内容",
},
Mask: {
Title: "面具启动页",
SubTitle: "新建聊天时,展示面具启动页",
},
Prompt: {
Disable: {
Title: "停用提示詞自動補",
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補",
Title: "停用提示詞自動補",
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補",
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
` ${builtin} 條,用戶定義 ${custom}`,
` ${builtin} 條,用戶定義 ${custom}`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
Add: "新增一條",
Search: "搜尋提示詞",
},
EditModal: {
Title: "编辑提示词",
},
},
HistoryCount: {
Title: "附帶歷史訊息數",
@@ -98,30 +135,31 @@ const tw: LocaleType = {
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可規避受控訪問限制",
SubTitle: "使用自己的 Key 可規避授權存取限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "帳戶餘額",
SubTitle(used: any) {
return `本月已使用 $${used}`;
SubTitle(used: any, total: any) {
return `本月已使用 $${used},訂閱總額 $${total}`;
},
IsChecking: "正在檢查…",
Check: "重新檢查",
NoAccess: "輸入API Key查看餘額",
},
AccessCode: {
Title: "訪問碼",
SubTitle: "現在是受控訪問狀態",
Placeholder: "請輸入訪問碼",
Title: "授權碼",
SubTitle: "目前是未授權存取狀態",
Placeholder: "請輸入授權碼",
},
Model: "模型 (model)",
Temperature: {
Title: "隨機性 (temperature)",
SubTitle: "值越大,回越隨機",
SubTitle: "值越大,回越隨機",
},
MaxTokens: {
Title: "單次回限制 (max_tokens)",
SubTitle: "單次互所用的最大 Token 數",
Title: "單次回限制 (max_tokens)",
SubTitle: "單次互所用的最大 Token 數",
},
PresencePenlty: {
Title: "話題新穎度 (presence_penalty)",
@@ -135,20 +173,64 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Topic:
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
Summarize:
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt且字數控制在 200 字以內",
"Use the language used by the user (e.g. en-us for english conversation, zh-hant for chinese conversation, etc.) to summarise the conversation in at most 200 words. The summary will be used as prompt for you to continue the conversation in the future.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
Copy: {
Success: "已複製到剪貼簿中",
Failed: "複製失敗,請賦予剪貼簿權限",
},
Context: {
Toast: (x: any) => `已設 ${x} 條前置上下文`,
Toast: (x: any) => `已設 ${x} 條前置上下文`,
Edit: "前置上下文和歷史記憶",
Add: "新增條",
Add: "新增條",
},
Plugin: { Name: "插件" },
Mask: {
Name: "面具",
Page: {
Title: "预设角色面具",
SubTitle: (count: number) => `${count} 个预设角色定义`,
Search: "搜索角色面具",
Create: "新建",
},
Item: {
Info: (count: number) => `包含 ${count} 条预设对话`,
Chat: "对话",
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
EditModal: {
Title: (readonly: boolean) =>
`编辑预设面具 ${readonly ? "(只读)" : ""}`,
Download: "下载预设",
Clone: "克隆预设",
},
Config: {
Avatar: "角色头像",
Name: "角色名称",
},
},
NewChat: {
Return: "返回",
Skip: "跳过",
Title: "挑选一个面具",
SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
More: "搜索更多",
NotShow: "不再展示",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
},
UI: {
Confirm: "确认",
Cancel: "取消",
Close: "关闭",
Create: "新建",
Edit: "编辑",
},
};

243
app/locales/vi.ts Normal file
View File

@@ -0,0 +1,243 @@
import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index";
const vi: LocaleType = {
WIP: "Sắp ra mắt...",
Error: {
Unauthorized:
"Truy cập chưa xác thực, vui lòng nhập mã truy cập trong trang cài đặt.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} tin nhắn`,
},
Chat: {
SubTitle: (count: number) => `${count} tin nhắn với ChatGPT`,
Actions: {
ChatList: "Xem danh sách chat",
CompressedHistory: "Nén tin nhắn trong quá khứ",
Export: "Xuất tất cả tin nhắn dưới dạng Markdown",
Copy: "Sao chép",
Stop: "Dừng",
Retry: "Thử lại",
Delete: "Xóa",
},
Rename: "Đổi tên",
Typing: "Đang nhập…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} để gửi`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter để xuống dòng";
}
return inputHints + ", / để tìm kiếm mẫu gợi ý";
},
Send: "Gửi",
Config: {
Reset: "Khôi phục cài đặt gốc",
SaveAs: "Lưu dưới dạng Mẫu",
},
},
Export: {
Title: "Tất cả tin nhắn",
Copy: "Sao chép tất cả",
Download: "Tải xuống",
MessageFromYou: "Tin nhắn của bạn",
MessageFromChatGPT: "Tin nhắn từ ChatGPT",
},
Memory: {
Title: "Lịch sử tin nhắn",
EmptyContent: "Chưa có tin nhắn",
Send: "Gửi tin nhắn trong quá khứ",
Copy: "Sao chép tin nhắn trong quá khứ",
Reset: "Đặt lại phiên",
ResetConfirm:
"Đặt lại sẽ xóa toàn bộ lịch sử trò chuyện hiện tại và bộ nhớ. Bạn có chắc chắn muốn đặt lại không?",
},
Home: {
NewChat: "Cuộc trò chuyện mới",
DeleteChat: "Xác nhận xóa các cuộc trò chuyện đã chọn?",
DeleteToast: "Đã xóa cuộc trò chuyện",
Revert: "Khôi phục",
},
Settings: {
Title: "Cài đặt",
SubTitle: "Tất cả cài đặt",
Actions: {
ClearAll: "Xóa toàn bộ dữ liệu",
ResetAll: "Khôi phục cài đặt gốc",
Close: "Đóng",
ConfirmResetAll: "Bạn chắc chắn muốn thiết lập lại tất cả cài đặt?",
ConfirmClearAll: "Bạn chắc chắn muốn thiết lập lại tất cả dữ liệu?",
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tất cả ngôn ngữ",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
vi: "Tiếng Việt",
ru: "Русский",
cs: "Čeština",
},
},
Avatar: "Ảnh đại diện",
FontSize: {
Title: "Font chữ",
SubTitle: "Thay đổi font chữ của nội dung trò chuyện",
},
Update: {
Version: (x: string) => `Phiên bản: ${x}`,
IsLatest: "Phiên bản mới nhất",
CheckUpdate: "Kiểm tra bản cập nhật",
IsChecking: "Kiểm tra bản cập nhật...",
FoundUpdate: (x: string) => `Phát hiện phiên bản mới: ${x}`,
GoToUpdate: "Cập nhật",
},
SendKey: "Phím gửi",
Theme: "Theme",
TightBorder: "Chế độ không viền",
SendPreviewBubble: {
Title: "Gửi bong bóng xem trước",
SubTitle: "Xem trước nội dung markdown bằng bong bóng",
},
Mask: {
Title: "Mask Splash Screen",
SubTitle: "Chớp màn hình khi bắt đầu cuộc trò chuyện mới",
},
Prompt: {
Disable: {
Title: "Vô hiệu hóa chức năng tự động hoàn thành",
SubTitle: "Nhập / để kích hoạt chức năng tự động hoàn thành",
},
List: "Danh sách mẫu gợi ý",
ListCount: (builtin: number, custom: number) =>
`${builtin} có sẵn, ${custom} do người dùng xác định`,
Edit: "Chỉnh sửa",
Modal: {
Title: "Danh sách mẫu gợi ý",
Add: "Thêm",
Search: "Tìm kiếm mẫu",
},
EditModal: {
Title: "Chỉnh sửa mẫu",
},
},
HistoryCount: {
Title: "Số lượng tin nhắn đính kèm",
SubTitle: "Số lượng tin nhắn trong quá khứ được gửi kèm theo mỗi yêu cầu",
},
CompressThreshold: {
Title: "Ngưỡng nén lịch sử tin nhắn",
SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng",
},
Token: {
Title: "API Key",
SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Hạn mức tài khoản",
SubTitle(used: any, total: any) {
return `Đã sử dụng $${used} trong tháng này, hạn mức $${total}`;
},
IsChecking: "Đang kiểm tra...",
Check: "Kiểm tra",
NoAccess: "Nhập API Key để kiểm tra hạn mức",
},
AccessCode: {
Title: "Mã truy cập",
SubTitle: "Đã bật kiểm soát truy cập",
Placeholder: "Nhập mã truy cập",
},
Model: "Mô hình",
Temperature: {
Title: "Tính ngẫu nhiên (temperature)",
SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên",
},
MaxTokens: {
Title: "Giới hạn số lượng token (max_tokens)",
SubTitle: "Số lượng token tối đa được sử dụng trong mỗi lần tương tác",
},
PresencePenlty: {
Title: "Chủ đề mới (presence_penalty)",
SubTitle: "Giá trị càng lớn tăng khả năng mở rộng sang các chủ đề mới",
},
},
Store: {
DefaultTopic: "Cuộc trò chuyện mới",
BotHello: "Xin chào! Mình có thể giúp gì cho bạn?",
Error: "Có lỗi xảy ra, vui lòng thử lại sau.",
Prompt: {
History: (content: string) =>
"Tóm tắt ngắn gọn cuộc trò chuyện giữa người dùng và AI: " + content,
Topic:
"Sử dụng 4 đến 5 từ tóm tắt cuộc trò chuyện này mà không có phần mở đầu, dấu chấm câu, dấu ngoặc kép, dấu chấm, ký hiệu hoặc văn bản bổ sung nào. Loại bỏ các dấu ngoặc kép kèm theo.",
Summarize:
"Tóm tắt cuộc trò chuyện này một cách ngắn gọn trong 200 từ hoặc ít hơn để sử dụng làm gợi ý cho ngữ cảnh tiếp theo.",
},
},
Copy: {
Success: "Sao chép vào bộ nhớ tạm",
Failed:
"Sao chép không thành công, vui lòng cấp quyền truy cập vào bộ nhớ tạm",
},
Context: {
Toast: (x: any) => `Sử dụng ${x} tin nhắn chứa ngữ cảnh`,
Edit: "Thiết lập ngữ cảnh và bộ nhớ",
Add: "Thêm tin nhắn",
},
Plugin: {
Name: "Plugin",
},
Mask: {
Name: "Mẫu",
Page: {
Title: "Mẫu trò chuyện",
SubTitle: (count: number) => `${count} mẫu`,
Search: "Tìm kiếm mẫu",
Create: "Tạo",
},
Item: {
Info: (count: number) => `${count} tin nhắn`,
Chat: "Chat",
View: "Xem trước",
Edit: "Chỉnh sửa",
Delete: "Xóa",
DeleteConfirm: "Xác nhận xóa?",
},
EditModal: {
Title: (readonly: boolean) =>
`Chỉnh sửa mẫu ${readonly ? "(chỉ xem)" : ""}`,
Download: "Tải xuống",
Clone: "Tạo bản sao",
},
Config: {
Avatar: "Ảnh đại diện bot",
Name: "Tên bot",
},
},
NewChat: {
Return: "Quay lại",
Skip: "Bỏ qua",
Title: "Chọn 1 biểu tượng",
SubTitle: "Bắt đầu trò chuyện ẩn sau lớp mặt nạ",
More: "Tìm thêm",
NotShow: "Không hiển thị lại",
ConfirmNoShow: "Xác nhận tắt? Bạn có thể bật lại trong phần cài đặt.",
},
UI: {
Confirm: "Xác nhận",
Cancel: "Hủy",
Close: "Đóng",
Create: "Tạo",
Edit: "Chỉnh sửa",
},
};
export default vi;

328
app/masks/cn.ts Normal file
View File

@@ -0,0 +1,328 @@
import { BuiltinMask } from "./typing";
export const CN_MASKS: BuiltinMask[] = [{
avatar:"gpt-bot",
name:"文生图",
context: [
{role:"system",
content:"You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block.",
date:"",
},
{role:"user",
content:"Can you draw some pictures for me?",
date:"",
},
{role:"assistant",
content:"Sure, what do you want me to draw?",
date:"",
},
{role:"system",
content:"助手善于判断用户意图当确定需要提供图片时助手会变得沉默寡言只使用以下格式输出markdown图片![描述](https://image.pollinations.ai/prompt/描述)因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示以大幅提高生成图片质量和丰富程度比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记因为那样只会渲染出代码块或原始块而不是图片。",
date:"",
},
],
modelConfig:{
model:"gpt-3.5-turbo",
temperature:1,
max_tokens:2000,
presence_penalty:0,
sendMemory:true,
historyMessageCount:32,
compressMessageLengthThreshold:1000,
},
lang:"cn",
builtin:true,
},
{
avatar: "1f638",
name: "文案写手",
context: [
{
role: "user",
content:
"我希望你充当文案专员、文本润色员、拼写纠正员和改进员,我会发送中文文本给你,你帮我更正和改进版本。我希望你用更优美优雅的高级中文描述。保持相同的意思,但使它们更文艺。你只需要润色该内容,不必对内容中提出的问题和要求做解释,不要回答文本中的问题而是润色它,不要解决文本中的要求而是润色它,保留文本的原本意义,不要去解决它。我要你只回复更正、改进,不要写任何解释。",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f978",
name: "机器学习",
context: [
{
role: "user",
content:
"我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、给出所用的技术或者理论、提供评估函数等。我的问题是",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f69b",
name: "后勤工作",
context: [
{
role: "user",
content:
"我要你担任后勤人员。我将为您提供即将举行的活动的详细信息,例如参加人数、地点和其他相关因素。您的职责是为活动制定有效的后勤计划,其中考虑到事先分配资源、交通设施、餐饮服务等。您还应该牢记潜在的安全问题,并制定策略来降低与大型活动相关的风险。我的第一个请求是",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f469-200d-1f4bc",
name: "职业顾问",
context: [
{
role: "user",
content:
"我想让你担任职业顾问。我将为您提供一个在职业生涯中寻求指导的人,您的任务是帮助他们根据自己的技能、兴趣和经验确定最适合的职业。您还应该对可用的各种选项进行研究,解释不同行业的就业市场趋势,并就哪些资格对追求特定领域有益提出建议。我的第一个请求是",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f9d1-200d-1f3eb",
name: "英专写手",
context: [
{
role: "user",
content:
"我想让你充当英文翻译员、拼写纠正员和改进员。我会用任何语言与你交谈,你会检测语言,翻译它并用我的文本的更正和改进版本用英文回答。我希望你用更优美优雅的高级英语单词和句子替换我简化的 A0 级单词和句子。保持相同的意思,但使它们更文艺。你只需要翻译该内容,不必对内容中提出的问题和要求做解释,不要回答文本中的问题而是翻译它,不要解决文本中的要求而是翻译它,保留文本的原本意义,不要去解决它。我要你只回复更正、改进,不要写任何解释。我的第一句话是:",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f4da",
name: "语言检测器",
context: [
{
role: "user",
content:
"我希望你充当语言检测器。我会用任何语言输入一个句子,你会回答我,我写的句子在你是用哪种语言写的。不要写任何解释或其他文字,只需回复语言名称即可。我的第一句话是:",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f4d5",
name: "小红书写手",
context: [
{
role: "user",
content:
"你的任务是以小红书博主的文章结构,以我给出的主题写一篇帖子推荐。你的回答应包括使用表情符号来增加趣味和互动,以及与每个段落相匹配的图片。请以一个引人入胜的介绍开始,为你的推荐设置基调。然后,提供至少三个与主题相关的段落,突出它们的独特特点和吸引力。在你的写作中使用表情符号,使它更加引人入胜和有趣。对于每个段落,请提供一个与描述内容相匹配的图片。这些图片应该视觉上吸引人,并帮助你的描述更加生动形象。我给出的主题是:",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 0,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f469-200d-2695-fe0f",
name: "心理医生",
context: [
{
role: "user",
content:
"现在你是世界上最优秀的心理咨询师,你具备以下能力和履历: 专业知识:你应该拥有心理学领域的扎实知识,包括理论体系、治疗方法、心理测量等,以便为你的咨询者提供专业、有针对性的建议。 临床经验:你应该具备丰富的临床经验,能够处理各种心理问题,从而帮助你的咨询者找到合适的解决方案。 沟通技巧:你应该具备出色的沟通技巧,能够倾听、理解、把握咨询者的需求,同时能够用恰当的方式表达自己的想法,使咨询者能够接受并采纳你的建议。 同理心:你应该具备强烈的同理心,能够站在咨询者的角度去理解他们的痛苦和困惑,从而给予他们真诚的关怀和支持。 持续学习:你应该有持续学习的意愿,跟进心理学领域的最新研究和发展,不断更新自己的知识和技能,以便更好地服务于你的咨询者。 良好的职业道德:你应该具备良好的职业道德,尊重咨询者的隐私,遵循专业规范,确保咨询过程的安全和有效性。 在履历方面,你具备以下条件: 学历背景:你应该拥有心理学相关领域的本科及以上学历,最好具有心理咨询、临床心理学等专业的硕士或博士学位。 专业资格:你应该具备相关的心理咨询师执业资格证书,如注册心理师、临床心理师等。 工作经历:你应该拥有多年的心理咨询工作经验,最好在不同类型的心理咨询机构、诊所或医院积累了丰富的实践经验。",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f4b8",
name: "创业点子王",
context: [
{
role: "user",
content:
"在企业 B2B SaaS 领域中想 3 个创业点子。创业点子应该有一个强大而引人注目的使命,并以某种方式使用人工智能。避免使用加密货币或区块链。创业点子应该有一个很酷很有趣的名字。这些想法应该足够引人注目,这样投资者才会兴奋地投资数百万美元。",
date: "",
},
{
role: "assistant",
content:
"1. VantageAI - 一个基于人工智能的企业智能平台,帮助中小企业利用数据分析和机器学习来优化其业务流程,提高生产效率并实现可持续发展。\n\n2. HoloLogix - 一个全新的日志处理平台,使用人工智能技术来分析和识别分散的数据源。它可以精确地分析和解释您的日志,从而与整个组织共享并提高数据可视化和分析效率。\n\n3. SmartPath - 一种基于数据的销售和营销自动化平台可以理解买家的购买行为并根据这些行为提供最佳的营销计划和过程。该平台可以与Salesforce等其他外部工具整合以更好地掌握您的客户关系管理。",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "270d-fe0f",
name: "互联网写手",
context: [
{
role: "user",
content:
"你是一个专业的互联网文章作者,擅长互联网技术介绍、互联网商业、技术应用等方面的写作。\n接下来你要根据用户给你的主题拓展生成用户想要的文字内容内容可能是一篇文章、一个开头、一段介绍文字、文章总结、文章结尾等等。\n要求语言通俗易懂、幽默有趣并且要以第一人称的口吻。",
date: "",
},
{
role: "assistant",
content:
"好的,我是一名专业的互联网文章作者,非常擅长撰写有关互联网技术介绍、商业应用和技术趋势等方面的内容。只需提供您感兴趣的主题,我就可以为您撰写出一篇生动有趣、通俗易懂的文章。如果遇到不认识的技术名词,我会尽力查询相关知识并告诉您。让我们开始吧!",
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
{
avatar: "1f63e",
name: "心灵导师",
context: [
{
role: "user",
content:
"从现在起你是一个充满哲学思维的心灵导师,当我每次输入一个疑问时你需要用一句富有哲理的名言警句来回答我,并且表明作者和出处\n\n\n要求字数不少于15个字不超过30字每次只返回一句且不输出额外的其他信息你需要使用中文和英文双语输出\n\n\n当你准备好的时候只需要回复“我已经准备好了”不需要输出任何其他内容",
date: "",
},
{
role: "assistant",
content: "我已经准备好了。",
date: "",
},
{
role: "user",
content: "我作业写不完怎么办",
date: "",
},
{
role: "assistant",
content:
'“行动是治愈恐惧的良药。” - 威廉·詹姆斯\n"Action is the antidote to fear." - William James',
date: "",
},
{
role: "user",
content: "论文被拒了怎么办",
date: "",
},
{
role: "assistant",
content:
'"失败是成功之母。" - 俗语\n"Failure is the mother of success." - Chinese proverb',
date: "",
},
],
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: false,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "cn",
builtin: true,
},
];

44
app/masks/en.ts Normal file
View File

@@ -0,0 +1,44 @@
import { BuiltinMask } from "./typing";
export const EN_MASKS: BuiltinMask[] = [
{
avatar: "1f916",
name: "Prompt Improvement",
context: [
{
role: "user",
content:
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
date: "",
},
{
role: "assistant",
content: "Shall we begin?",
date: "",
},
{
role: "user",
content:
"My promptI want you to act like Harry Potter from Harry Potter Series. I want you to respond and answer like Harry Potter using the tone, manner and vocabulary Harry Potter would use.",
date: "",
},
{
role: "assistant",
content:
'Revised Prompt:\n\n\nWrite a short story or dialogue in which Harry Potter interacts with someone (a friend or foe) using the tone, manner and vocabulary characteristic of his character in the Harry Potter series.\n\n\nSuggestions:\n\n\n\nInclude specific references to Hogwarts, magic spells, and magical creatures to create an authentic Harry Potter experience.\n\nEnsure that Harry\'s personality traits, such as his bravery, loyalty, and determination, are prominently displayed in the story or dialogue.\n\nIncorporate some humor or wit into the story, as these elements are often present in the Harry Potter series.\n\n\nQuestions:\n\n\n\nWho will be the other character interacting with Harry?\n\nWhat is the main conflict or goal of the story/dialogue?\n\nWhat setting or location would you like the story/dialogue to take place in?\n\n\nRemember, your options are:\nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session',
date: "",
},
],
modelConfig: {
model: "gpt-4",
temperature: 0.5,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
lang: "en",
builtin: true,
},
];

26
app/masks/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Mask } from "../store/mask";
import { CN_MASKS } from "./cn";
import { EN_MASKS } from "./en";
import { type BuiltinMask } from "./typing";
export { type BuiltinMask } from "./typing";
export const BUILTIN_MASK_ID = 100000;
export const BUILTIN_MASK_STORE = {
buildinId: BUILTIN_MASK_ID,
masks: {} as Record<number, Mask>,
get(id?: number) {
if (!id) return undefined;
return this.masks[id] as Mask | undefined;
},
add(m: BuiltinMask) {
const mask = { ...m, id: this.buildinId++ };
this.masks[mask.id] = mask;
return mask;
},
};
export const BUILTIN_MASKS: Mask[] = [...CN_MASKS, ...EN_MASKS].map((m) =>
BUILTIN_MASK_STORE.add(m),
);

3
app/masks/typing.ts Normal file
View File

@@ -0,0 +1,3 @@
import { type Mask } from "../store/mask";
export type BuiltinMask = Omit<Mask, "id">;

View File

@@ -1,14 +1,16 @@
import { Analytics } from "@vercel/analytics/react";
import "array.prototype.at";
import { Home } from "./components/home";
export default function App() {
import { getServerSideConfig } from "./config/server";
const serverConfig = getServerSideConfig();
export default async function App() {
return (
<>
<Home />
<Analytics />
{serverConfig?.isVercel && <Analytics />}
</>
);
}

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,15 +1,22 @@
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
import Locale from "./locales";
import type { ChatRequest, ChatResponse } from "./api/openai/typing";
import {
Message,
ModelConfig,
ModelType,
useAccessStore,
useAppConfig,
useChatStore,
} from "./store";
import { showToast } from "./components/ui-lib";
import { ACCESS_CODE_PREFIX } from "./constant";
const TIME_OUT_MS = 30000;
const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
options?: {
filterBot?: boolean;
stream?: boolean;
overrideModel?: ModelType;
},
): ChatRequest => {
let sendMessages = messages.map((v) => ({
@@ -17,53 +24,71 @@ const makeRequestParam = (
content: v.content,
}));
if (options?.filterBot) {
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
};
// override model config
if (options?.overrideModel) {
modelConfig.model = options.overrideModel;
}
return {
model: "gpt-3.5-turbo",
messages: sendMessages,
stream: options?.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
};
};
function getHeaders() {
export function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {};
if (accessStore.enabledAccessControl()) {
headers["access-code"] = accessStore.accessCode;
}
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
const validString = (x: string) => x && x.length > 0;
if (accessStore.token && accessStore.token.length > 0) {
headers["token"] = accessStore.token;
// use user's api key first
if (validString(accessStore.token)) {
headers.Authorization = makeBearer(accessStore.token);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers.Authorization = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
return headers;
}
export function requestOpenaiClient(path: string) {
const openaiUrl = useAccessStore.getState().openaiUrl;
return (body: any, method = "POST") =>
fetch("/api/openai", {
fetch(openaiUrl + path, {
method,
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
path,
...getHeaders(),
},
body: body && JSON.stringify(body),
headers: getHeaders(),
});
}
export async function requestChat(messages: Message[]) {
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
export async function requestChat(
messages: Message[],
options?: {
model?: ModelType;
},
) {
const req: ChatRequest = makeRequestParam(messages, {
overrideModel: options?.model,
});
const res = await requestOpenaiClient("v1/chat/completions")(req);
try {
const response = (await res.json()) as ChatReponse;
const response = (await res.json()) as ChatResponse;
return response;
} catch (error) {
console.error("[Request Chat] ", error, res.body);
@@ -76,74 +101,82 @@ export async function requestUsage() {
.getDate()
.toString()
.padStart(2, "0")}`;
const ONE_DAY = 24 * 60 * 60 * 1000;
const now = new Date(Date.now() + ONE_DAY);
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth);
const endDate = formatDate(now);
const res = await requestOpenaiClient(
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
)(null, "GET");
const endDate = formatDate(new Date(Date.now() + ONE_DAY));
try {
const response = (await res.json()) as {
total_usage: number;
error?: {
type: string;
message: string;
};
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;
};
};
if (response.error && response.error.type) {
showToast(response.error.message);
return;
}
const total = (await subs.json()) as {
hard_limit_usd?: number;
};
if (response.total_usage) {
response.total_usage = Math.round(response.total_usage) / 100;
}
return response.total_usage;
} catch (error) {
console.error("[Request usage] ", error, res.body);
if (response.error && response.error.type) {
showToast(response.error.message);
return;
}
if (response.total_usage) {
response.total_usage = Math.round(response.total_usage) / 100;
}
if (total.hard_limit_usd) {
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
}
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
};
}
export async function requestChatStream(
messages: Message[],
options?: {
filterBot?: boolean;
modelConfig?: ModelConfig;
overrideModel?: ModelType;
onMessage: (message: string, done: boolean) => void;
onError: (error: Error) => void;
onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void;
},
) {
const req = makeRequestParam(messages, {
stream: true,
filterBot: options?.filterBot,
overrideModel: options?.overrideModel,
});
// valid and assign model config
if (options?.modelConfig) {
Object.assign(req, filterConfig(options.modelConfig));
}
console.log("[Request] ", req);
const controller = new AbortController();
const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);
try {
const res = await fetch("/api/chat-stream", {
const openaiUrl = useAccessStore.getState().openaiUrl;
const res = await fetch(openaiUrl + "v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
path: "v1/chat/completions",
...getHeaders(),
},
body: JSON.stringify(req),
signal: controller.signal,
});
clearTimeout(reqTimeoutId);
let responseText = "";
@@ -160,14 +193,18 @@ export async function requestChatStream(
options?.onController?.(controller);
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
const text = decoder.decode(content?.value);
if (!content || !content.value) {
break;
}
const text = decoder.decode(content.value, { stream: true });
responseText += text;
const done = !content || content.done;
const done = content.done;
options?.onMessage(responseText, false);
if (done) {
@@ -177,12 +214,11 @@ export async function requestChatStream(
finish();
} else if (res.status === 401) {
console.error("Anauthorized");
responseText = Locale.Error.Unauthorized;
finish();
console.error("Unauthorized");
options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"));
options?.onError(new Error("Stream Error"), res.status);
}
} catch (err) {
console.error("NetWork Error", err);
@@ -190,7 +226,13 @@ export async function requestChatStream(
}
}
export async function requestWithPrompt(messages: Message[], prompt: string) {
export async function requestWithPrompt(
messages: Message[],
prompt: string,
options?: {
model?: ModelType;
},
) {
messages = messages.concat([
{
role: "user",
@@ -199,7 +241,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
},
]);
const res = await requestChat(messages);
const res = await requestChat(messages, options);
return res?.choices?.at(0)?.message?.content ?? "";
}
@@ -210,23 +252,30 @@ export const ControllerPool = {
addController(
sessionIndex: number,
messageIndex: number,
messageId: number,
controller: AbortController,
) {
const key = this.key(sessionIndex, messageIndex);
const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageIndex: number) {
const key = this.key(sessionIndex, messageIndex);
stop(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
const controller = this.controllers[key];
console.log(controller);
controller?.abort();
},
remove(sessionIndex: number, messageIndex: number) {
const key = this.key(sessionIndex, messageIndex);
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
},
hasPending() {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
},

View File

@@ -1,36 +1,93 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
import { StoreKey } from "../constant";
import { getHeaders } from "../requests";
import { BOT_HELLO } from "./chat";
import { ALL_MODELS } from "./config";
export interface AccessControlStore {
accessCode: string;
token: string;
needCode: boolean;
hideUserApiKey: boolean;
openaiUrl: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
isAuthorized: () => boolean;
fetch: () => void;
}
export const ACCESS_KEY = "access-control";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
openaiUrl: "/api/openai/",
enabledAccessControl() {
return queryMeta("access") === "enabled";
get().fetch();
return get().needCode;
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
set(() => ({ accessCode: code }));
},
updateToken(token: string) {
set((state) => ({ token }));
set(() => ({ token }));
},
isAuthorized() {
get().fetch();
// has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !get().enabledAccessControl()
);
},
fetch() {
if (fetchState > 0) return;
fetchState = 1;
fetch("/api/config", {
method: "post",
body: null,
headers: {
...getHeaders(),
},
})
.then((res) => res.json())
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
if (!res.enableGPT4) {
ALL_MODELS.forEach((model) => {
if (model.name.startsWith("gpt-4")) {
(model as any).available = false;
}
});
}
if ((res as any).botHello) {
BOT_HELLO.content = (res as any).botHello;
}
})
.catch(() => {
console.error("[Config] failed to fetch config");
})
.finally(() => {
fetchState = 2;
});
},
}),
{
name: ACCESS_KEY,
name: StoreKey.Access,
version: 1,
}
)
},
),
);

View File

@@ -1,517 +0,0 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { trimTopic } from "../utils";
import Locale from "../locales";
export type Message = ChatCompletionResponseMessage & {
date: string;
streaming?: boolean;
};
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
export interface ChatConfig {
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;
temperature: number;
max_tokens: number;
presence_penalty: number;
};
}
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: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-3.5-turbo",
available: true,
},
{
name: "gpt-3.5-turbo-0301",
available: true,
},
];
export function isValidModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available);
}
export function isValidNumber(x: number, min: number, max: number) {
return typeof x === "number" && x <= max && x >= min;
}
export function filterConfig(oldConfig: ModelConfig): Partial<ModelConfig> {
const config = Object.assign({}, oldConfig);
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, 2);
},
};
Object.keys(validator).forEach((k) => {
const key = k as keyof ModelConfig;
if (!validator[key](config[key])) {
delete config[key];
}
});
return config;
}
const DEFAULT_CONFIG: ChatConfig = {
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
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",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
},
};
export interface ChatStat {
tokenCount: number;
wordCount: number;
charCount: number;
}
export interface ChatSession {
id: number;
topic: string;
memoryPrompt: string;
context: Message[];
messages: Message[];
stat: ChatStat;
lastUpdate: string;
lastSummarizeIndex: number;
}
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = {
role: "assistant",
content: Locale.Store.BotHello,
date: "",
};
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
return {
id: Date.now(),
topic: DEFAULT_TOPIC,
memoryPrompt: "",
context: [],
messages: [],
stat: {
tokenCount: 0,
wordCount: 0,
charCount: 0,
},
lastUpdate: createDate,
lastSummarizeIndex: 0,
};
}
interface ChatStore {
config: ChatConfig;
sessions: ChatSession[];
currentSessionIndex: number;
clearSessions: () => void;
removeSession: (index: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
) => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
getConfig: () => ChatConfig;
resetConfig: () => void;
updateConfig: (updater: (config: ChatConfig) => void) => void;
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>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
config: {
...DEFAULT_CONFIG,
},
clearSessions() {
set(() => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
}));
},
resetConfig() {
set(() => ({ config: { ...DEFAULT_CONFIG } }));
},
getConfig() {
return get().config;
},
updateConfig(updater) {
const config = get().config;
updater(config);
set(() => ({ config }));
},
selectSession(index: number) {
set({
currentSessionIndex: index,
});
},
removeSession(index: number) {
set((state) => {
let nextIndex = state.currentSessionIndex;
const sessions = state.sessions;
if (sessions.length === 1) {
return {
currentSessionIndex: 0,
sessions: [createEmptySession()],
};
}
sessions.splice(index, 1);
if (nextIndex === index) {
nextIndex -= 1;
}
return {
currentSessionIndex: nextIndex,
sessions,
};
});
},
newSession() {
set((state) => ({
currentSessionIndex: 0,
sessions: [createEmptySession()].concat(state.sessions),
}));
},
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
if (index < 0 || index >= sessions.length) {
index = Math.min(sessions.length - 1, Math.max(0, index));
set(() => ({ currentSessionIndex: index }));
}
const session = sessions[index];
return session;
},
onNewMessage(message) {
get().updateCurrentSession((session) => {
session.lastUpdate = new Date().toLocaleString();
});
get().updateStat(message);
get().summarizeSession();
},
async onUserInput(content) {
const userMessage: Message = {
role: "user",
content,
date: new Date().toLocaleString(),
};
const botMessage: Message = {
content: "",
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) => {
session.messages.push(userMessage);
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, messageIndex);
} else {
botMessage.content = content;
set(() => ({}));
}
},
onError(error) {
botMessage.content += "\n\n" + Locale.Store.Error;
botMessage.streaming = false;
set(() => ({}));
ControllerPool.remove(sessionIndex, messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
messageIndex,
controller,
);
},
filterBot: !get().config.sendBotMessages,
modelConfig: get().config.modelConfig,
});
},
getMemoryPrompt() {
const session = get().currentSession();
return {
role: "system",
content: Locale.Store.Prompt.History(session.memoryPrompt),
date: "",
} as Message;
},
getMessagesWithMemory() {
const session = get().currentSession();
const config = get().config;
const n = session.messages.length;
const context = session.context.slice();
if (session.memoryPrompt && session.memoryPrompt.length > 0) {
const memoryPrompt = get().getMemoryPrompt();
context.push(memoryPrompt);
}
const recentMessages = context.concat(
session.messages.slice(Math.max(0, n - config.historyMessageCount)),
);
return recentMessages;
},
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
const messages = session?.messages;
updater(messages?.at(messageIndex));
set(() => ({ sessions }));
},
summarizeSession() {
const session = get().currentSession();
// 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)),
);
},
);
}
const config = get().config;
let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex,
);
const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
Math.max(0, n - config.historyMessageCount),
);
}
// add memory prompt
toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
const lastSummarizeIndex = session.messages.length;
console.log(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
config.compressMessageLengthThreshold,
);
if (historyMsgLength > config.compressMessageLengthThreshold) {
requestChatStream(
toBeSummarizedMsgs.concat({
role: "system",
content: Locale.Store.Prompt.Summarize,
date: "",
}),
{
filterBot: false,
onMessage(message, done) {
session.memoryPrompt = message;
if (done) {
console.log("[Memory] ", session.memoryPrompt);
session.lastSummarizeIndex = lastSummarizeIndex;
}
},
onError(error) {
console.error("[Summarize] ", error);
},
},
);
}
},
updateStat(message) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
set(() => ({ sessions }));
},
clearAllData() {
if (confirm(Locale.Store.ConfirmClearAll)) {
localStorage.clear();
location.reload();
}
},
}),
{
name: LOCAL_KEY,
version: 1.1,
migrate(persistedState, version) {
const state = persistedState as ChatStore;
if (version === 1) {
state.sessions.forEach((s) => (s.context = []));
}
return state;
},
},
),
);

524
app/store/chat.ts Normal file
View File

@@ -0,0 +1,524 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { type ChatCompletionResponseMessage } from "openai";
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
import { trimTopic } from "../utils";
import Locale from "../locales";
import { showToast } from "../components/ui-lib";
import { ModelType } from "./config";
import { createEmptyMask, Mask } from "./mask";
import { StoreKey } from "../constant";
export type Message = ChatCompletionResponseMessage & {
date: string;
streaming?: boolean;
isError?: boolean;
id?: number;
model?: ModelType;
};
export function createMessage(override: Partial<Message>): Message {
return {
id: Date.now(),
date: new Date().toLocaleString(),
role: "user",
content: "",
...override,
};
}
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
export interface ChatStat {
tokenCount: number;
wordCount: number;
charCount: number;
}
export interface ChatSession {
id: number;
topic: string;
memoryPrompt: string;
messages: Message[];
stat: ChatStat;
lastUpdate: number;
lastSummarizeIndex: number;
mask: Mask;
}
export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = createMessage({
role: "assistant",
content: Locale.Store.BotHello,
});
function createEmptySession(): ChatSession {
return {
id: Date.now() + Math.random(),
topic: DEFAULT_TOPIC,
memoryPrompt: "",
messages: [],
stat: {
tokenCount: 0,
wordCount: 0,
charCount: 0,
},
lastUpdate: Date.now(),
lastSummarizeIndex: 0,
mask: createEmptyMask(),
};
}
interface ChatStore {
sessions: ChatSession[];
currentSessionIndex: number;
globalId: number;
clearSessions: () => void;
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
) => void;
resetSession: () => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
clearAllData: () => void;
}
function countMessages(msgs: Message[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
}
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
globalId: 0,
clearSessions() {
set(() => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
}));
},
selectSession(index: number) {
set({
currentSessionIndex: index,
});
},
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(mask) {
const session = createEmptySession();
set(() => ({ globalId: get().globalId + 1 }));
session.id = get().globalId;
if (mask) {
session.mask = { ...mask };
session.topic = mask.name;
}
set((state) => ({
currentSessionIndex: 0,
sessions: [session].concat(state.sessions),
}));
},
deleteSession(index) {
const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index);
if (!deletedSession) return;
const sessions = get().sessions.slice();
sessions.splice(index, 1);
const currentIndex = get().currentSessionIndex;
let nextIndex = Math.min(
currentIndex - Number(index < currentIndex),
sessions.length - 1,
);
if (deletingLastSession) {
nextIndex = 0;
sessions.push(createEmptySession());
}
// for undo delete action
const restoreState = {
currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(),
};
set(() => ({
currentSessionIndex: nextIndex,
sessions,
}));
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set(() => restoreState);
},
},
5000,
);
},
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
if (index < 0 || index >= sessions.length) {
index = Math.min(sessions.length - 1, Math.max(0, index));
set(() => ({ currentSessionIndex: index }));
}
const session = sessions[index];
return session;
},
onNewMessage(message) {
get().updateCurrentSession((session) => {
session.lastUpdate = Date.now();
});
get().updateStat(message);
get().summarizeSession();
},
async onUserInput(content) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const userMessage: Message = createMessage({
role: "user",
content,
});
const botMessage: Message = createMessage({
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
model: modelConfig.model,
});
const systemInfo = createMessage({
role: "system",
content: `IMPRTANT: You are a virtual assistant powered by the ${
modelConfig.model
} model, now time is ${new Date().toLocaleString()}}`,
id: botMessage.id! + 1,
});
// get recent messages
const systemMessages = [systemInfo];
const recentMessages = get().getMessagesWithMemory();
const sendMessages = systemMessages.concat(
recentMessages.concat(userMessage),
);
const sessionIndex = get().currentSessionIndex;
const messageIndex = get().currentSession().messages.length + 1;
// save user's and bot's message
get().updateCurrentSession((session) => {
session.messages.push(userMessage);
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, statusCode) {
const isAborted = error.message.includes("aborted");
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else if (!isAborted) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
userMessage.isError = !isAborted;
botMessage.isError = !isAborted;
set(() => ({}));
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
botMessage.id ?? messageIndex,
controller,
);
},
modelConfig: { ...modelConfig },
});
},
getMemoryPrompt() {
const session = get().currentSession();
return {
role: "system",
content:
session.memoryPrompt.length > 0
? Locale.Store.Prompt.History(session.memoryPrompt)
: "",
date: "",
} as Message;
},
getMessagesWithMemory() {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length;
const context = session.mask.context.slice();
// long term memory
if (
modelConfig.sendMemory &&
session.memoryPrompt &&
session.memoryPrompt.length > 0
) {
const memoryPrompt = get().getMemoryPrompt();
context.push(memoryPrompt);
}
// get short term and unmemoried long term memory
const shortTermMemoryMessageIndex = Math.max(
0,
n - modelConfig.historyMessageCount,
);
const longTermMemoryMessageIndex = session.lastSummarizeIndex;
const oldestIndex = Math.max(
shortTermMemoryMessageIndex,
longTermMemoryMessageIndex,
);
const threshold = modelConfig.compressMessageLengthThreshold;
// get recent messages as many as possible
const reversedRecentMessages = [];
for (
let i = n - 1, count = 0;
i >= oldestIndex && count < threshold;
i -= 1
) {
const msg = messages[i];
if (!msg || msg.isError) continue;
count += msg.content.length;
reversedRecentMessages.push(msg);
}
// concat
const recentMessages = context.concat(reversedRecentMessages.reverse());
return recentMessages;
},
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void,
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
const messages = session?.messages;
updater(messages?.at(messageIndex));
set(() => ({ sessions }));
},
resetSession() {
get().updateCurrentSession((session) => {
session.messages = [];
session.memoryPrompt = "";
});
},
summarizeSession() {
const session = get().currentSession();
// remove error messages if any
const cleanMessages = session.messages.filter((msg) => !msg.isError);
// should summarize topic after chating more than 50 words
const SUMMARIZE_MIN_LEN = 50;
if (
session.topic === DEFAULT_TOPIC &&
countMessages(cleanMessages) >= SUMMARIZE_MIN_LEN
) {
requestWithPrompt(cleanMessages, Locale.Store.Prompt.Topic, {
model: "gpt-3.5-turbo",
}).then((res) => {
get().updateCurrentSession(
(session) =>
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
);
});
}
const modelConfig = session.mask.modelConfig;
let toBeSummarizedMsgs = cleanMessages.slice(
session.lastSummarizeIndex,
);
const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
Math.max(0, n - modelConfig.historyMessageCount),
);
}
// add memory prompt
toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
const lastSummarizeIndex = session.messages.length;
console.log(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
modelConfig.compressMessageLengthThreshold,
);
if (
historyMsgLength > modelConfig.compressMessageLengthThreshold &&
session.mask.modelConfig.sendMemory
) {
requestChatStream(
toBeSummarizedMsgs.concat({
role: "system",
content: Locale.Store.Prompt.Summarize,
date: "",
}),
{
overrideModel: "gpt-3.5-turbo",
onMessage(message, done) {
session.memoryPrompt = message;
if (done) {
console.log("[Memory] ", session.memoryPrompt);
session.lastSummarizeIndex = lastSummarizeIndex;
}
},
onError(error) {
console.error("[Summarize] ", error);
},
},
);
}
},
updateStat(message) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
set(() => ({ sessions }));
},
clearAllData() {
localStorage.clear();
location.reload();
},
}),
{
name: StoreKey.Chat,
version: 2,
migrate(persistedState, version) {
const state = persistedState as any;
const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
if (version < 2) {
newState.globalId = 0;
newState.sessions = [];
const oldSessions = state.sessions;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
newSession.mask.modelConfig.sendMemory = true;
newSession.mask.modelConfig.historyMessageCount = 4;
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
newState.sessions.push(newSession);
}
}
return newState;
},
},
),
);

168
app/store/config.ts Normal file
View File

@@ -0,0 +1,168 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
export const DEFAULT_CONFIG = {
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
sidebarWidth: 300,
disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
modelConfig: {
model: "gpt-3.5-turbo" as ModelType,
temperature: 0.5,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
};
export type ChatConfig = typeof DEFAULT_CONFIG;
export type ChatConfigStore = ChatConfig & {
reset: () => void;
update: (updater: (config: ChatConfig) => void) => void;
};
export type ModelConfig = ChatConfig["modelConfig"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
{
name: "gpt-4",
available: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-3.5-turbo",
available: true,
},
{
name: "gpt-3.5-turbo-0301",
available: true,
},
{
name: "qwen-v1", // 通义千问
available: false,
},
{
name: "ernie", // 文心一言
available: false,
},
{
name: "spark", // 讯飞星火
available: false,
},
{
name: "llama", // llama
available: false,
},
{
name: "chatglm", // chatglm-6b
available: false,
},
] as const;
export type ModelType = (typeof ALL_MODELS)[number]["name"];
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 limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available)
? name
: ALL_MODELS[4].name;
}
export const ModalConfigValidator = {
model(x: string) {
return limitModel(x) as ModelType;
},
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, 1, 1);
},
};
export const useAppConfig = create<ChatConfigStore>()(
persist(
(set, get) => ({
...DEFAULT_CONFIG,
reset() {
set(() => ({ ...DEFAULT_CONFIG }));
},
update(updater) {
const config = { ...get() };
updater(config);
set(() => config);
},
}),
{
name: StoreKey.Config,
version: 2,
migrate(persistedState, version) {
if (version === 2) return persistedState as any;
const state = persistedState as ChatConfig;
state.modelConfig.sendMemory = true;
state.modelConfig.historyMessageCount = 4;
state.modelConfig.compressMessageLengthThreshold = 1000;
state.dontShowMaskSplashScreen = false;
return state;
},
},
),
);

View File

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

100
app/store/mask.ts Normal file
View File

@@ -0,0 +1,100 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, Message } from "./chat";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { StoreKey } from "../constant";
export type Mask = {
id: number;
avatar: string;
name: string;
context: Message[];
modelConfig: ModelConfig;
lang: Lang;
builtin: boolean;
};
export const DEFAULT_MASK_STATE = {
masks: {} as Record<number, Mask>,
globalMaskId: 0,
};
export type MaskState = typeof DEFAULT_MASK_STATE;
type MaskStore = MaskState & {
create: (mask?: Partial<Mask>) => Mask;
update: (id: number, updater: (mask: Mask) => void) => void;
delete: (id: number) => void;
search: (text: string) => Mask[];
get: (id?: number) => Mask | null;
getAll: () => Mask[];
};
export const DEFAULT_MASK_ID = 1145141919810;
export const DEFAULT_MASK_AVATAR = "gpt-bot";
export const createEmptyMask = () =>
({
id: DEFAULT_MASK_ID,
avatar: DEFAULT_MASK_AVATAR,
name: DEFAULT_TOPIC,
context: [],
modelConfig: { ...useAppConfig.getState().modelConfig },
lang: getLang(),
builtin: false,
} as Mask);
export const useMaskStore = create<MaskStore>()(
persist(
(set, get) => ({
...DEFAULT_MASK_STATE,
create(mask) {
set(() => ({ globalMaskId: get().globalMaskId + 1 }));
const id = get().globalMaskId;
const masks = get().masks;
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
set(() => ({ masks }));
return masks[id];
},
update(id, updater) {
const masks = get().masks;
const mask = masks[id];
if (!mask) return;
const updateMask = { ...mask };
updater(updateMask);
masks[id] = updateMask;
set(() => ({ masks }));
},
delete(id) {
const masks = get().masks;
delete masks[id];
set(() => ({ masks }));
},
get(id) {
return get().masks[id ?? 1145141919810];
},
getAll() {
const userMasks = Object.values(get().masks).sort(
(a, b) => b.id - a.id,
);
return userMasks.concat(BUILTIN_MASKS);
},
search(text) {
return Object.values(get().masks);
},
}),
{
name: StoreKey.Mask,
version: 2,
},
),
);

View File

@@ -1,63 +1,78 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
import { getLang } from "../locales";
import { StoreKey } from "../constant";
export interface Prompt {
id?: number;
isUser?: boolean;
title: string;
content: string;
}
export interface PromptStore {
counter: number;
latestId: number;
prompts: Map<number, Prompt>;
prompts: Record<number, Prompt>;
add: (prompt: Prompt) => number;
get: (id: number) => Prompt | undefined;
remove: (id: number) => void;
search: (text: string) => Prompt[];
}
update: (id: number, updater: (prompt: Prompt) => void) => void;
export const PROMPT_KEY = "prompt-store";
getUserPrompts: () => Prompt[];
}
export const SearchService = {
ready: false,
engine: new Fuse<Prompt>([], { keys: ["title"] }),
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
userEngine: new Fuse<Prompt>([], { keys: ["title"] }),
count: {
builtin: 0,
},
allPrompts: [] as Prompt[],
builtinPrompts: [] as Prompt[],
init(prompts: Prompt[]) {
init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
if (this.ready) {
return;
}
this.engine.setCollection(prompts);
this.allPrompts = userPrompts.concat(builtinPrompts);
this.builtinPrompts = builtinPrompts.slice();
this.builtinEngine.setCollection(builtinPrompts);
this.userEngine.setCollection(userPrompts);
this.ready = true;
},
remove(id: number) {
this.engine.remove((doc) => doc.id === id);
this.userEngine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
this.engine.add(prompt);
this.userEngine.add(prompt);
},
search(text: string) {
const results = this.engine.search(text);
return results.map((v) => v.item);
const userResults = this.userEngine.search(text);
const builtinResults = this.builtinEngine.search(text);
return userResults.concat(builtinResults).map((v) => v.item);
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
counter: 0,
latestId: 0,
prompts: new Map(),
prompts: {},
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
prompts.set(prompt.id, prompt);
prompt.isUser = true;
prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
@@ -67,22 +82,58 @@ export const usePromptStore = create<PromptStore>()(
return prompt.id!;
},
get(id) {
const targetPrompt = get().prompts[id];
if (!targetPrompt) {
return SearchService.builtinPrompts.find((v) => v.id === id);
}
return targetPrompt;
},
remove(id) {
const prompts = get().prompts;
prompts.delete(id);
delete prompts[id];
SearchService.remove(id);
set(() => ({
prompts,
counter: get().counter + 1,
}));
},
getUserPrompts() {
const userPrompts = Object.values(get().prompts ?? {});
userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
return userPrompts;
},
update(id: number, updater) {
const prompt = get().prompts[id] ?? {
title: "",
content: "",
id,
};
SearchService.remove(id);
updater(prompt);
const prompts = get().prompts;
prompts[id] = prompt;
set(() => ({ prompts }));
SearchService.add(prompt);
},
search(text) {
if (text.length === 0) {
// return all rompts
return SearchService.allPrompts.concat([...get().getUserPrompts()]);
}
return SearchService.search(text) as Prompt[];
},
}),
{
name: PROMPT_KEY,
name: StoreKey.Prompt,
version: 1,
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";
@@ -92,24 +143,31 @@ export const usePromptStore = create<PromptStore>()(
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
const builtinPrompts = [res.en, res.cn]
.map((promptList: PromptList) => {
let fetchPrompts = [res.en, res.cn];
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts.map(
(promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
id: Math.random(),
title,
content,
} as Prompt),
);
})
.concat([...(state?.prompts?.values() ?? [])]);
const allPromptsForSearch = builtinPrompts.reduce(
(pre, cur) => pre.concat(cur),
[],
},
);
const userPrompts =
usePromptStore.getState().getUserPrompts() ?? [];
const allPromptsForSearch = builtinPrompts
.reduce((pre, cur) => pre.concat(cur), [])
.filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch);
SearchService.init(allPromptsForSearch, userPrompts);
});
},
},

View File

@@ -1,49 +1,87 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { getCurrentVersion } from "../utils";
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { requestUsage } from "../requests";
export interface UpdateStore {
lastUpdate: number;
remoteId: string;
remoteVersion: string;
getLatestCommitId: (force: boolean) => Promise<string>;
used?: number;
subscription?: number;
lastUpdateUsage: number;
version: string;
getLatestVersion: (force?: boolean) => Promise<void>;
updateUsage: (force?: boolean) => Promise<void>;
}
export const UPDATE_KEY = "chat-update";
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;
}
const ONE_MINUTE = 60 * 1000;
export const useUpdateStore = create<UpdateStore>()(
persist(
(set, get) => ({
lastUpdate: 0,
remoteId: "",
remoteVersion: "",
async getLatestCommitId(force = false) {
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins;
if (!shouldFetch) {
return getCurrentVersion();
}
lastUpdateUsage: 0,
version: "unknown",
async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") ?? "unknown" }));
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
if (!force && !overTenMins) return;
set(() => ({
lastUpdate: Date.now(),
}));
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 remoteId = (data[0].sha as string).substring(0, 7);
const remoteCommitTime = data[0].commit.committer.date;
const remoteId = new Date(remoteCommitTime).getTime().toString();
set(() => ({
lastUpdate: Date.now(),
remoteId,
remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
return getCurrentVersion();
}
},
async updateUsage(force = false) {
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
if (!overOneMinute && !force) return;
set(() => ({
lastUpdateUsage: Date.now(),
}));
const usage = await requestUsage();
if (usage) {
set(() => usage);
}
},
}),
{
name: UPDATE_KEY,
name: StoreKey.Update,
version: 1,
},
),

View File

@@ -1,4 +1,9 @@
@import "./animation.scss";
@import "./window.scss";
@mixin light {
--theme: light;
/* color */
--white: white;
--black: rgb(48, 48, 48);
@@ -18,6 +23,8 @@
}
@mixin dark {
--theme: dark;
/* color */
--white: rgb(30, 30, 30);
--black: rgb(187, 187, 187);
@@ -31,6 +38,10 @@
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
div:not(.no-dark) > svg {
filter: invert(0.5);
}
}
.light {
@@ -77,6 +88,9 @@
}
html {
height: var(--full-height);
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
}
body {
@@ -90,8 +104,7 @@ body {
justify-content: center;
align-items: center;
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;
touch-action: pan-x pan-y;
@media only screen and (max-width: 600px) {
background-color: var(--second);
@@ -126,8 +139,13 @@ select {
text-align: center;
}
label {
cursor: pointer;
}
input {
text-align: center;
font-family: inherit;
}
input[type="checkbox"] {
@@ -155,19 +173,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;
@@ -176,15 +186,41 @@ 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="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="text"],
input[type="password"] {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
@@ -194,6 +230,7 @@ input[type="text"] {
color: var(--black);
padding: 0 10px;
max-width: 50%;
font-family: inherit;
}
div.math {
@@ -211,6 +248,10 @@ div.math {
display: flex;
align-items: center;
justify-content: center;
@media screen and (max-width: 600px) {
align-items: flex-end;
}
}
.link {
@@ -260,11 +301,55 @@ pre {
.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;
}
}
.password-input-container {
max-width: 50%;
display: flex;
justify-content: flex-end;
.password-eye {
margin-right: 4px;
}
.password-input {
min-width: 80%;
}
}
.user-avatar {
height: 30px;
min-height: 30px;
width: 30px;
min-width: 30px;
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
border-radius: 10px;
}
.one-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -8,16 +8,22 @@
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
}
pre code.hljs {
pre code {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
code {
padding: 3px 5px;
}
.hljs,
pre {
background: #1a1b26;
color: #cbd2ea;
}
/*!
Theme: Tokyo-night-Dark
origin: https://github.com/enkia/tokyo-night-vscode-theme
@@ -99,11 +105,6 @@
color: #c0caf5;
}
.hljs {
background: #1a1b26;
color: #9aa5ce;
}
.hljs-emphasis {
font-style: italic;
}

View File

@@ -10,6 +10,7 @@
.window-header-title {
max-width: calc(100% - 100px);
overflow: hidden;
.window-header-main-title {
font-size: 20px;

Some files were not shown because too many files have changed in this diff Show More