Compare commits
937 Commits
v2.6
...
feat/voice
Author | SHA1 | Date | |
---|---|---|---|
|
52316785d1 | ||
|
3ba984d09e | ||
|
f274683d46 | ||
|
e20ce8e335 | ||
|
9fd750511c | ||
|
e2b15f785a | ||
|
028957fcdc | ||
|
a4c54cae60 | ||
|
cc0eae7153 | ||
|
066ca9e552 | ||
|
7c04a90d77 | ||
|
a8a65ac769 | ||
|
aec3c5d6cc | ||
|
a22141c2eb | ||
|
99aa064319 | ||
|
6aaf83f3c2 | ||
|
133ce39a13 | ||
|
8645214654 | ||
|
eebc334e02 | ||
|
038fa3b301 | ||
|
9a8497299d | ||
|
42d04d473e | ||
|
61ce3868b5 | ||
|
844c2a26bc | ||
|
a15c4d9c20 | ||
|
ff9f0e60ac | ||
|
2bf6111bf5 | ||
|
ad10a11903 | ||
|
c22153a4eb | ||
|
5348d57057 | ||
|
052524dabd | ||
|
5529ece220 | ||
|
e71094d4a8 | ||
|
2f53107581 | ||
|
98aa023d70 | ||
|
e1066434d0 | ||
|
86ae4b2a75 | ||
|
99fb9dcf11 | ||
|
1294817103 | ||
|
9775660da7 | ||
|
e7051353eb | ||
|
bd19e97cf8 | ||
|
8b821ac0c9 | ||
|
43e5dc2292 | ||
|
08fa22749a | ||
|
c197962851 | ||
|
44a51273be | ||
|
e3b3ae97bc | ||
|
410a22dc63 | ||
|
069766d581 | ||
|
f22e36e52f | ||
|
bc1794fb4a | ||
|
aacd26c7db | ||
|
ff166f7b4c | ||
|
22baebaf8c | ||
|
e756506c18 | ||
|
fd67f980a5 | ||
|
e2da3406d2 | ||
|
05b6d989b6 | ||
|
1d6ee64e1d | ||
|
bfefb99192 | ||
|
47ae874e4d | ||
|
d74f636558 | ||
|
b8f0822214 | ||
|
0869455612 | ||
|
bca74241e6 | ||
|
9d5801fb5f | ||
|
462a88ae82 | ||
|
887bec019a | ||
|
d0463b2089 | ||
|
deea4320ad | ||
|
bb26c03141 | ||
|
a5517a1a51 | ||
|
4511aa4d21 | ||
|
b25a0545f5 | ||
|
1a97bd55c7 | ||
|
bf711f2ad7 | ||
|
e1b065c74a | ||
|
eacc911cdc | ||
|
91a5f4af9a | ||
|
f6cdda9029 | ||
|
981a3629b6 | ||
|
118e8f541c | ||
|
3fa55f9022 | ||
|
1c73c6cc70 | ||
|
148c32a383 | ||
|
ca08062bf3 | ||
|
ecbab75a25 | ||
|
15d25df245 | ||
|
b2f7bdca34 | ||
|
32bcdb8982 | ||
|
ba3e7e7974 | ||
|
fae82a30e7 | ||
|
5ce1a55a4f | ||
|
1132816902 | ||
|
a4c1f1ac47 | ||
|
f619e9df24 | ||
|
43631a3271 | ||
|
04855de495 | ||
|
6206ceb49b | ||
|
628f3ba05f | ||
|
94f534f36f | ||
|
2f11e7c087 | ||
|
0739ced407 | ||
|
8fb34465cd | ||
|
04fe9fadb1 | ||
|
998c95da51 | ||
|
3bc771a0d9 | ||
|
bdc25adb41 | ||
|
7ec29c2eea | ||
|
b9995e7f70 | ||
|
31d9d2efcd | ||
|
a06a2989d2 | ||
|
0cf6614186 | ||
|
ddc16cc2be | ||
|
616f0b3e73 | ||
|
bd1056b13f | ||
|
5a7b9abe33 | ||
|
cc86923fd5 | ||
|
916d764477 | ||
|
4a21a8fdae | ||
|
49df5bd590 | ||
|
9eaf492d5b | ||
|
a80502f7db | ||
|
eade013138 | ||
|
912254751a | ||
|
c3c6f7f7ae | ||
|
5cf58d9446 | ||
|
3ba598633c | ||
|
406530ca69 | ||
|
f8b963df6d | ||
|
d17000975f | ||
|
64a8ba6212 | ||
|
350ddd2af1 | ||
|
c03abdac42 | ||
|
bb3cc2c821 | ||
|
0b814eff83 | ||
|
a91ac91977 | ||
|
422d70d928 | ||
|
a4cb859472 | ||
|
19137b79bc | ||
|
bd1e311674 | ||
|
c127db335a | ||
|
f5ed1604aa | ||
|
771cc9e583 | ||
|
7ad1e246a0 | ||
|
5c638251f8 | ||
|
0d189dc78e | ||
|
a9d7253143 | ||
|
199f29e63c | ||
|
c82efceae0 | ||
|
cad461b121 | ||
|
5af68ac545 | ||
|
c0ce70ccc7 | ||
|
753c518d33 | ||
|
b9ca6694f7 | ||
|
0c116251b1 | ||
|
e9def2cdc5 | ||
|
8ca525dc7a | ||
|
281fe6927a | ||
|
35471a41c8 | ||
|
bda3098a31 | ||
|
1e05eb1d60 | ||
|
6369b902bf | ||
|
aea794e522 | ||
|
7c3dfb7bae | ||
|
75057f9a91 | ||
|
7026bd926a | ||
|
ae19a0dc5f | ||
|
7d9a2132cb | ||
|
20f2f61349 | ||
|
4169431f2c | ||
|
45798f993d | ||
|
ae0d68c27e | ||
|
070b191ffe | ||
|
778e88cb56 | ||
|
d1fcfa05db | ||
|
f4754dc5fc | ||
|
0f885e73c3 | ||
|
75acd4c1aa | ||
|
3ef0621eb0 | ||
|
b1db9ead0d | ||
|
9cc6000d89 | ||
|
42515899d9 | ||
|
1d096eeb17 | ||
|
bc5b8f0ff9 | ||
|
bf412fd309 | ||
|
6e1f4246b8 | ||
|
ed88619623 | ||
|
37b8922348 | ||
|
4a23a0064b | ||
|
e012b1e5d1 | ||
|
2bc84ec908 | ||
|
8efe26ed57 | ||
|
24d7dc9bf9 | ||
|
4b7139d9ae | ||
|
1f35624121 | ||
|
860c5b8f20 | ||
|
a454c39ca0 | ||
|
6938910f08 | ||
|
2c63dde6c5 | ||
|
e69d20a209 | ||
|
0b731edd21 | ||
|
efdd61595e | ||
|
d676f88fed | ||
|
07f2cf7ad0 | ||
|
bcb520ed3b | ||
|
93cee18300 | ||
|
cae4655785 | ||
|
28c12606a4 | ||
|
54df355014 | ||
|
cf50299b14 | ||
|
2c12be62c4 | ||
|
4636a75b5e | ||
|
03756e364a | ||
|
ce1715c79e | ||
|
a62ab3c649 | ||
|
bfb7b988f4 | ||
|
84f41262f5 | ||
|
dda40e29f4 | ||
|
7df868e22a | ||
|
bf5e7aaa48 | ||
|
d76e744eab | ||
|
6f5699fe09 | ||
|
f9d916925e | ||
|
f9258878db | ||
|
ef9e86b50d | ||
|
b21931c667 | ||
|
06de3f5e69 | ||
|
261a8fd83b | ||
|
6527074cde | ||
|
f2485931d9 | ||
|
4f8a0b7711 | ||
|
2dde55050e | ||
|
6aade62ce2 | ||
|
45b88ebb2a | ||
|
dc7159a450 | ||
|
536ace8e10 | ||
|
16b2a3e66e | ||
|
6f135a0cce | ||
|
cf220dd2eb | ||
|
914f4fb862 | ||
|
7bdb68eecf | ||
|
3c510cfaf0 | ||
|
401fa198c9 | ||
|
600df4f63a | ||
|
74eb42c111 | ||
|
9876a1aeca | ||
|
d898ffce23 | ||
|
f1772f4625 | ||
|
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 | ||
|
dd6e79922a | ||
|
bf84269520 | ||
|
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 | ||
|
b799213a14 | ||
|
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 | ||
|
5a10b2a75f | ||
|
72b553efe8 |
14
.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"next/babel",
|
||||
{
|
||||
"preset-env": {
|
||||
"targets": {
|
||||
"browsers": ["> 0.25%, not dead"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# docker-compose env files
|
||||
.env
|
||||
|
||||
*.key
|
||||
*.key.pub
|
@@ -8,6 +8,16 @@ CODE=your-password
|
||||
# You can start service behind a proxy
|
||||
PROXY_URL=http://localhost:7890
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
||||
GOOGLE_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: https://generativelanguage.googleapis.com/
|
||||
# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
|
||||
GOOGLE_URL=
|
||||
|
||||
# Override openai api request base url. (optional)
|
||||
# Default: https://api.openai.com
|
||||
# Examples: http://your-openai-proxy.com
|
||||
@@ -15,9 +25,13 @@ BASE_URL=
|
||||
|
||||
# Specify OpenAI organization ID.(optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
OPENAI_ORG_ID=
|
||||
|
||||
# (optional)
|
||||
# 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.
|
||||
@@ -25,5 +39,11 @@ HIDE_USER_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to use GPT-4, set this value to 1.
|
||||
DISABLE_GPT4=
|
||||
# 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=
|
||||
|
||||
|
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Deployment**
|
||||
- [ ] Docker
|
||||
- [ ] Vercel
|
||||
- [ ] Server
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional Logs**
|
||||
Add any logs about the problem here.
|
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe the bug"
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: "Bug Description"
|
||||
description: "A clear and concise description of what the bug is."
|
||||
placeholder: "Explain the bug..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## To Reproduce"
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: "Steps to Reproduce"
|
||||
description: "Steps to reproduce the behavior:"
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Expected behavior"
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: "Expected Behavior"
|
||||
description: "A clear and concise description of what you expected to happen."
|
||||
placeholder: "Describe what you expected to happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Screenshots"
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: "Screenshots"
|
||||
description: "If applicable, add screenshots to help explain your problem."
|
||||
placeholder: "Paste your screenshots here or write 'N/A' if not applicable..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Deployment"
|
||||
- type: checkboxes
|
||||
id: deployment
|
||||
attributes:
|
||||
label: "Deployment Method"
|
||||
description: "Please select the deployment method you are using."
|
||||
options:
|
||||
- label: "Docker"
|
||||
- label: "Vercel"
|
||||
- label: "Server"
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Desktop (please complete the following information):"
|
||||
- type: input
|
||||
id: desktop-os
|
||||
attributes:
|
||||
label: "Desktop OS"
|
||||
description: "Your desktop operating system."
|
||||
placeholder: "e.g., Windows 10"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: desktop-browser
|
||||
attributes:
|
||||
label: "Desktop Browser"
|
||||
description: "Your desktop browser."
|
||||
placeholder: "e.g., Chrome, Safari"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: desktop-version
|
||||
attributes:
|
||||
label: "Desktop Browser Version"
|
||||
description: "Version of your desktop browser."
|
||||
placeholder: "e.g., 89.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Smartphone (please complete the following information):"
|
||||
- type: input
|
||||
id: smartphone-device
|
||||
attributes:
|
||||
label: "Smartphone Device"
|
||||
description: "Your smartphone device."
|
||||
placeholder: "e.g., iPhone X"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: smartphone-os
|
||||
attributes:
|
||||
label: "Smartphone OS"
|
||||
description: "Your smartphone operating system."
|
||||
placeholder: "e.g., iOS 14.4"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: smartphone-browser
|
||||
attributes:
|
||||
label: "Smartphone Browser"
|
||||
description: "Your smartphone browser."
|
||||
placeholder: "e.g., Safari"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: smartphone-version
|
||||
attributes:
|
||||
label: "Smartphone Browser Version"
|
||||
description: "Version of your smartphone browser."
|
||||
placeholder: "e.g., 14"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Additional Logs"
|
||||
- type: textarea
|
||||
id: additional-logs
|
||||
attributes:
|
||||
label: "Additional Logs"
|
||||
description: "Add any logs about the problem here."
|
||||
placeholder: "Paste any relevant logs here..."
|
||||
validations:
|
||||
required: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request]: "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Is your feature request related to a problem? Please describe."
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: "A clear and concise description of what the problem is. Example: I'm always frustrated when [...]"
|
||||
placeholder: "Explain the problem you are facing..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe the solution you'd like"
|
||||
- type: textarea
|
||||
id: desired-solution
|
||||
attributes:
|
||||
label: Solution Description
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: "Describe the solution you'd like..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe alternatives you've considered"
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: "Describe any alternative solutions or features you've considered..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Additional context"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
placeholder: "Add any other context or screenshots about the feature request here..."
|
||||
validations:
|
||||
required: false
|
20
.github/ISSUE_TEMPLATE/功能建议.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: 功能建议
|
||||
about: 请告诉我们你的灵光一闪
|
||||
title: "[Feature] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**这个功能与现有的问题有关吗?**
|
||||
如果有关,请在此列出链接或者描述问题。
|
||||
|
||||
**你想要什么功能或者有什么建议?**
|
||||
尽管告诉我们。
|
||||
|
||||
**有没有可以参考的同类竞品?**
|
||||
可以给出参考产品的链接或者截图。
|
||||
|
||||
**其他信息**
|
||||
可以说说你的其他考虑。
|
32
.github/ISSUE_TEMPLATE/反馈问题.md
vendored
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: 反馈问题
|
||||
about: 请告诉我们你遇到的问题
|
||||
title: "[Bug] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**反馈须知**
|
||||
|
||||
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。
|
||||
|
||||
请在下方中括号内输入 x 来表示你已经知晓相关内容。
|
||||
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
|
||||
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
|
||||
- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
|
||||
|
||||
**描述问题**
|
||||
请在此描述你遇到了什么问题。
|
||||
|
||||
**如何复现**
|
||||
请告诉我们你是通过什么操作触发的该问题。
|
||||
|
||||
**截图**
|
||||
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
|
||||
|
||||
**一些必要的信息**
|
||||
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
|
||||
- 浏览器: [比如 chrome, safari]
|
||||
- 版本: [填写设置页面的版本号]
|
||||
- 部署方式:[比如 vercel、docker 或者服务器部署]
|
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"
|
110
.github/workflows/app.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
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: 18
|
||||
- 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: aarch64
|
||||
rust_target: x86_64-apple-darwin,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: 18
|
||||
cache: 'yarn'
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.config.rust_target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.config.os }}
|
||||
- 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 }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||
args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
|
||||
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
|
||||
})
|
84
.github/workflows/deploy_preview.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: VercelPreviewDeployment
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
env:
|
||||
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_PR_DOMAIN_SUFFIX: ${{ secrets.VERCEL_PR_DOMAIN_SUFFIX }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deploy-preview:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
|
||||
id: extract_branch
|
||||
|
||||
- name: Hash branch name
|
||||
uses: pplanel/hash-calculator-action@v1.3.1
|
||||
id: hash_branch
|
||||
with:
|
||||
input: ${{ steps.extract_branch.outputs.branch }}
|
||||
method: MD5
|
||||
|
||||
- name: Set Environment Variables
|
||||
id: set_env
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: |
|
||||
echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_PR_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
id: cache-npm
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: npm-
|
||||
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
|
||||
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: vercel
|
||||
env:
|
||||
META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
|
||||
run: |
|
||||
set -e
|
||||
vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
|
||||
vercel build --token=${VERCEL_TOKEN}
|
||||
vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
|
||||
|
||||
DEFAULT_URL=$(vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }})
|
||||
ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} --scope ${VERCEL_TEAM}| awk '{print $3}')
|
||||
|
||||
echo "New preview URL: ${DEFAULT_URL}"
|
||||
echo "New alias URL: ${ALIAS_URL}"
|
||||
echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message: |
|
||||
Your build has completed!
|
||||
|
||||
[Preview deployment](${{ steps.vercel.outputs.VERCEL_URL }})
|
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.
|
40
.github/workflows/remove_deploy_preview.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Removedeploypreview
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
delete-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
|
||||
- name: Hash branch name
|
||||
uses: pplanel/hash-calculator-action@v1.3.1
|
||||
id: hash_branch
|
||||
with:
|
||||
input: ${{ steps.extract_branch.outputs.branch }}
|
||||
method: MD5
|
||||
|
||||
- name: Call the delete-deployment-preview.sh script
|
||||
env:
|
||||
META_TAG: ${{ steps.hash_branch.outputs.digest }}
|
||||
run: |
|
||||
bash ./scripts/delete-deployment-preview.sh
|
6
.github/workflows/sync.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
id: sync
|
||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||
with:
|
||||
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
||||
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
|
||||
upstream_sync_branch: main
|
||||
target_sync_branch: main
|
||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||
@@ -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
|
||||
|
3
.gitignore
vendored
@@ -41,3 +41,6 @@ dev
|
||||
|
||||
# docker-compose env files
|
||||
.env
|
||||
|
||||
*.key
|
||||
*.key.pub
|
35
Dockerfile
@@ -16,6 +16,7 @@ FROM base AS builder
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
ENV OPENAI_API_KEY=""
|
||||
ENV GOOGLE_API_KEY=""
|
||||
ENV CODE=""
|
||||
|
||||
WORKDIR /app
|
||||
@@ -31,6 +32,7 @@ RUN apk add proxychains-ng
|
||||
|
||||
ENV PROXY_URL=""
|
||||
ENV OPENAI_API_KEY=""
|
||||
ENV GOOGLE_API_KEY=""
|
||||
ENV CODE=""
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
@@ -41,21 +43,22 @@ COPY --from=builder /app/.next/server ./.next/server
|
||||
EXPOSE 3000
|
||||
|
||||
CMD if [ -n "$PROXY_URL" ]; then \
|
||||
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
||||
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
||||
port=$(echo $PROXY_URL | cut -d: -f3); \
|
||||
conf=/etc/proxychains.conf; \
|
||||
echo "strict_chain" > $conf; \
|
||||
echo "proxy_dns" >> $conf; \
|
||||
echo "remote_dns_subnet 224" >> $conf; \
|
||||
echo "tcp_read_time_out 15000" >> $conf; \
|
||||
echo "tcp_connect_time_out 8000" >> $conf; \
|
||||
echo "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; \
|
||||
proxychains -f $conf node server.js; \
|
||||
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); \
|
||||
conf=/etc/proxychains.conf; \
|
||||
echo "strict_chain" > $conf; \
|
||||
echo "proxy_dns" >> $conf; \
|
||||
echo "remote_dns_subnet 224" >> $conf; \
|
||||
echo "tcp_read_time_out 15000" >> $conf; \
|
||||
echo "tcp_connect_time_out 8000" >> $conf; \
|
||||
echo "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; \
|
||||
proxychains -f $conf node server.js; \
|
||||
else \
|
||||
node server.js; \
|
||||
node server.js; \
|
||||
fi
|
||||
|
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.
|
||||
|
176
README.md
@@ -1,19 +1,33 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="icon"/>
|
||||
<img src="./docs/images/head-cover.png" alt="icon"/>
|
||||
|
||||
<h1 align="center">ChatGPT Next Web</h1>
|
||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
||||
|
||||
English / [简体中文](./README_CN.md)
|
||||
|
||||
One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用。
|
||||
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||
|
||||
[Demo](https://chatgpt.nextweb.fun/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [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://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/assets/16968934/3ff423d5-5703-4772-8b6d-abec7eec3a4b) / [QQ 频道](https://github.com/Yidadaa/ChatGPT-Next-Web/assets/16968934/debfbee7-e682-4814-a601-f4403dac6d1d) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/NextChatDev)
|
||||
|
||||
[](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)
|
||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||
|
||||
[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%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
@@ -24,37 +38,39 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
## Features
|
||||
|
||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||
- Privacy first, all data stored locally in the browser
|
||||
- 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 is 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
|
||||
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어
|
||||
- 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)
|
||||
- [x] User Prompt: user can edit and save custom prompts to prompt list
|
||||
- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
|
||||
- [ ] Share as image, share to ShareGPT
|
||||
- [ ] Desktop App with tauri
|
||||
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
|
||||
- [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
|
||||
|
||||
- User login, accounts, cloud sync
|
||||
- UI text customize
|
||||
|
||||
## What's New
|
||||
|
||||
- 🚀 v2.10.1 support Google Gemini Pro model.
|
||||
- 🚀 v2.9.11 you can use azure endpoint now.
|
||||
- 🚀 v2.8 now we have a client that runs across all platforms!
|
||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
|
||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||
- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
|
||||
- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
|
||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
|
||||
- 极快的首屏加载速度(~100kb),支持流式响应
|
||||
@@ -62,7 +78,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
|
||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||
|
||||
## 开发计划
|
||||
@@ -70,20 +86,18 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||
- [x] 允许用户自行编辑内置 Prompt 列表
|
||||
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
|
||||
- [ ] 分享为图片,分享到 ShareGPT
|
||||
- [ ] 使用 tauri 打包桌面应用
|
||||
- [ ] 支持自部署的大语言模型
|
||||
- [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
|
||||
|
||||
@@ -126,7 +140,7 @@ After forking the project, due to the limitations imposed by GitHub, you need to
|
||||
|
||||
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
|
||||
|
||||
@@ -144,13 +158,13 @@ After adding or modifying this environment variable, please redeploy the project
|
||||
|
||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
|
||||
|
||||
### `OPENAI_API_KEY` (required)
|
||||
|
||||
Your openai api key.
|
||||
|
||||
### `CODE` (optional)
|
||||
|
||||
Access passsword, separated by comma.
|
||||
Access password, separated by comma.
|
||||
|
||||
### `OPENAI_API_KEY` (required)
|
||||
|
||||
Your openai api key, join multiple api keys with comma.
|
||||
|
||||
### `BASE_URL` (optional)
|
||||
|
||||
@@ -164,6 +178,28 @@ Override openai api request base url.
|
||||
|
||||
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).
|
||||
|
||||
### `GOOGLE_API_KEY` (optional)
|
||||
|
||||
Google Gemini Pro Api Key.
|
||||
|
||||
### `GOOGLE_URL` (optional)
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
@@ -176,6 +212,31 @@ If you do not want users to input their own API key, set this value to 1.
|
||||
|
||||
If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
### `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.
|
||||
|
||||
User `-all` to disable all default models, `+all` to enable all default models.
|
||||
|
||||
## Requirements
|
||||
|
||||
NodeJS >= 18, Docker >= 20
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
@@ -186,6 +247,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
|
||||
@@ -208,8 +272,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
|
||||
```
|
||||
|
||||
@@ -217,24 +281,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)
|
||||
@@ -266,11 +354,19 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
[@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)
|
||||
[@LeXwDeX](https://github.com/LeXwDeX)
|
||||
[@Licoy](https://github.com/Licoy)
|
||||
[@shangmin2009](https://github.com/shangmin2009)
|
||||
|
||||
### Contributor
|
||||
### Contributors
|
||||
|
||||
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
<a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatGPTNextWeb/ChatGPT-Next-Web" />
|
||||
</a>
|
||||
|
||||
## LICENSE
|
||||
|
||||
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
|
80
README_CN.md
@@ -1,14 +1,16 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="预览"/>
|
||||
|
||||
<h1 align="center">ChatGPT Next Web</h1>
|
||||
<h1 align="center">NextChat</h1>
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用。
|
||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||
|
||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt)
|
||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
||||
|
||||
[](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)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||

|
||||
@@ -19,7 +21,7 @@
|
||||
|
||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||
2. 点击右侧按钮开始部署:
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
|
||||
3. 部署完毕后,即可开始使用;
|
||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
||||
|
||||
@@ -68,7 +70,7 @@ code1,code2,code3
|
||||
|
||||
### `OPENAI_API_KEY` (必填项)
|
||||
|
||||
OpanAI 密钥,你在 openai 账户页面申请的 api key。
|
||||
OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
|
||||
|
||||
### `CODE` (可选)
|
||||
|
||||
@@ -90,6 +92,28 @@ OpenAI 接口代理 URL,如果你手动配置了 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)。
|
||||
|
||||
### `GOOGLE_API_KEY` (optional)
|
||||
|
||||
Google Gemini Pro 密钥.
|
||||
|
||||
### `GOOGLE_URL` (optional)
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||
@@ -98,9 +122,22 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
|
||||
|
||||
## 开发
|
||||
### `ENABLE_BALANCE_QUERY` (可选)
|
||||
|
||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
|
||||
如果你想启用余额查询功能,将此环境变量设置为 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`。
|
||||
> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
|
||||
|
||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||
|
||||
## 开发
|
||||
|
||||
点击下方按钮,开始二次开发:
|
||||
|
||||
@@ -110,13 +147,16 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||
BASE_URL=https://b.nextweb.fun/api/proxy
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
|
||||
2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
|
||||
3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
|
||||
3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
|
||||
|
||||
## 部署
|
||||
|
||||
@@ -130,8 +170,8 @@ OPENAI_API_KEY=<your api key here>
|
||||
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
|
||||
```
|
||||
|
||||
@@ -139,13 +179,19 @@ 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 环境变量=环境变量值` 来指定。
|
||||
|
||||
### 本地部署
|
||||
@@ -168,8 +214,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/)
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import md5 from "spark-md5";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { OPENAI_URL } from "./common";
|
||||
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
|
||||
|
||||
function getIP(req: NextRequest) {
|
||||
let ip = req.ip ?? req.headers.get("x-real-ip");
|
||||
@@ -17,19 +16,19 @@ function getIP(req: NextRequest) {
|
||||
|
||||
function parseApiKey(bearToken: string) {
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
||||
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
||||
|
||||
return {
|
||||
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
||||
apiKey: isOpenAiKey ? token : "",
|
||||
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
||||
apiKey: isApiKey ? token : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function auth(req: NextRequest) {
|
||||
export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
const authToken = req.headers.get("Authorization") ?? "";
|
||||
|
||||
// check if it is openai api key or user token
|
||||
const { accessCode, apiKey: token } = parseApiKey(authToken);
|
||||
const { accessCode, apiKey } = parseApiKey(authToken);
|
||||
|
||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||
|
||||
@@ -40,19 +39,33 @@ export function auth(req: NextRequest) {
|
||||
console.log("[User IP] ", getIP(req));
|
||||
console.log("[Time] ", new Date().toLocaleString());
|
||||
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
|
||||
return {
|
||||
error: true,
|
||||
msg: !accessCode ? "empty access code" : "wrong access code",
|
||||
};
|
||||
}
|
||||
|
||||
if (serverConfig.hideUserApiKey && !!apiKey) {
|
||||
return {
|
||||
error: true,
|
||||
msg: "you are not allowed to access with your own api key",
|
||||
};
|
||||
}
|
||||
|
||||
// if user does not provide an api key, inject system api key
|
||||
if (!token) {
|
||||
const apiKey = serverConfig.apiKey;
|
||||
if (apiKey) {
|
||||
if (!apiKey) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
const systemApiKey =
|
||||
modelProvider === ModelProvider.GeminiPro
|
||||
? serverConfig.googleApiKey
|
||||
: serverConfig.isAzure
|
||||
? serverConfig.azureApiKey
|
||||
: serverConfig.apiKey;
|
||||
if (systemApiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||
} else {
|
||||
console.log("[Auth] admin did not provide an api key");
|
||||
}
|
||||
|
@@ -1,59 +1,138 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant";
|
||||
import { collectModelTable } from "../utils/model";
|
||||
import { makeAzurePath } from "../azure";
|
||||
|
||||
export 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 controller = new AbortController();
|
||||
const authValue = req.headers.get("Authorization") ?? "";
|
||||
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
|
||||
var authValue,
|
||||
authHeaderName = "";
|
||||
if (serverConfig.isAzure) {
|
||||
authValue =
|
||||
req.headers
|
||||
.get("Authorization")
|
||||
?.trim()
|
||||
.replaceAll("Bearer ", "")
|
||||
.trim() ?? "";
|
||||
|
||||
authHeaderName = "api-key";
|
||||
} else {
|
||||
authValue = req.headers.get("Authorization") ?? "";
|
||||
authHeaderName = "Authorization";
|
||||
}
|
||||
|
||||
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
"/api/openai/",
|
||||
"",
|
||||
);
|
||||
|
||||
let baseUrl = BASE_URL;
|
||||
let baseUrl =
|
||||
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
if (process.env.OPENAI_ORG_ID) {
|
||||
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
||||
// this fix [Org ID] undefined in server side if not using custom point
|
||||
if (serverConfig.openaiOrgId !== undefined) {
|
||||
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 10 * 60 * 1000);
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}/${openaiPath}`;
|
||||
if (serverConfig.isAzure) {
|
||||
if (!serverConfig.azureApiVersion) {
|
||||
return NextResponse.json({
|
||||
error: true,
|
||||
message: `missing AZURE_API_VERSION in server env vars`,
|
||||
});
|
||||
}
|
||||
path = makeAzurePath(path, serverConfig.azureApiVersion);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authValue,
|
||||
...(process.env.OPENAI_ORG_ID && {
|
||||
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
||||
"Cache-Control": "no-store",
|
||||
[authHeaderName]: authValue,
|
||||
...(serverConfig.openaiOrgId && {
|
||||
"OpenAI-Organization": serverConfig.openaiOrgId,
|
||||
}),
|
||||
},
|
||||
cache: "no-store",
|
||||
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);
|
||||
|
||||
if (res.status === 401) {
|
||||
// to prevent browser prompt for credentials
|
||||
res.headers.delete("www-authenticate");
|
||||
}
|
||||
// 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 res;
|
||||
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||
// The browser will try to decode the response with brotli and fail
|
||||
newHeaders.delete("content-encoding");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
@@ -4,12 +4,15 @@ 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,
|
||||
enableGPT4: serverConfig.enableGPT4,
|
||||
disableGPT4: serverConfig.disableGPT4,
|
||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||
disableFastLink: serverConfig.disableFastLink,
|
||||
customModels: serverConfig.customModels,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
116
app/api/google/[...path]/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Google Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const authResult = auth(req, ModelProvider.GeminiPro);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const bearToken = req.headers.get("Authorization") ?? "";
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
|
||||
const key = token ? token : serverConfig.googleApiKey;
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `missing GOOGLE_API_KEY in server env vars`,
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}?key=${key}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
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,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = [
|
||||
"bom1",
|
||||
"cle1",
|
||||
"cpt1",
|
||||
"gru1",
|
||||
"hnd1",
|
||||
"iad1",
|
||||
"icn1",
|
||||
"kix1",
|
||||
"pdx1",
|
||||
"sfo1",
|
||||
"sin1",
|
||||
"syd1",
|
||||
];
|
@@ -1,15 +1,51 @@
|
||||
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { ModelProvider, 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);
|
||||
|
||||
const authResult = auth(req);
|
||||
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, ModelProvider.GPT);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
@@ -17,7 +53,18 @@ async function handle(
|
||||
}
|
||||
|
||||
try {
|
||||
return await requestOpenai(req);
|
||||
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));
|
||||
@@ -28,3 +75,22 @@ export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = [
|
||||
"arn1",
|
||||
"bom1",
|
||||
"cdg1",
|
||||
"cle1",
|
||||
"cpt1",
|
||||
"dub1",
|
||||
"fra1",
|
||||
"gru1",
|
||||
"hnd1",
|
||||
"iad1",
|
||||
"icn1",
|
||||
"kix1",
|
||||
"lhr1",
|
||||
"pdx1",
|
||||
"sfo1",
|
||||
"sin1",
|
||||
"syd1",
|
||||
];
|
||||
|
73
app/api/upstash/[action]/[...key]/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { action: string; key: string[] } },
|
||||
) {
|
||||
const requestUrl = new URL(req.url);
|
||||
const endpoint = requestUrl.searchParams.get("endpoint");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
const [...key] = params.key;
|
||||
// only allow to request to *.upstash.io
|
||||
if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.key.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// only allow upstash get and set method
|
||||
if (params.action !== "get" && params.action !== "set") {
|
||||
console.log("[Upstash Route] forbidden action ", params.action);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.action,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
|
||||
|
||||
const method = req.method;
|
||||
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",
|
||||
};
|
||||
|
||||
console.log("[Upstash Proxy]", targetUrl, fetchOptions);
|
||||
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 = "edge";
|
112
app/api/webdav/[...path]/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { STORAGE_KEY } from "../../../constant";
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
const folder = STORAGE_KEY;
|
||||
const fileName = `${folder}/backup.json`;
|
||||
|
||||
const requestUrl = new URL(req.url);
|
||||
let endpoint = requestUrl.searchParams.get("endpoint");
|
||||
if (!endpoint?.endsWith("/")) {
|
||||
endpoint += "/";
|
||||
}
|
||||
const endpointPath = params.path.join("/");
|
||||
|
||||
// only allow MKCOL, GET, PUT
|
||||
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for MKCOL request, only allow request ${folder}
|
||||
if (
|
||||
req.method == "MKCOL" &&
|
||||
!new URL(endpointPath).pathname.endsWith(folder)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for GET request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "GET" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for PUT request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "PUT" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint + endpointPath}`;
|
||||
|
||||
const method = req.method;
|
||||
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 = "edge";
|
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;
|
||||
}
|
@@ -1,16 +1,30 @@
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { ModelConfig, ModelType, useAccessStore } from "../store";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import {
|
||||
ACCESS_CODE_PREFIX,
|
||||
Azure,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
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 MultimodalContent {
|
||||
type: "text" | "image_url";
|
||||
text?: string;
|
||||
image_url?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestMessage {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
content: string | MultimodalContent[];
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
@@ -37,15 +51,53 @@ export interface LLMUsage {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LLMModel {
|
||||
name: string;
|
||||
available: boolean;
|
||||
provider: LLMModelProvider;
|
||||
}
|
||||
|
||||
export interface LLMModelProvider {
|
||||
id: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
}
|
||||
|
||||
export abstract class LLMApi {
|
||||
abstract chat(options: ChatOptions): Promise<void>;
|
||||
abstract usage(): Promise<LLMUsage>;
|
||||
abstract models(): Promise<LLMModel[]>;
|
||||
}
|
||||
|
||||
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() {
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
if (provider === ModelProvider.GeminiPro) {
|
||||
this.llm = new GeminiProApi();
|
||||
return;
|
||||
}
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
|
||||
@@ -54,30 +106,79 @@ export class ClientApi {
|
||||
prompts() {}
|
||||
|
||||
masks() {}
|
||||
}
|
||||
|
||||
export const api = new ClientApi();
|
||||
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 [NextChat]: 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 function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let headers: Record<string, string> = {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
|
||||
const modelConfig = useChatStore.getState().currentSession().mask.modelConfig;
|
||||
const isGoogle = modelConfig.model.startsWith("gemini");
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
const authHeader = isAzure ? "api-key" : "Authorization";
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
: isAzure
|
||||
? accessStore.azureApiKey
|
||||
: accessStore.openaiApiKey;
|
||||
const clientConfig = getClientConfig();
|
||||
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
|
||||
const validString = (x: string) => x && x.length > 0;
|
||||
|
||||
// use user's api key first
|
||||
if (validString(accessStore.token)) {
|
||||
headers.Authorization = makeBearer(accessStore.token);
|
||||
} else if (
|
||||
accessStore.enabledAccessControl() &&
|
||||
validString(accessStore.accessCode)
|
||||
) {
|
||||
headers.Authorization = makeBearer(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
// when using google api in app, not set auth header
|
||||
if (!(isGoogle && clientConfig?.isApp)) {
|
||||
// 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;
|
||||
|
@@ -3,17 +3,17 @@ export const ChatControllerPool = {
|
||||
controllers: {} as Record<string, AbortController>,
|
||||
|
||||
addController(
|
||||
sessionIndex: number,
|
||||
messageId: number,
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
controller: AbortController,
|
||||
) {
|
||||
const key = this.key(sessionIndex, messageId);
|
||||
const key = this.key(sessionId, messageId);
|
||||
this.controllers[key] = controller;
|
||||
return key;
|
||||
},
|
||||
|
||||
stop(sessionIndex: number, messageId: number) {
|
||||
const key = this.key(sessionIndex, messageId);
|
||||
stop(sessionId: string, messageId: string) {
|
||||
const key = this.key(sessionId, messageId);
|
||||
const controller = this.controllers[key];
|
||||
controller?.abort();
|
||||
},
|
||||
@@ -26,13 +26,12 @@ export const ChatControllerPool = {
|
||||
return Object.values(this.controllers).length > 0;
|
||||
},
|
||||
|
||||
remove(sessionIndex: number, messageId: number) {
|
||||
const key = this.key(sessionIndex, messageId);
|
||||
this.controllers[key]?.abort();
|
||||
remove(sessionId: string, messageId: string) {
|
||||
const key = this.key(sessionId, messageId);
|
||||
delete this.controllers[key];
|
||||
},
|
||||
|
||||
key(sessionIndex: number, messageIndex: number) {
|
||||
return `${sessionIndex},${messageIndex}`;
|
||||
key(sessionId: string, messageIndex: string) {
|
||||
return `${sessionId},${messageIndex}`;
|
||||
},
|
||||
};
|
||||
|
278
app/client/platforms/google.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
|
||||
export class GeminiProApi implements LLMApi {
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] gemini-pro response: ", res);
|
||||
|
||||
return (
|
||||
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
|
||||
res?.error?.message ||
|
||||
""
|
||||
);
|
||||
}
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
// const apiClient = this;
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
let multimodal = false;
|
||||
const messages = options.messages.map((v) => {
|
||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||
if (visionModel) {
|
||||
const images = getMessageImages(v);
|
||||
if (images.length > 0) {
|
||||
multimodal = true;
|
||||
parts = parts.concat(
|
||||
images.map((image) => {
|
||||
const imageType = image.split(";")[0].split(":")[1];
|
||||
const imageData = image.split(",")[1];
|
||||
return {
|
||||
inline_data: {
|
||||
mime_type: imageType,
|
||||
data: imageData,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
role: v.role.replace("assistant", "model").replace("system", "user"),
|
||||
parts: parts,
|
||||
};
|
||||
});
|
||||
|
||||
// google requires that role in neighboring messages must not be the same
|
||||
for (let i = 0; i < messages.length - 1; ) {
|
||||
// Check if current and next item both have the role "model"
|
||||
if (messages[i].role === messages[i + 1].role) {
|
||||
// Concatenate the 'parts' of the current and next item
|
||||
messages[i].parts = messages[i].parts.concat(messages[i + 1].parts);
|
||||
// Remove the next item
|
||||
messages.splice(i + 1, 1);
|
||||
} else {
|
||||
// Move to the next item
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// if (visionModel && messages.length > 1) {
|
||||
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
|
||||
// }
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
const requestPayload = {
|
||||
contents: messages,
|
||||
generationConfig: {
|
||||
// stopSequences: [
|
||||
// "Title"
|
||||
// ],
|
||||
temperature: modelConfig.temperature,
|
||||
maxOutputTokens: modelConfig.max_tokens,
|
||||
topP: modelConfig.top_p,
|
||||
// "topK": modelConfig.top_k,
|
||||
},
|
||||
safetySettings: [
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const accessStore = useAccessStore.getState();
|
||||
let baseUrl = accessStore.googleUrl;
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
let shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
try {
|
||||
let googleChatPath = visionModel
|
||||
? Google.VisionChatPath
|
||||
: Google.ChatPath;
|
||||
let chatPath = this.path(googleChatPath);
|
||||
|
||||
// let baseUrl = accessStore.googleUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/google/" + googleChatPath
|
||||
: chatPath;
|
||||
}
|
||||
|
||||
if (isApp) {
|
||||
baseUrl += `?key=${accessStore.googleApiKey}`;
|
||||
}
|
||||
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 remainText = "";
|
||||
let finished = false;
|
||||
|
||||
let existingTexts: string[] = [];
|
||||
const finish = () => {
|
||||
finished = true;
|
||||
options.onFinish(existingTexts.join(""));
|
||||
};
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainText.length > 0) {
|
||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||
const fetchText = remainText.slice(0, fetchCount);
|
||||
responseText += fetchText;
|
||||
remainText = remainText.slice(fetchCount);
|
||||
options.onUpdate?.(responseText, fetchText);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateResponseText);
|
||||
}
|
||||
|
||||
// start animaion
|
||||
animateResponseText();
|
||||
|
||||
fetch(
|
||||
baseUrl.replace("generateContent", "streamGenerateContent"),
|
||||
chatPayload,
|
||||
)
|
||||
.then((response) => {
|
||||
const reader = response?.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let partialData = "";
|
||||
|
||||
return reader?.read().then(function processText({
|
||||
done,
|
||||
value,
|
||||
}): Promise<any> {
|
||||
if (done) {
|
||||
if (response.status !== 200) {
|
||||
try {
|
||||
let data = JSON.parse(ensureProperEnding(partialData));
|
||||
if (data && data[0].error) {
|
||||
options.onError?.(new Error(data[0].error.message));
|
||||
} else {
|
||||
options.onError?.(new Error("Request failed"));
|
||||
}
|
||||
} catch (_) {
|
||||
options.onError?.(new Error("Request failed"));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Stream complete");
|
||||
// options.onFinish(responseText + remainText);
|
||||
finished = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
partialData += decoder.decode(value, { stream: true });
|
||||
|
||||
try {
|
||||
let data = JSON.parse(ensureProperEnding(partialData));
|
||||
|
||||
const textArray = data.reduce(
|
||||
(acc: string[], item: { candidates: any[] }) => {
|
||||
const texts = item.candidates.map((candidate) =>
|
||||
candidate.content.parts
|
||||
.map((part: { text: any }) => part.text)
|
||||
.join(""),
|
||||
);
|
||||
return acc.concat(texts);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (textArray.length > existingTexts.length) {
|
||||
const deltaArray = textArray.slice(existingTexts.length);
|
||||
existingTexts = textArray;
|
||||
remainText += deltaArray.join("");
|
||||
}
|
||||
} catch (error) {
|
||||
// console.log("[Response Animation] error: ", error,partialData);
|
||||
// skip error message when parsing json
|
||||
}
|
||||
|
||||
return reader.read().then(processText);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(baseUrl, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
const resJson = await res.json();
|
||||
if (resJson?.promptFeedback?.blockReason) {
|
||||
// being blocked
|
||||
options.onError?.(
|
||||
new Error(
|
||||
"Message is being blocked for reason: " +
|
||||
resJson.promptFeedback.blockReason,
|
||||
),
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
usage(): Promise<LLMUsage> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
path(path: string): string {
|
||||
return "/api/google/" + path;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProperEnding(str: string) {
|
||||
if (str.startsWith("[") && !str.endsWith("]")) {
|
||||
return str + "]";
|
||||
}
|
||||
return str;
|
||||
}
|
@@ -1,25 +1,82 @@
|
||||
import { REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
"use client";
|
||||
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, LLMUsage } from "../api";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
LLMUsage,
|
||||
MultimodalContent,
|
||||
} 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";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class ChatGPTApi implements LLMApi {
|
||||
public ChatPath = "v1/chat/completions";
|
||||
public UsagePath = "dashboard/billing/usage";
|
||||
public SubsPath = "dashboard/billing/subscription";
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
let openaiUrl = useAccessStore.getState().openaiUrl;
|
||||
if (openaiUrl.endsWith("/")) {
|
||||
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
|
||||
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",
|
||||
);
|
||||
}
|
||||
return [openaiUrl, path].join("/");
|
||||
|
||||
let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/proxy" + ApiPath.OpenAI
|
||||
: 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);
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
@@ -27,9 +84,10 @@ export class ChatGPTApi implements LLMApi {
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: v.content,
|
||||
content: visionModel ? v.content : getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
@@ -46,8 +104,22 @@ export class ChatGPTApi implements LLMApi {
|
||||
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.
|
||||
};
|
||||
|
||||
// add max_tokens to vision model
|
||||
if (visionModel) {
|
||||
Object.defineProperty(requestPayload, "max_tokens", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: modelConfig.max_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
@@ -55,7 +127,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(this.ChatPath);
|
||||
const chatPath = this.path(OpenaiPath.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
@@ -71,12 +143,35 @@ export class ChatGPTApi implements LLMApi {
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
console.log("[Response Animation] finished");
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainText.length > 0) {
|
||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||
const fetchText = remainText.slice(0, fetchCount);
|
||||
responseText += fetchText;
|
||||
remainText = remainText.slice(fetchCount);
|
||||
options.onUpdate?.(responseText, fetchText);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateResponseText);
|
||||
}
|
||||
|
||||
// start animaion
|
||||
animateResponseText();
|
||||
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
options.onFinish(responseText);
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,14 +225,19 @@ export class ChatGPTApi implements LLMApi {
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const delta = json.choices[0].delta.content;
|
||||
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);
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
console.error("[Request] parse error", text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
@@ -158,7 +258,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat reqeust", e);
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
@@ -177,14 +277,14 @@ export class ChatGPTApi implements LLMApi {
|
||||
const [used, subs] = await Promise.all([
|
||||
fetch(
|
||||
this.path(
|
||||
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
||||
`${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
},
|
||||
),
|
||||
fetch(this.path(this.SubsPath), {
|
||||
fetch(this.path(OpenaiPath.SubsPath), {
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
}),
|
||||
@@ -227,4 +327,36 @@ export class ChatGPTApi implements LLMApi {
|
||||
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,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
export { OpenaiPath };
|
||||
|
@@ -1,28 +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();
|
||||
|
||||
if (commands === undefined) return;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
97
app/components/auth.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
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),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
value={accessStore.googleApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.googleApiKey = 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>
|
||||
);
|
||||
}
|
@@ -27,6 +27,26 @@
|
||||
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 {
|
||||
@@ -37,10 +57,6 @@
|
||||
border: var(--border-in-light);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.icon-button-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -56,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;
|
||||
type?: "primary" | "danger";
|
||||
type?: ButtonType;
|
||||
text?: string;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
@@ -25,6 +29,8 @@ export function IconButton(props: {
|
||||
title={props.title}
|
||||
disabled={props.disabled}
|
||||
role="button"
|
||||
tabIndex={props.tabIndex}
|
||||
autoFocus={props.autoFocus}
|
||||
>
|
||||
{props.icon && (
|
||||
<div
|
||||
|
@@ -17,6 +17,8 @@ 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;
|
||||
@@ -25,7 +27,7 @@ export function ChatItem(props: {
|
||||
count: number;
|
||||
time: string;
|
||||
selected: boolean;
|
||||
id: number;
|
||||
id: string;
|
||||
index: number;
|
||||
narrow?: boolean;
|
||||
mask: Mask;
|
||||
@@ -59,7 +61,10 @@ export function ChatItem(props: {
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>
|
||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["chat-item-narrow-count"]}>
|
||||
{props.count}
|
||||
@@ -72,16 +77,18 @@ export function ChatItem(props: {
|
||||
<div className={styles["chat-item-count"]}>
|
||||
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>
|
||||
{new Date(props.time).toLocaleString()}
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles["chat-item-delete"]}
|
||||
onClickCapture={props.onDelete}
|
||||
onClickCapture={(e) => {
|
||||
props.onDelete?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
@@ -102,6 +109,7 @@ export function ChatList(props: { narrow?: boolean }) {
|
||||
);
|
||||
const chatStore = useChatStore();
|
||||
const navigate = useNavigate();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
const { destination, source } = result;
|
||||
@@ -141,8 +149,11 @@ export function ChatList(props: { narrow?: boolean }) {
|
||||
navigate(Path.Chat);
|
||||
selectSession(i);
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
|
||||
onDelete={async () => {
|
||||
if (
|
||||
(!props.narrow && !isMobileScreen) ||
|
||||
(await showConfirm(Locale.Home.DeleteChat))
|
||||
) {
|
||||
chatStore.deleteSession(i);
|
||||
}
|
||||
}}
|
||||
|
@@ -1,5 +1,47 @@
|
||||
@import "../styles/animation.scss";
|
||||
|
||||
.attach-images {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
bottom: 32px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.attach-image {
|
||||
cursor: default;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: var(--white);
|
||||
|
||||
.attach-image-mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: all ease 0.2s;
|
||||
}
|
||||
|
||||
.attach-image-mask:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
float: right;
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -14,13 +56,42 @@
|
||||
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: 24px;
|
||||
width: var(--icon-width);
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,11 +139,41 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -129,12 +230,10 @@
|
||||
|
||||
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)
|
||||
);
|
||||
$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 {
|
||||
@@ -174,3 +273,350 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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-item-image {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chat-message-item-images {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(var(--image-count), auto);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chat-message-item-image-multi {
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.chat-message-item-image,
|
||||
.chat-message-item-image-multi {
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
$calc-image-width: calc(100vw/3*2/var(--image-count));
|
||||
|
||||
.chat-message-item-image-multi {
|
||||
width: $calc-image-width;
|
||||
height: $calc-image-width;
|
||||
}
|
||||
|
||||
.chat-message-item-image {
|
||||
max-width: calc(100vw/3*2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
$max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count));
|
||||
$image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count));
|
||||
|
||||
.chat-message-item-image-multi {
|
||||
width: $image-width;
|
||||
height: $image-width;
|
||||
max-width: $max-image-width;
|
||||
max-height: $max-image-width;
|
||||
}
|
||||
|
||||
.chat-message-item-image {
|
||||
max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
cursor: text;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
}
|
||||
|
||||
.chat-input-panel-inner-attach {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.chat-input-panel-inner:has(.chat-input:focus) {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
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 {}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.chat-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
64
app/components/chat/speechRecorder.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import VoiceIcon from "@/app/icons/voice.svg";
|
||||
import { getLang, formatLang } from "@/app/locales";
|
||||
type SpeechRecognitionType =
|
||||
| typeof window.SpeechRecognition
|
||||
| typeof window.webkitSpeechRecognition;
|
||||
|
||||
export default function SpeechRecorder({
|
||||
textUpdater,
|
||||
onStop,
|
||||
}: {
|
||||
textUpdater: (text: string) => void;
|
||||
onStop?: () => void;
|
||||
}) {
|
||||
const [speechRecognition, setSpeechRecognition] =
|
||||
useState<SpeechRecognitionType | null>(null);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
useEffect(() => {
|
||||
if ("SpeechRecognition" in window) {
|
||||
setSpeechRecognition(new (window as any).SpeechRecognition());
|
||||
} else if ("webkitSpeechRecognition" in window) {
|
||||
setSpeechRecognition(new (window as any).webkitSpeechRecognition());
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{speechRecognition && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isRecording && speechRecognition) {
|
||||
speechRecognition.continuous = true;
|
||||
speechRecognition.lang = formatLang(getLang());
|
||||
console.log(speechRecognition.lang);
|
||||
speechRecognition.interimResults = true;
|
||||
speechRecognition.start();
|
||||
speechRecognition.onresult = function (event: any) {
|
||||
console.log(event);
|
||||
var transcript = event.results[0][0].transcript;
|
||||
console.log(transcript);
|
||||
textUpdater(transcript);
|
||||
};
|
||||
setIsRecording(true);
|
||||
} else {
|
||||
speechRecognition.stop();
|
||||
setIsRecording(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRecording ? (
|
||||
<button className="p-2 rounded-full bg-blue-500 hover:bg-blue-600 ring-4 ring-blue-200 transition animate-pulse">
|
||||
<VoiceIcon fill={"white"} />
|
||||
</button>
|
||||
) : (
|
||||
<button className="p-2 rounded-full bg-zinc-100 hover:bg-zinc-200 transition">
|
||||
<VoiceIcon fill={"#8282A5"} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -10,7 +10,10 @@ 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`;
|
||||
// Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
|
||||
// Old CDN broken, so I had to switch to this one
|
||||
// Author: https://github.com/H0llyW00dzZ
|
||||
return `https://fastly.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
|
||||
}
|
||||
|
||||
export function AvatarPicker(props: {
|
||||
@@ -18,6 +21,7 @@ export function AvatarPicker(props: {
|
||||
}) {
|
||||
return (
|
||||
<EmojiPicker
|
||||
width={"100%"}
|
||||
lazyLoadEmojis
|
||||
theme={EmojiTheme.AUTO}
|
||||
getEmojiUrl={getEmojiUrl}
|
||||
|
@@ -4,7 +4,8 @@ import GithubIcon from "../icons/github.svg";
|
||||
import ResetIcon from "../icons/reload.svg";
|
||||
import { ISSUE_URL } from "../constant";
|
||||
import Locale from "../locales";
|
||||
import { downloadAs } from "../utils";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -25,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
|
||||
clearAndSaveData() {
|
||||
try {
|
||||
downloadAs(
|
||||
JSON.stringify(localStorage),
|
||||
"chatgpt-next-web-snapshot.json",
|
||||
);
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
@@ -57,10 +55,11 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
<IconButton
|
||||
icon={<ResetIcon />}
|
||||
text="Clear All Data"
|
||||
onClick={() =>
|
||||
confirm(Locale.Settings.Actions.ConfirmClearAll) &&
|
||||
this.clearAndSaveData()
|
||||
}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
|
||||
this.clearAndSaveData();
|
||||
}
|
||||
}}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
|
@@ -94,6 +94,7 @@
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -131,7 +132,8 @@
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
transform: scale(2);
|
||||
height: 50%;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -184,6 +186,64 @@
|
||||
max-width: calc(100% - 104px);
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
|
||||
code,
|
||||
pre {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.message-images {
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(var(--image-count), auto);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
$image-width: calc(calc(100vw/2)/var(--image-count));
|
||||
|
||||
.message-image-multi {
|
||||
width: $image-width;
|
||||
height: $image-width;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
max-width: calc(100vw/3*2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
$max-image-width: calc(900px/3*2/var(--image-count));
|
||||
$image-width: calc(80vw/3*2/var(--image-count));
|
||||
|
||||
.message-image-multi {
|
||||
width: $image-width;
|
||||
height: $image-width;
|
||||
max-width: $max-image-width;
|
||||
max-height: $max-image-width;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
max-width: calc(100vw/3*2);
|
||||
}
|
||||
}
|
||||
|
||||
.message-image-multi {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.message-image,
|
||||
.message-image-multi {
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
&-assistant {
|
||||
@@ -207,6 +267,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
.default-theme {
|
||||
}
|
||||
}
|
||||
.default-theme {}
|
||||
}
|
@@ -1,23 +1,45 @@
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
/* 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, showToast } from "./ui-lib";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
Select,
|
||||
showImageModal,
|
||||
showModal,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import { IconButton } from "./button";
|
||||
import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
getMessageImages,
|
||||
useMobileScreen,
|
||||
} from "../utils";
|
||||
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.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 { useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { MessageSelector, useMessageSelector } from "./message-selector";
|
||||
import { Avatar } from "./emoji";
|
||||
import { MaskAvatar } from "./mask";
|
||||
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 { prettyObject } from "../utils/format";
|
||||
import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ClientApi } from "../client/api";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -26,7 +48,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
||||
<Modal
|
||||
title={Locale.Export.Title}
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
{Locale.Exporter.Description.Title}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ minHeight: "40vh" }}>
|
||||
<MessageExporter />
|
||||
</div>
|
||||
@@ -112,7 +149,7 @@ export function MessageExporter() {
|
||||
];
|
||||
const { currentStep, setCurrentStepIndex, currentStepIndex } =
|
||||
useSteps(steps);
|
||||
const formats = ["text", "image"] as const;
|
||||
const formats = ["text", "image", "json"] as const;
|
||||
type ExportFormat = (typeof formats)[number];
|
||||
|
||||
const [exportConfig, setExportConfig] = useState({
|
||||
@@ -134,7 +171,7 @@ export function MessageExporter() {
|
||||
if (exportConfig.includeContext) {
|
||||
ret.push(...session.mask.context);
|
||||
}
|
||||
ret.push(...session.messages.filter((m, i) => selection.has(m.id ?? i)));
|
||||
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||
return ret;
|
||||
}, [
|
||||
exportConfig.includeContext,
|
||||
@@ -142,7 +179,21 @@ export function MessageExporter() {
|
||||
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
|
||||
@@ -150,109 +201,231 @@ export function MessageExporter() {
|
||||
index={currentStepIndex}
|
||||
onStepChange={setCurrentStepIndex}
|
||||
/>
|
||||
|
||||
<div className={styles["message-exporter-body"]}>
|
||||
{currentStep.value === "select" && (
|
||||
<>
|
||||
<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
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep.value === "preview" && (
|
||||
<>
|
||||
{exportConfig.format === "text" ? (
|
||||
<MarkdownPreviewer
|
||||
messages={selectedMessages}
|
||||
topic={session.topic}
|
||||
/>
|
||||
) : (
|
||||
<ImagePreviewer
|
||||
messages={selectedMessages}
|
||||
topic={session.topic}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<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={getMessageTextContent(m)} 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 config = useAppConfig();
|
||||
const onRenderMsgs = (msgs: ChatMessage[]) => {
|
||||
setShouldExport(false);
|
||||
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
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 && (
|
||||
<>
|
||||
<div className={styles["preview-actions"]}>
|
||||
{props.showCopy && (
|
||||
<IconButton
|
||||
text={Locale.Export.Copy}
|
||||
bordered
|
||||
shadow
|
||||
icon={<CopyIcon />}
|
||||
onClick={props.copy}
|
||||
></IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
text={Locale.Export.Copy}
|
||||
text={Locale.Export.Download}
|
||||
bordered
|
||||
shadow
|
||||
icon={<CopyIcon />}
|
||||
onClick={props.copy}
|
||||
icon={<DownloadIcon />}
|
||||
onClick={props.download}
|
||||
></IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
text={Locale.Export.Download}
|
||||
bordered
|
||||
shadow
|
||||
icon={<DownloadIcon />}
|
||||
onClick={props.download}
|
||||
></IconButton>
|
||||
<IconButton
|
||||
text={Locale.Export.Share}
|
||||
bordered
|
||||
shadow
|
||||
icon={<ShareIcon />}
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
></IconButton>
|
||||
</div>
|
||||
<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;
|
||||
@@ -265,6 +438,7 @@ export function ImagePreviewer(props: {
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const copy = () => {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
toBlob(dom).then((blob) => {
|
||||
@@ -278,6 +452,7 @@ export function ImagePreviewer(props: {
|
||||
])
|
||||
.then(() => {
|
||||
showToast(Locale.Copy.Success);
|
||||
refreshPreview();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[Copy Image] ", e);
|
||||
@@ -288,63 +463,109 @@ export function ImagePreviewer(props: {
|
||||
|
||||
const isMobile = useMobileScreen();
|
||||
|
||||
const download = () => {
|
||||
const download = async () => {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
toPng(dom)
|
||||
.then((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
if (isMobile) {
|
||||
const image = new Image();
|
||||
image.src = blob;
|
||||
const win = window.open("");
|
||||
win?.document.write(image.outerHTML);
|
||||
const isApp = getClientConfig()?.isApp;
|
||||
|
||||
try {
|
||||
const blob = await toPng(dom);
|
||||
if (!blob) return;
|
||||
|
||||
if (isMobile || (isApp && window.__TAURI__)) {
|
||||
if (isApp && window.__TAURI__) {
|
||||
const result = await window.__TAURI__.dialog.save({
|
||||
defaultPath: `${props.topic}.png`,
|
||||
filters: [
|
||||
{
|
||||
name: "PNG Files",
|
||||
extensions: ["png"],
|
||||
},
|
||||
{
|
||||
name: "All Files",
|
||||
extensions: ["*"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result !== null) {
|
||||
const response = await fetch(blob);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
|
||||
showToast(Locale.Download.Success);
|
||||
} else {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
} else {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${props.topic}.png`;
|
||||
link.href = blob;
|
||||
link.click();
|
||||
showImageModal(blob);
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log("[Export Image] ", e));
|
||||
} else {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${props.topic}.png`;
|
||||
link.href = blob;
|
||||
link.click();
|
||||
refreshPreview();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPreview = () => {
|
||||
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} />
|
||||
<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"}>
|
||||
<ChatGptIcon />
|
||||
<NextImage
|
||||
src={ChatGptIcon.src}
|
||||
alt="logo"
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles["main-title"]}>ChatGPT Next Web</div>
|
||||
<div className={styles["main-title"]}>NextChat</div>
|
||||
<div className={styles["sub-title"]}>
|
||||
github.com/Yidadaa/ChatGPT-Next-Web
|
||||
</div>
|
||||
<div className={styles["icons"]}>
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
<ExportAvatar avatar={config.avatar} />
|
||||
<span className={styles["icon-space"]}>&</span>
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<ExportAvatar avatar={mask.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
Model: {mask.modelConfig.model}
|
||||
{Locale.Exporter.Model}: {mask.modelConfig.model}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
Messages: {props.messages.length}
|
||||
{Locale.Exporter.Messages}: {props.messages.length}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
Topic: {session.topic}
|
||||
{Locale.Exporter.Topic}: {session.topic}
|
||||
</div>
|
||||
<div className={styles["chat-info-item"]}>
|
||||
Time:{" "}
|
||||
{Locale.Exporter.Time}:{" "}
|
||||
{new Date(
|
||||
props.messages.at(-1)?.date ?? Date.now(),
|
||||
).toLocaleString()}
|
||||
@@ -358,19 +579,44 @@ export function ImagePreviewer(props: {
|
||||
key={i}
|
||||
>
|
||||
<div className={styles["avatar"]}>
|
||||
{m.role === "user" ? (
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
)}
|
||||
<ExportAvatar
|
||||
avatar={m.role === "user" ? config.avatar : mask.avatar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${styles["body"]} `}>
|
||||
<div className={styles["body"]}>
|
||||
<Markdown
|
||||
content={m.content}
|
||||
content={getMessageTextContent(m)}
|
||||
fontSize={config.fontSize}
|
||||
defaultShow
|
||||
/>
|
||||
{getMessageImages(m).length == 1 && (
|
||||
<img
|
||||
key={i}
|
||||
src={getMessageImages(m)[0]}
|
||||
alt="message"
|
||||
className={styles["message-image"]}
|
||||
/>
|
||||
)}
|
||||
{getMessageImages(m).length > 1 && (
|
||||
<div
|
||||
className={styles["message-images"]}
|
||||
style={
|
||||
{
|
||||
"--image-count": getMessageImages(m).length,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{getMessageImages(m).map((src, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={src}
|
||||
alt="message"
|
||||
className={styles["message-image-multi"]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -389,8 +635,10 @@ export function MarkdownPreviewer(props: {
|
||||
props.messages
|
||||
.map((m) => {
|
||||
return m.role === "user"
|
||||
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
||||
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
||||
? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
|
||||
: `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
|
||||
m,
|
||||
).trim()}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
@@ -400,13 +648,58 @@ export function MarkdownPreviewer(props: {
|
||||
const download = () => {
|
||||
downloadAs(mdText, `${props.topic}.md`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewActions copy={copy} download={download} />
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
color: var(--black);
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 480px;
|
||||
min-height: 370px;
|
||||
max-width: 1200px;
|
||||
|
||||
display: flex;
|
||||
@@ -61,24 +61,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +153,7 @@
|
||||
|
||||
.sidebar-sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400px;
|
||||
font-weight: 400;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
@@ -162,6 +174,7 @@
|
||||
user-select: none;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.chat-item:hover {
|
||||
@@ -185,7 +198,7 @@
|
||||
|
||||
.chat-item-delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
@@ -194,7 +207,7 @@
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
transform: translateX(-10px);
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete:hover {
|
||||
@@ -283,15 +296,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item-delete {
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.sidebar-tail {
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
@@ -322,242 +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;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.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-container {
|
||||
max-width: var(--message-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -566,3 +334,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rtl-screen {
|
||||
direction: rtl;
|
||||
}
|
||||
|
@@ -12,9 +12,11 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import { getCSSVar, useMobileScreen } from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Path, SlotID } from "../constant";
|
||||
import { ModelProvider, Path, SlotID } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import { getISOLang, getLang } from "../locales";
|
||||
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
@@ -23,6 +25,10 @@ import {
|
||||
} from "react-router-dom";
|
||||
import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ClientApi } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@@ -80,6 +86,17 @@ export function useSwitchTheme() {
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
function useHtmlLang() {
|
||||
useEffect(() => {
|
||||
const lang = getISOLang();
|
||||
const htmlLang = document.documentElement.lang;
|
||||
|
||||
if (lang !== htmlLang) {
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||
|
||||
@@ -92,9 +109,16 @@ const useHasHydrated = () => {
|
||||
|
||||
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 =
|
||||
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
|
||||
googleFontUrl +
|
||||
"/css2?family=" +
|
||||
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
|
||||
"&display=swap";
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
@@ -102,7 +126,10 @@ 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);
|
||||
|
||||
useEffect(() => {
|
||||
loadAsyncGoogleFont();
|
||||
@@ -112,30 +139,61 @@ function Screen() {
|
||||
<div
|
||||
className={
|
||||
styles.container +
|
||||
` ${
|
||||
config.tightBorder && !isMobileScreen
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
{isAuth ? (
|
||||
<>
|
||||
<AuthPage />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
|
||||
<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 className={styles["window-content"]} id={SlotID.AppBody}>
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={<Chat />} />
|
||||
<Route path={Path.NewChat} element={<NewChat />} />
|
||||
<Route path={Path.Masks} element={<MaskPage />} />
|
||||
<Route path={Path.Chat} element={<Chat />} />
|
||||
<Route path={Path.Settings} element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLoadData() {
|
||||
const config = useAppConfig();
|
||||
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
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 />;
|
||||
|
@@ -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,24 +5,28 @@ 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; onError: () => void }) {
|
||||
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) => {
|
||||
props.onError();
|
||||
setHasError(true);
|
||||
console.error("[Mermaid] ", e.message);
|
||||
});
|
||||
}
|
||||
@@ -34,17 +38,20 @@ export function Mermaid(props: { code: string; onError: () => void }) {
|
||||
if (!svg) return;
|
||||
const text = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([text], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const win = window.open(url);
|
||||
if (win) {
|
||||
win.onload = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
showImageModal(URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-dark"
|
||||
style={{ cursor: "pointer", overflow: "auto" }}
|
||||
className="no-dark mermaid"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
overflow: "auto",
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={() => viewSvgInNewWindow()}
|
||||
>
|
||||
@@ -55,37 +62,66 @@ export function Mermaid(props: { code: string; onError: () => void }) {
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const refText = ref.current?.innerText;
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const renderMermaid = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||
if (mermaidDom) {
|
||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||
}
|
||||
}, [props.children]);
|
||||
}, 600);
|
||||
|
||||
if (mermaidCode) {
|
||||
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
|
||||
}
|
||||
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]}
|
||||
@@ -101,6 +137,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || "";
|
||||
const isInternal = /^\/#/i.test(href);
|
||||
@@ -109,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.content}
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -126,55 +163,23 @@ export function Markdown(
|
||||
} & React.DOMAttributes<HTMLDivElement>,
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
const renderedHeight = useRef(0);
|
||||
const inView = useRef(!!props.defaultShow);
|
||||
|
||||
const parent = props.parentRef?.current;
|
||||
const md = mdRef.current;
|
||||
|
||||
const checkInView = () => {
|
||||
if (parent && md) {
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const parentTop = parentBounds.top - twoScreenHeight;
|
||||
const parentBottom = parentBounds.bottom + twoScreenHeight;
|
||||
const isOverlap =
|
||||
Math.max(parentTop, mdBounds.top) <=
|
||||
Math.min(parentBottom, mdBounds.bottom);
|
||||
inView.current = isOverlap;
|
||||
}
|
||||
|
||||
if (inView.current && md) {
|
||||
renderedHeight.current = Math.max(
|
||||
renderedHeight.current,
|
||||
md.getBoundingClientRect().height,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => checkInView(), 1);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
height:
|
||||
!inView.current && renderedHeight.current > 0
|
||||
? renderedHeight.current
|
||||
: "auto",
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||
dir="auto"
|
||||
>
|
||||
{inView.current &&
|
||||
(props.loading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<MarkdownContent content={props.content} />
|
||||
))}
|
||||
{props.loading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<MarkdownContent content={props.content} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -11,28 +11,65 @@ 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, ModelConfig, useAppConfig, useChatStore } from "../store";
|
||||
import { ROLES } from "../client/api";
|
||||
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib";
|
||||
import {
|
||||
ChatMessage,
|
||||
createMessage,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
import { MultimodalContent, 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 { downloadAs, readFromFile } from "../utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
getMessageImages,
|
||||
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";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
|
||||
export function MaskAvatar(props: { mask: Mask }) {
|
||||
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.mask.avatar} />
|
||||
// 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.mask.modelConfig.model} />
|
||||
<Avatar model={props.model} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +94,11 @@ export function MaskConfig(props: {
|
||||
});
|
||||
};
|
||||
|
||||
const copyMaskLink = () => {
|
||||
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
|
||||
copyToClipboard(maskLink);
|
||||
};
|
||||
|
||||
const globalConfig = useAppConfig();
|
||||
|
||||
return (
|
||||
@@ -88,7 +130,10 @@ export function MaskConfig(props: {
|
||||
onClick={() => setShowPicker(true)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</ListItem>
|
||||
@@ -117,6 +162,20 @@ export function MaskConfig(props: {
|
||||
}}
|
||||
></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}
|
||||
@@ -125,15 +184,20 @@ export function MaskConfig(props: {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.mask.syncGlobalConfig}
|
||||
onChange={(e) => {
|
||||
onChange={async (e) => {
|
||||
const checked = e.currentTarget.checked;
|
||||
if (
|
||||
e.currentTarget.checked &&
|
||||
confirm(Locale.Mask.Config.Sync.Confirm)
|
||||
checked &&
|
||||
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
|
||||
) {
|
||||
props.updateMask((mask) => {
|
||||
mask.syncGlobalConfig = e.currentTarget.checked;
|
||||
mask.syncGlobalConfig = checked;
|
||||
mask.modelConfig = { ...globalConfig.modelConfig };
|
||||
});
|
||||
} else if (!checked) {
|
||||
props.updateMask((mask) => {
|
||||
mask.syncGlobalConfig = checked;
|
||||
});
|
||||
}
|
||||
}}
|
||||
></input>
|
||||
@@ -153,6 +217,7 @@ export function MaskConfig(props: {
|
||||
}
|
||||
|
||||
function ContextPromptItem(props: {
|
||||
index: number;
|
||||
prompt: ChatMessage;
|
||||
update: (prompt: ChatMessage) => void;
|
||||
remove: () => void;
|
||||
@@ -162,30 +227,40 @@ function ContextPromptItem(props: {
|
||||
return (
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
{!focusingInput && (
|
||||
<Select
|
||||
value={props.prompt.role}
|
||||
className={chatStyle["context-role"]}
|
||||
onChange={(e) =>
|
||||
props.update({
|
||||
...props.prompt,
|
||||
role: e.target.value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<>
|
||||
<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}
|
||||
value={getMessageTextContent(props.prompt)}
|
||||
type="text"
|
||||
className={chatStyle["context-content"]}
|
||||
rows={focusingInput ? 5 : 1}
|
||||
onFocus={() => setFocusingInput(true)}
|
||||
onBlur={() => setFocusingInput(false)}
|
||||
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,
|
||||
@@ -211,8 +286,8 @@ export function ContextPrompts(props: {
|
||||
}) {
|
||||
const context = props.context;
|
||||
|
||||
const addContextPrompt = (prompt: ChatMessage) => {
|
||||
props.updateContext((context) => context.push(prompt));
|
||||
const addContextPrompt = (prompt: ChatMessage, i: number) => {
|
||||
props.updateContext((context) => context.splice(i, 0, prompt));
|
||||
};
|
||||
|
||||
const removeContextPrompt = (i: number) => {
|
||||
@@ -220,36 +295,104 @@ export function ContextPrompts(props: {
|
||||
};
|
||||
|
||||
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
|
||||
props.updateContext((context) => (context[i] = prompt));
|
||||
props.updateContext((context) => {
|
||||
const images = getMessageImages(context[i]);
|
||||
context[i] = prompt;
|
||||
if (images.length > 0) {
|
||||
const text = getMessageTextContent(context[i]);
|
||||
const newContext: MultimodalContent[] = [{ type: "text", text }];
|
||||
for (const img of images) {
|
||||
newContext.push({ type: "image_url", image_url: { url: img } });
|
||||
}
|
||||
context[i].content = newContext;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 }}>
|
||||
{context.map((c, i) => (
|
||||
<ContextPromptItem
|
||||
key={i}
|
||||
prompt={c}
|
||||
update={(prompt) => updateContextPrompt(i, prompt)}
|
||||
remove={() => removeContextPrompt(i)}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Context.Add}
|
||||
bordered
|
||||
className={chatStyle["context-prompt-button"]}
|
||||
onClick={() =>
|
||||
addContextPrompt({
|
||||
role: "user",
|
||||
content: "",
|
||||
date: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
@@ -271,24 +414,26 @@ export function MaskPage() {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
||||
|
||||
// simple search, will refactor later
|
||||
// refactored already, now it accurate
|
||||
const onSearch = (text: string) => {
|
||||
setSearchText(text);
|
||||
if (text.length > 0) {
|
||||
const result = allMasks.filter((m) => m.name.includes(text));
|
||||
const result = allMasks.filter((m) =>
|
||||
m.name.toLowerCase().includes(text.toLowerCase()),
|
||||
);
|
||||
setSearchMasks(result);
|
||||
} else {
|
||||
setSearchMasks(allMasks);
|
||||
}
|
||||
};
|
||||
|
||||
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
|
||||
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), FileName.Masks);
|
||||
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
|
||||
};
|
||||
|
||||
const importFromFile = () => {
|
||||
@@ -330,11 +475,13 @@ export function MaskPage() {
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
onClick={downloadAll}
|
||||
text={Locale.UI.Export}
|
||||
/>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
text={Locale.UI.Import}
|
||||
bordered
|
||||
onClick={() => importFromFile()}
|
||||
/>
|
||||
@@ -397,7 +544,7 @@ export function MaskPage() {
|
||||
<div className={styles["mask-item"]} key={m.id}>
|
||||
<div className={styles["mask-header"]}>
|
||||
<div className={styles["mask-icon"]}>
|
||||
<MaskAvatar mask={m} />
|
||||
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
|
||||
</div>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>{m.name}</div>
|
||||
@@ -434,8 +581,8 @@ export function MaskPage() {
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
text={Locale.Mask.Item.Delete}
|
||||
onClick={() => {
|
||||
if (confirm(Locale.Mask.Item.DeleteConfirm)) {
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
|
||||
maskStore.delete(m.id);
|
||||
}
|
||||
}}
|
||||
@@ -482,7 +629,7 @@ export function MaskPage() {
|
||||
<MaskConfig
|
||||
mask={editingMask}
|
||||
updateMask={(updater) =>
|
||||
maskStore.update(editingMaskId!, updater)
|
||||
maskStore.updateMask(editingMaskId!, updater)
|
||||
}
|
||||
readonly={editingMask.builtin}
|
||||
/>
|
||||
|
@@ -58,8 +58,8 @@
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 40px);
|
||||
flex: 1;
|
||||
max-width: calc(100% - 80px);
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
@@ -71,6 +71,12 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { Updater } from "../typing";
|
||||
import { IconButton } from "./button";
|
||||
@@ -7,6 +7,7 @@ import { MaskAvatar } from "./mask";
|
||||
import Locale from "../locales";
|
||||
|
||||
import styles from "./message-selector.module.scss";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
|
||||
function useShiftRange() {
|
||||
const [startIndex, setStartIndex] = useState<number>();
|
||||
@@ -51,9 +52,9 @@ function useShiftRange() {
|
||||
}
|
||||
|
||||
export function useMessageSelector() {
|
||||
const [selection, setSelection] = useState(new Set<number>());
|
||||
const updateSelection: Updater<Set<number>> = (updater) => {
|
||||
const newSelection = new Set<number>(selection);
|
||||
const [selection, setSelection] = useState(new Set<string>());
|
||||
const updateSelection: Updater<Set<string>> = (updater) => {
|
||||
const newSelection = new Set<string>(selection);
|
||||
updater(newSelection);
|
||||
setSelection(newSelection);
|
||||
};
|
||||
@@ -65,36 +66,50 @@ export function useMessageSelector() {
|
||||
}
|
||||
|
||||
export function MessageSelector(props: {
|
||||
selection: Set<number>;
|
||||
updateSelection: Updater<Set<number>>;
|
||||
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 messages = session.messages.filter(
|
||||
(m, i) =>
|
||||
m.id && // messsage must has id
|
||||
isValid(m) &&
|
||||
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
||||
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<number>());
|
||||
const isInSearchResult = (id: number) => {
|
||||
const [searchIds, setSearchIds] = useState(new Set<string>());
|
||||
const isInSearchResult = (id: string) => {
|
||||
return searchInput.length === 0 || searchIds.has(id);
|
||||
};
|
||||
const doSearch = (text: string) => {
|
||||
const searchResuts = new Set<number>();
|
||||
const searchResults = new Set<string>();
|
||||
if (text.length > 0) {
|
||||
messages.forEach((m) =>
|
||||
m.content.includes(text) ? searchResuts.add(m.id!) : null,
|
||||
getMessageTextContent(m).includes(text)
|
||||
? searchResults.add(m.id!)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
setSearchIds(searchResuts);
|
||||
setSearchIds(searchResults);
|
||||
};
|
||||
|
||||
// for range selection
|
||||
@@ -126,6 +141,8 @@ export function MessageSelector(props: {
|
||||
// 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"]}>
|
||||
@@ -155,7 +172,7 @@ export function MessageSelector(props: {
|
||||
props.updateSelection((selection) => {
|
||||
selection.clear();
|
||||
messages
|
||||
.slice(messageCount - 10)
|
||||
.slice(messageCount - LATEST_COUNT)
|
||||
.forEach((m) => selection.add(m.id!));
|
||||
})
|
||||
}
|
||||
@@ -174,6 +191,8 @@ export function MessageSelector(props: {
|
||||
<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
|
||||
@@ -183,7 +202,6 @@ export function MessageSelector(props: {
|
||||
key={i}
|
||||
onClick={() => {
|
||||
props.updateSelection((selection) => {
|
||||
const id = m.id ?? i;
|
||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||
});
|
||||
onClickIndex(i);
|
||||
@@ -193,7 +211,10 @@ export function MessageSelector(props: {
|
||||
{m.role === "user" ? (
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={m.model || session.mask.modelConfig.model}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles["body"]}>
|
||||
@@ -201,9 +222,13 @@ export function MessageSelector(props: {
|
||||
{new Date(m.date).toLocaleString()}
|
||||
</div>
|
||||
<div className={`${styles["content"]} one-line`}>
|
||||
{m.content}
|
||||
{getMessageTextContent(m)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
|
||||
import { ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { List, ListItem, Select } from "./ui-lib";
|
||||
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}>
|
||||
@@ -22,11 +25,13 @@ export function ModelConfigList(props: {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{ALL_MODELS.map((v) => (
|
||||
<option value={v.name} key={v.name} disabled={!v.available}>
|
||||
{v.name}
|
||||
</option>
|
||||
))}
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={v.name} key={i}>
|
||||
{v.displayName}({v.provider?.providerName})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
@@ -48,14 +53,33 @@ export function ModelConfigList(props: {
|
||||
}}
|
||||
></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={100}
|
||||
max={32000}
|
||||
min={1024}
|
||||
max={512000}
|
||||
value={props.modelConfig.max_tokens}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
@@ -67,27 +91,84 @@ export function ModelConfigList(props: {
|
||||
}
|
||||
></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>
|
||||
|
||||
{props.modelConfig.model.startsWith("gemini") ? null : (
|
||||
<>
|
||||
<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}
|
||||
@@ -96,7 +177,7 @@ export function ModelConfigList(props: {
|
||||
title={props.modelConfig.historyMessageCount.toString()}
|
||||
value={props.modelConfig.historyMessageCount}
|
||||
min="0"
|
||||
max="32"
|
||||
max="64"
|
||||
step="1"
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
|
@@ -14,22 +14,16 @@ import Locale from "../locales";
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { useCommand } from "../command";
|
||||
|
||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||||
const xmin = Math.max(aRect.x, bRect.x);
|
||||
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
||||
const ymin = Math.max(aRect.y, bRect.y);
|
||||
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
||||
const width = xmax - xmin;
|
||||
const height = ymax - ymin;
|
||||
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
||||
return intersectionArea;
|
||||
}
|
||||
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 mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -92,14 +86,16 @@ export function NewChat() {
|
||||
const { state } = useLocation();
|
||||
|
||||
const startChat = (mask?: Mask) => {
|
||||
chatStore.newSession(mask);
|
||||
setTimeout(() => navigate(Path.Chat), 1);
|
||||
setTimeout(() => {
|
||||
chatStore.newSession(mask);
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
useCommand({
|
||||
mask: (id) => {
|
||||
try {
|
||||
const mask = maskStore.get(parseInt(id));
|
||||
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);
|
||||
@@ -125,8 +121,8 @@ export function NewChat() {
|
||||
{!state?.fromHome && (
|
||||
<IconButton
|
||||
text={Locale.NewChat.NotShow}
|
||||
onClick={() => {
|
||||
if (confirm(Locale.NewChat.ConfirmNoShow)) {
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
|
||||
startChat();
|
||||
config.update(
|
||||
(config) => (config.dontShowMaskSplashScreen = true),
|
||||
|
@@ -5,6 +5,8 @@
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.edit-prompt-modal {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
@@ -8,14 +8,17 @@ 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,
|
||||
@@ -24,9 +27,9 @@ import {
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useMobileScreen } from "../utils";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showToast } from "./ui-lib";
|
||||
import { showConfirm, showToast } from "./ui-lib";
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
@@ -37,14 +40,11 @@ function useHotKey() {
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
const n = chatStore.sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = chatStore.currentSessionIndex;
|
||||
if (e.altKey || e.ctrlKey) {
|
||||
if (e.key === "ArrowUp") {
|
||||
chatStore.selectSession(limit(i - 1));
|
||||
chatStore.nextSession(-1);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
chatStore.selectSession(limit(i + 1));
|
||||
chatStore.nextSession(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -59,31 +59,57 @@ function useDragSideBar() {
|
||||
|
||||
const config = useAppConfig();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 50) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
const toggleSideBar = () => {
|
||||
config.update((config) => {
|
||||
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
// Remembers the initial width each time the mouse is pressed
|
||||
startX.current = e.clientX;
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
const dragStartTime = Date.now();
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 20) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => {
|
||||
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = nextWidth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||
window.removeEventListener("pointermove", handleDragMove);
|
||||
window.removeEventListener("pointerup", handleDragEnd);
|
||||
|
||||
// if user click the drag icon, should toggle the sidebar
|
||||
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||
if (shouldFireClick) {
|
||||
toggleSideBar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleDragMove);
|
||||
window.addEventListener("pointerup", handleDragEnd);
|
||||
};
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldNarrow =
|
||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||
@@ -91,13 +117,13 @@ function useDragSideBar() {
|
||||
useEffect(() => {
|
||||
const barWidth = shouldNarrow
|
||||
? NARROW_SIDEBAR_WIDTH
|
||||
: limit(config.sidebarWidth ?? 300);
|
||||
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
onDragStart,
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
@@ -106,9 +132,14 @@ export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const isIOSMobile = useMemo(
|
||||
() => isIOS() && isMobileScreen,
|
||||
[isMobileScreen],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
@@ -117,9 +148,15 @@ export function SideBar(props: { className?: string }) {
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
style={{
|
||||
// #3016 disable transition on ios mobile screen
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={styles["sidebar-header"]}>
|
||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
NextChat
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
@@ -133,7 +170,13 @@ export function SideBar(props: { className?: string }) {
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
@@ -160,9 +203,9 @@ export function SideBar(props: { className?: string }) {
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={() => {
|
||||
if (confirm(Locale.Home.DeleteChat)) {
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||
}
|
||||
}}
|
||||
@@ -174,7 +217,7 @@ export function SideBar(props: { className?: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
@@ -198,8 +241,10 @@ export function SideBar(props: { className?: string }) {
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -14,17 +14,24 @@
|
||||
|
||||
.popover-content {
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
animation: slide-in 0.3s ease;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.popover-content {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.popover-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
@@ -62,6 +69,7 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
margin-bottom: 20px;
|
||||
animation: slide-in ease 0.3s;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.list .list-item:last-child {
|
||||
@@ -72,11 +80,26 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
background-color: var(--white);
|
||||
border-radius: 12px;
|
||||
width: 60vw;
|
||||
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;
|
||||
@@ -89,11 +112,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,11 +238,11 @@
|
||||
.select-with-icon {
|
||||
position: relative;
|
||||
max-width: fit-content;
|
||||
|
||||
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
padding: 10px 25px 10px 10px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
border-radius: 10px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
@@ -227,4 +258,57 @@
|
||||
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,9 +1,16 @@
|
||||
/* 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, { HTMLProps, useEffect, useState } from "react";
|
||||
@@ -19,10 +26,10 @@ export function Popover(props: {
|
||||
<div className={styles.popover}>
|
||||
{props.children}
|
||||
{props.open && (
|
||||
<div className={styles["popover-content"]}>
|
||||
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
||||
{props.content}
|
||||
</div>
|
||||
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
||||
)}
|
||||
{props.open && (
|
||||
<div className={styles["popover-content"]}>{props.content}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -40,9 +47,13 @@ export function ListItem(props: {
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
icon?: JSX.Element;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["list-item"] + ` ${props.className}`}>
|
||||
<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"]}>
|
||||
@@ -59,14 +70,12 @@ export function ListItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function List(props: {
|
||||
children:
|
||||
| Array<JSX.Element | null | undefined>
|
||||
| JSX.Element
|
||||
| null
|
||||
| undefined;
|
||||
}) {
|
||||
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() {
|
||||
@@ -87,8 +96,10 @@ export function Loading() {
|
||||
|
||||
interface ModalProps {
|
||||
title: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
actions?: JSX.Element[];
|
||||
children?: any;
|
||||
actions?: React.ReactNode[];
|
||||
defaultMax?: boolean;
|
||||
footer?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function Modal(props: ModalProps) {
|
||||
@@ -107,19 +118,37 @@ export function Modal(props: ModalProps) {
|
||||
// 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"]}>
|
||||
@@ -262,3 +291,197 @@ export function Select(
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@@ -1,15 +1,4 @@
|
||||
const COMMIT_ID: string = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
return childProcess
|
||||
.execSync('git log -1 --format="%at000" --date=unix')
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
import tauriConfig from "../../src-tauri/tauri.conf.json";
|
||||
|
||||
export const getBuildConfig = () => {
|
||||
if (typeof process === "undefined") {
|
||||
@@ -18,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,15 +1,38 @@
|
||||
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;
|
||||
|
||||
BASE_URL?: string;
|
||||
PROXY_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;
|
||||
|
||||
// google only
|
||||
GOOGLE_API_KEY?: string;
|
||||
GOOGLE_URL?: string;
|
||||
|
||||
// google tag manager
|
||||
GTM_ID?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,15 +57,54 @@ export const getServerSideConfig = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
|
||||
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
const apiKey = apiKeys[randomIndex];
|
||||
console.log(
|
||||
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||
);
|
||||
|
||||
return {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseUrl: process.env.BASE_URL,
|
||||
apiKey,
|
||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||
|
||||
isAzure,
|
||||
azureUrl: process.env.AZURE_URL,
|
||||
azureApiKey: process.env.AZURE_API_KEY,
|
||||
azureApiVersion: process.env.AZURE_API_VERSION,
|
||||
|
||||
isGoogle,
|
||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
||||
googleUrl: process.env.GOOGLE_URL,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
code: process.env.CODE,
|
||||
codes: ACCESS_CODES,
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
baseUrl: process.env.BASE_URL,
|
||||
|
||||
proxyUrl: process.env.PROXY_URL,
|
||||
isVercel: !!process.env.VERCEL,
|
||||
|
||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||
enableGPT4: !process.env.DISABLE_GPT4,
|
||||
disableGPT4,
|
||||
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
};
|
||||
};
|
||||
|
253
app/constant.ts
@@ -3,20 +3,33 @@ 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_API_HOST = "https://api.nextchat.dev";
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
|
||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
Settings = "/settings",
|
||||
NewChat = "/new-chat",
|
||||
Masks = "/masks",
|
||||
Auth = "/auth",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
OpenAI = "/api/openai",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
AppBody = "app-body",
|
||||
CustomModel = "custom-model",
|
||||
}
|
||||
|
||||
export enum FileName {
|
||||
@@ -31,14 +44,252 @@ export enum StoreKey {
|
||||
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 = "ak-";
|
||||
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",
|
||||
Google = "Google",
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
}
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
ListModelPath: "v1/models",
|
||||
};
|
||||
|
||||
export const Azure = {
|
||||
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
||||
};
|
||||
|
||||
export const Google = {
|
||||
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
||||
ChatPath: "v1beta/models/gemini-pro:generateContent",
|
||||
VisionChatPath: "v1beta/models/gemini-pro-vision:generateContent",
|
||||
|
||||
// /api/openai/v1/chat/completions
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
Knowledge cutoff: {{cutoff}}
|
||||
Current model: {{model}}
|
||||
Current time: {{time}}
|
||||
Latex inline: $x^2$
|
||||
Latex block: $$e=mc^2$$
|
||||
`;
|
||||
|
||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
||||
|
||||
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
default: "2021-09",
|
||||
"gpt-4-turbo-preview": "2023-12",
|
||||
"gpt-4-1106-preview": "2023-04",
|
||||
"gpt-4-0125-preview": "2023-12",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
// After improvements,
|
||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||
"gemini-pro": "2023-12",
|
||||
};
|
||||
|
||||
export const DEFAULT_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0314",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-turbo-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-1106-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0125-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-vision-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0125",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-1106",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gemini-pro",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "google",
|
||||
providerName: "Google",
|
||||
providerType: "google",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gemini-pro-vision",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "google",
|
||||
providerName: "Google",
|
||||
providerType: "google",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
export const MAX_RENDER_MSG_COUNT = 45;
|
||||
|
19
app/global.d.ts
vendored
@@ -9,3 +9,22 @@ 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>;
|
||||
writeTextFile(path: string, data: string): 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 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="分组 1" style="stroke:#333333; stroke-width:1; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67 " /></g></g></svg>
|
||||
<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;stroke-opacity:1;stroke-dasharray:0 0" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)"/><path id="路径 6" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,28 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30"
|
||||
height="30" viewBox="0 0 30 30" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="29.999999999999996" height="29.999999999999996" />
|
||||
<rect id="path_1" x="0" y="0" width="20.45454545454545" height="20.45454545454545" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
|
||||
<rect fill="#E7F8FF" opacity="1"
|
||||
transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)" x="0" y="0"
|
||||
width="29.999999999999996" height="29.999999999999996" rx="10" />
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<g opacity="1"
|
||||
transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
|
||||
<mask id="bg-mask-1" fill="white">
|
||||
<use xlink:href="#path_1"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-1)">
|
||||
<path id="分组 1" fill-rule="evenodd" style="fill:#333333"
|
||||
transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)" opacity="1"
|
||||
d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z " />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<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>
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
app/icons/bot.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
@@ -1,28 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30"
|
||||
height="30" viewBox="0 0 30 30" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="29.999999999999996" height="29.999999999999996" />
|
||||
<rect id="path_1" x="0" y="0" width="20.45454545454545" height="20.45454545454545" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
|
||||
<rect fill="#E7F8FF" opacity="1"
|
||||
transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)" x="0" y="0"
|
||||
width="29.999999999999996" height="29.999999999999996" rx="10" />
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<g opacity="1"
|
||||
transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
|
||||
<mask id="bg-mask-1" fill="white">
|
||||
<use xlink:href="#path_1"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-1)">
|
||||
<path id="分组 1" fill-rule="evenodd" style="fill:#1F948C"
|
||||
transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)" opacity="1"
|
||||
d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z " />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<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:#1f948c" 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>
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.9 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 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>
|
||||
<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: 736 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 +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>
|
||||
<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"><g opacity="1" transform="translate(0 0) rotate(0) translate(1.0001220703125 2) rotate(0)"><path id="路径 1" style="fill:#333;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 id="路径 2" style="fill:#333;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"/></g><g opacity="1" transform="translate(0 0) rotate(0) translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs></svg>
|
Before Width: | Height: | Size: 4.5 KiB 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" 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="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 id="路径 2" style="fill:#333;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"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs></svg>
|
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -1,27 +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="0.8" 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:#A6A6A6; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(1.3333533333333334 1.3333333333333333) rotate(0 6.666673333333334 6.666666666666666)"
|
||||
d="M6.67,0C2.98,0 0,2.98 0,6.67C0,8.36 0,13.33 0,13.33C0,13.33 4.68,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67C13.33,2.98 10.35,0 6.67,0Z " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#A6A6A6; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(4.666666666666666 6) rotate(0 3 0)" d="M0,0L6,0 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#A6A6A6; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(4.666666666666666 8.666666666666666) rotate(0 3 0)" d="M0,0L6,0 " />
|
||||
<path id="路径 4"
|
||||
style="stroke:#A6A6A6; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(4.666666666666666 11.333333333333332) rotate(0 1.6666666666666665 0)"
|
||||
d="M0,0L3.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=".8" 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:#a6a6a6;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M6.67,0C2.98,0 0,2.98 0,6.67C0,8.36 0,13.33 0,13.33C0,13.33 4.68,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67C13.33,2.98 10.35,0 6.67,0Z" transform="translate(1.3333533333333334 1.3333333333333333) rotate(0 6.666673333333334 6.666666666666666)"/><path id="路径 2" style="stroke:#a6a6a6;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L6,0" transform="translate(4.666666666666666 6) rotate(0 3 0)"/><path id="路径 3" style="stroke:#a6a6a6;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L6,0" transform="translate(4.666666666666666 8.666666666666666) rotate(0 3 0)"/><path id="路径 4" style="stroke:#a6a6a6;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L3.33,0" transform="translate(4.666666666666666 11.333333333333332) rotate(0 1.6666666666666665 0)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
app/icons/chatgpt.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
@@ -1,16 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="43"
|
||||
height="44" viewBox="0 0 43 44" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="43" height="43.580135196270106" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0.000001981943071882597) rotate(0 21.5 21.790067598135053)">
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path id="分组 1" fill-rule="evenodd" style="fill:#8BCAE0"
|
||||
transform="translate(0 0) rotate(0 21.5 21.790067598135053)" opacity="0.27"
|
||||
d="M40.17 17.84L40.17 17.84C40.53 16.73 40.72 15.57 40.72 14.41C40.72 12.48 40.21 10.58 39.23 8.92C37.27 5.51 33.64 3.41 29.71 3.41C28.94 3.41 28.16 3.49 27.41 3.65C25.35 1.33 22.39 0 19.29 0L19.22 0L19.19 0C14.43 0 10.21 3.07 8.74 7.6C5.68 8.23 3.03 10.15 1.48 12.87C0.51 14.54 0 16.45 0 18.38C0 21.1 1.01 23.73 2.83 25.74C2.47 26.85 2.28 28.01 2.28 29.17C2.28 31.1 2.79 33 3.77 34.66C6.14 38.8 10.92 40.93 15.59 39.93C17.65 42.25 20.61 43.58 23.71 43.58L23.78 43.58L23.81 43.58C28.57 43.58 32.8 40.51 34.26 35.97C37.33 35.35 39.97 33.43 41.52 30.71C42.49 29.03 43 27.13 43 25.2C43 22.48 41.99 19.86 40.17 17.84Z M18.817 38.6948C18.727 38.7448 18.647 38.7948 18.557 38.8448C20.017 40.0648 21.867 40.7348 23.777 40.7348L23.787 40.7348C28.287 40.7248 31.937 37.0648 31.947 32.5648L31.947 22.4348C31.937 22.3848 31.907 22.3548 31.877 22.3348L28.207 20.2148L28.207 32.4548C28.207 32.9648 27.937 33.4348 27.487 33.6848L18.817 38.6948Z M17.3932 36.223L26.1632 31.163C26.2032 31.133 26.2232 31.093 26.2232 31.053L26.2132 31.053L26.2132 26.813L15.6232 32.933C15.1832 33.183 14.6432 33.183 14.2032 32.933L5.52317 27.923C5.44317 27.873 5.32317 27.803 5.26317 27.763C5.18317 28.223 5.14317 28.693 5.14317 29.163C5.14317 30.593 5.52317 31.993 6.23317 33.233L6.23317 33.233C7.70317 35.763 10.3932 37.313 13.3132 37.313C14.7432 37.313 16.1532 36.943 17.3932 36.223Z M8.20584 11.013C8.20584 10.923 8.20584 10.783 8.20584 10.713C6.41583 11.373 4.90584 12.643 3.95583 14.293L3.95583 14.293C3.24583 15.533 2.86583 16.943 2.86583 18.373C2.86583 21.293 4.41583 23.983 6.94584 25.443L15.7158 30.513C15.7558 30.533 15.8058 30.533 15.8358 30.503L19.5058 28.383L8.91584 22.273C8.47583 22.023 8.20584 21.553 8.20584 21.043L8.20584 21.033L8.20584 11.013Z M36.0546 18.1303L27.2846 13.0603C27.2446 13.0403 27.1946 13.0503 27.1646 13.0703L23.4946 15.1903L34.0846 21.3103C34.5246 21.5603 34.7946 22.0203 34.7946 22.5303C34.7946 22.5303 34.7946 22.5403 34.7946 22.5403L34.7946 32.8603C38.0046 31.6803 40.1446 28.6203 40.1446 25.2003C40.1446 22.2803 38.5846 19.5903 36.0546 18.1303Z M16.8345 12.4124C16.8045 12.4424 16.7845 12.4824 16.7845 12.5224L16.7845 12.5224L16.7845 16.7624L27.3745 10.6424C27.5945 10.5224 27.8445 10.4524 28.0945 10.4524C28.3445 10.4524 28.5845 10.5224 28.8045 10.6424L37.4845 15.6624C37.5645 15.7124 37.6545 15.7624 37.7345 15.8124L37.7345 15.8124C37.8145 15.3524 37.8545 14.8924 37.8545 14.4324C37.8545 9.92236 34.1945 6.26236 29.6845 6.26236C28.2545 6.26236 26.8545 6.64236 25.6045 7.35236L16.8345 12.4124Z M19.2209 2.84925C14.7109 2.84925 11.0509 6.49925 11.0509 11.0093L11.0509 21.1393C11.0609 21.1893 11.0809 21.2193 11.1209 21.2393L14.7909 23.3593L14.8009 11.1293L14.8009 11.1193C14.8009 10.6193 15.0709 10.1493 15.5109 9.89925L24.1909 4.88925C24.2609 4.83925 24.3809 4.77925 24.4409 4.73925C22.9809 3.51925 21.1309 2.84925 19.2209 2.84925Z M16.783 24.5101L21.503 27.2401L26.223 24.5101L26.223 19.0601L21.503 16.3401L16.783 19.0701L16.783 24.5101Z " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="43" height="44" fill="none" viewBox="0 0 43 44"><defs><rect id="path_0" width="43" height="43.58" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0.000001981943071882597) rotate(0 21.5 21.790067598135053)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="分组 1" fill-rule="evenodd" style="fill:#8bcae0" d="M40.17 17.84L40.17 17.84C40.53 16.73 40.72 15.57 40.72 14.41C40.72 12.48 40.21 10.58 39.23 8.92C37.27 5.51 33.64 3.41 29.71 3.41C28.94 3.41 28.16 3.49 27.41 3.65C25.35 1.33 22.39 0 19.29 0L19.22 0L19.19 0C14.43 0 10.21 3.07 8.74 7.6C5.68 8.23 3.03 10.15 1.48 12.87C0.51 14.54 0 16.45 0 18.38C0 21.1 1.01 23.73 2.83 25.74C2.47 26.85 2.28 28.01 2.28 29.17C2.28 31.1 2.79 33 3.77 34.66C6.14 38.8 10.92 40.93 15.59 39.93C17.65 42.25 20.61 43.58 23.71 43.58L23.78 43.58L23.81 43.58C28.57 43.58 32.8 40.51 34.26 35.97C37.33 35.35 39.97 33.43 41.52 30.71C42.49 29.03 43 27.13 43 25.2C43 22.48 41.99 19.86 40.17 17.84Z M18.817 38.6948C18.727 38.7448 18.647 38.7948 18.557 38.8448C20.017 40.0648 21.867 40.7348 23.777 40.7348L23.787 40.7348C28.287 40.7248 31.937 37.0648 31.947 32.5648L31.947 22.4348C31.937 22.3848 31.907 22.3548 31.877 22.3348L28.207 20.2148L28.207 32.4548C28.207 32.9648 27.937 33.4348 27.487 33.6848L18.817 38.6948Z M17.3932 36.223L26.1632 31.163C26.2032 31.133 26.2232 31.093 26.2232 31.053L26.2132 31.053L26.2132 26.813L15.6232 32.933C15.1832 33.183 14.6432 33.183 14.2032 32.933L5.52317 27.923C5.44317 27.873 5.32317 27.803 5.26317 27.763C5.18317 28.223 5.14317 28.693 5.14317 29.163C5.14317 30.593 5.52317 31.993 6.23317 33.233L6.23317 33.233C7.70317 35.763 10.3932 37.313 13.3132 37.313C14.7432 37.313 16.1532 36.943 17.3932 36.223Z M8.20584 11.013C8.20584 10.923 8.20584 10.783 8.20584 10.713C6.41583 11.373 4.90584 12.643 3.95583 14.293L3.95583 14.293C3.24583 15.533 2.86583 16.943 2.86583 18.373C2.86583 21.293 4.41583 23.983 6.94584 25.443L15.7158 30.513C15.7558 30.533 15.8058 30.533 15.8358 30.503L19.5058 28.383L8.91584 22.273C8.47583 22.023 8.20584 21.553 8.20584 21.043L8.20584 21.033L8.20584 11.013Z M36.0546 18.1303L27.2846 13.0603C27.2446 13.0403 27.1946 13.0503 27.1646 13.0703L23.4946 15.1903L34.0846 21.3103C34.5246 21.5603 34.7946 22.0203 34.7946 22.5303C34.7946 22.5303 34.7946 22.5403 34.7946 22.5403L34.7946 32.8603C38.0046 31.6803 40.1446 28.6203 40.1446 25.2003C40.1446 22.2803 38.5846 19.5903 36.0546 18.1303Z M16.8345 12.4124C16.8045 12.4424 16.7845 12.4824 16.7845 12.5224L16.7845 12.5224L16.7845 16.7624L27.3745 10.6424C27.5945 10.5224 27.8445 10.4524 28.0945 10.4524C28.3445 10.4524 28.5845 10.5224 28.8045 10.6424L37.4845 15.6624C37.5645 15.7124 37.6545 15.7624 37.7345 15.8124L37.7345 15.8124C37.8145 15.3524 37.8545 14.8924 37.8545 14.4324C37.8545 9.92236 34.1945 6.26236 29.6845 6.26236C28.2545 6.26236 26.8545 6.64236 25.6045 7.35236L16.8345 12.4124Z M19.2209 2.84925C14.7109 2.84925 11.0509 6.49925 11.0509 11.0093L11.0509 21.1393C11.0609 21.1893 11.0809 21.2193 11.1209 21.2393L14.7909 23.3593L14.8009 11.1293L14.8009 11.1193C14.8009 10.6193 15.0709 10.1493 15.5109 9.89925L24.1909 4.88925C24.2609 4.83925 24.3809 4.77925 24.4409 4.73925C22.9809 3.51925 21.1309 2.84925 19.2209 2.84925Z M16.783 24.5101L21.503 27.2401L26.223 24.5101L26.223 19.0601L21.503 16.3401L16.783 19.0701L16.783 24.5101Z" opacity=".27" transform="translate(0 0) rotate(0 21.5 21.790067598135053)"/></g></g></svg>
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.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(2.6666666666666665 5) rotate(0 5.333333333333333 4.833333333333333)" d="M1,9.67L9.67,9.67L10.67,0L0,0L1,9.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.667333333333333 8.334133333333334) rotate(0 0 1.6666999999999998)" d="M0,0L0,3.33 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.334133333333334 8.333166666666667) rotate(0 0 1.666283333333333)" d="M0,0L0,3.33 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 1) rotate(0 4 2)" d="M0,4L5.44,0L8,4 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" 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,9.67L9.67,9.67L10.67,0L0,0L1,9.67Z" transform="translate(2.6666666666666665 5) rotate(0 5.333333333333333 4.833333333333333)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,3.33" transform="translate(6.667333333333333 8.334133333333334) rotate(0 0 1.6666999999999998)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,3.33" transform="translate(9.334133333333334 8.333166666666667) rotate(0 0 1.666283333333333)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,4L5.44,0L8,4" transform="translate(4 1) rotate(0 4 2)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,21 +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 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"
|
||||
d="M0,0L10.67,10.67 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2.6666666666666665 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"
|
||||
d="M0,10.67L10.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,10.67" transform="translate(2.6666666666666665 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,10.67L10.67,0" transform="translate(2.6666666666666665 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"/></g></g></svg>
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 846 B |
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" fill="none" viewBox="0 0 16 16"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="#fff"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="fill:#333;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 id="路径 2" style="fill:#333;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"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></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" 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="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"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></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.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>
|
||||
<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,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" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)"/></g></g></svg>
|
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 832 B |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 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(-90 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,0 " /></g></g></svg>
|
||||
<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>
|
Before Width: | Height: | Size: 575 B 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" fill="none" viewBox="0 0 16 16"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="#fff"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" fill-rule="evenodd" style="fill:#333" 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" opacity="1"/><path id="路径 2" fill-rule="evenodd" style="fill:#333" 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" opacity="1"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs></svg>
|
After Width: | Height: | Size: 1.3 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(10.5 11) rotate(0 1.4166666666666665 1.8333333333333333)" d="M2.83,0L2.83,3C2.83,3.37 2.53,3.67 2.17,3.67L0,3.67 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2.6666666666666665 1.3333333333333333) rotate(0 5.333333333333333 6.666666666666666)" d="M10.67,4L10.67,0.67C10.67,0.3 10.37,0 10,0L0.67,0C0.3,0 0,0.3 0,0.67L0,12.67C0,13.03 0.3,13.33 0.67,13.33L2.67,13.33 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 5.333333333333333) rotate(0 2.333333333333333 0)" d="M0,0L4.67,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7.666666666666666 7.666666666666666) rotate(0 2.833333333333333 3.5)" d="M0,7L5.67,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 8) rotate(0 1.3333333333333333 0)" d="M0,0L2.67,0 " /></g></g></svg>
|
||||
<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="M2.83,0L2.83,3C2.83,3.37 2.53,3.67 2.17,3.67L0,3.67" transform="translate(10.5 11) rotate(0 1.4166666666666665 1.8333333333333333)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M10.67,4L10.67,0.67C10.67,0.3 10.37,0 10,0L0.67,0C0.3,0 0,0.3 0,0.67L0,12.67C0,13.03 0.3,13.33 0.67,13.33L2.67,13.33" transform="translate(2.6666666666666665 1.3333333333333333) rotate(0 5.333333333333333 6.666666666666666)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L4.67,0" transform="translate(5.333333333333333 5.333333333333333) rotate(0 2.333333333333333 0)"/><path id="路径 4" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,7L5.67,0" transform="translate(7.666666666666666 7.666666666666666) rotate(0 2.833333333333333 3.5)"/><path id="路径 5" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L2.67,0" transform="translate(5.333333333333333 8) rotate(0 1.3333333333333333 0)"/></g></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 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,4 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7071 5.70711C20.0976 5.31658 20.0976 4.68342 19.7071 4.29289C19.3166 3.90237 18.6834 3.90237 18.2929 4.29289L14.032 8.55382C13.4365 8.20193 12.7418 8 12 8C9.79086 8 8 9.79086 8 12C8 12.7418 8.20193 13.4365 8.55382 14.032L4.29289 18.2929C3.90237 18.6834 3.90237 19.3166 4.29289 19.7071C4.68342 20.0976 5.31658 20.0976 5.70711 19.7071L9.96803 15.4462C10.5635 15.7981 11.2582 16 12 16C14.2091 16 16 14.2091 16 12C16 11.2582 15.7981 10.5635 15.4462 9.96803L19.7071 5.70711ZM12.518 10.0677C12.3528 10.0236 12.1792 10 12 10C10.8954 10 10 10.8954 10 12C10 12.1792 10.0236 12.3528 10.0677 12.518L12.518 10.0677ZM11.482 13.9323L13.9323 11.482C13.9764 11.6472 14 11.8208 14 12C14 13.1046 13.1046 14 12 14C11.8208 14 11.6472 13.9764 11.482 13.9323ZM15.7651 4.8207C14.6287 4.32049 13.3675 4 12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C1.92276 13.7326 2.86706 15.0637 4.21194 16.3739L5.62626 14.9596C4.4555 13.8229 3.61144 12.6531 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C12.7719 6 13.5135 6.13385 14.2193 6.36658L15.7651 4.8207ZM12 18C11.2282 18 10.4866 17.8661 9.78083 17.6334L8.23496 19.1793C9.37136 19.6795 10.6326 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C22.0773 10.2674 21.133 8.93627 19.7881 7.62611L18.3738 9.04043C19.5446 10.1771 20.3887 11.3469 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18Z" fill="#000000"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M19.7071 5.70711C20.0976 5.31658 20.0976 4.68342 19.7071 4.29289C19.3166 3.90237 18.6834 3.90237 18.2929 4.29289L14.032 8.55382C13.4365 8.20193 12.7418 8 12 8C9.79086 8 8 9.79086 8 12C8 12.7418 8.20193 13.4365 8.55382 14.032L4.29289 18.2929C3.90237 18.6834 3.90237 19.3166 4.29289 19.7071C4.68342 20.0976 5.31658 20.0976 5.70711 19.7071L9.96803 15.4462C10.5635 15.7981 11.2582 16 12 16C14.2091 16 16 14.2091 16 12C16 11.2582 15.7981 10.5635 15.4462 9.96803L19.7071 5.70711ZM12.518 10.0677C12.3528 10.0236 12.1792 10 12 10C10.8954 10 10 10.8954 10 12C10 12.1792 10.0236 12.3528 10.0677 12.518L12.518 10.0677ZM11.482 13.9323L13.9323 11.482C13.9764 11.6472 14 11.8208 14 12C14 13.1046 13.1046 14 12 14C11.8208 14 11.6472 13.9764 11.482 13.9323ZM15.7651 4.8207C14.6287 4.32049 13.3675 4 12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C1.92276 13.7326 2.86706 15.0637 4.21194 16.3739L5.62626 14.9596C4.4555 13.8229 3.61144 12.6531 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C12.7719 6 13.5135 6.13385 14.2193 6.36658L15.7651 4.8207ZM12 18C11.2282 18 10.4866 17.8661 9.78083 17.6334L8.23496 19.1793C9.37136 19.6795 10.6326 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C22.0773 10.2674 21.133 8.93627 19.7881 7.62611L18.3738 9.04043C19.5446 10.1771 20.3887 11.3469 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18Z" clip-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,4 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.30147 15.5771C4.77832 14.2684 3.6904 12.7726 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C14.1843 6 16.1261 7.07185 17.6986 8.42294C19.2218 9.73158 20.3097 11.2274 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18C9.81574 18 7.87402 16.9282 6.30147 15.5771ZM12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C2.00757 13.8624 3.23268 15.5772 4.99812 17.0941C6.75717 18.6054 9.14754 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C21.9925 10.1376 20.7674 8.42276 19.002 6.90595C17.2429 5.39462 14.8525 4 12 4ZM10 12C10 10.8954 10.8955 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8955 14 10 13.1046 10 12ZM12 8C9.7909 8 8.00004 9.79086 8.00004 12C8.00004 14.2091 9.7909 16 12 16C14.2092 16 16 14.2091 16 12C16 9.79086 14.2092 8 12 8Z" fill="#000000"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6.30147 15.5771C4.77832 14.2684 3.6904 12.7726 3.18002 12C3.6904 11.2274 4.77832 9.73158 6.30147 8.42294C7.87402 7.07185 9.81574 6 12 6C14.1843 6 16.1261 7.07185 17.6986 8.42294C19.2218 9.73158 20.3097 11.2274 20.8201 12C20.3097 12.7726 19.2218 14.2684 17.6986 15.5771C16.1261 16.9282 14.1843 18 12 18C9.81574 18 7.87402 16.9282 6.30147 15.5771ZM12 4C9.14754 4 6.75717 5.39462 4.99812 6.90595C3.23268 8.42276 2.00757 10.1376 1.46387 10.9698C1.05306 11.5985 1.05306 12.4015 1.46387 13.0302C2.00757 13.8624 3.23268 15.5772 4.99812 17.0941C6.75717 18.6054 9.14754 20 12 20C14.8525 20 17.2429 18.6054 19.002 17.0941C20.7674 15.5772 21.9925 13.8624 22.5362 13.0302C22.947 12.4015 22.947 11.5985 22.5362 10.9698C21.9925 10.1376 20.7674 8.42276 19.002 6.90595C17.2429 5.39462 14.8525 4 12 4ZM10 12C10 10.8954 10.8955 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8955 14 10 13.1046 10 12ZM12 8C9.7909 8 8.00004 9.79086 8.00004 12C8.00004 14.2091 9.7909 16 12 16C14.2092 16 16 14.2091 16 12C16 9.79086 14.2092 8 12 8Z" clip-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.3 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/image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" height="16" width="16" version="1.1" xml:space="preserve" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/><g class="currentLayer" style=""><title>Layer 1</title><g id="svg_1" class="" fill="#333" fill-opacity="1"><polygon points="2.4690866470336914,2.4690725803375244 4.447190761566162,2.4690725803375244 4.447190761566162,1.6882386207580566 1.6882381439208984,1.6882386207580566 1.6882381439208984,4.44719123840332 2.4690866470336914,4.44719123840332 " id="svg_2" fill="#333" fill-opacity="1"/><polygon points="11.552804470062256,1.6882386207580566 11.552804470062256,2.4690725803375244 13.530910968780518,2.4690725803375244 13.530910968780518,4.44719123840332 14.311760425567627,4.44719123840332 14.311760425567627,1.6882386207580566 " id="svg_3" fill="#333" fill-opacity="1"/><polygon points="13.530910968780518,13.530919075012207 11.552804470062256,13.530919075012207 11.552804470062256,14.311760902404785 14.311760425567627,14.311760902404785 14.311760425567627,11.552801132202148 13.530910968780518,11.552801132202148 " id="svg_4" fill="#333" fill-opacity="1"/><polygon points="2.4690866470336914,11.552801132202148 1.6882381439208984,11.552801132202148 1.6882381439208984,14.311760902404785 4.447190761566162,14.311760902404785 4.447190761566162,13.530919075012207 2.4690866470336914,13.530919075012207 " id="svg_5" fill="#333" fill-opacity="1"/><path d="M8.830417847409231,6.243117030680995 c0.68169614081525,0 1.2363241834494423,-0.5546280426341942 1.2363241834494423,-1.2363241834494423 S9.51214001610201,3.770468663782117 8.830417847409231,3.770468663782117 s-1.2363241834494423,0.5546280426341942 -1.2363241834494423,1.2363241834494423 S8.14872170659398,6.243117030680995 8.830417847409231,6.243117030680995 z" id="svg_6" fill="#333" fill-opacity="1"/><polygon points="3.7704806327819824,12.229532241821289 12.229516506195068,12.229532241821289 12.229516506195068,9.709510803222656 10.70320463180542,8.099010467529297 8.852166652679443,9.175727844238281 6.275332450866699,7.334256172180176 3.7704806327819824,9.977211952209473 " id="svg_7" fill="#333" fill-opacity="1"/></g></g></svg>
|
After Width: | Height: | Size: 2.2 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(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,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="M4,8L0,4L4,0" transform="translate(6.333333333333333 4) rotate(0 2 4)"/></g></g></svg>
|
Before Width: | Height: | Size: 573 B 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(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>
|
||||
<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.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" transform="translate(3.6666666666666665 3.6666666666666665) rotate(0 4.333333333333333 4.333333333333333)"/><path id="路径 2" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(7.166666666666666 0.3333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 3" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(12 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 4" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(14 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 5" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(12 12) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 6" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(7.166666666666666 14) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 7" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(2.333333333333333 12) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 8" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(0.3333333333333333 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)"/><path id="路径 9" fill-rule="evenodd" style="fill:#333" 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" opacity="1" transform="translate(2.333333333333333 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)"/></g></g></svg>
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 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"><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>
|
||||
<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="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"/></g></g><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs></svg>
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
1
app/icons/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#fff" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none" style="" class="" /><g class="currentLayer" style=""><title>Layer 1</title><circle cx="4" cy="8" r="1.926" fill="#333" id="svg_1" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="2" repeatCount="indefinite" to="2" values="2;1.2;2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1" /></circle><circle cx="8" cy="8" r="1.2736" fill="#333" fill-opacity=".3" id="svg_2" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="1.2" repeatCount="indefinite" to="1.2" values="1.2;2;1.2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from=".5" repeatCount="indefinite" to=".5" values=".5;1;.5" /></circle><circle cx="12" cy="8" r="1.926" fill="#333" id="svg_3" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="2" repeatCount="indefinite" to="2" values="2;1.2;2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1" /></circle></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |