Compare commits
1084 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d1d8b1f393 | ||
|
9da455a7ea | ||
|
2bd6342309 | ||
|
5e73577088 | ||
|
5156a80738 | ||
|
a9d605ed30 | ||
|
7d1fae32cd | ||
|
9f17b45bc4 | ||
|
a64c9dae42 | ||
|
5fbf4c394c | ||
|
1e5153173c | ||
|
011b52d07d | ||
|
d033168d80 | ||
|
fdca9e59de | ||
|
549a2fd206 | ||
|
a0cd939bfd | ||
|
4f52679ec6 | ||
|
b52e237044 | ||
|
17b329bb63 | ||
|
3a654ba199 | ||
|
5ba3fc9321 | ||
|
a46f08154e | ||
|
0f6ed9c293 | ||
|
295864d36b | ||
|
be6d45e49f | ||
|
22b6987249 | ||
|
64647b0bb3 | ||
|
a5a1f2e8ad | ||
|
8bd39f33e0 | ||
|
be9774943b | ||
|
943214c6a7 | ||
|
ca792669fc | ||
|
78d68a9949 | ||
|
6b2db97347 | ||
|
2bfb362832 | ||
|
cb140e482f | ||
|
e6b72ac1ff | ||
|
8032e6d68d | ||
|
c7e0a6f37f | ||
|
1141cd2e6e | ||
|
c9dd953817 | ||
|
b7ffca031e | ||
|
fd2f441e02 | ||
|
3b3ebda34b | ||
|
87e3d663a2 | ||
|
d0a1d910d4 | ||
|
33b97082fa | ||
|
d93f05f511 | ||
|
3dc29197e1 | ||
|
fbc0236748 | ||
|
1304a3943a | ||
|
8c0ba1aee2 | ||
|
47154773f2 | ||
|
deca9bf06d | ||
|
6ab4c9be2e | ||
|
3a519612a0 | ||
|
d1ec26ae83 | ||
|
9cb889c34f | ||
|
77f27b7948 | ||
|
dbee5bc515 | ||
|
54a5332834 | ||
|
8af164c99f | ||
|
c7321fddfb | ||
|
c5ca278253 | ||
|
638fdd8c3e | ||
|
3407b57a51 | ||
|
836bf836d3 | ||
|
6e52d14180 | ||
|
1096fe55e1 | ||
|
d2d615c84a | ||
|
9f26c8cecb | ||
|
cd1f082c52 | ||
|
b90dfb48ee | ||
|
70e67a686b | ||
|
d1c3d900ab | ||
|
e8a4ad1001 | ||
|
ccac85b079 | ||
|
f92fefb6cd | ||
|
cc565cf57e | ||
|
4a56e9c884 | ||
|
d11d906fa2 | ||
|
65c4a0c319 | ||
|
55bcf78efe | ||
|
f5a2ce52aa | ||
|
0dcbfd746e | ||
|
06e043d3f1 | ||
|
986d34fb3e | ||
|
5296ab1b09 | ||
|
b5d3348d99 | ||
|
1e77df381a | ||
|
ec33281ff5 | ||
|
ba67f13ad5 | ||
|
1604ed91fb | ||
|
74fcaab5e9 | ||
|
adee435ea6 | ||
|
ea59ab5176 | ||
|
f78008c1b2 | ||
|
f54db695af | ||
|
f21f922160 | ||
|
fd413c7b52 | ||
|
3e2c5af4b5 | ||
|
bdb49b1171 | ||
|
5933b3d7eb | ||
|
4c8d606fae | ||
|
13c1d2fd2b | ||
|
e35c807216 | ||
|
5a2cc6f154 | ||
|
88f8c43472 | ||
|
ef3e8e6fac | ||
|
1505372e20 | ||
|
ad5093ce05 | ||
|
b558d1afc6 | ||
|
ddfd05b008 | ||
|
d2ad01a9ff | ||
|
64a17abfe2 | ||
|
04b638aa06 | ||
|
31e30906d0 | ||
|
bc00be9065 | ||
|
4a599e986f | ||
|
f1ca03e378 | ||
|
f3d5fc7a84 | ||
|
3bfcdf9c41 | ||
|
144200e315 | ||
|
398e229c77 | ||
|
6a61fe5776 | ||
|
9835206452 | ||
|
70b0580fb7 | ||
|
23eb7732d7 | ||
|
26e50cefea | ||
|
588e907181 | ||
|
eae7d6260f | ||
|
175b4e7f92 | ||
|
b050417ab1 | ||
|
37b49400db | ||
|
ebcb2e7837 | ||
|
f1e7db6a88 | ||
|
2ba0929458 | ||
|
83fed42997 | ||
|
2c4626709c | ||
|
59fbadd9eb | ||
|
adb860b464 | ||
|
8d8790586d | ||
|
61ca60c550 | ||
|
d713d01600 | ||
|
372ea0f845 | ||
|
61888708f5 | ||
|
c900459f73 | ||
|
2c92f75c86 | ||
|
3e1514239c | ||
|
0707a1d49a | ||
|
9521f19507 | ||
|
bd69116df2 | ||
|
6535986484 | ||
|
e03db9c2d5 | ||
|
038790370c | ||
|
261bf0b298 | ||
|
5a7bdcfe59 | ||
|
4f3261b262 | ||
|
48e6087b1b | ||
|
28103c901d | ||
|
368701610f | ||
|
b589f48aa9 | ||
|
74e32efa7d | ||
|
dc555b2206 | ||
|
859cf6930f | ||
|
6f83fbd212 | ||
|
a7be4780ba | ||
|
b5e89d4440 | ||
|
89bc98d26b | ||
|
31a73273ac | ||
|
2a820ea6e7 | ||
|
6484d4f0bd | ||
|
461e48be32 | ||
|
ff60ffca3e | ||
|
1bbf310c46 | ||
|
8469f448b5 | ||
|
e36abc3ac6 | ||
|
605dd72354 | ||
|
9770b65146 | ||
|
57158890c3 | ||
|
6f8f490fdd | ||
|
415e9dc913 | ||
|
1487762925 | ||
|
c73a91a0f5 | ||
|
5dced28088 | ||
|
38f6956e71 | ||
|
93da9dd1f4 | ||
|
505c8cde81 | ||
|
f7a6fa9873 | ||
|
70a2d11b8c | ||
|
2c077aca5a | ||
|
efef6b098d | ||
|
885f2a3226 | ||
|
1f94ae7888 | ||
|
05aa520669 | ||
|
b0c32159e7 | ||
|
4a182517da | ||
|
107b584085 | ||
|
6d8416f838 | ||
|
876a0cb6f7 | ||
|
4904612523 | ||
|
3bd76b9156 | ||
|
dd757e7d04 | ||
|
71d06647d0 | ||
|
0ae57589a9 | ||
|
22a6819f7b | ||
|
5e23ad2db1 | ||
|
e009ea57b9 | ||
|
63c93a42b5 | ||
|
e6cc1625b5 | ||
|
f65b0128e7 | ||
|
507b7fee56 | ||
|
925d28495a | ||
|
19dd71eb05 | ||
|
b3fd56c2c1 | ||
|
d8b6ebf6cb | ||
|
bdaac17886 | ||
|
8cac51abbe | ||
|
0113d4499b | ||
|
7562ab3c1c | ||
|
d3de07ecf3 | ||
|
b146cd889f | ||
|
bc0f184098 | ||
|
1debde3046 | ||
|
4d3fdbdd80 | ||
|
90c201d438 | ||
|
1e22abab80 | ||
|
792c37735e | ||
|
6d8ece530f | ||
|
3499dfb285 | ||
|
30c656ceda | ||
|
a1c7f86ff3 | ||
|
4952e41132 | ||
|
e1142216ec | ||
|
a4040fc1ee | ||
|
f84572443f | ||
|
fbf6bc1979 | ||
|
3aeb2c1230 | ||
|
50eb7a5f98 | ||
|
484d1e7396 | ||
|
a632e13334 | ||
|
aa3f96f89c | ||
|
16685ddb6c | ||
|
e78b15b9f0 | ||
|
35b0bd76f8 | ||
|
1d7286c161 | ||
|
5a7ec38ecd | ||
|
ada4e3cdcd | ||
|
ed62c87156 | ||
|
840277f584 | ||
|
ffddb7fb2b | ||
|
b380421fd5 | ||
|
20882a7598 | ||
|
9d3dff47d2 | ||
|
db5c7aba78 | ||
|
e8e01aa60d | ||
|
ae8226907f | ||
|
803b66ae9d | ||
|
20a508e2d6 | ||
|
25ada1eae9 | ||
|
808e4b38a3 | ||
|
a496bc5a63 | ||
|
c94713475f | ||
|
d45c61109f | ||
|
99220d72da | ||
|
887eaef1aa | ||
|
836a00e104 | ||
|
8ee5061046 | ||
|
61852b4021 | ||
|
0b7de6f7b2 | ||
|
9834a67cbd | ||
|
d85a4a0c9f | ||
|
67c8ec6d7e | ||
|
2a2dd7ea19 | ||
|
153e7ac7e4 | ||
|
9420fd4946 | ||
|
b14c5cd89c | ||
|
4ab9141429 | ||
|
769c2f9f49 | ||
|
c41c498a9c | ||
|
d1096582a5 | ||
|
543989151f | ||
|
7da83987e4 | ||
|
523d553dac | ||
|
ff6f0e9546 | ||
|
3e63f6ba34 | ||
|
811b92d24e | ||
|
bc5ddc4541 | ||
|
44e43729bf | ||
|
203067c936 | ||
|
f3b508c088 | ||
|
081d84f848 | ||
|
e381add944 | ||
|
75d4eca722 | ||
|
56904ad6b2 | ||
|
b5ef552c25 | ||
|
cbabb9392c | ||
|
531b3dcf9e | ||
|
d975daf3f0 | ||
|
6137d551fe | ||
|
cf625e3542 | ||
|
e354fca4a4 | ||
|
129e7afc16 | ||
|
e83e0f6a33 | ||
|
cf4f928b25 | ||
|
e4d955e3f9 | ||
|
13576087f4 | ||
|
e74f6f3183 | ||
|
8302d1d3c1 | ||
|
b7320e6834 | ||
|
3193fe0235 | ||
|
7c2fa9f8a4 | ||
|
e5f6133127 | ||
|
0198c5b781 | ||
|
f535073ae4 | ||
|
b9895ecadd | ||
|
322eb66fdf | ||
|
f0abdc80eb | ||
|
70a4f94c81 | ||
|
f7e4b36746 | ||
|
4e6b71ace5 | ||
|
c916cd1a87 | ||
|
442a529a72 | ||
|
e1243f3d59 | ||
|
62f8cd1db6 | ||
|
af5f67d459 | ||
|
13424a893e | ||
|
30473ec41e | ||
|
24d382c70d | ||
|
3ddedc903e | ||
|
e3c279b8be | ||
|
d909b676c5 | ||
|
08a8ac9971 | ||
|
7073fd2f3b | ||
|
a9f67a48a1 | ||
|
fd058cc693 | ||
|
c98df33003 | ||
|
3f8b14f5f8 | ||
|
1c335a68e0 | ||
|
fb98050d9f | ||
|
91cb7aa13d | ||
|
ab0f7cc0c9 | ||
|
b51f7f9a25 | ||
|
d275e32e70 | ||
|
22cf78d506 | ||
|
c7c318b31e | ||
|
a4d012828c | ||
|
72cfebd2bc | ||
|
a832cfb343 | ||
|
9ef680db57 | ||
|
2c09f4ef90 | ||
|
c44454bf74 | ||
|
ad1b9b7f6d | ||
|
15e063e1b5 | ||
|
c00a63e4c3 | ||
|
dbda27b1d6 | ||
|
9eef879dbe | ||
|
cb29423636 | ||
|
d754eecf07 | ||
|
1e308782ee | ||
|
69139cd85b | ||
|
f59235bd5a | ||
|
2930ba0457 | ||
|
1513881eed | ||
|
5e361f6748 | ||
|
b182543a21 | ||
|
cbce8b444f | ||
|
38e92cfc62 | ||
|
a7764dc6d5 | ||
|
c74552c4c5 | ||
|
7a0b437626 | ||
|
c7e9f13d2e | ||
|
51e816ad6d | ||
|
dd047fd58f | ||
|
300f023d2a | ||
|
a282a80a6e | ||
|
e668e17655 | ||
|
a913d9728c | ||
|
15862d393b | ||
|
48a6cdd50a | ||
|
09b05cde7f | ||
|
da294038a8 | ||
|
332f3fb8e8 | ||
|
8e4743e719 | ||
|
4a663e539a | ||
|
98ac7ee277 | ||
|
28c457730a | ||
|
9b61fe9df2 | ||
|
b55b01cb13 | ||
|
90d8f3117f | ||
|
c7e976c8c5 | ||
|
6014b765f4 | ||
|
ca295588c4 | ||
|
437334355f | ||
|
3432d4df29 | ||
|
05fcb96181 | ||
|
6bb27f90a4 | ||
|
c10e8382a9 | ||
|
6653a31eb7 | ||
|
42561d04de | ||
|
9a285ab935 | ||
|
81771568be | ||
|
795b43f174 | ||
|
52203b50eb | ||
|
0373b2c9dd | ||
|
fe2c1c4ec6 | ||
|
596b6542e8 | ||
|
6897bf1254 | ||
|
8a6a13e583 | ||
|
b718285125 | ||
|
e5ab918ef9 | ||
|
6c6a2d08db | ||
|
9e6617e3ca | ||
|
94a50f92e1 | ||
|
5c8be2a8f6 | ||
|
1dcf2d80b4 | ||
|
1197521921 | ||
|
3863cfe786 | ||
|
54bd07702c | ||
|
a75e2b0c0e | ||
|
29fd9b23fe | ||
|
089e3b8946 | ||
|
9c36fcec81 | ||
|
74fa065266 | ||
|
38f2495cf6 | ||
|
4131fccbe0 | ||
|
f2d748cfe4 | ||
|
197ec0c29c | ||
|
0a2af9335c | ||
|
a52fa28ed1 | ||
|
78ed24dbf6 | ||
|
cda074fe24 | ||
|
823032617d | ||
|
5963459499 | ||
|
0bc2c71b0c | ||
|
b4e350e189 | ||
|
941e46490f | ||
|
4df92e903a | ||
|
6ba02d0d50 | ||
|
2dc122831b | ||
|
f3f84e523a | ||
|
0cdee25b5b | ||
|
92b0314c14 | ||
|
ad2bc7da96 | ||
|
d8b606dc83 | ||
|
5ce53dbcf4 | ||
|
c5c1a9ab3c | ||
|
564709aa98 | ||
|
475158a145 | ||
|
829df56733 | ||
|
ee55f8790e | ||
|
6d19fb3909 | ||
|
9057712c8f | ||
|
5d6e7de667 | ||
|
8f6f70879c | ||
|
3120087992 | ||
|
0ec4cc223f | ||
|
60c7be31b6 | ||
|
2388f853c9 | ||
|
1e8d4763bb | ||
|
be4834688d | ||
|
3937dad6a6 | ||
|
463251dcc1 | ||
|
3adca26808 | ||
|
97a8bb52d6 | ||
|
7748a980f9 | ||
|
b044e274aa | ||
|
6c3d4a11cc | ||
|
98afd5516b | ||
|
ea6926cad3 | ||
|
3298961748 | ||
|
0140f771c6 | ||
|
64c4f512ce | ||
|
5b1d45c1a9 | ||
|
efbd1c15a9 | ||
|
bce74890dc | ||
|
eb93a43d13 | ||
|
1dd75b63de | ||
|
6caf79121b | ||
|
5cdd34a388 | ||
|
38c8ee8cd2 | ||
|
d5c33a1183 | ||
|
058e28911a | ||
|
25a3bb351d | ||
|
1607640bed | ||
|
5f0cda829f | ||
|
b003a374b8 | ||
|
5f7c262759 | ||
|
f3ab6b27c9 | ||
|
d62eb56886 | ||
|
c2724c6aad | ||
|
0cde1683b6 | ||
|
128f596f93 | ||
|
27fae8eed0 | ||
|
82ec4474c2 | ||
|
0d85dae239 | ||
|
7893693706 | ||
|
25ce6af36e | ||
|
c0269254e8 | ||
|
2f2aefd48e | ||
|
c05de45d99 | ||
|
8f66da1128 | ||
|
b5eaa8272b | ||
|
7ee062e1de | ||
|
8915af9b6d | ||
|
ae1ef3215b | ||
|
5d06fa217c | ||
|
50b1f7db12 | ||
|
fb82806956 | ||
|
0658a93962 | ||
|
f33be37070 | ||
|
9278d7f851 | ||
|
35ba898c97 | ||
|
15f8d13d81 | ||
|
946e508e8f | ||
|
35b4125b98 | ||
|
91d8f9d73e | ||
|
c636993989 | ||
|
65a24c16bc | ||
|
15ce114440 | ||
|
1722f75dcb | ||
|
be597a551d | ||
|
56bc945335 | ||
|
fa9ceb5875 | ||
|
0ccf5a16c7 | ||
|
93c666b03d | ||
|
d5c28b506c | ||
|
3318f01aa1 | ||
|
1641019091 | ||
|
3572969866 | ||
|
ad1fe8762b | ||
|
c9c3e32a78 | ||
|
f4c99c9cf7 | ||
|
57447c24c8 | ||
|
514ed03378 | ||
|
078b039cf1 | ||
|
a120670785 | ||
|
9c05d136f5 | ||
|
daeb8a67a3 | ||
|
c64888cccd | ||
|
6fc2637870 | ||
|
fc4df23075 | ||
|
3ae502986b | ||
|
67f23d202a | ||
|
3c38b9c93b | ||
|
152a00baf5 | ||
|
405d02023c | ||
|
4ba2d0d7e0 | ||
|
38ce230cf5 | ||
|
c6696313bf | ||
|
89717c9b15 | ||
|
3aca699ad9 | ||
|
f3ec9f02eb | ||
|
1c7f3e6057 | ||
|
db63a5a670 | ||
|
0a2cccfd6e | ||
|
113bd24796 | ||
|
374a27a72e | ||
|
567eff9ef4 | ||
|
bfe33af03d | ||
|
7ebc5eb70f | ||
|
52287fd01d | ||
|
e0f2bc2725 | ||
|
4355ec3b44 | ||
|
0c2f11ffd7 | ||
|
65e2f38fac | ||
|
aeaefbf8a9 | ||
|
719cb3ae05 | ||
|
08816c8f7d | ||
|
1031203e3f | ||
|
63ef6e7d3d | ||
|
590c9bb978 | ||
|
5055a8c7a3 | ||
|
284c3f27e7 | ||
|
468f198a6b | ||
|
b0c6f6d4d3 | ||
|
473061b4d5 | ||
|
c4921f3da7 | ||
|
91b871ef3b | ||
|
698be6671c | ||
|
47c546fafa | ||
|
9d1a84858c | ||
|
4dd5bf71ea | ||
|
ea253e3bae | ||
|
1d42e955fc | ||
|
e636d486f5 | ||
|
818cc545eb | ||
|
e7858495e6 | ||
|
af3a273a56 | ||
|
6264c02543 | ||
|
80d5bfd7c0 | ||
|
3fa8b0ad14 | ||
|
184a0b9481 | ||
|
4e80096ee4 | ||
|
0fb775d71a | ||
|
76fdd047e7 | ||
|
8590750e4c | ||
|
590bd8e4bb | ||
|
b4cb8c3d75 | ||
|
21aa015a79 | ||
|
dd5b9d420b | ||
|
2593de5872 | ||
|
d2ae740d5f | ||
|
c56c6074e9 | ||
|
426ce7fd35 | ||
|
2a191aacb7 | ||
|
50cd33dbb2 | ||
|
e6b49a60c0 | ||
|
a7e9356c16 | ||
|
58cc1c8482 | ||
|
88df4a2223 | ||
|
d046100875 | ||
|
0d4611052e | ||
|
bdb03e07fc | ||
|
1d790b9e8d | ||
|
24bf15af4f | ||
|
6410aa214e | ||
|
a023308d52 | ||
|
2516851056 | ||
|
a3a77006ff | ||
|
ebbd0128f1 | ||
|
773641aa16 | ||
|
57514e91b6 | ||
|
a9d336baf5 | ||
|
37da759fd5 | ||
|
2ebc26953d | ||
|
c8ba72b185 | ||
|
35fe6e2f54 | ||
|
f3d35e0ef3 | ||
|
68327907a9 | ||
|
44fe85b56d | ||
|
72cbb156ae | ||
|
5e274130a6 | ||
|
94be03ec4f | ||
|
6502855a70 | ||
|
6bbdaf7ab0 | ||
|
846e323840 | ||
|
c198c33778 | ||
|
c1cc3d1d1f | ||
|
6b5e5a15d7 | ||
|
7ac03b4d89 | ||
|
6bd75fae33 | ||
|
3cdaf62fa1 | ||
|
9aea6c5585 | ||
|
d533895637 | ||
|
aa74a74c5c | ||
|
46f0a256d7 | ||
|
e33ad07e16 | ||
|
786d62905d | ||
|
75594b8fe5 | ||
|
96d2f05eb7 | ||
|
887f93181c | ||
|
9f4a80f6ae | ||
|
3e65ef3bea | ||
|
4ca34e0436 | ||
|
bb3f6ee086 | ||
|
89f0a4875c | ||
|
aa2be9b96c | ||
|
707c1a2f7e | ||
|
ed14a0029a | ||
|
989661e4df | ||
|
8874c687d8 | ||
|
58e1997bcb | ||
|
3e3055d7df | ||
|
b68d6e9d1a | ||
|
cf9d200b7c | ||
|
e84da3089a | ||
|
fee38b8d13 | ||
|
e0a69a9e57 | ||
|
0bdc95f9c4 | ||
|
4c9e4e507c | ||
|
adb50fe64f | ||
|
e883aefcc3 | ||
|
cd7e8bbd3e | ||
|
99317f759b | ||
|
7ff6a8651a | ||
|
4e1e1d4fef | ||
|
d34676c5b2 | ||
|
4cf659c29b | ||
|
ec61a5b32d | ||
|
58f726c602 | ||
|
bb0c3835f4 | ||
|
e9642c7505 | ||
|
f0b4ef5917 | ||
|
1f12753c68 | ||
|
0439d122a5 | ||
|
4dad7f2ab6 | ||
|
ce75dc502b | ||
|
23f6c2e8c9 | ||
|
d3461dd69b | ||
|
05b1b8b240 | ||
|
3118ba4466 | ||
|
35cec0f1df | ||
|
a19d238483 | ||
|
a57fa2e9ad | ||
|
c2b36cdffa | ||
|
600b1814a1 | ||
|
76e6957a8a | ||
|
18df79ce00 | ||
|
f14b413b7c | ||
|
d0e73bd6b2 | ||
|
f27b25a62e | ||
|
6d8c7ba140 | ||
|
af497c96ec | ||
|
697c7a8dfe | ||
|
3f5a189591 | ||
|
bcb18ff2f4 | ||
|
b1ba3df989 | ||
|
203ac0970d | ||
|
e5329dc28a | ||
|
f8ef6278a5 | ||
|
7f13a8d2bc | ||
|
b0b078c0fb | ||
|
c282433095 | ||
|
8d7f3bd215 | ||
|
f6c268dc1e | ||
|
48f25b0799 | ||
|
50cfbaaab5 | ||
|
de775511d0 | ||
|
a524a60c46 | ||
|
6cf2fa02e5 | ||
|
1a8cb877db | ||
|
33727aad62 | ||
|
ac79d810d0 | ||
|
2b912c6834 | ||
|
cf2404743d | ||
|
38bffd423c | ||
|
42711d76d6 | ||
|
789f3d993c | ||
|
d5376ab090 | ||
|
62ad7e5ad3 | ||
|
f0cb2fc21a | ||
|
3fe521421f | ||
|
85445c4ef2 | ||
|
b4faf3faad | ||
|
5f2745c32a | ||
|
9ef6713aa1 | ||
|
736c66f46a | ||
|
98b699c483 | ||
|
93044590cc | ||
|
30676d118f | ||
|
e306ac0197 | ||
|
65c6b4af82 | ||
|
a402f646fe | ||
|
db29fed99a | ||
|
94a2104b55 | ||
|
0f886a1ece | ||
|
3db024b24d | ||
|
75bf75d552 | ||
|
5bbe59c9a2 | ||
|
28e447ea4a | ||
|
31a874e24e | ||
|
987412db51 | ||
|
94ab5c7abf | ||
|
d358fb256b | ||
|
e00652ce86 | ||
|
086effa6cb | ||
|
de000a8b4e | ||
|
8f75317820 | ||
|
dbb016c9e4 | ||
|
2831b91e94 | ||
|
923b2594df | ||
|
44874fb5e0 | ||
|
5b3d7ccb46 | ||
|
daecd3efa1 | ||
|
86e4b58117 | ||
|
34426d86dc | ||
|
50a915b7b6 | ||
|
775ba2596a | ||
|
7141962cce | ||
|
d5a4527e9d | ||
|
cf775e3487 | ||
|
44e1bed57a | ||
|
915ba07f86 | ||
|
a852c5d0c3 | ||
|
8cde6cd4d7 | ||
|
a70d59eb45 | ||
|
06534fa0ae | ||
|
8a548ef252 | ||
|
99c854ce1d | ||
|
46fe3c520c | ||
|
8568fc3544 | ||
|
5ba0aef799 | ||
|
71cbf86b2c | ||
|
aed6b34950 | ||
|
e9076c1748 | ||
|
8b0cf7d248 | ||
|
9aa794248f | ||
|
b357e2ecef | ||
|
68e27e9513 | ||
|
7b4e4c2172 | ||
|
9e602eb575 | ||
|
4618c624c8 | ||
|
5979bdd48e | ||
|
2170392bdf | ||
|
e9335d9508 | ||
|
5f444c1c82 | ||
|
a3de277c43 | ||
|
b5b8593e7f | ||
|
03163d6a61 | ||
|
bd90caa99d | ||
|
cada0aa70b | ||
|
cbde357d94 | ||
|
75488e4bd5 | ||
|
d1bdf4a292 | ||
|
c73c063ad5 | ||
|
9b000ff242 | ||
|
a282937468 | ||
|
6da3aab046 | ||
|
ff2589c97f | ||
|
a9f000e7ef | ||
|
d9be63e6cb | ||
|
1a626a68f0 | ||
|
330504b91e | ||
|
6bb0166055 | ||
|
b22988e6b8 | ||
|
f7edac961f | ||
|
5b9b120fa6 | ||
|
f07e4fc87f | ||
|
46eb870c46 | ||
|
a0e192b6e4 | ||
|
dc3fa6c780 | ||
|
dd5604f5d9 | ||
|
170936a96e | ||
|
93c9974019 | ||
|
377579e802 | ||
|
881cf082c2 | ||
|
bfc924bc2a | ||
|
dbd92b2db9 | ||
|
505a68093d | ||
|
f2b81a2f23 | ||
|
c49dbab127 | ||
|
36adfe87fb | ||
|
cdfcf0f068 | ||
|
c3676091ee | ||
|
ec19b86ade | ||
|
ec43f4e6ab | ||
|
7bf74c6a5d | ||
|
cbb50c14e1 | ||
|
d42622a5c1 | ||
|
f9bee1485b | ||
|
ff72e8abab | ||
|
c87eee1fda | ||
|
960aa90c32 | ||
|
5a10b2a75f | ||
|
72b553efe8 | ||
|
c4210be3c7 | ||
|
28a49827ff | ||
|
736869454b | ||
|
db9084b0dc | ||
|
c39d75b448 | ||
|
71a546dd44 | ||
|
96e3d3a22c | ||
|
0ad91101a4 | ||
|
e9df58709e | ||
|
a80dcaa1c3 | ||
|
d29b7fa1c7 | ||
|
89551774db | ||
|
be58d3afb6 | ||
|
652d803739 | ||
|
2688914125 | ||
|
1d489cfcea | ||
|
cb55ce084c | ||
|
dae7da0e4e | ||
|
e4630e6a0f | ||
|
6d9abf11b8 | ||
|
7e8def50aa | ||
|
2b7f72deec | ||
|
9b1f25140e | ||
|
f4caa0029e | ||
|
15e046b3ce | ||
|
9d0485fa22 | ||
|
eeef92b068 | ||
|
a1418fe33c | ||
|
8f21e736dd | ||
|
222301307f | ||
|
1b19fdfe11 | ||
|
1f2ef1cdb7 | ||
|
c394b21423 | ||
|
696e84ea88 | ||
|
4a17dca7b9 | ||
|
b3f38f3264 | ||
|
9bcfe6461d | ||
|
4a82a91f2d | ||
|
24cd905911 | ||
|
6d62ab4257 | ||
|
f6ff32f339 | ||
|
bc523d302b | ||
|
1facbb2906 | ||
|
a5eb87e835 | ||
|
8265436437 | ||
|
96545bd523 | ||
|
dd5ee68808 | ||
|
e773ed2912 | ||
|
9949fc4646 | ||
|
d88da1f6ab | ||
|
fe8e3f2bcf | ||
|
4b9d753254 | ||
|
5ba385b74d | ||
|
96c0a5c911 | ||
|
ec655f5182 | ||
|
093df395c7 | ||
|
92d2373d77 | ||
|
319959ad6e | ||
|
de1e4b4c6c | ||
|
c2e79d22d2 | ||
|
596c9b1d27 | ||
|
40223e6b3f | ||
|
eec1dd6448 | ||
|
6c1261d28d | ||
|
4697ea1c1a | ||
|
6e20031dce | ||
|
a5fe9bc6d6 | ||
|
637660df2a | ||
|
e1549d109e | ||
|
418d270d74 | ||
|
5f8fc3d155 | ||
|
fce3b3ce7b | ||
|
074bd9f045 | ||
|
b1ea26467d | ||
|
48ebd74859 | ||
|
ef5b7ce853 | ||
|
e0053d57f7 | ||
|
06268543d0 | ||
|
58eadd6d7b | ||
|
f250594e97 | ||
|
c1b6828ed4 | ||
|
328ecd1cfb | ||
|
14b3f300ae | ||
|
435e82c824 | ||
|
aeda7520fe | ||
|
f54fb177da | ||
|
56ef8e3ebf | ||
|
717c123b82 | ||
|
132f6c8420 | ||
|
116e16e30d | ||
|
5dbb6afc60 | ||
|
ae8050a3f7 | ||
|
8870e966a6 | ||
|
f5a5cffdec | ||
|
220c622f8f | ||
|
e509749421 | ||
|
a69cec89fb | ||
|
8f5c289818 | ||
|
5e544891aa | ||
|
1aaf4ae5bc | ||
|
9f3188fe45 | ||
|
b2fc7d476a | ||
|
c37885e743 | ||
|
0209ace221 | ||
|
a9c6c681ce | ||
|
b0cd8579f1 | ||
|
ba0753c418 | ||
|
d58545d340 | ||
|
6419ce345f | ||
|
f78a27491a | ||
|
7ef20f22c2 | ||
|
ace3f7f532 | ||
|
fb32770486 | ||
|
98de486534 | ||
|
6c1144f6f4 | ||
|
59edcc3e2e | ||
|
30040a0366 | ||
|
a604644da7 | ||
|
64af3ccf0c | ||
|
401c1364be | ||
|
c7c58ef031 | ||
|
0f9fe92fa3 | ||
|
bbf3d044cb | ||
|
7ed8517771 | ||
|
2deb5cbc9e | ||
|
fbacfba92a | ||
|
fcce9968e9 | ||
|
662b46101c | ||
|
3cda44e05b | ||
|
a7a8aad9bc | ||
|
2e01a93a4a | ||
|
c4ca05865d | ||
|
23287e9b47 | ||
|
ffa7302571 | ||
|
6b36b255ef | ||
|
d5bd77cca1 | ||
|
e5e2f6c2e1 | ||
|
708c6829f7 | ||
|
aeb986243c | ||
|
e654cee3c8 | ||
|
71aa9d05ed | ||
|
725054c7d5 | ||
|
bfad3df3b3 | ||
|
2efba6ea6f | ||
|
e7796f902a | ||
|
b23adf9d5d | ||
|
7345639af3 | ||
|
818629e58b | ||
|
1761289716 | ||
|
5d2fb8791c | ||
|
4fcb1498c2 | ||
|
8d0a420cc8 | ||
|
2e3aa940fb | ||
|
4cdb2f0fa3 | ||
|
79f58f5c6a | ||
|
56d098c317 | ||
|
4ca0ed3779 | ||
|
a3ca8ea5c4 | ||
|
ae479f4a92 | ||
|
ab826363ea | ||
|
e1ce1f2f40 | ||
|
209a727fe9 | ||
|
4d45c07bf2 | ||
|
d053bee8d3 | ||
|
596a46846a | ||
|
0b36daf6ac | ||
|
ffb0fa25f1 | ||
|
8966fd3b23 | ||
|
b6a7104b60 | ||
|
8a92904287 | ||
|
a62bca442e | ||
|
82ad0573be | ||
|
6a74d62e98 | ||
|
5185166e3b | ||
|
693dcf12d6 | ||
|
ee0f847827 | ||
|
55281ed548 | ||
|
2390da1165 | ||
|
06d503152b | ||
|
2e9e69d66c | ||
|
e3d2dd7279 | ||
|
7e8973c9ff | ||
|
78568ef7c0 | ||
|
35ccfb14c2 | ||
|
b2c711a5e8 | ||
|
21da781350 | ||
|
beb04d8181 | ||
|
072a35b4ee | ||
|
735afc64f6 | ||
|
e68f17b463 | ||
|
dfcae92ae0 | ||
|
1ec228e46c | ||
|
d1653ded9a | ||
|
8da581695f | ||
|
9e46ca31dd | ||
|
ad1c8ffe21 | ||
|
6b30e2beb0 | ||
|
d75b7d49b8 | ||
|
53173d9053 | ||
|
e68207be2c | ||
|
146ef1bf49 | ||
|
b178bd6bd6 | ||
|
bd85d9a36a | ||
|
c03b4adec0 | ||
|
b7b16aa33b | ||
|
789a779775 | ||
|
fdc8278b90 | ||
|
525a2ff9a7 | ||
|
78c9e66c1f | ||
|
3038dfdb27 | ||
|
7cd9f644ee | ||
|
64e78329ec | ||
|
e49acda806 | ||
|
b79845fcaa | ||
|
76ef5ef9a9 | ||
|
cc053b148d | ||
|
2979df1d82 | ||
|
b3a0b252ad | ||
|
c305ba3c92 | ||
|
12d4081311 | ||
|
124938ecc9 | ||
|
ea3e8a7459 | ||
|
dc3883ed1a | ||
|
bd69c8f5dd | ||
|
b751861bfb | ||
|
d035523f30 | ||
|
f9906aba7f | ||
|
410695d823 | ||
|
cd198ee1fa | ||
|
2c35c26749 | ||
|
f042d07ee7 | ||
|
4ce5f89c66 | ||
|
e0fa0d1936 | ||
|
424799191c | ||
|
96d65f7d39 | ||
|
98fd08d9e5 | ||
|
faafb45d83 | ||
|
75b3561594 |
14
.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"next/babel",
|
||||
{
|
||||
"preset-env": {
|
||||
"targets": {
|
||||
"browsers": ["> 0.25%, not dead"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
38
.env.template
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
# Your openai api key. (required)
|
||||
OPENAI_API_KEY=sk-xxxx
|
||||
|
||||
# Access passsword, separated by comma. (optional)
|
||||
CODE=your-password
|
||||
|
||||
# You can start service behind a proxy
|
||||
PROXY_URL=http://localhost:7890
|
||||
|
||||
# Override openai api request base url. (optional)
|
||||
# Default: https://api.openai.com
|
||||
# Examples: http://your-openai-proxy.com
|
||||
BASE_URL=
|
||||
|
||||
# Specify OpenAI organization ID.(optional)
|
||||
# Default: Empty
|
||||
OPENAI_ORG_ID=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to use GPT-4, set this value to 1.
|
||||
DISABLE_GPT4=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
HIDE_USER_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do want users to query balance, set this value to 1.
|
||||
ENABLE_BALANCE_QUERY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you want to disable parse settings from url, set this value to 1.
|
||||
DISABLE_FAST_LINK=
|
4
.github/ISSUE_TEMPLATE/功能建议.md
vendored
@@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
|
||||
|
||||
> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
|
||||
|
||||
**这个功能与现有的问题有关吗?**
|
||||
如果有关,请在此列出链接或者描述问题。
|
||||
|
||||
|
10
.github/ISSUE_TEMPLATE/反馈问题.md
vendored
@@ -7,10 +7,18 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
|
||||
|
||||
> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
|
||||
|
||||
**反馈须知**
|
||||
> 请在下方中括号内输入 x 来表示你已经知晓相关内容。
|
||||
|
||||
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。
|
||||
|
||||
请在下方中括号内输入 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) 中搜索了此次反馈的问题,没有找到解答。
|
||||
|
||||
**描述问题**
|
||||
请在此描述你遇到了什么问题。
|
||||
|
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
105
.github/workflows/app.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: Release App
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.result }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
||||
- name: create release
|
||||
id: create-release
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-tauri:
|
||||
needs: create-release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
arch: aarch64
|
||||
rust_target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
arch: x86_64
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.config.rust_target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.config.rust_target }}
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.config.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: install frontend dependencies
|
||||
run: yarn install # change this to npm or pnpm depending on which one you use
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||
|
||||
publish-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-release, build-tauri]
|
||||
|
||||
steps:
|
||||
- name: publish release
|
||||
id: publish-release
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
release_id: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
15
.github/workflows/issue-translator.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Issue Translator
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
|
6
.github/workflows/sync.yml
vendored
@@ -5,7 +5,7 @@ permissions:
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *" # every hour
|
||||
- cron: "0 0 * * *" # every day
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -35,6 +35,6 @@ jobs:
|
||||
- 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]."
|
||||
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0"
|
||||
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates"
|
||||
exit 1
|
||||
|
10
.gitignore
vendored
@@ -36,7 +36,11 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
dev
|
||||
|
||||
public/prompts.json
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# docker-compose env files
|
||||
.env
|
||||
|
||||
*.key
|
||||
*.key.pub
|
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn config set registry 'https://registry.npm.taobao.org'
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com/'
|
||||
RUN yarn install
|
||||
|
||||
FROM base AS builder
|
||||
@@ -41,6 +41,7 @@ COPY --from=builder /app/.next/server ./.next/server
|
||||
EXPOSE 3000
|
||||
|
||||
CMD if [ -n "$PROXY_URL" ]; then \
|
||||
export HOSTNAME="127.0.0.1"; \
|
||||
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
||||
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
||||
port=$(echo $PROXY_URL | cut -d: -f3); \
|
||||
@@ -50,6 +51,8 @@ CMD if [ -n "$PROXY_URL" ]; then \
|
||||
echo "remote_dns_subnet 224" >> $conf; \
|
||||
echo "tcp_read_time_out 15000" >> $conf; \
|
||||
echo "tcp_connect_time_out 8000" >> $conf; \
|
||||
echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
|
||||
echo "localnet ::1/128" >> $conf; \
|
||||
echo "[ProxyList]" >> $conf; \
|
||||
echo "$protocol $host $port" >> $conf; \
|
||||
cat /etc/proxychains.conf; \
|
||||
|
88
LICENSE
@@ -1,75 +1,21 @@
|
||||
版权所有(c)<2023><Zhang Yifei>
|
||||
MIT License
|
||||
|
||||
反996许可证版本1.0
|
||||
Copyright (c) 2023 Zhang Yifei
|
||||
|
||||
在符合下列条件的情况下,
|
||||
特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品”
|
||||
)的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使
|
||||
用、复制,修改,衍生利用、散布,发布和再许可:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不
|
||||
得自行修改。
|
||||
2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或
|
||||
经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和
|
||||
标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可
|
||||
执行,则个人或法人实体必须遵守国际劳工标准的核心公约。
|
||||
3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同
|
||||
意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和
|
||||
标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该
|
||||
等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规
|
||||
情况的有关当局报告或投诉上述违反许可证的行为的权利。
|
||||
|
||||
该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用
|
||||
性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均
|
||||
不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。
|
||||
|
||||
|
||||
------------------------- ENGLISH ------------------------------
|
||||
|
||||
|
||||
Copyright (c) <2023> <Zhang Yifei>
|
||||
|
||||
Anti 996 License Version 1.0 (Draft)
|
||||
|
||||
Permission is hereby granted to any individual or legal entity obtaining a copy
|
||||
of this licensed work (including the source code, documentation and/or related
|
||||
items, hereinafter collectively referred to as the "licensed work"), free of
|
||||
charge, to deal with the licensed work for any purpose, including without
|
||||
limitation, the rights to use, reproduce, modify, prepare derivative works of,
|
||||
publish, distribute and sublicense the licensed work, subject to the following
|
||||
conditions:
|
||||
|
||||
1. The individual or the legal entity must conspicuously display, without
|
||||
modification, this License on each redistributed or derivative copy of the
|
||||
Licensed Work.
|
||||
|
||||
2. The individual or the legal entity must strictly comply with all applicable
|
||||
laws, regulations, rules and standards of the jurisdiction relating to
|
||||
labor and employment where the individual is physically located or where
|
||||
the individual was born or naturalized; or where the legal entity is
|
||||
registered or is operating (whichever is stricter). In case that the
|
||||
jurisdiction has no such laws, regulations, rules and standards or its
|
||||
laws, regulations, rules and standards are unenforceable, the individual
|
||||
or the legal entity are required to comply with Core International Labor
|
||||
Standards.
|
||||
|
||||
3. The individual or the legal entity shall not induce or force its
|
||||
employee(s), whether full-time or part-time, or its independent
|
||||
contractor(s), in any methods, to agree in oral or written form,
|
||||
to directly or indirectly restrict, weaken or relinquish his or
|
||||
her rights or remedies under such laws, regulations, rules and
|
||||
standards relating to labor and employment as mentioned above,
|
||||
no matter whether such written or oral agreement are enforceable
|
||||
under the laws of the said jurisdiction, nor shall such individual
|
||||
or the legal entity limit, in any methods, the rights of its employee(s)
|
||||
or independent contractor(s) from reporting or complaining to the copyright
|
||||
holder or relevant authorities monitoring the compliance of the license
|
||||
about its violation(s) of the said license.
|
||||
|
||||
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION
|
||||
WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
189
README.md
@@ -5,13 +5,25 @@
|
||||
|
||||
English / [简体中文](./README_CN.md)
|
||||
|
||||
One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用。
|
||||
一键免费部署你的跨平台私人 ChatGPT 应用。
|
||||
|
||||
[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
||||
[![Web][Web-image]][web-url]
|
||||
[![Windows][Windows-image]][download-url]
|
||||
[![MacOS][MacOS-image]][download-url]
|
||||
[![Linux][Linux-image]][download-url]
|
||||
|
||||
[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
[Web App](https://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/mortiest_ricky) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
||||
|
||||
[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
|
||||
[web-url]: https://chatgpt.nextweb.fun
|
||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
|
||||
[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
|
||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||
|
||||
[](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)
|
||||
|
||||
@@ -24,50 +36,65 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
## Features
|
||||
|
||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||
- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
|
||||
- Fully compatible with self-deployed llms, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
|
||||
- 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, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||
- [ ] User Prompt: user can edit and save custom prompts to prompt list
|
||||
- [ ] Desktop App with tauri
|
||||
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
|
||||
- [ ] Plugins: support network search, caculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||
- [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)
|
||||
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
||||
- [x] Desktop App with tauri
|
||||
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
|
||||
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||
|
||||
### Not in Plan
|
||||
## What's New
|
||||
|
||||
- User login, accounts, cloud sync
|
||||
- UI text customize
|
||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
|
||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
|
||||
- 🚀 v2.8 now we have a client that runs across all platforms!
|
||||
- 🚀 v2.9.11 you can use azure endpoint now.
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||
- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
|
||||
- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
|
||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
|
||||
- 极快的首屏加载速度(~100kb),支持流式响应
|
||||
- 隐私安全,所有数据保存在用户浏览器本地
|
||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||
- 一键导出聊天记录,完整的 Markdown 支持
|
||||
- 多国语言支持: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)
|
||||
- [ ] 允许用户自行编辑内置 Prompt 列表
|
||||
- [ ] 使用 tauri 打包桌面应用
|
||||
- [ ] 支持自部署的大语言模型
|
||||
- [x] 允许用户自行编辑内置 Prompt 列表
|
||||
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
|
||||
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
||||
- [x] 使用 tauri 打包桌面应用
|
||||
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
|
||||
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 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
|
||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -98,7 +125,9 @@ We recommend that you follow the steps below to re-deploy:
|
||||
|
||||
### Enable Automatic Updates
|
||||
|
||||
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:
|
||||
> 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:
|
||||
|
||||

|
||||
|
||||
@@ -106,9 +135,9 @@ After forking the project, due to the limitations imposed by Github, you need to
|
||||
|
||||
### 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.
|
||||
If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
||||
|
||||
You can star or watch this project or follow author to get release notifictions in time.
|
||||
You can star or watch this project or follow author to get release notifications in time.
|
||||
|
||||
## Access Password
|
||||
|
||||
@@ -126,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project
|
||||
|
||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
|
||||
|
||||
### `CODE` (optional)
|
||||
|
||||
Access password, separated by comma.
|
||||
|
||||
### `OPENAI_API_KEY` (required)
|
||||
|
||||
Your openai api key.
|
||||
|
||||
### `CODE` (optional)
|
||||
|
||||
Access passsword, separated by comma.
|
||||
|
||||
### `BASE_URL` (optional)
|
||||
|
||||
> Default: `https://api.openai.com`
|
||||
@@ -142,6 +171,59 @@ Access passsword, separated by comma.
|
||||
|
||||
Override openai api request base url.
|
||||
|
||||
### `OPENAI_ORG_ID` (optional)
|
||||
|
||||
Specify OpenAI organization ID.
|
||||
|
||||
### `AZURE_URL` (optional)
|
||||
|
||||
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
|
||||
Azure deploy url.
|
||||
|
||||
### `AZURE_API_KEY` (optional)
|
||||
|
||||
Azure Api Key.
|
||||
|
||||
### `AZURE_API_VERSION` (optional)
|
||||
|
||||
Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to input their own API key, set this value to 1.
|
||||
|
||||
### `DISABLE_GPT4` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
### `ENABLE_BALANCE_QUERY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do want users to query balance, set this value to 1, or you should set it to 0.
|
||||
|
||||
### `DISABLE_FAST_LINK` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you want to disable parse settings from url, set this to 1.
|
||||
|
||||
### `CUSTOM_MODELS` (optional)
|
||||
|
||||
> Default: Empty
|
||||
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
|
||||
|
||||
To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name:displayName` to customize model name, separated by comma.
|
||||
|
||||
## Requirements
|
||||
|
||||
NodeJS >= 18, Docker >= 20
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
@@ -152,6 +234,9 @@ Before starting development, you must create a new `.env.local` file at project
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
# if you are not able to access openai service, use this BASE_URL
|
||||
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
|
||||
```
|
||||
|
||||
### Local Development
|
||||
@@ -174,8 +259,8 @@ yarn dev
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="your-password" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=your-password \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -183,24 +268,48 @@ You can start service behind a proxy:
|
||||
|
||||
```shell
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="your-password" \
|
||||
-e PROXY_URL="http://localhost:7890" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=your-password \
|
||||
-e PROXY_URL=http://localhost:7890 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
If your proxy needs password, use:
|
||||
|
||||
```shell
|
||||
-e PROXY_URL="http://127.0.0.1:7890 user pass"
|
||||
```
|
||||
|
||||
### Shell
|
||||
|
||||
```shell
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
## Synchronizing Chat Records (UpStash)
|
||||
|
||||
| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
> Please go to the [docs][./docs] directory for more documentation instructions.
|
||||
|
||||
- [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md)
|
||||
- [Frequent Ask Questions](./docs/faq-en.md)
|
||||
- [How to add a new translation](./docs/translation.md)
|
||||
- [How to use Vercel (No English)](./docs/vercel-cn.md)
|
||||
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Translation
|
||||
|
||||
If you want to add a new translation, read this [document](./docs/translation.md).
|
||||
|
||||
## Donation
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
||||
@@ -221,6 +330,20 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
[@chazzhou](https://github.com/chazzhou)
|
||||
[@hauy](https://github.com/hauy)
|
||||
[@Corwin006](https://github.com/Corwin006)
|
||||
[@yankunsong](https://github.com/yankunsong)
|
||||
[@ypwhs](https://github.com/ypwhs)
|
||||
[@fxxxchao](https://github.com/fxxxchao)
|
||||
[@hotic](https://github.com/hotic)
|
||||
[@WingCH](https://github.com/WingCH)
|
||||
[@jtung4](https://github.com/jtung4)
|
||||
[@micozhu](https://github.com/micozhu)
|
||||
[@jhansion](https://github.com/jhansion)
|
||||
[@Sha1rholder](https://github.com/Sha1rholder)
|
||||
[@AnsonHyq](https://github.com/AnsonHyq)
|
||||
[@synwith](https://github.com/synwith)
|
||||
[@piksonGit](https://github.com/piksonGit)
|
||||
[@ouyangzhiping](https://github.com/ouyangzhiping)
|
||||
[@wenjiavv](https://github.com/wenjiavv)
|
||||
|
||||
### Contributor
|
||||
|
||||
@@ -228,4 +351,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
|
||||
## LICENSE
|
||||
|
||||
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
|
96
README_CN.md
@@ -15,16 +15,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||
- 精心设计的 UI,响应式设计,支持深色模式
|
||||
- 极快的首屏加载速度(~100kb)
|
||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||
- 一键导出聊天记录,完整的 Markdown 支持
|
||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||
|
||||
## 开始使用
|
||||
|
||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||
@@ -44,6 +34,8 @@
|
||||
|
||||
### 打开自动更新
|
||||
|
||||
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
|
||||
|
||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
|
||||
|
||||

|
||||
@@ -72,7 +64,7 @@ code1,code2,code3
|
||||
|
||||
## 环境变量
|
||||
|
||||
> 本项目大多数配置项都通过环境变量来设置。
|
||||
> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。
|
||||
|
||||
### `OPENAI_API_KEY` (必填项)
|
||||
|
||||
@@ -94,9 +86,47 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
|
||||
|
||||
## 开发
|
||||
### `OPENAI_ORG_ID` (可选)
|
||||
|
||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
|
||||
指定 OpenAI 中的组织 ID。
|
||||
|
||||
### `AZURE_URL` (可选)
|
||||
|
||||
> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
|
||||
Azure 部署地址。
|
||||
|
||||
### `AZURE_API_KEY` (可选)
|
||||
|
||||
Azure 密钥。
|
||||
|
||||
### `AZURE_API_VERSION` (可选)
|
||||
|
||||
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||
|
||||
### `DISABLE_GPT4` (可选)
|
||||
|
||||
如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
|
||||
|
||||
### `ENABLE_BALANCE_QUERY` (可选)
|
||||
|
||||
如果你想启用余额查询功能,将此环境变量设置为 1 即可。
|
||||
|
||||
### `DISABLE_FAST_LINK` (可选)
|
||||
|
||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||
|
||||
### `CUSTOM_MODELS` (可选)
|
||||
|
||||
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
|
||||
|
||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||
|
||||
## 开发
|
||||
|
||||
点击下方按钮,开始二次开发:
|
||||
|
||||
@@ -106,25 +136,31 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||
BASE_URL=https://a.nextweb.fun/api/proxy
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT;
|
||||
2. 执行 `yarn install && yarn dev` 即可。
|
||||
1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
|
||||
2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
|
||||
3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
|
||||
|
||||
## 部署
|
||||
|
||||
### 容器部署 (推荐)
|
||||
|
||||
> 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
|
||||
> 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="页面访问密码" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=页面访问密码 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -132,13 +168,21 @@ docker run -d -p 3000:3000 \
|
||||
|
||||
```shell
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="页面访问密码" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=页面访问密码 \
|
||||
--net=host \
|
||||
-e PROXY_URL="http://127.0.0.1:7890" \
|
||||
-e PROXY_URL=http://127.0.0.1:7890 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
如果你的本地代理需要账号密码,可以使用:
|
||||
|
||||
```shell
|
||||
-e PROXY_URL="http://127.0.0.1:7890 user password"
|
||||
```
|
||||
|
||||
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
|
||||
|
||||
### 本地部署
|
||||
|
||||
在控制台运行下方命令:
|
||||
@@ -147,6 +191,8 @@ docker run -d -p 3000:3000 \
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
### 捐赠者
|
||||
@@ -157,8 +203,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
|
||||
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
|
||||
### 相关项目
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
|
||||
|
||||
## 开源协议
|
||||
|
||||
> 反对 996,从我开始。
|
||||
|
||||
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
|
71
app/api/auth.ts
Normal 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";
|
||||
|
||||
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 } = parseApiKey(authToken);
|
||||
|
||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
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) && !apiKey) {
|
||||
return {
|
||||
error: true,
|
||||
msg: !accessCode ? "empty access code" : "wrong access code",
|
||||
};
|
||||
}
|
||||
|
||||
// if user does not provide an api key, inject system api key
|
||||
if (!apiKey) {
|
||||
const serverApiKey = serverConfig.isAzure
|
||||
? serverConfig.azureApiKey
|
||||
: serverConfig.apiKey;
|
||||
|
||||
if (serverApiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set(
|
||||
"Authorization",
|
||||
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
|
||||
);
|
||||
} else {
|
||||
console.log("[Auth] admin did not provide an api key");
|
||||
}
|
||||
} else {
|
||||
console.log("[Auth] use user api key");
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
};
|
||||
}
|
@@ -1,64 +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, { stream: true }));
|
||||
}
|
||||
},
|
||||
});
|
||||
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);
|
||||
return new Response(
|
||||
["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
};
|
@@ -1,29 +1,117 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
|
||||
import { collectModelTable } from "../utils/model";
|
||||
import { makeAzurePath } from "../azure";
|
||||
|
||||
const OPENAI_URL = "api.openai.com";
|
||||
const DEFAULT_PROTOCOL = "https";
|
||||
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
|
||||
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function requestOpenai(req: NextRequest) {
|
||||
const apiKey = req.headers.get("token");
|
||||
const openaiPath = req.headers.get("path");
|
||||
const controller = new AbortController();
|
||||
|
||||
let baseUrl = BASE_URL;
|
||||
const authValue = req.headers.get("Authorization") ?? "";
|
||||
const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization";
|
||||
|
||||
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
"/api/openai/",
|
||||
"",
|
||||
);
|
||||
|
||||
let baseUrl =
|
||||
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
return fetch(`${baseUrl}/${openaiPath}`, {
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
if (serverConfig.isAzure) {
|
||||
if (!serverConfig.azureApiVersion) {
|
||||
return NextResponse.json({
|
||||
error: true,
|
||||
message: `missing AZURE_API_VERSION in server env vars`,
|
||||
});
|
||||
}
|
||||
path = makeAzurePath(path, serverConfig.azureApiVersion);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Cache-Control": "no-store",
|
||||
[authHeaderName]: authValue,
|
||||
...(serverConfig.openaiOrgId && {
|
||||
"OpenAI-Organization": serverConfig.openaiOrgId,
|
||||
}),
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
});
|
||||
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse gpt4 request
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const modelTable = collectModelTable(
|
||||
DEFAULT_MODELS,
|
||||
serverConfig.customModels,
|
||||
);
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (modelTable[jsonBody?.model ?? ""].available === false) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] gpt4 filter", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
@@ -1,21 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getServerSideConfig } from "../../config/server";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// Danger! Don not write any secret value here!
|
||||
// Danger! Do not hard code any secret value here!
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
hideUserApiKey: serverConfig.hideUserApiKey,
|
||||
disableGPT4: serverConfig.disableGPT4,
|
||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||
disableFastLink: serverConfig.disableFastLink,
|
||||
customModels: serverConfig.customModels,
|
||||
};
|
||||
|
||||
declare global {
|
||||
type DangerConfig = typeof DANGER_CONFIG;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({
|
||||
needCode: serverConfig.needCode,
|
||||
});
|
||||
async function handle() {
|
||||
return NextResponse.json(DANGER_CONFIG);
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
|
43
app/api/cors/[...path]/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const [protocol, ...subpath] = params.path;
|
||||
const targetUrl = `${protocol}://${subpath.join("/")}`;
|
||||
|
||||
const method = req.headers.get("method") ?? undefined;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
export const runtime = "nodejs";
|
77
app/api/openai/[...path]/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { OpenaiPath } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { requestOpenai } from "../../common";
|
||||
|
||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||
|
||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
const config = getServerSideConfig();
|
||||
|
||||
if (config.disableGPT4) {
|
||||
remoteModelRes.data = remoteModelRes.data.filter(
|
||||
(m) => !m.id.startsWith("gpt-4"),
|
||||
);
|
||||
}
|
||||
|
||||
return remoteModelRes;
|
||||
}
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[OpenAI Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + subpath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const authResult = auth(req);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await requestOpenai(req);
|
||||
|
||||
// list models
|
||||
if (subpath === OpenaiPath.ListModelPath && response.status === 200) {
|
||||
const resJson = (await response.json()) as OpenAIListModelResponse;
|
||||
const availableModels = getModels(resJson);
|
||||
return NextResponse.json(availableModels, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
@@ -1,31 +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");
|
||||
res.headers.set("Cache-Control", "no-cache");
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", req.body, e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: JSON.stringify(e),
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
import type {
|
||||
CreateChatCompletionRequest,
|
||||
CreateChatCompletionResponse,
|
||||
} from "openai";
|
||||
|
||||
export type ChatRequest = CreateChatCompletionRequest;
|
||||
export type ChatResponse = CreateChatCompletionResponse;
|
9
app/azure.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function makeAzurePath(path: string, apiVersion: string) {
|
||||
// should omit /v1 prefix
|
||||
path = path.replaceAll("v1/", "");
|
||||
|
||||
// should add api-key to query string
|
||||
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
|
||||
|
||||
return path;
|
||||
}
|
156
app/client/api.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
|
||||
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
|
||||
export type ChatModel = ModelType;
|
||||
|
||||
export interface RequestMessage {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
model: string;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
stream?: boolean;
|
||||
presence_penalty?: number;
|
||||
frequency_penalty?: number;
|
||||
}
|
||||
|
||||
export interface ChatOptions {
|
||||
messages: RequestMessage[];
|
||||
config: LLMConfig;
|
||||
|
||||
onUpdate?: (message: string, chunk: string) => void;
|
||||
onFinish: (message: string) => void;
|
||||
onError?: (err: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
}
|
||||
|
||||
export interface LLMUsage {
|
||||
used: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LLMModel {
|
||||
name: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export abstract class LLMApi {
|
||||
abstract chat(options: ChatOptions): Promise<void>;
|
||||
abstract usage(): Promise<LLMUsage>;
|
||||
abstract models(): Promise<LLMModel[]>;
|
||||
abstract speech(input: string): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
type ProviderName = "openai" | "azure" | "claude" | "palm";
|
||||
|
||||
interface Model {
|
||||
name: string;
|
||||
provider: ProviderName;
|
||||
ctxlen: number;
|
||||
}
|
||||
|
||||
interface ChatProvider {
|
||||
name: ProviderName;
|
||||
apiConfig: {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
summaryModel: Model;
|
||||
};
|
||||
models: Model[];
|
||||
|
||||
chat: () => void;
|
||||
usage: () => void;
|
||||
}
|
||||
|
||||
export class ClientApi {
|
||||
public llm: LLMApi;
|
||||
|
||||
constructor() {
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
|
||||
config() {}
|
||||
|
||||
prompts() {}
|
||||
|
||||
masks() {}
|
||||
|
||||
async share(messages: ChatMessage[], avatarUrl: string | null = null) {
|
||||
const msgs = messages
|
||||
.map((m) => ({
|
||||
from: m.role === "user" ? "human" : "gpt",
|
||||
value: m.content,
|
||||
}))
|
||||
.concat([
|
||||
{
|
||||
from: "human",
|
||||
value:
|
||||
"Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web",
|
||||
},
|
||||
]);
|
||||
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
|
||||
// Please do not modify this message
|
||||
|
||||
console.log("[Share]", messages, msgs);
|
||||
const clientConfig = getClientConfig();
|
||||
const proxyUrl = "/sharegpt";
|
||||
const rawUrl = "https://sharegpt.com/api/conversations";
|
||||
const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
|
||||
const res = await fetch(shareUrl, {
|
||||
body: JSON.stringify({
|
||||
avatarUrl,
|
||||
items: msgs,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const resJson = await res.json();
|
||||
console.log("[Share]", resJson);
|
||||
if (resJson.id) {
|
||||
return `https://shareg.pt/${resJson.id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ClientApi();
|
||||
|
||||
export function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
};
|
||||
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
const authHeader = isAzure ? "api-key" : "Authorization";
|
||||
const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
|
||||
|
||||
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
|
||||
const validString = (x: string) => x && x.length > 0;
|
||||
|
||||
// use user's api key first
|
||||
if (validString(apiKey)) {
|
||||
headers[authHeader] = makeBearer(apiKey);
|
||||
} else if (
|
||||
accessStore.enabledAccessControl() &&
|
||||
validString(accessStore.accessCode)
|
||||
) {
|
||||
headers[authHeader] = makeBearer(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
37
app/client/controller.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// To store message streaming controller
|
||||
export const ChatControllerPool = {
|
||||
controllers: {} as Record<string, AbortController>,
|
||||
|
||||
addController(
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
controller: AbortController,
|
||||
) {
|
||||
const key = this.key(sessionId, messageId);
|
||||
this.controllers[key] = controller;
|
||||
return key;
|
||||
},
|
||||
|
||||
stop(sessionId: string, messageId: string) {
|
||||
const key = this.key(sessionId, messageId);
|
||||
const controller = this.controllers[key];
|
||||
controller?.abort();
|
||||
},
|
||||
|
||||
stopAll() {
|
||||
Object.values(this.controllers).forEach((v) => v.abort());
|
||||
},
|
||||
|
||||
hasPending() {
|
||||
return Object.values(this.controllers).length > 0;
|
||||
},
|
||||
|
||||
remove(sessionId: string, messageId: string) {
|
||||
const key = this.key(sessionId, messageId);
|
||||
delete this.controllers[key];
|
||||
},
|
||||
|
||||
key(sessionId: string, messageIndex: string) {
|
||||
return `${sessionId},${messageIndex}`;
|
||||
},
|
||||
};
|
330
app/client/platforms/openai.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { makeAzurePath } from "@/app/azure";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class ChatGPTApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
|
||||
if (isAzure && !accessStore.isValidAzure()) {
|
||||
throw Error(
|
||||
"incomplete azure config, please check it in your settings page",
|
||||
);
|
||||
}
|
||||
|
||||
let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
if (isAzure) {
|
||||
path = makeAzurePath(path, accessStore.azureApiVersion);
|
||||
}
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: v.content,
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(OpenaiPath.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let finished = false;
|
||||
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
options.onFinish(responseText);
|
||||
finished = true;
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[OpenAI] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text) as {
|
||||
choices: Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
const delta = json.choices[0]?.delta?.content;
|
||||
if (delta) {
|
||||
responseText += delta;
|
||||
options.onUpdate?.(responseText, delta);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
const formatDate = (d: Date) =>
|
||||
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
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(new Date(Date.now() + ONE_DAY));
|
||||
|
||||
const [used, subs] = await Promise.all([
|
||||
fetch(
|
||||
this.path(
|
||||
`${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
},
|
||||
),
|
||||
fetch(this.path(OpenaiPath.SubsPath), {
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
}),
|
||||
]);
|
||||
|
||||
if (used.status === 401) {
|
||||
throw new Error(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (!used.ok || !subs.ok) {
|
||||
throw new Error("Failed to query usage from openai");
|
||||
}
|
||||
|
||||
const response = (await used.json()) as {
|
||||
total_usage?: number;
|
||||
error?: {
|
||||
type: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const total = (await subs.json()) as {
|
||||
hard_limit_usd?: number;
|
||||
};
|
||||
|
||||
if (response.error && response.error.type) {
|
||||
throw Error(response.error.message);
|
||||
}
|
||||
|
||||
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,
|
||||
total: total.hard_limit_usd,
|
||||
} as LLMUsage;
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
if (this.disableListModels) {
|
||||
return DEFAULT_MODELS.slice();
|
||||
}
|
||||
|
||||
const res = await fetch(this.path(OpenaiPath.ListModelPath), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
const resJson = (await res.json()) as OpenAIListModelResponse;
|
||||
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
|
||||
console.log("[Models]", chatModels);
|
||||
|
||||
if (!chatModels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return chatModels.map((m) => ({
|
||||
name: m.id,
|
||||
available: true,
|
||||
}));
|
||||
}
|
||||
|
||||
public cache: Record<string, ArrayBuffer> = {};
|
||||
|
||||
async speech(input: string): Promise<ArrayBuffer> {
|
||||
if (this.cache[input]) return this.cache[input].slice(0);
|
||||
|
||||
const res = await fetch(this.path(OpenaiPath.Speech), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "tts-1",
|
||||
input: input,
|
||||
voice: "onyx",
|
||||
}),
|
||||
});
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
this.cache[input] = arrayBuffer.slice(0);
|
||||
return arrayBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
export { OpenaiPath };
|
75
app/command.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import Locale from "./locales";
|
||||
|
||||
type Command = (param: string) => void;
|
||||
interface Commands {
|
||||
fill?: Command;
|
||||
submit?: Command;
|
||||
mask?: Command;
|
||||
code?: Command;
|
||||
settings?: Command;
|
||||
}
|
||||
|
||||
export function useCommand(commands: Commands = {}) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, commands]);
|
||||
}
|
||||
|
||||
interface ChatCommands {
|
||||
new?: Command;
|
||||
newm?: Command;
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
del?: Command;
|
||||
}
|
||||
|
||||
export const ChatCommandPrefix = ":";
|
||||
|
||||
export function useChatCommand(commands: ChatCommands = {}) {
|
||||
function extract(userInput: string) {
|
||||
return (
|
||||
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
||||
) as keyof ChatCommands;
|
||||
}
|
||||
|
||||
function search(userInput: string) {
|
||||
const input = extract(userInput);
|
||||
const desc = Locale.Chat.Commands;
|
||||
return Object.keys(commands)
|
||||
.filter((c) => c.startsWith(input))
|
||||
.map((c) => ({
|
||||
title: desc[c as keyof ChatCommands],
|
||||
content: ChatCommandPrefix + c,
|
||||
}));
|
||||
}
|
||||
|
||||
function match(userInput: string) {
|
||||
const command = extract(userInput);
|
||||
const matched = typeof commands[command] === "function";
|
||||
|
||||
return {
|
||||
matched,
|
||||
invoke: () => matched && commands[command]!(userInput),
|
||||
};
|
||||
}
|
||||
|
||||
return { match, search };
|
||||
}
|
36
app/components/auth.module.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
.auth-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.auth-logo {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.auth-tips {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
margin: 3vh 0;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
86
app/components/auth.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import styles from "./auth.module.scss";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { useAccessStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import { useEffect } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function AuthPage() {
|
||||
const navigate = useNavigate();
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const goHome = () => navigate(Path.Home);
|
||||
const goChat = () => navigate(Path.Chat);
|
||||
const resetAccessCode = () => {
|
||||
accessStore.update((access) => {
|
||||
access.openaiApiKey = "";
|
||||
access.accessCode = "";
|
||||
});
|
||||
}; // Reset access code to empty string
|
||||
|
||||
useEffect(() => {
|
||||
if (getClientConfig()?.isApp) {
|
||||
navigate(Path.Settings);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles["auth-page"]}>
|
||||
<div className={`no-dark ${styles["auth-logo"]}`}>
|
||||
<BotIcon />
|
||||
</div>
|
||||
|
||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Auth.Input}
|
||||
value={accessStore.accessCode}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className={styles["auth-actions"]}>
|
||||
<IconButton
|
||||
text={Locale.Auth.Confirm}
|
||||
type="primary"
|
||||
onClick={goChat}
|
||||
/>
|
||||
<IconButton
|
||||
text={Locale.Auth.Later}
|
||||
onClick={() => {
|
||||
resetAccessCode();
|
||||
goHome();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -18,6 +18,35 @@
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
path {
|
||||
fill: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: rgba($color: red, $alpha: 0.8);
|
||||
border-color: rgba($color: red, $alpha: 0.5);
|
||||
background-color: rgba($color: red, $alpha: 0.05);
|
||||
|
||||
&:hover {
|
||||
border-color: red;
|
||||
background-color: rgba($color: red, $alpha: 0.1);
|
||||
}
|
||||
|
||||
path {
|
||||
fill: red !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
@@ -28,10 +57,6 @@
|
||||
border: var(--border-in-light);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.icon-button-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -47,9 +72,12 @@
|
||||
}
|
||||
|
||||
.icon-button-text {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
@@ -2,16 +2,20 @@ import * as React from "react";
|
||||
|
||||
import styles from "./button.module.scss";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
export function IconButton(props: {
|
||||
onClick?: () => void;
|
||||
icon: JSX.Element;
|
||||
icon?: JSX.Element;
|
||||
type?: ButtonType;
|
||||
text?: string;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
noDark?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
@@ -19,18 +23,26 @@ export function IconButton(props: {
|
||||
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"
|
||||
tabIndex={props.tabIndex}
|
||||
autoFocus={props.autoFocus}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
import {
|
||||
DragDropContext,
|
||||
@@ -10,7 +12,13 @@ import {
|
||||
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";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useMobileScreen } from "../utils";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@@ -19,9 +27,19 @@ export function ChatItem(props: {
|
||||
count: number;
|
||||
time: string;
|
||||
selected: boolean;
|
||||
id: number;
|
||||
id: string;
|
||||
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 (
|
||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||
{(provided) => (
|
||||
@@ -30,18 +48,48 @@ export function ChatItem(props: {
|
||||
props.selected && styles["chat-item-selected"]
|
||||
}`}
|
||||
onClick={props.onClick}
|
||||
ref={provided.innerRef}
|
||||
ref={(ele) => {
|
||||
draggableRef.current = ele;
|
||||
provided.innerRef(ele);
|
||||
}}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
|
||||
props.count,
|
||||
)}`}
|
||||
>
|
||||
<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)}
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>
|
||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["chat-item-narrow-count"]}>
|
||||
{props.count}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||
</div>
|
||||
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||
<div className={styles["chat-item-info"]}>
|
||||
<div className={styles["chat-item-count"]}>
|
||||
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles["chat-item-delete"]}
|
||||
onClickCapture={(e) => {
|
||||
props.onDelete?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,16 +98,18 @@ export function ChatItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatList() {
|
||||
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
|
||||
useChatStore((state) => [
|
||||
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 isMobileScreen = useMobileScreen();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
const { destination, source } = result;
|
||||
@@ -89,14 +139,26 @@ export function ChatList() {
|
||||
{sessions.map((item, i) => (
|
||||
<ChatItem
|
||||
title={item.topic}
|
||||
time={item.lastUpdate}
|
||||
time={new Date(item.lastUpdate).toLocaleString()}
|
||||
count={item.messages.length}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
index={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={() => chatStore.deleteSession(i)}
|
||||
onClick={() => {
|
||||
navigate(Path.Chat);
|
||||
selectSession(i);
|
||||
}}
|
||||
onDelete={async () => {
|
||||
if (
|
||||
(!props.narrow && !isMobileScreen) ||
|
||||
(await showConfirm(Locale.Home.DeleteChat))
|
||||
) {
|
||||
chatStore.deleteSession(i);
|
||||
}
|
||||
}}
|
||||
narrow={props.narrow}
|
||||
mask={item.mask}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
@@ -14,13 +14,43 @@
|
||||
padding: 4px 10px;
|
||||
animation: slide-in ease 0.3s;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all ease 0.3s;
|
||||
margin-bottom: 10px;
|
||||
transition: width ease 0.3s;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
width: var(--icon-width);
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
transition: all ease 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--delay: 0.5s;
|
||||
width: var(--full-width);
|
||||
transition-delay: var(--delay);
|
||||
|
||||
.text {
|
||||
transition-delay: var(--delay);
|
||||
opacity: 1;
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +83,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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-insert {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
opacity: 0.2;
|
||||
transition: all ease 0.3s;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.context-prompt-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
.context-drag {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.context-drag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.5;
|
||||
transition: all ease 0.3s;
|
||||
}
|
||||
|
||||
.context-role {
|
||||
margin-right: 10px;
|
||||
@@ -81,27 +155,364 @@
|
||||
}
|
||||
|
||||
.memory-prompt {
|
||||
margin-top: 20px;
|
||||
|
||||
.memory-prompt-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.memory-prompt-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-context {
|
||||
margin: 20px 0 0 0;
|
||||
padding: 4px 0;
|
||||
|
||||
border-top: var(--border-in-light);
|
||||
border-bottom: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow) inset;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: var(--black);
|
||||
transition: all ease 0.3s;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
$linear: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
mask-image: $linear;
|
||||
|
||||
@mixin show {
|
||||
transform: translateY(0);
|
||||
position: relative;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@mixin hide {
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
transition: all ease 0.1s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-tips {
|
||||
@include show;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&-revert-btn {
|
||||
color: var(--primary);
|
||||
@include hide;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
border-color: var(--primary);
|
||||
|
||||
.clear-context-tips {
|
||||
@include hide;
|
||||
}
|
||||
|
||||
.clear-context-revert-btn {
|
||||
@include show;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.chat-body-main-title {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.chat-body-title {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&:last-child {
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-message-header {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-header {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.chat-message-actions {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
transition: all ease 0.3s;
|
||||
transform: scale(0.9) translateY(5px);
|
||||
margin: 0 10px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
max-width: var(--message-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&:hover {
|
||||
.chat-message-edit {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chat-message-actions {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-avatar {
|
||||
position: relative;
|
||||
|
||||
.chat-message-edit {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
button {
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
/* Specific styles for iOS devices */
|
||||
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.chat-message-edit {
|
||||
top: -8%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-status {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat-message-item {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
border: var(--border-in-light);
|
||||
position: relative;
|
||||
transition: all ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-message-action-date {
|
||||
font-size: 12px;
|
||||
opacity: 0.2;
|
||||
white-space: nowrap;
|
||||
transition: all ease 0.6s;
|
||||
color: var(--black);
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container > .chat-message-item {
|
||||
background-color: var(--second);
|
||||
|
||||
&:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
border-top: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
|
||||
.chat-input-actions {
|
||||
.chat-input-action {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin single-line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prompt-hints {
|
||||
min-height: 20px;
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
background-color: var(--white);
|
||||
border: var(--border-in-light);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.prompt-hint {
|
||||
color: var(--black);
|
||||
padding: 6px 10px;
|
||||
animation: slide-in ease 0.3s;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.3s;
|
||||
border: transparent 1px solid;
|
||||
margin: 4px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.hint-title {
|
||||
font-size: 12px;
|
||||
font-weight: bolder;
|
||||
|
||||
@include single-line();
|
||||
}
|
||||
.hint-content {
|
||||
font-size: 12px;
|
||||
|
||||
@include single-line();
|
||||
}
|
||||
|
||||
&-selected,
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-panel-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
padding: 10px 90px 10px 14px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.chat-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
59
app/components/emoji.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -1,7 +1,11 @@
|
||||
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 { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -20,6 +24,15 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
this.setState({ hasError: true, error, info });
|
||||
}
|
||||
|
||||
clearAndSaveData() {
|
||||
try {
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Render error message
|
||||
@@ -31,13 +44,25 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
<code>{this.state.info?.componentStack}</code>
|
||||
</pre>
|
||||
|
||||
<a href={ISSUE_URL} className="report">
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<a href={ISSUE_URL} className="report">
|
||||
<IconButton
|
||||
text="Report This Error"
|
||||
icon={<GithubIcon />}
|
||||
bordered
|
||||
/>
|
||||
</a>
|
||||
<IconButton
|
||||
text="Report This Error"
|
||||
icon={<GithubIcon />}
|
||||
icon={<ResetIcon />}
|
||||
text="Clear All Data"
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
|
||||
this.clearAndSaveData();
|
||||
}
|
||||
}}
|
||||
bordered
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
218
app/components/exporter.module.scss
Normal file
@@ -0,0 +1,218 @@
|
||||
.message-exporter {
|
||||
&-body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.export-content {
|
||||
white-space: break-spaces;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.steps {
|
||||
background-color: var(--gray);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
box-shadow: var(--card-shadow) inset;
|
||||
|
||||
.steps-progress {
|
||||
$padding: 5px;
|
||||
height: calc(100% - 2 * $padding);
|
||||
width: calc(100% - 2 * $padding);
|
||||
position: absolute;
|
||||
top: $padding;
|
||||
left: $padding;
|
||||
|
||||
&-inner {
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-color: var(--white);
|
||||
transition: all ease 0.3s;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.steps-inner {
|
||||
display: flex;
|
||||
transform: scale(1);
|
||||
|
||||
.step {
|
||||
flex-grow: 1;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
color: var(--black);
|
||||
opacity: 0.5;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
$radius: 8px;
|
||||
|
||||
&-finished {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&-current {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.step-index {
|
||||
background-color: var(--gray);
|
||||
border: var(--border-in-light);
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
padding: 0px 5px;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-previewer {
|
||||
.preview-body {
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: var(--card-shadow) inset;
|
||||
background-color: var(--gray);
|
||||
|
||||
.chat-info {
|
||||
background-color: var(--second);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.icons {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 50%;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icons {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-space {
|
||||
font-size: 12px;
|
||||
margin: 0 10px;
|
||||
font-weight: bolder;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-info-item {
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
padding: 2px 15px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--card-shadow);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
|
||||
.avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.body {
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
max-width: calc(100% - 104px);
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
|
||||
code,
|
||||
pre {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-assistant {
|
||||
.body {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&-user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: var(--second);
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-theme {
|
||||
}
|
||||
}
|
662
app/components/exporter.tsx
Normal file
@@ -0,0 +1,662 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
import styles from "./exporter.module.scss";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
Select,
|
||||
showImageModal,
|
||||
showModal,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import { IconButton } from "./button";
|
||||
import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
|
||||
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.png";
|
||||
import ShareIcon from "../icons/share.svg";
|
||||
import BotIcon from "../icons/bot.png";
|
||||
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { MessageSelector, useMessageSelector } from "./message-selector";
|
||||
import { Avatar } from "./emoji";
|
||||
import dynamic from "next/dynamic";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import { toBlob, toPng } from "html-to-image";
|
||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
||||
import { api } from "../client/api";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
|
||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Export.Title}
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
{Locale.Exporter.Description.Title}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ minHeight: "40vh" }}>
|
||||
<MessageExporter />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useSteps(
|
||||
steps: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
}>,
|
||||
) {
|
||||
const stepCount = steps.length;
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const nextStep = () =>
|
||||
setCurrentStepIndex((currentStepIndex + 1) % stepCount);
|
||||
const prevStep = () =>
|
||||
setCurrentStepIndex((currentStepIndex - 1 + stepCount) % stepCount);
|
||||
|
||||
return {
|
||||
currentStepIndex,
|
||||
setCurrentStepIndex,
|
||||
nextStep,
|
||||
prevStep,
|
||||
currentStep: steps[currentStepIndex],
|
||||
};
|
||||
}
|
||||
|
||||
function Steps<
|
||||
T extends {
|
||||
name: string;
|
||||
value: string;
|
||||
}[],
|
||||
>(props: { steps: T; onStepChange?: (index: number) => void; index: number }) {
|
||||
const steps = props.steps;
|
||||
const stepCount = steps.length;
|
||||
|
||||
return (
|
||||
<div className={styles["steps"]}>
|
||||
<div className={styles["steps-progress"]}>
|
||||
<div
|
||||
className={styles["steps-progress-inner"]}
|
||||
style={{
|
||||
width: `${((props.index + 1) / stepCount) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className={styles["steps-inner"]}>
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`${styles["step"]} ${
|
||||
styles[i <= props.index ? "step-finished" : ""]
|
||||
} ${i === props.index && styles["step-current"]} clickable`}
|
||||
onClick={() => {
|
||||
props.onStepChange?.(i);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
<span className={styles["step-index"]}>{i + 1}</span>
|
||||
<span className={styles["step-name"]}>{step.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageExporter() {
|
||||
const steps = [
|
||||
{
|
||||
name: Locale.Export.Steps.Select,
|
||||
value: "select",
|
||||
},
|
||||
{
|
||||
name: Locale.Export.Steps.Preview,
|
||||
value: "preview",
|
||||
},
|
||||
];
|
||||
const { currentStep, setCurrentStepIndex, currentStepIndex } =
|
||||
useSteps(steps);
|
||||
const formats = ["text", "image", "json"] as const;
|
||||
type ExportFormat = (typeof formats)[number];
|
||||
|
||||
const [exportConfig, setExportConfig] = useState({
|
||||
format: "image" as ExportFormat,
|
||||
includeContext: true,
|
||||
});
|
||||
|
||||
function updateExportConfig(updater: (config: typeof exportConfig) => void) {
|
||||
const config = { ...exportConfig };
|
||||
updater(config);
|
||||
setExportConfig(config);
|
||||
}
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const { selection, updateSelection } = useMessageSelector();
|
||||
const selectedMessages = useMemo(() => {
|
||||
const ret: ChatMessage[] = [];
|
||||
if (exportConfig.includeContext) {
|
||||
ret.push(...session.mask.context);
|
||||
}
|
||||
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||
return ret;
|
||||
}, [
|
||||
exportConfig.includeContext,
|
||||
session.messages,
|
||||
session.mask.context,
|
||||
selection,
|
||||
]);
|
||||
function preview() {
|
||||
if (exportConfig.format === "text") {
|
||||
return (
|
||||
<MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
} else if (exportConfig.format === "json") {
|
||||
return (
|
||||
<JsonPreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Steps
|
||||
steps={steps}
|
||||
index={currentStepIndex}
|
||||
onStepChange={setCurrentStepIndex}
|
||||
/>
|
||||
<div
|
||||
className={styles["message-exporter-body"]}
|
||||
style={currentStep.value !== "select" ? { display: "none" } : {}}
|
||||
>
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Export.Format.Title}
|
||||
subTitle={Locale.Export.Format.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={exportConfig.format}
|
||||
onChange={(e) =>
|
||||
updateExportConfig(
|
||||
(config) =>
|
||||
(config.format = e.currentTarget.value as ExportFormat),
|
||||
)
|
||||
}
|
||||
>
|
||||
{formats.map((f) => (
|
||||
<option key={f} value={f}>
|
||||
{f}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Export.IncludeContext.Title}
|
||||
subTitle={Locale.Export.IncludeContext.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportConfig.includeContext}
|
||||
onChange={(e) => {
|
||||
updateExportConfig(
|
||||
(config) => (config.includeContext = e.currentTarget.checked),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
<MessageSelector
|
||||
selection={selection}
|
||||
updateSelection={updateSelection}
|
||||
defaultSelectAll
|
||||
/>
|
||||
</div>
|
||||
{currentStep.value === "preview" && (
|
||||
<div className={styles["message-exporter-body"]}>{preview()}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderExport(props: {
|
||||
messages: ChatMessage[];
|
||||
onRender: (messages: ChatMessage[]) => void;
|
||||
}) {
|
||||
const domRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!domRef.current) return;
|
||||
const dom = domRef.current;
|
||||
const messages = Array.from(
|
||||
dom.getElementsByClassName(EXPORT_MESSAGE_CLASS_NAME),
|
||||
);
|
||||
|
||||
if (messages.length !== props.messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderMsgs = messages.map((v, i) => {
|
||||
const [role, _] = v.id.split(":");
|
||||
return {
|
||||
id: i.toString(),
|
||||
role: role as any,
|
||||
content: role === "user" ? v.textContent ?? "" : v.innerHTML,
|
||||
date: "",
|
||||
};
|
||||
});
|
||||
|
||||
props.onRender(renderMsgs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={domRef}>
|
||||
{props.messages.map((m, i) => (
|
||||
<div
|
||||
key={i}
|
||||
id={`${m.role}:${i}`}
|
||||
className={EXPORT_MESSAGE_CLASS_NAME}
|
||||
>
|
||||
<Markdown content={m.content} defaultShow />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewActions(props: {
|
||||
download: () => void;
|
||||
copy: () => void;
|
||||
showCopy?: boolean;
|
||||
messages?: ChatMessage[];
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [shouldExport, setShouldExport] = useState(false);
|
||||
|
||||
const onRenderMsgs = (msgs: ChatMessage[]) => {
|
||||
setShouldExport(false);
|
||||
|
||||
api
|
||||
.share(msgs)
|
||||
.then((res) => {
|
||||
if (!res) return;
|
||||
showModal({
|
||||
title: Locale.Export.Share,
|
||||
children: [
|
||||
<input
|
||||
type="text"
|
||||
value={res}
|
||||
key="input"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "unset",
|
||||
}}
|
||||
readOnly
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
></input>,
|
||||
],
|
||||
actions: [
|
||||
<IconButton
|
||||
icon={<CopyIcon />}
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
key="copy"
|
||||
onClick={() => copyToClipboard(res)}
|
||||
/>,
|
||||
],
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.open(res, "_blank");
|
||||
}, 800);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[Share]", e);
|
||||
showToast(prettyObject(e));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const share = async () => {
|
||||
if (props.messages?.length) {
|
||||
setLoading(true);
|
||||
setShouldExport(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["preview-actions"]}>
|
||||
{props.showCopy && (
|
||||
<IconButton
|
||||
text={Locale.Export.Copy}
|
||||
bordered
|
||||
shadow
|
||||
icon={<CopyIcon />}
|
||||
onClick={props.copy}
|
||||
></IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
text={Locale.Export.Download}
|
||||
bordered
|
||||
shadow
|
||||
icon={<DownloadIcon />}
|
||||
onClick={props.download}
|
||||
></IconButton>
|
||||
<IconButton
|
||||
text={Locale.Export.Share}
|
||||
bordered
|
||||
shadow
|
||||
icon={loading ? <LoadingIcon /> : <ShareIcon />}
|
||||
onClick={share}
|
||||
></IconButton>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
right: "200vw",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{shouldExport && (
|
||||
<RenderExport
|
||||
messages={props.messages ?? []}
|
||||
onRender={onRenderMsgs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExportAvatar(props: { avatar: string }) {
|
||||
if (props.avatar === DEFAULT_MASK_AVATAR) {
|
||||
return (
|
||||
<img
|
||||
src={BotIcon.src}
|
||||
width={30}
|
||||
height={30}
|
||||
alt="bot"
|
||||
className="user-avatar"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Avatar avatar={props.avatar} />;
|
||||
}
|
||||
|
||||
export function ImagePreviewer(props: {
|
||||
messages: ChatMessage[];
|
||||
topic: string;
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const mask = session.mask;
|
||||
const config = useAppConfig();
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const copy = () => {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
toBlob(dom).then((blob) => {
|
||||
if (!blob) return;
|
||||
try {
|
||||
navigator.clipboard
|
||||
.write([
|
||||
new ClipboardItem({
|
||||
"image/png": blob,
|
||||
}),
|
||||
])
|
||||
.then(() => {
|
||||
showToast(Locale.Copy.Success);
|
||||
refreshPreview();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[Copy Image] ", e);
|
||||
showToast(Locale.Copy.Failed);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isMobile = useMobileScreen();
|
||||
|
||||
const download = async () => {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
|
||||
const isApp = getClientConfig()?.isApp;
|
||||
|
||||
try {
|
||||
const blob = await toPng(dom);
|
||||
if (!blob) return;
|
||||
|
||||
if (isMobile || (isApp && window.__TAURI__)) {
|
||||
if (isApp && window.__TAURI__) {
|
||||
const result = await window.__TAURI__.dialog.save({
|
||||
defaultPath: `${props.topic}.png`,
|
||||
filters: [
|
||||
{
|
||||
name: "PNG Files",
|
||||
extensions: ["png"],
|
||||
},
|
||||
{
|
||||
name: "All Files",
|
||||
extensions: ["*"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result !== null) {
|
||||
const response = await fetch(blob);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
|
||||
showToast(Locale.Download.Success);
|
||||
} else {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
} else {
|
||||
showImageModal(blob);
|
||||
}
|
||||
} else {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${props.topic}.png`;
|
||||
link.href = blob;
|
||||
link.click();
|
||||
refreshPreview();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPreview = () => {
|
||||
const dom = previewRef.current;
|
||||
if (dom) {
|
||||
dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["image-previewer"]}>
|
||||
<PreviewActions
|
||||
copy={copy}
|
||||
download={download}
|
||||
showCopy={!isMobile}
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div
|
||||
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
|
||||
ref={previewRef}
|
||||
>
|
||||
<div className={styles["chat-info"]}>
|
||||
<div className={styles["logo"] + " no-dark"}>
|
||||
<NextImage
|
||||
src={ChatGptIcon.src}
|
||||
alt="logo"
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles["main-title"]}>ChatGPT Next Web</div>
|
||||
<div className={styles["sub-title"]}>
|
||||
github.com/Yidadaa/ChatGPT-Next-Web
|
||||
</div>
|
||||
<div className={styles["icons"]}>
|
||||
<ExportAvatar avatar={config.avatar} />
|
||||
<span className={styles["icon-space"]}>&</span>
|
||||
<ExportAvatar avatar={mask.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
{Locale.Exporter.Model}: {mask.modelConfig.model}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
{Locale.Exporter.Messages}: {props.messages.length}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
{Locale.Exporter.Topic}: {session.topic}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
{Locale.Exporter.Time}:{" "}
|
||||
{new Date(
|
||||
props.messages.at(-1)?.date ?? Date.now(),
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.messages.map((m, i) => {
|
||||
return (
|
||||
<div
|
||||
className={styles["message"] + " " + styles["message-" + m.role]}
|
||||
key={i}
|
||||
>
|
||||
<div className={styles["avatar"]}>
|
||||
<ExportAvatar
|
||||
avatar={m.role === "user" ? config.avatar : mask.avatar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles["body"]}>
|
||||
<Markdown
|
||||
content={m.content}
|
||||
fontSize={config.fontSize}
|
||||
defaultShow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarkdownPreviewer(props: {
|
||||
messages: ChatMessage[];
|
||||
topic: string;
|
||||
}) {
|
||||
const mdText =
|
||||
`# ${props.topic}\n\n` +
|
||||
props.messages
|
||||
.map((m) => {
|
||||
return m.role === "user"
|
||||
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
||||
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const copy = () => {
|
||||
copyToClipboard(mdText);
|
||||
};
|
||||
const download = () => {
|
||||
downloadAs(mdText, `${props.topic}.md`);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<PreviewActions
|
||||
copy={copy}
|
||||
download={download}
|
||||
showCopy={true}
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div className="markdown-body">
|
||||
<pre className={styles["export-content"]}>{mdText}</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function JsonPreviewer(props: {
|
||||
messages: ChatMessage[];
|
||||
topic: string;
|
||||
}) {
|
||||
const msgs = {
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `${Locale.FineTuned.Sysmessage} ${props.topic}`,
|
||||
},
|
||||
...props.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
],
|
||||
};
|
||||
const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
|
||||
const minifiedJson = JSON.stringify(msgs);
|
||||
|
||||
const copy = () => {
|
||||
copyToClipboard(minifiedJson);
|
||||
};
|
||||
const download = () => {
|
||||
downloadAs(JSON.stringify(msgs), `${props.topic}.json`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewActions
|
||||
copy={copy}
|
||||
download={download}
|
||||
showCopy={false}
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div className="markdown-body" onClick={copy}>
|
||||
<Markdown content={mdText} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,6 +1,3 @@
|
||||
@import "./window.scss";
|
||||
@import "../styles/animation.scss";
|
||||
|
||||
@mixin container {
|
||||
background-color: var(--white);
|
||||
border: var(--border-in-light);
|
||||
@@ -9,7 +6,7 @@
|
||||
color: var(--black);
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 480px;
|
||||
min-height: 370px;
|
||||
max-width: 1200px;
|
||||
|
||||
display: flex;
|
||||
@@ -50,25 +47,50 @@
|
||||
flex-direction: column;
|
||||
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
transition: width ease 0.1s;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
.sidebar-drag {
|
||||
background-color: rgba($color: #000000, $alpha: 0.01);
|
||||
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-drag {
|
||||
$width: 10px;
|
||||
$width: 14px;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: $width;
|
||||
background-color: var(--black);
|
||||
background-color: rgba($color: #000000, $alpha: 0);
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
transition: all ease 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 0.2;
|
||||
svg {
|
||||
opacity: 0;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,19 +148,19 @@
|
||||
.sidebar-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400px;
|
||||
font-weight: 400;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
@@ -152,7 +174,7 @@
|
||||
user-select: none;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.chat-item:hover {
|
||||
@@ -167,16 +189,17 @@
|
||||
font-size: 14px;
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
width: 200px;
|
||||
width: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-item-delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
@@ -184,7 +207,7 @@
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
right: 10px;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete:hover {
|
||||
@@ -197,6 +220,7 @@
|
||||
color: rgb(166, 166, 166);
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-item-count,
|
||||
@@ -206,6 +230,88 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -220,254 +326,6 @@
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
max-width: var(--message-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
&:hover {
|
||||
.chat-message-top-actions {
|
||||
opacity: 1;
|
||||
right: 10px;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-avatar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chat-message-status {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
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%;
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
border: var(--border-in-light);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-message-top-actions {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: -26px;
|
||||
left: 100px;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-message-top-action {
|
||||
opacity: 0.5;
|
||||
color: var(--black);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container > .chat-message-item {
|
||||
background-color: var(--second);
|
||||
}
|
||||
|
||||
.chat-message-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-message-action-date {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
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 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prompt-hints {
|
||||
min-height: 20px;
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
background-color: var(--white);
|
||||
border: var(--border-in-light);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.prompt-hint {
|
||||
color: var(--black);
|
||||
padding: 6px 10px;
|
||||
animation: slide-in ease 0.3s;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.3s;
|
||||
border: transparent 1px solid;
|
||||
margin: 4px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.hint-title {
|
||||
font-size: 12px;
|
||||
font-weight: bolder;
|
||||
|
||||
@include single-line();
|
||||
}
|
||||
.hint-content {
|
||||
font-size: 12px;
|
||||
|
||||
@include single-line();
|
||||
}
|
||||
|
||||
&-selected,
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-panel-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
padding: 10px 90px 10px 14px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -476,3 +334,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rtl-screen {
|
||||
direction: rtl;
|
||||
}
|
||||
|
@@ -2,32 +2,39 @@
|
||||
|
||||
require("../polyfill");
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "regenerator-runtime/runtime";
|
||||
|
||||
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 { useChatStore } from "../store";
|
||||
import { getCSSVar, isMobileScreen } from "../utils";
|
||||
import Locale from "../locales";
|
||||
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 { getISOLang, getLang } from "../locales";
|
||||
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { api } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
<div className={styles["loading-content"]}>
|
||||
<div className={styles["loading-content"] + " no-dark"}>
|
||||
{!props.noLogo && <BotIcon />}
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
@@ -38,12 +45,20 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
const Chat = dynamic(async () => (await import("./chat")).Chat, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
function useSwitchTheme() {
|
||||
const config = useChatStore((state) => state.config);
|
||||
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");
|
||||
@@ -56,68 +71,32 @@ function useSwitchTheme() {
|
||||
}
|
||||
|
||||
const metaDescriptionDark = document.querySelector(
|
||||
'meta[name="theme-color"][media]',
|
||||
'meta[name="theme-color"][media*="dark"]',
|
||||
);
|
||||
const metaDescriptionLight = document.querySelector(
|
||||
'meta[name="theme-color"]:not([media])',
|
||||
'meta[name="theme-color"][media*="light"]',
|
||||
);
|
||||
|
||||
if (config.theme === "auto") {
|
||||
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||
} else {
|
||||
const themeColor = getCSSVar("--themeColor");
|
||||
const themeColor = getCSSVar("--theme-color");
|
||||
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||
}
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(500, Math.max(220, x));
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 100) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = chatStore.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);
|
||||
};
|
||||
|
||||
function useHtmlLang() {
|
||||
useEffect(() => {
|
||||
if (isMobileScreen()) {
|
||||
return;
|
||||
const lang = getISOLang();
|
||||
const htmlLang = document.documentElement.lang;
|
||||
|
||||
if (lang !== htmlLang) {
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
|
||||
);
|
||||
}, [chatStore.config.sidebarWidth]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
@@ -130,129 +109,97 @@ const useHasHydrated = () => {
|
||||
return hasHydrated;
|
||||
};
|
||||
|
||||
function _Home() {
|
||||
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
||||
(state) => [
|
||||
state.newSession,
|
||||
state.currentSessionIndex,
|
||||
state.removeSession,
|
||||
],
|
||||
);
|
||||
const chatStore = useChatStore();
|
||||
const loading = !useHasHydrated();
|
||||
const [showSideBar, setShowSideBar] = useState(true);
|
||||
const loadAsyncGoogleFont = () => {
|
||||
const linkEl = document.createElement("link");
|
||||
const proxyFontUrl = "/google-fonts";
|
||||
const remoteFontUrl = "https://fonts.googleapis.com";
|
||||
const googleFontUrl =
|
||||
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href =
|
||||
googleFontUrl +
|
||||
"/css2?family=" +
|
||||
encodeURIComponent("Noto Sans: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 isAuth = location.pathname === Path.Auth;
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldTightBorder =
|
||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown } = useDragSideBar();
|
||||
|
||||
useSwitchTheme();
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
useEffect(() => {
|
||||
loadAsyncGoogleFont();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
className={
|
||||
styles.container +
|
||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<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>
|
||||
{isAuth ? (
|
||||
<>
|
||||
<AuthPage />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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={chatStore.deleteSession}
|
||||
/>
|
||||
</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 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>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Home.NewChat}
|
||||
onClick={() => {
|
||||
createNewSession();
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className={styles["window-content"]}>
|
||||
{openSettings ? (
|
||||
<Settings
|
||||
closeSettings={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chat
|
||||
key="chat"
|
||||
showSideBar={() => setShowSideBar(true)}
|
||||
sideBarShowing={showSideBar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLoadData() {
|
||||
const config = useAppConfig();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const models = await api.llm.models();
|
||||
config.mergeModels(models);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
useSwitchTheme();
|
||||
useLoadData();
|
||||
useHtmlLang();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Config] got config from build time", getClientConfig());
|
||||
useAccessStore.getState().fetch();
|
||||
}, []);
|
||||
|
||||
if (!useHasHydrated()) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<_Home></_Home>
|
||||
<Router>
|
||||
<Screen />
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,13 @@
|
||||
.input-range {
|
||||
border: var(--border-in-light);
|
||||
border-radius: 10px;
|
||||
padding: 5px 15px 5px 10px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 40%;
|
||||
|
||||
input[type="range"] {
|
||||
max-width: calc(100% - 34px);
|
||||
}
|
||||
}
|
||||
|
@@ -5,101 +5,180 @@ import RemarkBreaks from "remark-breaks";
|
||||
import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { showImageModal } from "./ui-lib";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.code && ref.current) {
|
||||
mermaid
|
||||
.run({
|
||||
nodes: [ref.current],
|
||||
suppressErrors: true,
|
||||
})
|
||||
.catch((e) => {
|
||||
setHasError(true);
|
||||
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" });
|
||||
showImageModal(URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-dark mermaid"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
overflow: "auto",
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={() => viewSvgInNewWindow()}
|
||||
>
|
||||
{props.code}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const refText = ref.current?.innerText;
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
|
||||
const renderMermaid = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||
if (mermaidDom) {
|
||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||
}
|
||||
}, 600);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(renderMermaid, 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refText]);
|
||||
|
||||
return (
|
||||
<pre ref={ref}>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
copyToClipboard(code);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
<>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
<pre ref={ref}>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
copyToClipboard(code);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDollarNumber(text: string) {
|
||||
let escapedText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || " ";
|
||||
|
||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||
char = "\\$";
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(
|
||||
() => escapeDollarNumber(props.content),
|
||||
[props.content],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[
|
||||
RehypeHighlight,
|
||||
{
|
||||
detect: false,
|
||||
ignoreMissing: true,
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || "";
|
||||
const isInternal = /^\/#/i.test(href);
|
||||
const target = isInternal ? "_self" : aProps.target ?? "_blank";
|
||||
return <a {...aProps} target={target} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export const MarkdownContent = React.memo(_MarkDownContent);
|
||||
|
||||
export function Markdown(
|
||||
props: {
|
||||
content: string;
|
||||
loading?: boolean;
|
||||
fontSize?: number;
|
||||
parentRef: RefObject<HTMLDivElement>;
|
||||
parentRef?: RefObject<HTMLDivElement>;
|
||||
defaultShow?: boolean;
|
||||
} & React.DOMAttributes<HTMLDivElement>,
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const parent = props.parentRef.current;
|
||||
const md = mdRef.current;
|
||||
const rendered = useRef(true); // disable lazy loading for bad ux
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// to triggr rerender
|
||||
setCounter(counter + 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.loading]);
|
||||
|
||||
const inView =
|
||||
rendered.current ||
|
||||
(() => {
|
||||
if (parent && md) {
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const isInRange = (x: number) =>
|
||||
x <= parentBounds.bottom && x >= parentBounds.top;
|
||||
const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
||||
|
||||
if (inView) {
|
||||
rendered.current = true;
|
||||
}
|
||||
|
||||
return inView;
|
||||
}
|
||||
})();
|
||||
|
||||
const shouldLoading = props.loading || !inView;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{ fontSize: `${props.fontSize ?? 14}px` }}
|
||||
style={{
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||
dir="auto"
|
||||
>
|
||||
{shouldLoading ? (
|
||||
{props.loading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[
|
||||
RehypeHighlight,
|
||||
{
|
||||
detect: false,
|
||||
ignoreMissing: true,
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
}}
|
||||
linkTarget={"_blank"}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
<MarkdownContent content={props.content} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
108
app/components/mask.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
624
app/components/mask.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
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 DragIcon from "../icons/drag.svg";
|
||||
|
||||
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
|
||||
import {
|
||||
ChatMessage,
|
||||
createMessage,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
import { ROLES } from "../client/api";
|
||||
import {
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
Popover,
|
||||
Select,
|
||||
showConfirm,
|
||||
} from "./ui-lib";
|
||||
import { Avatar, AvatarPicker } from "./emoji";
|
||||
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import chatStyle from "./chat.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { copyToClipboard, downloadAs, readFromFile } from "../utils";
|
||||
import { Updater } from "../typing";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
import { FileName, Path } from "../constant";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
Draggable,
|
||||
OnDragEndResponder,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
// drag and drop helper function
|
||||
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
||||
const result = [...list];
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
|
||||
return props.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.avatar} />
|
||||
) : (
|
||||
<Avatar model={props.model} />
|
||||
);
|
||||
}
|
||||
|
||||
export function MaskConfig(props: {
|
||||
mask: Mask;
|
||||
updateMask: Updater<Mask>;
|
||||
extraListItems?: JSX.Element;
|
||||
readonly?: boolean;
|
||||
shouldSyncFromGlobal?: 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;
|
||||
// if user changed current session mask, it will disable auto sync
|
||||
mask.syncGlobalConfig = false;
|
||||
});
|
||||
};
|
||||
|
||||
const copyMaskLink = () => {
|
||||
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
|
||||
copyToClipboard(maskLink);
|
||||
};
|
||||
|
||||
const globalConfig = useAppConfig();
|
||||
|
||||
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
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</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>
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.HideContext.Title}
|
||||
subTitle={Locale.Mask.Config.HideContext.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.mask.hideContext}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.hideContext = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Share.Title}
|
||||
subTitle={Locale.Mask.Config.Share.SubTitle}
|
||||
>
|
||||
<IconButton
|
||||
icon={<CopyIcon />}
|
||||
text={Locale.Mask.Config.Share.Action}
|
||||
onClick={copyMaskLink}
|
||||
/>
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
{props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Sync.Title}
|
||||
subTitle={Locale.Mask.Config.Sync.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.mask.syncGlobalConfig}
|
||||
onChange={async (e) => {
|
||||
const checked = e.currentTarget.checked;
|
||||
if (
|
||||
checked &&
|
||||
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
|
||||
) {
|
||||
props.updateMask((mask) => {
|
||||
mask.syncGlobalConfig = checked;
|
||||
mask.modelConfig = { ...globalConfig.modelConfig };
|
||||
});
|
||||
} else if (!checked) {
|
||||
props.updateMask((mask) => {
|
||||
mask.syncGlobalConfig = checked;
|
||||
});
|
||||
}
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
) : null}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<ModelConfigList
|
||||
modelConfig={{ ...props.mask.modelConfig }}
|
||||
updateConfig={updateConfig}
|
||||
/>
|
||||
{props.extraListItems}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextPromptItem(props: {
|
||||
index: number;
|
||||
prompt: ChatMessage;
|
||||
update: (prompt: ChatMessage) => void;
|
||||
remove: () => void;
|
||||
}) {
|
||||
const [focusingInput, setFocusingInput] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
{!focusingInput && (
|
||||
<>
|
||||
<div className={chatStyle["context-drag"]}>
|
||||
<DragIcon />
|
||||
</div>
|
||||
<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);
|
||||
// If the selection is not removed when the user loses focus, some
|
||||
// extensions like "Translate" will always display a floating bar
|
||||
window?.getSelection()?.removeAllRanges();
|
||||
}}
|
||||
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: ChatMessage[];
|
||||
updateContext: (updater: (context: ChatMessage[]) => void) => void;
|
||||
}) {
|
||||
const context = props.context;
|
||||
|
||||
const addContextPrompt = (prompt: ChatMessage, i: number) => {
|
||||
props.updateContext((context) => context.splice(i, 0, prompt));
|
||||
};
|
||||
|
||||
const removeContextPrompt = (i: number) => {
|
||||
props.updateContext((context) => context.splice(i, 1));
|
||||
};
|
||||
|
||||
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
|
||||
props.updateContext((context) => (context[i] = prompt));
|
||||
};
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
const newContext = reorder(
|
||||
context,
|
||||
result.source.index,
|
||||
result.destination.index,
|
||||
);
|
||||
props.updateContext((context) => {
|
||||
context.splice(0, context.length, ...newContext);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="context-prompt-list">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{context.map((c, i) => (
|
||||
<Draggable
|
||||
draggableId={c.id || i.toString()}
|
||||
index={i}
|
||||
key={c.id}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<ContextPromptItem
|
||||
index={i}
|
||||
prompt={c}
|
||||
update={(prompt) => updateContextPrompt(i, prompt)}
|
||||
remove={() => removeContextPrompt(i)}
|
||||
/>
|
||||
<div
|
||||
className={chatStyle["context-prompt-insert"]}
|
||||
onClick={() => {
|
||||
addContextPrompt(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: "",
|
||||
date: new Date().toLocaleString(),
|
||||
}),
|
||||
i + 1,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
{props.context.length === 0 && (
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Context.Add}
|
||||
bordered
|
||||
className={chatStyle["context-prompt-button"]}
|
||||
onClick={() =>
|
||||
addContextPrompt(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: "",
|
||||
date: "",
|
||||
}),
|
||||
props.context.length,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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;
|
||||
|
||||
// refactored already, now it accurate
|
||||
const onSearch = (text: string) => {
|
||||
setSearchText(text);
|
||||
if (text.length > 0) {
|
||||
const result = allMasks.filter((m) =>
|
||||
m.name.toLowerCase().includes(text.toLowerCase()),
|
||||
);
|
||||
setSearchMasks(result);
|
||||
} else {
|
||||
setSearchMasks(allMasks);
|
||||
}
|
||||
};
|
||||
|
||||
const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
|
||||
const editingMask =
|
||||
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
|
||||
const closeMaskModal = () => setEditingMaskId(undefined);
|
||||
|
||||
const downloadAll = () => {
|
||||
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), 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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
//if the content is a single mask.
|
||||
if (importMasks.name) {
|
||||
maskStore.create(importMasks);
|
||||
}
|
||||
} 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}
|
||||
text={Locale.UI.Export}
|
||||
/>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
text={Locale.UI.Import}
|
||||
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}>
|
||||
{ALL_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 avatar={m.avatar} model={m.modelConfig.model} />
|
||||
</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)} / ${
|
||||
ALL_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={async () => {
|
||||
if (await showConfirm(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.updateMask(editingMaskId!, updater)
|
||||
}
|
||||
readonly={editingMask.builtin}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
82
app/components/message-selector.module.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
.message-selector {
|
||||
.message-filter {
|
||||
display: flex;
|
||||
|
||||
.search-bar {
|
||||
max-width: unset;
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
|
||||
.search-bar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin-top: 20px;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
overflow: hidden;
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&-selected {
|
||||
background-color: var(--second);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: var(--border-in-light);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
max-width: calc(100% - 80px);
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
235
app/components/message-selector.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { Updater } from "../typing";
|
||||
import { IconButton } from "./button";
|
||||
import { Avatar } from "./emoji";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import Locale from "../locales";
|
||||
|
||||
import styles from "./message-selector.module.scss";
|
||||
|
||||
function useShiftRange() {
|
||||
const [startIndex, setStartIndex] = useState<number>();
|
||||
const [endIndex, setEndIndex] = useState<number>();
|
||||
const [shiftDown, setShiftDown] = useState(false);
|
||||
|
||||
const onClickIndex = (index: number) => {
|
||||
if (shiftDown && startIndex !== undefined) {
|
||||
setEndIndex(index);
|
||||
} else {
|
||||
setStartIndex(index);
|
||||
setEndIndex(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Shift") return;
|
||||
setShiftDown(true);
|
||||
};
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Shift") return;
|
||||
setShiftDown(false);
|
||||
setStartIndex(undefined);
|
||||
setEndIndex(undefined);
|
||||
};
|
||||
|
||||
window.addEventListener("keyup", onKeyUp);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onClickIndex,
|
||||
startIndex,
|
||||
endIndex,
|
||||
};
|
||||
}
|
||||
|
||||
export function useMessageSelector() {
|
||||
const [selection, setSelection] = useState(new Set<string>());
|
||||
const updateSelection: Updater<Set<string>> = (updater) => {
|
||||
const newSelection = new Set<string>(selection);
|
||||
updater(newSelection);
|
||||
setSelection(newSelection);
|
||||
};
|
||||
|
||||
return {
|
||||
selection,
|
||||
updateSelection,
|
||||
};
|
||||
}
|
||||
|
||||
export function MessageSelector(props: {
|
||||
selection: Set<string>;
|
||||
updateSelection: Updater<Set<string>>;
|
||||
defaultSelectAll?: boolean;
|
||||
onSelected?: (messages: ChatMessage[]) => void;
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||
const allMessages = useMemo(() => {
|
||||
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
|
||||
if (startIndex === session.messages.length - 1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
return session.messages.slice(startIndex);
|
||||
}, [session.messages, session.clearContextIndex]);
|
||||
|
||||
const messages = useMemo(
|
||||
() =>
|
||||
allMessages.filter(
|
||||
(m, i) =>
|
||||
m.id && // message must have id
|
||||
isValid(m) &&
|
||||
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
|
||||
),
|
||||
[allMessages],
|
||||
);
|
||||
const messageCount = messages.length;
|
||||
const config = useAppConfig();
|
||||
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [searchIds, setSearchIds] = useState(new Set<string>());
|
||||
const isInSearchResult = (id: string) => {
|
||||
return searchInput.length === 0 || searchIds.has(id);
|
||||
};
|
||||
const doSearch = (text: string) => {
|
||||
const searchResults = new Set<string>();
|
||||
if (text.length > 0) {
|
||||
messages.forEach((m) =>
|
||||
m.content.includes(text) ? searchResults.add(m.id!) : null,
|
||||
);
|
||||
}
|
||||
setSearchIds(searchResults);
|
||||
};
|
||||
|
||||
// for range selection
|
||||
const { startIndex, endIndex, onClickIndex } = useShiftRange();
|
||||
|
||||
const selectAll = () => {
|
||||
props.updateSelection((selection) =>
|
||||
messages.forEach((m) => selection.add(m.id!)),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultSelectAll) {
|
||||
selectAll();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (startIndex === undefined || endIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const [start, end] = [startIndex, endIndex].sort((a, b) => a - b);
|
||||
props.updateSelection((selection) => {
|
||||
for (let i = start; i <= end; i += 1) {
|
||||
selection.add(messages[i].id ?? i);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [startIndex, endIndex]);
|
||||
|
||||
const LATEST_COUNT = 4;
|
||||
|
||||
return (
|
||||
<div className={styles["message-selector"]}>
|
||||
<div className={styles["message-filter"]}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={Locale.Select.Search}
|
||||
className={styles["filter-item"] + " " + styles["search-bar"]}
|
||||
value={searchInput}
|
||||
onInput={(e) => {
|
||||
setSearchInput(e.currentTarget.value);
|
||||
doSearch(e.currentTarget.value);
|
||||
}}
|
||||
></input>
|
||||
|
||||
<div className={styles["actions"]}>
|
||||
<IconButton
|
||||
text={Locale.Select.All}
|
||||
bordered
|
||||
className={styles["filter-item"]}
|
||||
onClick={selectAll}
|
||||
/>
|
||||
<IconButton
|
||||
text={Locale.Select.Latest}
|
||||
bordered
|
||||
className={styles["filter-item"]}
|
||||
onClick={() =>
|
||||
props.updateSelection((selection) => {
|
||||
selection.clear();
|
||||
messages
|
||||
.slice(messageCount - LATEST_COUNT)
|
||||
.forEach((m) => selection.add(m.id!));
|
||||
})
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
text={Locale.Select.Clear}
|
||||
bordered
|
||||
className={styles["filter-item"]}
|
||||
onClick={() =>
|
||||
props.updateSelection((selection) => selection.clear())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["messages"]}>
|
||||
{messages.map((m, i) => {
|
||||
if (!isInSearchResult(m.id!)) return null;
|
||||
const id = m.id ?? i;
|
||||
const isSelected = props.selection.has(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles["message"]} ${
|
||||
props.selection.has(m.id!) && styles["message-selected"]
|
||||
}`}
|
||||
key={i}
|
||||
onClick={() => {
|
||||
props.updateSelection((selection) => {
|
||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||
});
|
||||
onClickIndex(i);
|
||||
}}
|
||||
>
|
||||
<div className={styles["avatar"]}>
|
||||
{m.role === "user" ? (
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
) : (
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={m.model || session.mask.modelConfig.model}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles["body"]}>
|
||||
<div className={styles["date"]}>
|
||||
{new Date(m.date).toLocaleString()}
|
||||
</div>
|
||||
<div className={`${styles["content"]} one-line`}>
|
||||
{m.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
216
app/components/model-config.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||
}) {
|
||||
const allModels = useAllModels();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem title={Locale.Settings.Model}>
|
||||
<Select
|
||||
value={props.modelConfig.model}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.model = ModalConfigValidator.model(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={v.name} key={i}>
|
||||
{v.displayName}
|
||||
</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.TopP.Title}
|
||||
subTitle={Locale.Settings.TopP.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={(props.modelConfig.top_p ?? 1).toFixed(1)}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.top_p = ModalConfigValidator.top_p(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.MaxTokens.Title}
|
||||
subTitle={Locale.Settings.MaxTokens.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min={1024}
|
||||
max={512000}
|
||||
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.PresencePenalty.Title}
|
||||
subTitle={Locale.Settings.PresencePenalty.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.FrequencyPenalty.Title}
|
||||
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={props.modelConfig.frequency_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.frequency_penalty =
|
||||
ModalConfigValidator.frequency_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InjectSystemPrompts.Title}
|
||||
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.modelConfig.enableInjectSystemPrompts}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.enableInjectSystemPrompts = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InputTemplate.Title}
|
||||
subTitle={Locale.Settings.InputTemplate.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={props.modelConfig.template}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.template = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</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="64"
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
125
app/components/new-chat.module.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
@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: 2vh;
|
||||
animation: slide-in ease 0.45s;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
|
||||
.skip {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.masks {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
align-items: center;
|
||||
padding-top: 20px;
|
||||
|
||||
$linear: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
|
||||
-webkit-mask-image: $linear;
|
||||
mask-image: $linear;
|
||||
|
||||
animation: slide-in ease 0.5s;
|
||||
|
||||
.mask-row {
|
||||
display: flex;
|
||||
// justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
184
app/components/new-chat.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
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";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
|
||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||
return (
|
||||
<div className={styles["mask"]} onClick={props.onClick}>
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useMaskGroup(masks: Mask[]) {
|
||||
const [groups, setGroups] = useState<Mask[][]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const computeGroup = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
computeGroup();
|
||||
|
||||
window.addEventListener("resize", computeGroup);
|
||||
return () => window.removeEventListener("resize", computeGroup);
|
||||
// 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 maskRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { state } = useLocation();
|
||||
|
||||
const startChat = (mask?: Mask) => {
|
||||
setTimeout(() => {
|
||||
chatStore.newSession(mask);
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
useCommand({
|
||||
mask: (id) => {
|
||||
try {
|
||||
const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
|
||||
startChat(mask ?? undefined);
|
||||
} catch {
|
||||
console.error("[New Chat] failed to create chat from mask id=", id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (maskRef.current) {
|
||||
maskRef.current.scrollLeft =
|
||||
(maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
|
||||
}
|
||||
}, [groups]);
|
||||
|
||||
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={async () => {
|
||||
if (await showConfirm(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.More}
|
||||
onClick={() => navigate(Path.Masks)}
|
||||
icon={<EyeIcon />}
|
||||
bordered
|
||||
shadow
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
text={Locale.NewChat.Skip}
|
||||
onClick={() => startChat()}
|
||||
icon={<LightningIcon />}
|
||||
type="primary"
|
||||
shadow
|
||||
className={styles["skip"]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles["masks"]} ref={maskRef}>
|
||||
{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>
|
||||
);
|
||||
}
|
@@ -1,34 +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;
|
||||
}
|
||||
|
||||
.password-input-container {
|
||||
max-width: 50%;
|
||||
.edit-prompt-modal {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
|
||||
.password-eye {
|
||||
margin-right: 4px;
|
||||
.edit-prompt-title {
|
||||
max-width: unset;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
min-width: 80%;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
250
app/components/sidebar.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useEffect, useRef, useMemo } 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 DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
|
||||
import Locale from "../locales";
|
||||
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
|
||||
import {
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
REPO_URL,
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showConfirm, 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.altKey || e.ctrlKey) {
|
||||
if (e.key === "ArrowUp") {
|
||||
chatStore.nextSession(-1);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
chatStore.nextSession(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 ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const toggleSideBar = () => {
|
||||
config.update((config) => {
|
||||
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
// Remembers the initial width each time the mouse is pressed
|
||||
startX.current = e.clientX;
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
const dragStartTime = Date.now();
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 20) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => {
|
||||
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = nextWidth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||
window.removeEventListener("pointermove", handleDragMove);
|
||||
window.removeEventListener("pointerup", handleDragEnd);
|
||||
|
||||
// if user click the drag icon, should toggle the sidebar
|
||||
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||
if (shouldFireClick) {
|
||||
toggleSideBar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleDragMove);
|
||||
window.addEventListener("pointerup", handleDragEnd);
|
||||
};
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldNarrow =
|
||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||
|
||||
useEffect(() => {
|
||||
const barWidth = shouldNarrow
|
||||
? NARROW_SIDEBAR_WIDTH
|
||||
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||
|
||||
return {
|
||||
onDragStart,
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const isIOSMobile = useMemo(
|
||||
() => isIOS() && isMobileScreen,
|
||||
[isMobileScreen],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
style={{
|
||||
// #3016 disable transition on ios mobile screen
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
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={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { 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={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(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" rel="noopener noreferrer">
|
||||
<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"]}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -35,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 {
|
||||
@@ -43,6 +62,7 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
margin-bottom: 20px;
|
||||
animation: slide-in ease 0.3s;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.list .list-item:last-child {
|
||||
@@ -53,11 +73,26 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
background-color: var(--white);
|
||||
border-radius: 12px;
|
||||
width: 50vw;
|
||||
width: 80vw;
|
||||
max-width: 900px;
|
||||
min-width: 300px;
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
--modal-padding: 20px;
|
||||
|
||||
&-max {
|
||||
width: 95vw;
|
||||
max-width: unset;
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.modal-content {
|
||||
max-height: unset !important;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: var(--modal-padding);
|
||||
display: flex;
|
||||
@@ -70,11 +105,19 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
cursor: pointer;
|
||||
.modal-header-actions {
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.2);
|
||||
.modal-header-action {
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +132,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;
|
||||
@@ -103,6 +148,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;
|
||||
@@ -122,7 +179,7 @@
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: 5vh;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
@@ -171,12 +228,80 @@
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.modal-container {
|
||||
width: 90vw;
|
||||
.select-with-icon {
|
||||
position: relative;
|
||||
max-width: fit-content;
|
||||
|
||||
.modal-content {
|
||||
max-height: 50vh;
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
padding: 10px 35px 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;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.selector {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
|
||||
&-content {
|
||||
.list {
|
||||
max-height: 90vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.list-item {
|
||||
cursor: pointer;
|
||||
background-color: var(--white);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,20 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
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 ConfirmIcon from "../icons/confirm.svg";
|
||||
import CancelIcon from "../icons/cancel.svg";
|
||||
import MaxIcon from "../icons/max.svg";
|
||||
import MinIcon from "../icons/min.svg";
|
||||
|
||||
import Locale from "../locales";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import React from "react";
|
||||
import React, { HTMLProps, useEffect, useState } from "react";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
export function Popover(props: {
|
||||
children: JSX.Element;
|
||||
@@ -29,16 +41,41 @@ 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;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={styles["list-item"] + ` ${props.className || ""}`}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<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 }) {
|
||||
return <div className={styles.list}>{props.children}</div>;
|
||||
export function List(props: { children: React.ReactNode; id?: string }) {
|
||||
return (
|
||||
<div className={styles.list} id={props.id}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Loading() {
|
||||
@@ -59,24 +96,59 @@ export function Loading() {
|
||||
|
||||
interface ModalProps {
|
||||
title: string;
|
||||
children?: JSX.Element;
|
||||
actions?: JSX.Element[];
|
||||
children?: any;
|
||||
actions?: React.ReactNode[];
|
||||
defaultMax?: boolean;
|
||||
footer?: React.ReactNode;
|
||||
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
|
||||
}, []);
|
||||
|
||||
const [isMax, setMax] = useState(!!props.defaultMax);
|
||||
|
||||
return (
|
||||
<div className={styles["modal-container"]}>
|
||||
<div
|
||||
className={
|
||||
styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
|
||||
}
|
||||
>
|
||||
<div className={styles["modal-header"]}>
|
||||
<div className={styles["modal-title"]}>{props.title}</div>
|
||||
|
||||
<div className={styles["modal-close-btn"]} onClick={props.onClose}>
|
||||
<CloseIcon />
|
||||
<div className={styles["modal-header-actions"]}>
|
||||
<div
|
||||
className={styles["modal-header-action"]}
|
||||
onClick={() => setMax(!isMax)}
|
||||
>
|
||||
{isMax ? <MinIcon /> : <MaxIcon />}
|
||||
</div>
|
||||
<div
|
||||
className={styles["modal-header-action"]}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["modal-content"]}>{props.children}</div>
|
||||
|
||||
<div className={styles["modal-footer"]}>
|
||||
{props.footer}
|
||||
<div className={styles["modal-actions"]}>
|
||||
{props.actions?.map((action, i) => (
|
||||
<div key={i} className={styles["modal-action"]}>
|
||||
@@ -116,6 +188,7 @@ export type ToastProps = {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export function Toast(props: ToastProps) {
|
||||
@@ -125,7 +198,10 @@ export function Toast(props: ToastProps) {
|
||||
<span>{props.content}</span>
|
||||
{props.action && (
|
||||
<button
|
||||
onClick={props.action.onClick}
|
||||
onClick={() => {
|
||||
props.action?.onClick?.();
|
||||
props.onClose?.();
|
||||
}}
|
||||
className={styles["toast-action"]}
|
||||
>
|
||||
{props.action.text}
|
||||
@@ -159,7 +235,7 @@ export function showToast(
|
||||
close();
|
||||
}, delay);
|
||||
|
||||
root.render(<Toast content={content} action={action} />);
|
||||
root.render(<Toast content={content} action={action} onClose={close} />);
|
||||
}
|
||||
|
||||
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
|
||||
@@ -175,3 +251,237 @@ export function Input(props: InputProps) {
|
||||
></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>
|
||||
);
|
||||
}
|
||||
|
||||
export function showConfirm(content: any) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "modal-mask";
|
||||
document.body.appendChild(div);
|
||||
|
||||
const root = createRoot(div);
|
||||
const closeModal = () => {
|
||||
root.unmount();
|
||||
div.remove();
|
||||
};
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
root.render(
|
||||
<Modal
|
||||
title={Locale.UI.Confirm}
|
||||
actions={[
|
||||
<IconButton
|
||||
key="cancel"
|
||||
text={Locale.UI.Cancel}
|
||||
onClick={() => {
|
||||
resolve(false);
|
||||
closeModal();
|
||||
}}
|
||||
icon={<CancelIcon />}
|
||||
tabIndex={0}
|
||||
bordered
|
||||
shadow
|
||||
></IconButton>,
|
||||
<IconButton
|
||||
key="confirm"
|
||||
text={Locale.UI.Confirm}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
resolve(true);
|
||||
closeModal();
|
||||
}}
|
||||
icon={<ConfirmIcon />}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
bordered
|
||||
shadow
|
||||
></IconButton>,
|
||||
]}
|
||||
onClose={closeModal}
|
||||
>
|
||||
{content}
|
||||
</Modal>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function PromptInput(props: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
rows?: number;
|
||||
}) {
|
||||
const [input, setInput] = useState(props.value);
|
||||
const onInput = (value: string) => {
|
||||
props.onChange(value);
|
||||
setInput(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className={styles["modal-input"]}
|
||||
autoFocus
|
||||
value={input}
|
||||
onInput={(e) => onInput(e.currentTarget.value)}
|
||||
rows={props.rows ?? 3}
|
||||
></textarea>
|
||||
);
|
||||
}
|
||||
|
||||
export function showPrompt(content: any, value = "", rows = 3) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "modal-mask";
|
||||
document.body.appendChild(div);
|
||||
|
||||
const root = createRoot(div);
|
||||
const closeModal = () => {
|
||||
root.unmount();
|
||||
div.remove();
|
||||
};
|
||||
|
||||
return new Promise<string>((resolve) => {
|
||||
let userInput = value;
|
||||
|
||||
root.render(
|
||||
<Modal
|
||||
title={content}
|
||||
actions={[
|
||||
<IconButton
|
||||
key="cancel"
|
||||
text={Locale.UI.Cancel}
|
||||
onClick={() => {
|
||||
closeModal();
|
||||
}}
|
||||
icon={<CancelIcon />}
|
||||
bordered
|
||||
shadow
|
||||
tabIndex={0}
|
||||
></IconButton>,
|
||||
<IconButton
|
||||
key="confirm"
|
||||
text={Locale.UI.Confirm}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
resolve(userInput);
|
||||
closeModal();
|
||||
}}
|
||||
icon={<ConfirmIcon />}
|
||||
bordered
|
||||
shadow
|
||||
tabIndex={0}
|
||||
></IconButton>,
|
||||
]}
|
||||
onClose={closeModal}
|
||||
>
|
||||
<PromptInput
|
||||
onChange={(val) => (userInput = val)}
|
||||
value={value}
|
||||
rows={rows}
|
||||
></PromptInput>
|
||||
</Modal>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function showImageModal(img: string) {
|
||||
showModal({
|
||||
title: Locale.Export.Image.Modal,
|
||||
children: (
|
||||
<div>
|
||||
<img
|
||||
src={img}
|
||||
alt="preview"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
></img>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function Selector<T>(props: {
|
||||
items: Array<{
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
value: T;
|
||||
}>;
|
||||
defaultSelectedValue?: T;
|
||||
onSelection?: (selection: T[]) => void;
|
||||
onClose?: () => void;
|
||||
multiple?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
|
||||
<div className={styles["selector-content"]}>
|
||||
<List>
|
||||
{props.items.map((item, i) => {
|
||||
const selected = props.defaultSelectedValue === item.value;
|
||||
return (
|
||||
<ListItem
|
||||
className={styles["selector-item"]}
|
||||
key={i}
|
||||
title={item.title}
|
||||
subTitle={item.subTitle}
|
||||
onClick={() => {
|
||||
props.onSelection?.([item.value]);
|
||||
props.onClose?.();
|
||||
}}
|
||||
>
|
||||
{selected ? (
|
||||
<div
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: "var(--primary)",
|
||||
borderRadius: 10,
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
55
app/components/voice/voice.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.voice-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba($color: #000000, $alpha: 0.9);
|
||||
color: white;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.top,
|
||||
.bottom {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
font-size: 1.5em;
|
||||
color: rgba($color: #fff, $alpha: 0.6);
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: rgba($color: #00ff00, $alpha: 0.2);
|
||||
}
|
||||
|
||||
.top.active {
|
||||
background-color: white;
|
||||
|
||||
&::after {
|
||||
content: "☁️";
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.top:hover {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.top {
|
||||
}
|
||||
|
||||
.center {
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
opacity: 0.2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
}
|
||||
}
|
117
app/components/voice/voice.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useChatStore } from "@/app/store";
|
||||
import style from "./voice.module.scss";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import SpeechRecognition, {
|
||||
useSpeechRecognition,
|
||||
} from "react-speech-recognition";
|
||||
import { IconButton } from "../button";
|
||||
import { api } from "@/app/client/api";
|
||||
|
||||
function findLast<T>(array: T[], predictor: (_: T) => boolean) {
|
||||
for (let i = array.length - 1; i >= 0; i -= 1) {
|
||||
if (predictor(array[i])) {
|
||||
return array[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function VoicePage() {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const lastAssistantMessage = useMemo(
|
||||
() => findLast(session.messages, (m) => m.role === "assistant"),
|
||||
[session.messages],
|
||||
);
|
||||
const lastUserMessage = useMemo(
|
||||
() => findLast(session.messages, (m) => m.role === "user"),
|
||||
[session.messages],
|
||||
);
|
||||
const speech = useSpeechRecognition({
|
||||
clearTranscriptOnListen: true,
|
||||
});
|
||||
|
||||
if (!speech.browserSupportsSpeechRecognition) {
|
||||
throw Error("your browser does not support speech recognition api");
|
||||
}
|
||||
|
||||
function startVoice() {
|
||||
SpeechRecognition.startListening({
|
||||
language: "zh-CN",
|
||||
});
|
||||
sourceNodeRef.current?.stop();
|
||||
}
|
||||
|
||||
function stopVoice() {
|
||||
SpeechRecognition.stopListening();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!speech.listening) {
|
||||
if (
|
||||
speech.finalTranscript.length > 0 &&
|
||||
speech.finalTranscript !== lastUserMessage?.content
|
||||
) {
|
||||
chatStore.onUserInput(speech.finalTranscript);
|
||||
}
|
||||
}
|
||||
}, [speech.listening]);
|
||||
|
||||
const [loadingTTS, setLoadingTTS] = useState(false);
|
||||
const sourceNodeRef = useRef<AudioBufferSourceNode>();
|
||||
|
||||
function speak() {
|
||||
const content = lastAssistantMessage?.content;
|
||||
if (!content) return;
|
||||
setLoadingTTS(true);
|
||||
api.llm.speech(content).then(async (arrayBuffer) => {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as any).webkitAudioContext)();
|
||||
const source = audioContext.createBufferSource();
|
||||
try {
|
||||
sourceNodeRef.current?.stop();
|
||||
} catch {}
|
||||
sourceNodeRef.current = source;
|
||||
// 设置音频源的 buffer 属性
|
||||
source.buffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
// 连接到默认的输出设备(通常是扬声器)
|
||||
source.connect(audioContext.destination);
|
||||
// 开始播放
|
||||
setLoadingTTS(false);
|
||||
source.start(0);
|
||||
});
|
||||
}
|
||||
|
||||
const lastStream = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
lastAssistantMessage?.streaming !== lastStream.current &&
|
||||
lastStream.current
|
||||
) {
|
||||
speak();
|
||||
}
|
||||
lastStream.current = !!lastAssistantMessage?.streaming;
|
||||
}, [lastAssistantMessage?.streaming]);
|
||||
|
||||
return (
|
||||
<div className={style["voice-page"]}>
|
||||
<div className={style["top"] + ` ${style["active"]}`} onClick={speak}>
|
||||
{lastAssistantMessage?.content}
|
||||
</div>
|
||||
<div className={style["center"]}></div>
|
||||
<div
|
||||
className={style["bottom"] + ` ${speech.listening && style["active"]}`}
|
||||
onClick={() => {
|
||||
if (speech.listening) {
|
||||
stopVoice();
|
||||
} else {
|
||||
startVoice();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{speech.transcript || lastUserMessage?.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,18 +1,4 @@
|
||||
const COMMIT_ID: string = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
return (
|
||||
childProcess
|
||||
// .execSync("git describe --tags --abbrev=0")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim()
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
import tauriConfig from "../../src-tauri/tauri.conf.json";
|
||||
|
||||
export const getBuildConfig = () => {
|
||||
if (typeof process === "undefined") {
|
||||
@@ -21,7 +7,38 @@ export const getBuildConfig = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const buildMode = process.env.BUILD_MODE ?? "standalone";
|
||||
const isApp = !!process.env.BUILD_APP;
|
||||
const version = "v" + tauriConfig.package.version;
|
||||
|
||||
const commitInfo = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
const commitDate: string = childProcess
|
||||
.execSync('git log -1 --format="%at000" --date=unix')
|
||||
.toString()
|
||||
.trim();
|
||||
const commitHash: string = childProcess
|
||||
.execSync('git log --pretty=format:"%H" -n 1')
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
return { commitDate, commitHash };
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return {
|
||||
commitDate: "unknown",
|
||||
commitHash: "unknown",
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
commitId: COMMIT_ID,
|
||||
version,
|
||||
...commitInfo,
|
||||
buildMode,
|
||||
isApp,
|
||||
};
|
||||
};
|
||||
|
||||
export type BuildConfig = ReturnType<typeof getBuildConfig>;
|
||||
|
27
app/config/client.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BuildConfig, getBuildConfig } from "./build";
|
||||
|
||||
export function getClientConfig() {
|
||||
if (typeof document !== "undefined") {
|
||||
// client side
|
||||
return JSON.parse(queryMeta("config")) as BuildConfig;
|
||||
}
|
||||
|
||||
if (typeof process !== "undefined") {
|
||||
// server side
|
||||
return getBuildConfig();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
@@ -1,12 +1,31 @@
|
||||
import md5 from "spark-md5";
|
||||
import { DEFAULT_MODELS } from "../constant";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PROXY_URL?: string; // docker only
|
||||
|
||||
OPENAI_API_KEY?: string;
|
||||
CODE?: string;
|
||||
PROXY_URL?: string;
|
||||
|
||||
BASE_URL?: string;
|
||||
OPENAI_ORG_ID?: string; // openai only
|
||||
|
||||
VERCEL?: string;
|
||||
BUILD_MODE?: "standalone" | "export";
|
||||
BUILD_APP?: string; // is building desktop app
|
||||
|
||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
|
||||
CUSTOM_MODELS?: string; // to control custom models
|
||||
|
||||
// azure only
|
||||
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
||||
AZURE_API_KEY?: string;
|
||||
AZURE_API_VERSION?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,12 +50,39 @@ export const getServerSideConfig = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
|
||||
return {
|
||||
baseUrl: process.env.BASE_URL,
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||
|
||||
isAzure,
|
||||
azureUrl: process.env.AZURE_URL,
|
||||
azureApiKey: process.env.AZURE_API_KEY,
|
||||
azureApiVersion: process.env.AZURE_API_VERSION,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
code: process.env.CODE,
|
||||
codes: ACCESS_CODES,
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
|
||||
proxyUrl: process.env.PROXY_URL,
|
||||
isVercel: !!process.env.VERCEL,
|
||||
|
||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||
disableGPT4,
|
||||
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
};
|
||||
};
|
||||
|
149
app/constant.ts
@@ -1,8 +1,157 @@
|
||||
import { GPTText } from "./utils/prompts/gpt-text";
|
||||
import { GPTVoice } from "./utils/prompts/gpt-voice";
|
||||
|
||||
export const OWNER = "Yidadaa";
|
||||
export const REPO = "ChatGPT-Next-Web";
|
||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
||||
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
|
||||
export const RELEASE_URL = `${REPO_URL}/releases`;
|
||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const DEFAULT_CORS_HOST = "https://a.nextweb.fun";
|
||||
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
Settings = "/settings",
|
||||
NewChat = "/new-chat",
|
||||
Masks = "/masks",
|
||||
Auth = "/auth",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "/api/cors",
|
||||
OpenAI = "/api/openai",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
AppBody = "app-body",
|
||||
CustomModel = "custom-model",
|
||||
}
|
||||
|
||||
export enum FileName {
|
||||
Masks = "masks.json",
|
||||
Prompts = "prompts.json",
|
||||
}
|
||||
|
||||
export enum StoreKey {
|
||||
Chat = "chat-next-web-store",
|
||||
Access = "access-control",
|
||||
Config = "app-config",
|
||||
Mask = "mask-store",
|
||||
Prompt = "prompt-store",
|
||||
Update = "chat-update",
|
||||
Sync = "sync",
|
||||
}
|
||||
|
||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||
export const MAX_SIDEBAR_WIDTH = 500;
|
||||
export const MIN_SIDEBAR_WIDTH = 230;
|
||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
|
||||
export const ACCESS_CODE_PREFIX = "nk-";
|
||||
|
||||
export const LAST_INPUT_KEY = "last-input";
|
||||
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
|
||||
|
||||
export const STORAGE_KEY = "chatgpt-next-web";
|
||||
|
||||
export const REQUEST_TIMEOUT_MS = 60000;
|
||||
|
||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
||||
|
||||
export enum ServiceProvider {
|
||||
OpenAI = "OpenAI",
|
||||
Azure = "Azure",
|
||||
}
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
ListModelPath: "v1/models",
|
||||
Speech: "v1/audio/speech",
|
||||
};
|
||||
|
||||
export const Azure = {
|
||||
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
// export const DEFAULT_SYSTEM_TEMPLATE = GPTText;
|
||||
export const DEFAULT_SYSTEM_TEMPLATE = GPTVoice;
|
||||
|
||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
||||
|
||||
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
default: "2021-09",
|
||||
"gpt-4-1106-preview": "2023-04",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
};
|
||||
|
||||
export const DEFAULT_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0613",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0314",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0613",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-1106-preview",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-vision-preview",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0613",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-1106",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k-0613",
|
||||
available: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
export const MAX_RENDER_MSG_COUNT = 45;
|
||||
|
18
app/global.d.ts
vendored
@@ -9,3 +9,21 @@ declare module "*.scss" {
|
||||
}
|
||||
|
||||
declare module "*.svg";
|
||||
|
||||
declare interface Window {
|
||||
__TAURI__?: {
|
||||
writeText(text: string): Promise<void>;
|
||||
invoke(command: string, payload?: Record<string, unknown>): Promise<any>;
|
||||
dialog: {
|
||||
save(options?: Record<string, unknown>): Promise<string | null>;
|
||||
};
|
||||
fs: {
|
||||
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
||||
};
|
||||
notification:{
|
||||
requestPermission(): Promise<Permission>;
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -1,23 +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(8 5.333333333333333) rotate(0 0 2.6666666666666665)" d="M0,0L0,5.33 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(5.333333333333333 8) rotate(0 2.6666666666666665 0)" d="M0,0L5.33,0 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,5.33" transform="translate(8 5.333333333333333) rotate(0 0 2.6666666666666665)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L5.33,0" transform="translate(5.333333333333333 8) rotate(0 2.6666666666666665 0)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
1
app/icons/black-bot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30" fill="none" viewBox="0 0 30 30"><defs><rect id="path_0" width="30" height="30" x="0" y="0"/><rect id="path_1" width="20.455" height="20.455" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)"><rect width="30" height="30" x="0" y="0" fill="#E7F8FF" opacity="1" rx="10" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)"/><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></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="#fff"><use xlink:href="#path_1"/></mask><g mask="url(#bg-mask-1)"><path id="分组 1" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)"/></g></g></g></g></svg>
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/icons/bot.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
@@ -1 +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(8.002766666666666 2) rotate(0 0 4.649916666666667)" d="M0,9.3L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 7.333333333333333) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 14) rotate(0 4 0)" d="M8,0L0,0 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M8,0L4,4L0,0" transform="translate(4 4) rotate(0 4 2)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M8,0L4,4L0,0" transform="translate(4 8) rotate(0 4 2)"/></g></g></svg>
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 706 B |
@@ -1,25 +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.3333323286384866 1.3334133333333331) rotate(0 6.66666716901409 6.66666)"
|
||||
d="M5.01,13.33C4.69,12.27 4.19,11.47 3.53,10.95C2.55,10.17 0.97,10.65 0.39,9.84C-0.19,9.04 0.8,7.55 1.15,6.67C1.49,5.79 -0.18,5.48 0.02,5.23C0.15,5.07 0.99,4.59 2.55,3.79C3,1.26 4.63,0 7.47,0C11.71,0 13.33,3.6 13.33,5.89C13.33,8.18 11.37,10.65 8.58,11.18C8.33,11.55 8.69,12.26 9.66,13.33 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(6.374029736345404 3.9567867125879106) rotate(0 2.8215982497276006 2.4327734241007346)"
|
||||
d="M2.1,3.33C1.91,4.42 2.14,4.93 2.79,4.86C3.44,4.79 3.84,4.52 3.97,4.05C4.99,4.33 5.54,4.09 5.63,3.33C5.75,2.18 5.13,1.26 4.88,1.26C4.63,1.26 3.97,1.23 3.97,0.88C3.97,0.52 3.2,0.33 2.5,0.33C1.81,0.33 2.23,-0.14 1.27,0.04C0.64,0.17 0.26,0.44 0.13,0.88C-0.09,1.72 -0.03,2.31 0.32,2.66C0.67,3 1.26,3.22 2.1,3.33Z " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(8.193033333333332 8.500066666666665) rotate(0 0.9868499999999998 1.1846833333333333)"
|
||||
d="M1.97,0C1.63,0.21 1.17,0.56 0.97,0.83C0.48,1.52 0.09,1.93 0,2.37 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M5.01,13.33C4.69,12.27 4.19,11.47 3.53,10.95C2.55,10.17 0.97,10.65 0.39,9.84C-0.19,9.04 0.8,7.55 1.15,6.67C1.49,5.79 -0.18,5.48 0.02,5.23C0.15,5.07 0.99,4.59 2.55,3.79C3,1.26 4.63,0 7.47,0C11.71,0 13.33,3.6 13.33,5.89C13.33,8.18 11.37,10.65 8.58,11.18C8.33,11.55 8.69,12.26 9.66,13.33" transform="translate(1.3333323286384866 1.3334133333333331) rotate(0 6.66666716901409 6.66666)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M2.1,3.33C1.91,4.42 2.14,4.93 2.79,4.86C3.44,4.79 3.84,4.52 3.97,4.05C4.99,4.33 5.54,4.09 5.63,3.33C5.75,2.18 5.13,1.26 4.88,1.26C4.63,1.26 3.97,1.23 3.97,0.88C3.97,0.52 3.2,0.33 2.5,0.33C1.81,0.33 2.23,-0.14 1.27,0.04C0.64,0.17 0.26,0.44 0.13,0.88C-0.09,1.72 -0.03,2.31 0.32,2.66C0.67,3 1.26,3.22 2.1,3.33Z" transform="translate(6.374029736345404 3.9567867125879106) rotate(0 2.8215982497276006 2.4327734241007346)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M1.97,0C1.63,0.21 1.17,0.56 0.97,0.83C0.48,1.52 0.09,1.93 0,2.37" transform="translate(8.193033333333332 8.500066666666665) rotate(0 0.9868499999999998 1.1846833333333333)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
1
app/icons/break.svg
Normal 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)"><g opacity="1" transform="translate(1.0001220703125 2) rotate(0)"><path id="路径 1" style="fill:#333333; opacity:1;" d="M13.275,-0.27515c0.261,0.26101 0.3915,0.57606 0.3915,0.94515v10.66c0,0.36907 -0.1305,0.68413 -0.3915,0.9452c-0.261,0.261 -0.57603,0.3915 -0.9451,0.3915h-10.66002c-0.36909,0 -0.68415,-0.1305 -0.94516,-0.3915c-0.26101,-0.26107 -0.39151,-0.57613 -0.39151,-0.9452v-10.66c0,-0.3691 0.1305,-0.68415 0.39151,-0.94515c0.26101,-0.26101 0.57606,-0.39151 0.94516,-0.39151h10.66002c0.36907,0 0.6841,0.1305 0.9451,0.39151zM1.66655,11.33c0,0.0022 0.00111,0.0033 0.00333,0.0033h10.66002c0.0022,0 0.0033,-0.0011 0.0033,-0.0033v-10.66c0,-0.00222 -0.0011,-0.00333 -0.0033,-0.00333l-10.66002,0c-0.00222,0 -0.00333,0.00111 -0.00333,0.00333z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M9.76327,7.50715c-0.02999,0.02563 -0.06201,0.04842 -0.09604,0.06837c-0.03403,0.01995 -0.06956,0.03674 -0.10658,0.05039c-0.03702,0.01364 -0.07495,0.02391 -0.11379,0.03082c-0.03885,0.00691 -0.07799,0.01035 -0.11744,0.0103c-0.03945,-0.00004 -0.07859,-0.00356 -0.11742,-0.01055c-0.03883,-0.00699 -0.07674,-0.01734 -0.11373,-0.03106c-0.03699,-0.01372 -0.07248,-0.03059 -0.10647,-0.05061c-0.03399,-0.02002 -0.06596,-0.04288 -0.0959,-0.06858l-1.89578,-1.62728l-1.89578,1.62728c-0.02993,0.0257 -0.0619,0.04856 -0.09589,0.06858c-0.03399,0.02002 -0.06949,0.03689 -0.10648,0.05061c-0.03699,0.01372 -0.07489,0.02407 -0.11372,0.03106c-0.03883,0.00699 -0.07797,0.01051 -0.11742,0.01055c-0.03945,0.00005 -0.0786,-0.00339 -0.11744,-0.0103c-0.03885,-0.00691 -0.07678,-0.01718 -0.11379,-0.03082c-0.03702,-0.01365 -0.07255,-0.03044 -0.10658,-0.05039c-0.03404,-0.01995 -0.06605,-0.04274 -0.09604,-0.06837l-1.90593,-1.629l-1.89671,1.62808c-0.06708,0.05758 -0.14263,0.10013 -0.22664,0.12766c-0.08401,0.02753 -0.17009,0.03793 -0.25824,0.03121c-0.08815,-0.00671 -0.17166,-0.03004 -0.25053,-0.06998c-0.07887,-0.03994 -0.14709,-0.09345 -0.20467,-0.16054c-0.02851,-0.03321 -0.05351,-0.06889 -0.07499,-0.10703c-0.02148,-0.03814 -0.03904,-0.07801 -0.05267,-0.11961c-0.01363,-0.04159 -0.02307,-0.08412 -0.02832,-0.12758c-0.00525,-0.04346 -0.00622,-0.08701 -0.00289,-0.13066c0.00333,-0.04365 0.01088,-0.08655 0.02266,-0.12871c0.01178,-0.04216 0.02755,-0.08277 0.04733,-0.12182c0.01978,-0.03905 0.04317,-0.07579 0.07019,-0.11024c0.02701,-0.03444 0.05713,-0.06592 0.09035,-0.09443l2.32999,-2c0.02994,-0.02569 0.06191,-0.04855 0.0959,-0.06857c0.03399,-0.02003 0.06948,-0.0369 0.10647,-0.05062c0.03699,-0.01372 0.0749,-0.02407 0.11373,-0.03106c0.03883,-0.00699 0.07797,-0.01051 0.11742,-0.01055c0.03945,-0.00004 0.0786,0.00339 0.11744,0.0103c0.03884,0.00691 0.07677,0.01718 0.11379,0.03082c0.03702,0.01365 0.07255,0.03044 0.10658,0.05039c0.03404,0.01995 0.06605,0.04274 0.09604,0.06837l1.90592,1.629l1.89671,-1.62808c0.02998,-0.02573 0.062,-0.04862 0.09605,-0.06866c0.03405,-0.02005 0.0696,-0.03693 0.10665,-0.05065c0.03705,-0.01372 0.07503,-0.02407 0.11392,-0.03104c0.03889,-0.00697 0.07809,-0.01045 0.1176,-0.01045c0.03951,0 0.07872,0.00348 0.11761,0.01045c0.03889,0.00697 0.07686,0.01732 0.11391,0.03104c0.03705,0.01372 0.0726,0.0306 0.10665,0.05065c0.03405,0.02004 0.06607,0.04293 0.09605,0.06866l1.89671,1.62808l1.90595,-1.629c0.03,-0.02563 0.062,-0.04842 0.096,-0.06837c0.03407,-0.01995 0.0696,-0.03674 0.1066,-0.05038c0.037,-0.01365 0.07493,-0.02392 0.1138,-0.03083c0.03887,-0.00691 0.078,-0.01034 0.1174,-0.0103c0.03947,0.00004 0.0786,0.00356 0.1174,0.01055c0.03887,0.00699 0.0768,0.01734 0.1138,0.03106c0.037,0.01372 0.07247,0.03059 0.1064,0.05062c0.034,0.02002 0.06597,0.04288 0.0959,0.06857l2.33,2c0.06713,0.05758 0.12067,0.12581 0.1606,0.20468c0.03993,0.07887 0.06327,0.16237 0.07,0.25052c0.00667,0.08815 -0.00377,0.17424 -0.0313,0.25825c-0.02747,0.08401 -0.07,0.15955 -0.1276,0.22663c-0.02853,0.03322 -0.06,0.06334 -0.0944,0.09035c-0.03447,0.02701 -0.07123,0.05041 -0.1103,0.07019c-0.03907,0.01977 -0.07967,0.03555 -0.1218,0.04733c-0.04213,0.01177 -0.08503,0.01932 -0.1287,0.02265c-0.04367,0.00333 -0.08723,0.00236 -0.1307,-0.00289c-0.04347,-0.00525 -0.086,-0.01469 -0.1276,-0.02832c-0.0416,-0.01363 -0.08147,-0.03118 -0.1196,-0.05267c-0.03813,-0.02148 -0.0738,-0.04648 -0.107,-0.07499l-1.8967,-1.62808z"></path></g><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)" ></g></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 4.5 KiB |
1
app/icons/cancel.svg
Normal 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="M13.9967,8.00337c0,-0.81625 -0.1569,-1.59615 -0.4707,-2.3397c-0.30307,-0.71824 -0.73117,-1.3542 -1.2843,-1.90789c-0.55287,-0.55348 -1.18783,-0.98185 -1.9049,-1.28512c-0.74182,-0.31375 -1.51963,-0.47062 -2.33343,-0.47062c-0.81634,0 -1.59621,0.15693 -2.3396,0.47079c-0.71828,0.30325 -1.35419,0.73165 -1.90774,1.2852c-0.55355,0.55354 -0.98195,1.18945 -1.2852,1.90774c-0.31386,0.74339 -0.47079,1.52326 -0.47079,2.3396c0,0.8138 0.15687,1.59161 0.47062,2.33343c0.30327,0.71707 0.73164,1.35203 1.28512,1.9049c0.55369,0.55313 1.18965,0.98123 1.90789,1.2843c0.74355,0.3138 1.52345,0.4707 2.3397,0.4707c0.81371,0 1.59155,-0.15683 2.33353,-0.4705c0.717,-0.30313 1.35203,-0.7312 1.9051,-1.2842c0.553,-0.55307 0.98107,-1.1881 1.2842,-1.9051c0.31367,-0.74198 0.4705,-1.51982 0.4705,-2.33353zM15.33,8.00337c0,0.99387 -0.1919,1.94478 -0.5757,2.85273c-0.37067,0.87673 -0.89383,1.65297 -1.5695,2.3287c-0.67573,0.67567 -1.45197,1.19883 -2.3287,1.5695c-0.90795,0.3838 -1.85886,0.5757 -2.85273,0.5757c-0.99612,0 -1.94882,-0.19183 -2.8581,-0.5755c-0.8781,-0.3706 -1.65537,-0.89377 -2.33181,-1.5695c-0.67631,-0.6756 -1.19992,-1.45187 -1.57081,-2.3288c-0.38396,-0.90784 -0.57594,-1.85878 -0.57594,-2.85283c0,-0.99629 0.19192,-1.94903 0.57577,-2.8582c0.37081,-0.87829 0.89439,-1.6556 1.57074,-2.33195c0.67635,-0.67635 1.45367,-1.19993 2.33195,-1.57074c0.90917,-0.38385 1.86191,-0.57577 2.8582,-0.57577c0.99405,0 1.94499,0.19198 2.85283,0.57594c0.87693,0.37089 1.6532,0.8945 2.3288,1.57081c0.67573,0.67644 1.1989,1.45371 1.5695,2.33181c0.38367,0.90928 0.5755,1.86198 0.5755,2.8581z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M5.4714,4.5286l6,6c0.03093,0.03093 0.05857,0.0646 0.0829,0.101c0.02433,0.0364 0.04487,0.07483 0.0616,0.1153c0.01673,0.0404 0.0294,0.08207 0.038,0.125c0.00853,0.04293 0.0128,0.0863 0.0128,0.1301c0,0.0438 -0.00427,0.08717 -0.0128,0.1301c-0.0086,0.04293 -0.02127,0.0846 -0.038,0.125c-0.01673,0.04047 -0.03727,0.0789 -0.0616,0.1153c-0.02433,0.0364 -0.05197,0.07007 -0.0829,0.101c-0.03093,0.03093 -0.0646,0.05857 -0.101,0.0829c-0.0364,0.02433 -0.07483,0.04487 -0.1153,0.0616c-0.0404,0.01673 -0.08207,0.0294 -0.125,0.038c-0.04293,0.00853 -0.0863,0.0128 -0.1301,0.0128c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.0086 -0.0846,-0.02127 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02433 -0.07007,-0.05197 -0.101,-0.0829l-6,-6c-0.03095,-0.03095 -0.05859,-0.06463 -0.08291,-0.10102c-0.02432,-0.0364 -0.04486,-0.07482 -0.06161,-0.11526c-0.01675,-0.04044 -0.0294,-0.08213 -0.03794,-0.12506c-0.00854,-0.04293 -0.01281,-0.08629 -0.01281,-0.13006c0,-0.04377 0.00427,-0.08713 0.01281,-0.13006c0.00854,-0.04293 0.02119,-0.08462 0.03794,-0.12506c0.01675,-0.04045 0.03729,-0.07887 0.06161,-0.11526c0.02432,-0.0364 0.05196,-0.07007 0.08291,-0.10102c0.03095,-0.03095 0.06462,-0.05859 0.10102,-0.08291c0.03639,-0.02432 0.07481,-0.04486 0.11526,-0.06161c0.04044,-0.01675 0.08213,-0.0294 0.12506,-0.03794c0.04293,-0.00854 0.08629,-0.01281 0.13006,-0.01281c0.04377,0 0.08713,0.00427 0.13006,0.01281c0.04293,0.00854 0.08462,0.02119 0.12506,0.03794c0.04044,0.01675 0.07886,0.03729 0.11526,0.06161c0.03639,0.02432 0.07007,0.05196 0.10102,0.08291z"></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/chat-settings.svg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
app/icons/chatgpt.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
1
app/icons/cloud-fail.svg
Normal file
After Width: | Height: | Size: 6.2 KiB |
1
app/icons/cloud-success.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M4.00337,11.6633M4.00337,11.6633c-0.8391,0 -1.5514,-0.20763 -2.13691,-0.6229c-0.79984,-0.56727 -1.19975,-1.41519 -1.19975,-2.54377c0,-1.2521 0.52073,-2.20348 1.56218,-2.85415c0.68561,-0.42835 1.38711,-0.64252 2.10448,-0.64252v0.66667l-0.64163,-0.18098l0.64163,0.18098l-0.64163,-0.18098c0.65543,-2.32379 2.09264,-3.48569 4.31163,-3.48569c2.30635,0 3.74023,0.99523 4.30163,2.98569l-0.6416,0.18098l0.0729,-0.66267l-0.0729,0.66267l0.0729,-0.66267c2.3958,0.26354 3.5937,1.5411 3.5937,3.83267c0,2.21778 -1.10887,3.32667 -3.3266,3.32667c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.00853 -0.0846,-0.0212 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02427 -0.07007,-0.0519 -0.101,-0.0829c-0.031,-0.03093 -0.05863,-0.0646 -0.0829,-0.101c-0.02433,-0.0364 -0.04487,-0.07483 -0.0616,-0.1153c-0.0168,-0.0404 -0.02947,-0.08207 -0.038,-0.125c-0.00853,-0.04293 -0.0128,-0.0863 -0.0128,-0.1301c0,-0.04373 0.00427,-0.08707 0.0128,-0.13c0.00853,-0.04293 0.0212,-0.08463 0.038,-0.1251c0.01673,-0.04047 0.03727,-0.0789 0.0616,-0.1153c0.02427,-0.0364 0.0519,-0.07007 0.0829,-0.101c0.03093,-0.03093 0.0646,-0.05857 0.101,-0.0829c0.0364,-0.02433 0.07483,-0.04487 0.1153,-0.0616c0.0404,-0.01673 0.08207,-0.02937 0.125,-0.0379c0.04293,-0.00853 0.0863,-0.0128 0.1301,-0.0128c1.32887,0 1.9933,-0.66446 1.9933,-1.99337c0,-1.4951 -0.80207,-2.33088 -2.4062,-2.50733c-0.27,-0.02971 -0.495,-0.22026 0,0c-0.27,-0.02971 -0.495,-0.22026 -0.5688,-0.4817c-0.37873,-1.34287 -1.38484,-2.01431 -3.01833,-2.01431c-1.54613,0 -2.55559,0.8381 -3.02836,2.51431c-0.08103,0.28728 -0.34314,0.48569 0,0c-0.08103,0.28728 -0.34314,0.48569 -0.64164,0.48569c-0.46252,0 -0.92852,0.14666 -1.39801,0.43998c-0.62355,0.38957 -0.93532,0.96402 -0.93532,1.72336c0,0.66927 0.21258,1.15467 0.63775,1.45621c0.35449,0.25144 0.80969,0.37716 1.36558,0.37716c0.04378,0 0.08713,0.00427 0.13006,0.0128c0.04293,0.00853 0.08462,0.02117 0.12507,0.0379c0.04044,0.01673 0.07886,0.03727 0.11525,0.0616c0.0364,0.02433 0.07008,0.05197 0.10103,0.0829c0.03095,0.03093 0.05859,0.0646 0.08291,0.101c0.02432,0.0364 0.04485,0.07483 0.0616,0.1153c0.01675,0.04047 0.0294,0.08217 0.03794,0.1251c0.00854,0.04293 0.01281,0.08627 0.01281,0.13c0,0.0438 -0.00427,0.08717 -0.01281,0.1301c-0.00854,0.04293 -0.02119,0.0846 -0.03794,0.125c-0.01675,0.04047 -0.03728,0.0789 -0.0616,0.1153c-0.02432,0.0364 -0.05196,0.07007 -0.08291,0.101c-0.03095,0.031 -0.06463,0.05863 -0.10103,0.0829c-0.03639,0.02433 -0.07481,0.04487 -0.11525,0.0616c-0.04045,0.0168 -0.08214,0.02947 -0.12507,0.038c-0.04293,0.00853 -0.08628,0.0128 -0.13006,0.0128z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.42578,10.4903l2,1.66l-0.42578,0.513l-0.52012,-0.4171l2.67002,-3.32998c0.0274,-0.03415 0.05783,-0.06531 0.0913,-0.09346c0.03353,-0.02815 0.0695,-0.05277 0.1079,-0.07384c0.03833,-0.02107 0.07837,-0.0382 0.1201,-0.05138c0.04173,-0.01319 0.08437,-0.02217 0.1279,-0.02696c0.04353,-0.00479 0.0871,-0.00528 0.1307,-0.00149c0.0436,0.00379 0.0864,0.01181 0.1284,0.02404c0.04207,0.01223 0.0825,0.02844 0.1213,0.04863c0.03887,0.0202 0.07537,0.04399 0.1095,0.07137c0.0342,0.02738 0.06537,0.05783 0.0935,0.09135c0.02813,0.03352 0.05273,0.06946 0.0738,0.10783c0.02107,0.03837 0.0382,0.07843 0.0514,0.12017c0.0132,0.04174 0.0222,0.08437 0.027,0.12788c0.00473,0.04351 0.00523,0.08707 0.0015,0.13068c-0.0038,0.04361 -0.01183,0.08643 -0.0241,0.12846c-0.0122,0.04203 -0.0284,0.08247 -0.0486,0.1213c-0.0202,0.03884 -0.044,0.07534 -0.0714,0.10949l-2.66998,3.33001c-0.23308,0.2907 -0.65919,0.3339 -0.9459,0.0959l-2,-1.66c-0.03369,-0.02793 -0.06432,-0.0589 -0.0919,-0.0929c-0.02758,-0.034 -0.05158,-0.07037 -0.072,-0.1091c-0.02042,-0.03867 -0.03687,-0.079 -0.04934,-0.121c-0.01248,-0.04193 -0.02074,-0.0847 -0.02479,-0.1283c-0.00405,-0.0436 -0.0038,-0.08717 0.00073,-0.1307c0.00453,-0.04353 0.01326,-0.0862 0.0262,-0.128c0.01294,-0.0418 0.02983,-0.08197 0.05068,-0.1205c0.02085,-0.03847 0.04526,-0.07453 0.07321,-0.1082c0.02796,-0.03373 0.05893,-0.06437 0.09292,-0.0919c0.03399,-0.0276 0.07035,-0.0516 0.10907,-0.072c0.03872,-0.02047 0.07906,-0.03693 0.12102,-0.0494c0.04196,-0.01247 0.08473,-0.02073 0.12831,-0.0248c0.04359,-0.004 0.08715,-0.00373 0.13069,0.0008c0.04354,0.00453 0.08622,0.01327 0.12804,0.0262c0.04181,0.01293 0.08197,0.02983 0.12046,0.0507c0.03849,0.0208 0.07457,0.0452 0.10826,0.0732z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 4.7 KiB |
1
app/icons/config.svg
Normal file
After Width: | Height: | Size: 15 KiB |
1
app/icons/confirm.svg
Normal 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="M5.99607,12.8916c-0.03633,0.02413 -0.07466,0.04453 -0.11499,0.0612c-0.04034,0.01667 -0.08191,0.02923 -0.12471,0.0377c-0.04281,0.00853 -0.08603,0.0128 -0.12967,0.0128c-0.04364,0 -0.08686,-0.00423 -0.12966,-0.0127c-0.04281,-0.00847 -0.08438,-0.02103 -0.12472,-0.0377c-0.04034,-0.01667 -0.07867,-0.03707 -0.115,-0.0612c-0.03633,-0.0242 -0.06997,-0.0517 -0.1009,-0.0825l-3.96,-3.93998c-0.03103,-0.03087 -0.05876,-0.06448 -0.08317,-0.10081c-0.02441,-0.03634 -0.04505,-0.07471 -0.0619,-0.1151c-0.01685,-0.0404 -0.0296,-0.08206 -0.03825,-0.12497c-0.00865,-0.04291 -0.01303,-0.08626 -0.01314,-0.13003c-0.00011,-0.04377 0.00405,-0.08714 0.01248,-0.13009c0.00843,-0.04295 0.02097,-0.08467 0.03762,-0.12516c0.01665,-0.04048 0.03709,-0.07895 0.06132,-0.11541c0.02423,-0.03646 0.05178,-0.0702 0.08265,-0.10123c0.03087,-0.03103 0.06448,-0.05876 0.10081,-0.08317c0.03634,-0.02441 0.07471,-0.04505 0.11511,-0.0619c0.04039,-0.01685 0.08205,-0.0296 0.12496,-0.03825c0.04291,-0.00865 0.08626,-0.01303 0.13003,-0.01314c0.04377,-0.00011 0.08714,0.00405 0.13009,0.01248c0.04295,0.00843 0.08467,0.02097 0.12516,0.03762c0.04048,0.01665 0.07895,0.03709 0.11541,0.06132c0.03646,0.02423 0.07021,0.05178 0.10124,0.08265l3.48968,3.47207l8.23978,-8.20196c0.031,-0.03088 0.06473,-0.05844 0.1012,-0.08268c0.03647,-0.02423 0.07493,-0.04468 0.1154,-0.06134c0.04047,-0.01666 0.0822,-0.02921 0.1252,-0.03765c0.04293,-0.00844 0.0863,-0.01261 0.1301,-0.01251c0.04373,0.0001 0.08707,0.00447 0.13,0.01311c0.04293,0.00864 0.0846,0.02138 0.125,0.03822c0.0404,0.01685 0.07877,0.03747 0.1151,0.06188c0.03633,0.0244 0.06993,0.05211 0.1008,0.08314c0.0624,0.06265 0.1104,0.13486 0.144,0.21661c0.03367,0.08175 0.0504,0.16683 0.0502,0.25524c-0.0002,0.08841 -0.0173,0.17341 -0.0513,0.25501c-0.03407,0.08159 -0.08243,0.15357 -0.1451,0.21594l-8.70996,8.66999c-0.03093,0.0308 -0.06455,0.0583 -0.10087,0.0825z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 2.2 KiB |
1
app/icons/connection.svg
Normal file
After Width: | Height: | Size: 8.1 KiB |
@@ -1 +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.333333333333333 1.6666666666666665) rotate(0 5 5)" d="M0,2.48L0,0.94C0,0.42 0.42,0 0.94,0L9.06,0C9.58,0 10,0.42 10,0.94L10,9.06C10,9.58 9.58,10 9.06,10L7.51,10 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.6666666666666665 4.333333333333333) rotate(0 5 5)" d="M0.94,0C0.42,0 0,0.42 0,0.94L0,9.06C0,9.58 0.42,10 0.94,10L9.06,10C9.58,10 10,9.58 10,9.06L10,0.94C10,0.42 9.58,0 9.06,0L0.94,0Z " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,2.48L0,0.94C0,0.42 0.42,0 0.94,0L9.06,0C9.58,0 10,0.42 10,0.94L10,9.06C10,9.58 9.58,10 9.06,10L7.51,10" transform="translate(4.333333333333333 1.6666666666666665) rotate(0 5 5)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0.94,0C0.42,0 0,0.42 0,0.94L0,9.06C0,9.58 0.42,10 0.94,10L9.06,10C9.58,10 10,9.58 10,9.06L10,0.94C10,0.42 9.58,0 9.06,0L0.94,0Z" transform="translate(1.6666666666666665 4.333333333333333) rotate(0 5 5)"/></g></g></svg>
|
Before Width: | Height: | Size: 1010 B After Width: | Height: | Size: 981 B |
@@ -1 +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.333333333333485) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0L4.91,1.76L1.76,1.76L1.76,4.91L0,6.67L1.76,8.42L1.76,11.58L4.91,11.58L6.67,13.33L8.42,11.58L11.58,11.58L11.58,8.42L13.33,6.67L11.58,4.91L11.58,1.76L8.42,1.76L6.67,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.44771525016904) rotate(0 2.4732087011352872 2.442809041582063)" d="M4,0.55C2.17,-0.78 0,0.55 0,1.89C1.67,1.89 3.33,2.22 3.33,4.89C4.67,4.89 5.83,1.89 4,0.55Z " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 852 B |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
1
app/icons/down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(-90 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M4,8L0,4L4,0" transform="translate(6.333333333333333 4) rotate(0 2 4)"/></g></g></svg>
|
After Width: | Height: | Size: 555 B |
@@ -1 +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 2) rotate(0 6 6)" d="M1,12L11,12C11.55,12 12,11.55 12,11L12,1C12,0.45 11.55,0 11,0L1,0C0.45,0 0,0.45 0,1L0,11C0,11.55 0.45,12 1,12Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 10.333333333333332) rotate(0 6.666666666666666 0.6666666666666666)" d="M0,0L3.67,0L4.33,1.33L9,1.33L9.67,0L13.33,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(14 8.666666666666666) rotate(0 0 1.6666666666666665)" d="M0,3.33L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6 7.333333333333333) rotate(0 2 1)" d="M0,0L2,2L4,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 4) rotate(0 0 2.6666666666666665)" d="M0,5.33L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2 8.666666666666666) rotate(0 0 1.6666666666666665)" d="M0,3.33L0,0 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M1,12L11,12C11.55,12 12,11.55 12,11L12,1C12,0.45 11.55,0 11,0L1,0C0.45,0 0,0.45 0,1L0,11C0,11.55 0.45,12 1,12Z" transform="translate(2 2) rotate(0 6 6)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L3.67,0L4.33,1.33L9,1.33L9.67,0L13.33,0" transform="translate(1.3333333333333333 10.333333333333332) rotate(0 6.666666666666666 0.6666666666666666)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3.33L0,0" transform="translate(14 8.666666666666666) rotate(0 0 1.6666666666666665)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L2,2L4,0" transform="translate(6 7.333333333333333) rotate(0 2 1)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,5.33L0,0" transform="translate(8 4) rotate(0 0 2.6666666666666665)"/><path id="路径 6" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3.33L0,0" transform="translate(2 8.666666666666666) rotate(0 0 1.6666666666666665)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
1
app/icons/drag.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" fill-rule="evenodd" style="fill:#333333" opacity="1" d="M6.33663,3.33c0,0.74 -0.6,1.34 -1.34,1.34c-0.73,0 -1.33,-0.6 -1.33,-1.34c0,-0.73 0.6,-1.33 1.33,-1.33c0.74,0 1.34,0.6 1.34,1.33zM4.99663,9.33c-0.73,0 -1.33,-0.59 -1.33,-1.33c0,-0.74 0.6,-1.33 1.33,-1.33c0.74,0 1.34,0.59 1.34,1.33c0,0.74 -0.6,1.33 -1.34,1.33zM4.99663,14c-0.73,0 -1.33,-0.6 -1.33,-1.33c0,-0.74 0.6,-1.34 1.33,-1.34c0.74,0 1.34,0.6 1.34,1.34c0,0.73 -0.6,1.33 -1.34,1.33z"></path><path id="路径 2" fill-rule="evenodd" style="fill:#333333" opacity="1" d="M12.3366,3.33c0,0.74 -0.6,1.34 -1.34,1.34c-0.73,0 -1.32997,-0.6 -1.32997,-1.34c0,-0.73 0.59997,-1.33 1.32997,-1.33c0.74,0 1.34,0.6 1.34,1.33zM10.9966,9.33c-0.73,0 -1.32997,-0.59 -1.32997,-1.33c0,-0.74 0.59997,-1.33 1.32997,-1.33c0.74,0 1.34,0.59 1.34,1.33c0,0.74 -0.6,1.33 -1.34,1.33zM10.9966,14c-0.73,0 -1.32997,-0.6 -1.32997,-1.33c0,-0.74 0.59997,-1.34 1.32997,-1.34c0.74,0 1.34,0.6 1.34,1.34c0,0.73 -0.6,1.33 -1.34,1.33z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1 +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.2400716519614834 2.3333321805983163) rotate(0 6.785117896431597 4.552683909700841)" d="M12.27,9.11C13.36,8.34 13.83,6.94 13.43,5.67C13.02,4.39 11.78,3.69 10.44,3.69L9.67,3.69C9.16,1.72 7.5,0.27 5.47,0.03C3.45,-0.2 1.5,0.84 0.56,2.64C-0.38,4.45 -0.11,6.64 1.23,8.17 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 7.666666666666666) rotate(0 0.00140000000000029 3)" d="M0,6L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.8786 11.5454) rotate(0 2.1213333333333333 1.0606666666666662)" d="M4.24,0L2.12,2.12L0,0 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M12.27,9.11C13.36,8.34 13.83,6.94 13.43,5.67C13.02,4.39 11.78,3.69 10.44,3.69L9.67,3.69C9.16,1.72 7.5,0.27 5.47,0.03C3.45,-0.2 1.5,0.84 0.56,2.64C-0.38,4.45 -0.11,6.64 1.23,8.17" transform="translate(1.2400716519614834 2.3333321805983163) rotate(0 6.785117896431597 4.552683909700841)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,6L0,0" transform="translate(8 7.666666666666666) rotate(0 0.00140000000000029 3)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M4.24,0L2.12,2.12L0,0" transform="translate(5.8786 11.5454) rotate(0 2.1213333333333333 1.0606666666666662)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,29 +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.6666666666666665 1.644694921083138) rotate(0 5.333333333333333 4.287969206125098)"
|
||||
d="M7.11,8.51C7.92,8.35 8.64,8.06 9.21,7.64C10.17,6.91 10.67,5.79 10.67,4.69C10.67,3.91 10.37,3.19 9.86,2.58C9.58,2.24 10.41,-0.31 9.67,0.03C8.94,0.37 7.86,1.13 7.29,0.97C6.68,0.79 6.02,0.69 5.33,0.69C4.73,0.69 4.16,0.76 3.62,0.9C2.83,1.1 2.09,0.36 1.33,0.03C0.58,-0.29 0.99,2.34 0.77,2.62C0.28,3.22 0,3.93 0,4.69C0,5.79 0.6,6.91 1.56,7.64C2.21,8.12 3.01,8.42 3.91,8.58 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(6.000666666666666 10.220633333333332) rotate(0 0.2896166666666667 2.058116666666666)"
|
||||
d="M0.58,0C0.19,0.43 0,0.83 0,1.21C0,1.59 0,2.56 0,4.12 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(9.781533333333332 10.158866666666666) rotate(0 0.2744333333333332 2.0890166666666663)"
|
||||
d="M0,0C0.37,0.48 0.55,0.91 0.55,1.29C0.55,1.68 0.55,2.64 0.55,4.18 " />
|
||||
<path id="路径 4"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 10.405166666666666) rotate(0 2.0004 1.050416666666667)"
|
||||
d="M0,0C0.3,0.04 0.52,0.17 0.67,0.41C0.88,0.77 1.69,2.1 2.61,2.1C3.22,2.1 3.68,2.1 4,2.1 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M7.11,8.51C7.92,8.35 8.64,8.06 9.21,7.64C10.17,6.91 10.67,5.79 10.67,4.69C10.67,3.91 10.37,3.19 9.86,2.58C9.58,2.24 10.41,-0.31 9.67,0.03C8.94,0.37 7.86,1.13 7.29,0.97C6.68,0.79 6.02,0.69 5.33,0.69C4.73,0.69 4.16,0.76 3.62,0.9C2.83,1.1 2.09,0.36 1.33,0.03C0.58,-0.29 0.99,2.34 0.77,2.62C0.28,3.22 0,3.93 0,4.69C0,5.79 0.6,6.91 1.56,7.64C2.21,8.12 3.01,8.42 3.91,8.58" transform="translate(2.6666666666666665 1.644694921083138) rotate(0 5.333333333333333 4.287969206125098)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0.58,0C0.19,0.43 0,0.83 0,1.21C0,1.59 0,2.56 0,4.12" transform="translate(6.000666666666666 10.220633333333332) rotate(0 0.2896166666666667 2.058116666666666)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0C0.37,0.48 0.55,0.91 0.55,1.29C0.55,1.68 0.55,2.64 0.55,4.18" transform="translate(9.781533333333332 10.158866666666666) rotate(0 0.2744333333333332 2.0890166666666663)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0C0.3,0.04 0.52,0.17 0.67,0.41C0.88,0.77 1.69,2.1 2.61,2.1C3.22,2.1 3.68,2.1 4,2.1" transform="translate(2 10.405166666666666) rotate(0 2.0004 1.050416666666667)"/></g></g></svg>
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
1
app/icons/left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M4,8L0,4L4,0" transform="translate(6.333333333333333 4) rotate(0 2 4)"/></g></g></svg>
|
After Width: | Height: | Size: 553 B |
@@ -1 +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.340166666666667 4.21550000000002) rotate(0 3.6666666666666665 3.666666666666666)" d="M0,3.67C0,5.69 1.64,7.33 3.67,7.33C5.69,7.33 7.33,5.69 7.33,3.67C7.33,1.64 5.69,0 3.67,0C1.64,0 0,1.64 0,3.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.166666666666666 12.1719333333333) rotate(0 0.4100499999999994 0.41240499999999997)" d="M0.82,0.82L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.0068366666666666 3.0654333333332033) rotate(0 0.3411483333333332 0.34309999999999974)" d="M0.68,0.69L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 1.2155666666667457) rotate(0 0 0.5)" d="M0,1L0,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(13.333266666666667 8.21550000000002) rotate(0 0.6666666666666666 0)" d="M1.33,0L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.5108 3.065499999999929) rotate(0 0.41123333333333295 0.41123333333333295)" d="M0,0.82L0.82,0 " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(5.673499999999999 5.5488333333332776) rotate(0 1.1666666666666665 2.333333333333333)" opacity="1" d="M2.33,0C1.04,0 0,1.04 0,2.33C0,3.62 1.04,4.67 2.33,4.67L2.33,0Z " /><path id="路径 8" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333966666666666 7.8821666666667625) rotate(0 0.6666666666666666 0)" d="M0,0L1.33,0 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.348133333333333 12.3125) rotate(0 0.3421333333333335 0.3421266666666665)" d="M0,0.68L0.68,0 " /><path id="路径 10" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 13.548763333333454) rotate(0 0 0.6666666666666666)" d="M0,1.33L0,0 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(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>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
1
app/icons/lightning.svg
Normal 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
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(2 3.333333333333333) rotate(0 6 5.666666666666666)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(4.64 6.715000010822796) rotate(14.999999999999998 1 1.3333333333333335)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(9.31 6.714999802665079) rotate(165.00000507213028 1.000000156118488 1.3333335414913166)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(9.666599999999999 2.492504620561264) rotate(0 2.4176172657482775 2.2535810230527007)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(1.492667974925419 2.4926141635940393) rotate(0 2.4203326792039572 2.253609584869647)"/><path id="路径 6" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0C0.17,0.43 0.73,1.09 1.67,0.29C2.6,1.09 3.17,0.43 3.33,0" transform="translate(6.5 11.67) rotate(0 1.6666666666666665 0.33333029691911636)"/></g></g></svg>
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,41 +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 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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L3.33,3.3" transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3.3L3.33,0" transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3.3,3.3L0,0" transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3.3,0L0,3.3" transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L3,0L3,3" transform="translate(11 2) rotate(0 1.5 1.5)"/><path id="路径 6" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3,0L3,3L0,3" transform="translate(11 11) rotate(0 1.5 1.5)"/><path id="路径 7" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3,3L0,3L0,0" transform="translate(2 11) rotate(0 1.5 1.5)"/><path id="路径 8" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3L0,0L3,0" transform="translate(2 2) rotate(0 1.5 1.5)"/></g></g></svg>
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,25 +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.649903333333333 3.983233333333333) rotate(0 5.333331666666666 0)"
|
||||
d="M0,0L10.67,0 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2.649903333333333 7.983233333333333) rotate(0 5.333331666666666 0)"
|
||||
d="M0,0L10.67,0 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2.649903333333333 11.983233333333333) rotate(0 5.333331666666666 0)"
|
||||
d="M0,0L10.67,0 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L10.67,0" transform="translate(2.649903333333333 3.983233333333333) rotate(0 5.333331666666666 0)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L10.67,0" transform="translate(2.649903333333333 7.983233333333333) rotate(0 5.333331666666666 0)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L10.67,0" transform="translate(2.649903333333333 11.983233333333333) rotate(0 5.333331666666666 0)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1020 B |
@@ -1,45 +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 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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L3.33,3.3" transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3.3L3.33,0" transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3.3,3.3L0,0" transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3.3,0L0,3.3" transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,3L3,3" transform="translate(10.666666666666666 2.333333333333333) rotate(0 1.5 1.5)"/><path id="路径 6" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3,0L3,3L0,3" transform="translate(2.333333333333333 2.333333333333333) rotate(0 1.5 1.5)"/><path id="路径 7" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M3,3L3,0L0,0" transform="translate(2.333333333333333 10.666666666666666) rotate(0 1.5 1.5)"/><path id="路径 8" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,3L0,0L2.97,0" transform="translate(10.666666666666666 10.666666666666666) rotate(0 1.4832500000000004 1.5)"/></g></g></svg>
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.0 KiB |
1
app/icons/pause.svg
Normal 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/pin.svg
Normal file
After Width: | Height: | Size: 5.8 KiB |
1
app/icons/plugin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="fill:#333;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 id="路径 2" style="fill:#333;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 id="路径 3" style="fill:#333;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 id="路径 4" style="fill:#333;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"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs></svg>
|
After Width: | Height: | Size: 3.5 KiB |
1
app/icons/prompt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="分组 1" style="stroke:#333;stroke-width:1.3;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(5.333333333333333 1.3333333333333333) rotate(0 4.666666666666666 4.666666666666666)"/><path id="路径 9" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M8.01,0L0,8.01" transform="translate(1.847983333333333 6.1381) rotate(0 4.006941666666666 4.006933333333333)"/></g></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
app/icons/robot.svg
Normal file
After Width: | Height: | Size: 12 KiB |
@@ -1,17 +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 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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" 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" transform="translate(2 1.3333333333333333) rotate(0 6.333333333333333 6.5)"/></g></g></svg>
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 645 B |
@@ -1,33 +1 @@
|
||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||
<svg width="30" height="14" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#fff">
|
||||
<circle cx="15" cy="15" r="15" fill="var(--primary, red)">
|
||||
<animate attributeName="r" from="15" to="15"
|
||||
begin="0s" dur="0.8s"
|
||||
values="15;9;15" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="fill-opacity" from="1" to="1"
|
||||
begin="0s" dur="0.8s"
|
||||
values="1;.5;1" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="60" cy="15" r="9" fill-opacity="0.3" fill="var(--primary, red)">
|
||||
<animate attributeName="r" from="9" to="9"
|
||||
begin="0s" dur="0.8s"
|
||||
values="9;15;9" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="fill-opacity" from="0.5" to="0.5"
|
||||
begin="0s" dur="0.8s"
|
||||
values=".5;1;.5" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="105" cy="15" r="15" fill="var(--primary, red)">
|
||||
<animate attributeName="r" from="15" to="15"
|
||||
begin="0s" dur="0.8s"
|
||||
values="15;9;15" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
<animate attributeName="fill-opacity" from="1" to="1"
|
||||
begin="0s" dur="0.8s"
|
||||
values="1;.5;1" calcMode="linear"
|
||||
repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="14" fill="#fff" viewBox="0 0 120 30"><circle cx="15" cy="15" r="15" fill="var(--primary, red)"><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="15" repeatCount="indefinite" to="15" values="15;9;15"/><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1"/></circle><circle cx="60" cy="15" r="9" fill="var(--primary, red)" fill-opacity=".3"><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="9" repeatCount="indefinite" to="9" values="9;15;9"/><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from=".5" repeatCount="indefinite" to=".5" values=".5;1;.5"/></circle><circle cx="105" cy="15" r="15" fill="var(--primary, red)"><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="15" repeatCount="indefinite" to="15" values="15;9;15"/><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1"/></circle></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
1
app/icons/upload.svg
Normal file
After Width: | Height: | Size: 11 KiB |
@@ -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 |
@@ -2,18 +2,25 @@
|
||||
import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/highlight.scss";
|
||||
import { getBuildConfig } from "./config/build";
|
||||
import { getClientConfig } from "./config/client";
|
||||
import { type Metadata } from "next";
|
||||
|
||||
const buildConfig = getBuildConfig();
|
||||
|
||||
export const metadata = {
|
||||
export const metadata: 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: "default",
|
||||
},
|
||||
themeColor: "#fafafa",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,23 +31,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
|
||||
name="theme-color"
|
||||
content="#151515"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<meta name="version" content={buildConfig.commitId} />
|
||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||
<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>
|
||||
|
289
app/locales/ar.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
const ar: PartialLocaleType = {
|
||||
WIP: "قريبًا...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "تحتاج إلى رمز الوصول",
|
||||
Tips: "يرجى إدخال رمز الوصول أدناه",
|
||||
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
|
||||
Input: "رمز الوصول",
|
||||
Confirm: "تأكيد",
|
||||
Later: "لاحقًا",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} رسائل`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "الانتقال إلى قائمة الدردشة",
|
||||
CompressedHistory: "ملخص ضغط ذاكرة التاريخ",
|
||||
Export: "تصدير جميع الرسائل كـ Markdown",
|
||||
Copy: "نسخ",
|
||||
Stop: "توقف",
|
||||
Retry: "إعادة المحاولة",
|
||||
Delete: "حذف",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "توقف",
|
||||
ToBottom: "إلى آخر",
|
||||
Theme: {
|
||||
auto: "تلقائي",
|
||||
light: "نمط فاتح",
|
||||
dark: "نمط داكن",
|
||||
},
|
||||
Prompt: "الاقتراحات",
|
||||
Masks: "الأقنعة",
|
||||
Clear: "مسح السياق",
|
||||
Settings: "الإعدادات",
|
||||
},
|
||||
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",
|
||||
Share: "مشاركة على ShareGPT",
|
||||
Format: {
|
||||
Title: "صيغة التصدير",
|
||||
SubTitle: "Markdown أو صورة PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "تضمين السياق",
|
||||
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا",
|
||||
},
|
||||
Steps: {
|
||||
Select: "تحديد",
|
||||
Preview: "معاينة",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "بحث",
|
||||
All: "تحديد الكل",
|
||||
Latest: "تحديد أحدث",
|
||||
Clear: "مسح",
|
||||
},
|
||||
Memory: {
|
||||
Title: "اقتراحات الذاكرة",
|
||||
EmptyContent: "لا شيء حتى الآن.",
|
||||
Send: "إرسال الذاكرة",
|
||||
Copy: "نسخ الذاكرة",
|
||||
Reset: "إعادة التعيين",
|
||||
ResetConfirm:
|
||||
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "دردشة جديدة",
|
||||
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟",
|
||||
DeleteToast: "تم حذف الدردشة",
|
||||
Revert: "التراجع",
|
||||
},
|
||||
Settings: {
|
||||
Title: "الإعدادات",
|
||||
SubTitle: "جميع الإعدادات",
|
||||
|
||||
Lang: {
|
||||
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
|
||||
All: "كل اللغات",
|
||||
},
|
||||
Avatar: "الصورة الرمزية",
|
||||
FontSize: {
|
||||
Title: "حجم الخط",
|
||||
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "حقن تلميحات النظام",
|
||||
SubTitle:
|
||||
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "نموذج الإدخال",
|
||||
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => ` الإصدار: ${x}`,
|
||||
IsLatest: "أحدث إصدار",
|
||||
CheckUpdate: "التحقق من التحديث",
|
||||
IsChecking: "جارٍ التحقق من التحديث...",
|
||||
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`,
|
||||
GoToUpdate: "التحديث",
|
||||
},
|
||||
SendKey: "مفتاح الإرسال",
|
||||
Theme: "السمة",
|
||||
TightBorder: "حدود ضيقة",
|
||||
SendPreviewBubble: {
|
||||
Title: "عرض معاينة الـ Send",
|
||||
SubTitle: "معاينة Markdown في فقاعة",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
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: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "رصيد الحساب",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`;
|
||||
},
|
||||
IsChecking: "جارٍ التحقق...",
|
||||
Check: "التحقق",
|
||||
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
||||
},
|
||||
|
||||
Model: "النموذج",
|
||||
Temperature: {
|
||||
Title: "الحرارة",
|
||||
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "الحد الأقصى للرموز",
|
||||
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "تأثير الوجود",
|
||||
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "تأثير التكرار",
|
||||
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "محادثة جديدة",
|
||||
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟",
|
||||
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.",
|
||||
Prompt: {
|
||||
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content,
|
||||
Topic:
|
||||
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.",
|
||||
Summarize:
|
||||
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "تم النسخ إلى الحافظة",
|
||||
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`,
|
||||
Edit: "الاقتراحات السياقية والذاكرة",
|
||||
Add: "إضافة اقتراح",
|
||||
Clear: "مسح السياق",
|
||||
Revert: "التراجع",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "المكوّن الإضافي",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "أنت مساعد ي",
|
||||
},
|
||||
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: "اسم الروبوت",
|
||||
Sync: {
|
||||
Title: "استخدام الإعدادات العامة",
|
||||
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة",
|
||||
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "إخفاء اقتراحات السياق",
|
||||
SubTitle: "عدم عرض اقتراحات السياق في الدردشة",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "العودة",
|
||||
Skip: "ابدأ فقط",
|
||||
Title: "اختيار قناع",
|
||||
SubTitle: "دردشة مع الروح وراء القناع",
|
||||
More: "المزيد",
|
||||
NotShow: "عدم العرض مرة أخرى",
|
||||
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "تأكيد",
|
||||
Cancel: "إلغاء",
|
||||
Close: "إغلاق",
|
||||
Create: "إنشاء",
|
||||
Edit: "تعديل",
|
||||
},
|
||||
Exporter: {
|
||||
Model: "النموذج",
|
||||
Messages: "الرسائل",
|
||||
Topic: "الموضوع",
|
||||
Time: "الوقت",
|
||||
},
|
||||
};
|
||||
|
||||
export default ar;
|