Compare commits
662 Commits
feat/voice
...
v2.15.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df62736ff6 | ||
|
|
6a464b3e5f | ||
|
|
57fcda80df | ||
|
|
db39fbc419 | ||
|
|
3dabe47c78 | ||
|
|
affc194cde | ||
|
|
03fa580a55 | ||
|
|
d0dce654bf | ||
|
|
169323e238 | ||
|
|
71df415b14 | ||
|
|
6bb01bc564 | ||
|
|
07c6fe5975 | ||
|
|
5964181cb2 | ||
|
|
4b8288a2b2 | ||
|
|
88b1c1c6a5 | ||
|
|
1234deabfa | ||
|
|
45306bbb6c | ||
|
|
18e2403b01 | ||
|
|
e578c5f3ad | ||
|
|
61245e3d7e | ||
|
|
7804182d0d | ||
|
|
f2195154f6 | ||
|
|
35f77f45a2 | ||
|
|
992c3a5d3a | ||
|
|
d51d7b6797 | ||
|
|
23ac2efd89 | ||
|
|
daeffb2dc6 | ||
|
|
db58ca6c1d | ||
|
|
2ff292cbfa | ||
|
|
5a81393863 | ||
|
|
116a73d398 | ||
|
|
cf0c057164 | ||
|
|
fe5a4f4447 | ||
|
|
c1b74201e4 | ||
|
|
27828d9ca8 | ||
|
|
2bd799fac6 | ||
|
|
9275f2d753 | ||
|
|
7455978ee5 | ||
|
|
7c0acc7b77 | ||
|
|
f32dd69acf | ||
|
|
80b8f956a9 | ||
|
|
caf50b6e6c | ||
|
|
b590d0857c | ||
|
|
982019307c | ||
|
|
09aec7b22e | ||
|
|
f9a047aad4 | ||
|
|
85704570f3 | ||
|
|
53dcae9e9c | ||
|
|
04e1ab63bb | ||
|
|
ed9aae531e | ||
|
|
6ab6b3dbca | ||
|
|
7180ed9a60 | ||
|
|
0a5522d28c | ||
|
|
c7bc93b32b | ||
|
|
d30351e7b0 | ||
|
|
886ffc0af8 | ||
|
|
4fdd997108 | ||
|
|
236736deea | ||
|
|
2b317f60c8 | ||
|
|
3ec67f9f47 | ||
|
|
6435e7a30e | ||
|
|
078305f5ac | ||
|
|
801b62543a | ||
|
|
877668b629 | ||
|
|
f652f73260 | ||
|
|
b2965e1deb | ||
|
|
2214689920 | ||
|
|
9326ff9d08 | ||
|
|
271f58d9cf | ||
|
|
cac99e3908 | ||
|
|
97a4a910e0 | ||
|
|
19c7a84548 | ||
|
|
571ce11e53 | ||
|
|
d2cb984ced | ||
|
|
7fc0d11931 | ||
|
|
341a52a615 | ||
|
|
d58b99d602 | ||
|
|
f7a5f836db | ||
|
|
d212df8b95 | ||
|
|
f3f6dc57c3 | ||
|
|
29b5cd9436 | ||
|
|
f5209fc344 | ||
|
|
b6bb1673d4 | ||
|
|
7b6fe66f2a | ||
|
|
c2fc0b4979 | ||
|
|
0b758941a4 | ||
|
|
492b55c893 | ||
|
|
4060e367ad | ||
|
|
c99cd31b6b | ||
|
|
718782f5b1 | ||
|
|
0c3fb5b2ce | ||
|
|
4ec6b067e7 | ||
|
|
1748dd6a3b | ||
|
|
95332e50ed | ||
|
|
8496980cf9 | ||
|
|
ffe32694b0 | ||
|
|
3d5b21154b | ||
|
|
4b9697e336 | ||
|
|
b0e9a542ba | ||
|
|
8b67536c23 | ||
|
|
cd49c12181 | ||
|
|
a6b14c7910 | ||
|
|
e275abdb9c | ||
|
|
09a90665d5 | ||
|
|
6649fbdfd0 | ||
|
|
64a0ffee7b | ||
|
|
b529118f31 | ||
|
|
39d7d9f13a | ||
|
|
fcd55df969 | ||
|
|
1e59948358 | ||
|
|
1102ef6e6b | ||
|
|
7ce2e8f4c4 | ||
|
|
fd1c656bdd | ||
|
|
82298a760a | ||
|
|
b84bb72e07 | ||
|
|
495b321d0a | ||
|
|
8a38cdc1d7 | ||
|
|
e210db7bd9 | ||
|
|
f64763b74b | ||
|
|
e033bef544 | ||
|
|
d88cc575e5 | ||
|
|
e3f499be0c | ||
|
|
87325fad74 | ||
|
|
86220573b6 | ||
|
|
65ed6b02a4 | ||
|
|
98093a1f31 | ||
|
|
00990dc195 | ||
|
|
122aa94c9f | ||
|
|
3da5284a07 | ||
|
|
cd920364f8 | ||
|
|
e2e8a45104 | ||
|
|
fb5fc13f72 | ||
|
|
edb92f7bfb | ||
|
|
1980f43b9f | ||
|
|
c3c3dd5154 | ||
|
|
356155fd30 | ||
|
|
6f75ef8f0a | ||
|
|
ca865a80dc | ||
|
|
8f759d1c3e | ||
|
|
44787637f2 | ||
|
|
cf1c8e8f2a | ||
|
|
d948be2372 | ||
|
|
cc28aef625 | ||
|
|
036358de7c | ||
|
|
0958b9ee12 | ||
|
|
aff1d7ecd6 | ||
|
|
42fdbd9bb8 | ||
|
|
034c82e514 | ||
|
|
14ff46b5cd | ||
|
|
c9099ca0a5 | ||
|
|
58b144b345 | ||
|
|
af21c57e77 | ||
|
|
624e4dbaaf | ||
|
|
9bbb4f396b | ||
|
|
b2c1644d69 | ||
|
|
5629f842da | ||
|
|
f900283b09 | ||
|
|
b667eff6bd | ||
|
|
54fdf40f5a | ||
|
|
690542145d | ||
|
|
94c4cf0624 | ||
|
|
3da717d9fc | ||
|
|
0902efc719 | ||
|
|
d7e2ee63d8 | ||
|
|
7deb36ee1f | ||
|
|
bfe4e88246 | ||
|
|
9ab45c3969 | ||
|
|
fec80c6c51 | ||
|
|
a6b7432358 | ||
|
|
3486954e07 | ||
|
|
150fc84b9b | ||
|
|
b023a00445 | ||
|
|
d0e296adf8 | ||
|
|
aa40015e9b | ||
|
|
141ce2c99a | ||
|
|
4a95dcb6e9 | ||
|
|
1610675c8f | ||
|
|
724c814bfe | ||
|
|
764c0cb865 | ||
|
|
8a4b8a84d6 | ||
|
|
8ec6acc55a | ||
|
|
b6a022b0ef | ||
|
|
716899c030 | ||
|
|
d9e407fd2b | ||
|
|
deb140de73 | ||
|
|
3c1e5e7978 | ||
|
|
4a8e85c28a | ||
|
|
8498cadae8 | ||
|
|
8c83fe23a1 | ||
|
|
a8c65e3d27 | ||
|
|
324d30bef9 | ||
|
|
46cb48023e | ||
|
|
1c24ca58c7 | ||
|
|
9193a9a0e0 | ||
|
|
957244ba2e | ||
|
|
ac599aa47c | ||
|
|
67a90ffb76 | ||
|
|
feaa6f9bf0 | ||
|
|
753bf3b924 | ||
|
|
b3219f57c8 | ||
|
|
a17df037af | ||
|
|
dfc36e5210 | ||
|
|
c359b92ddc | ||
|
|
e1d6131f13 | ||
|
|
6a0bda00f5 | ||
|
|
f85ec95877 | ||
|
|
a024980c03 | ||
|
|
fd9e94e078 | ||
|
|
f6a6c51d15 | ||
|
|
966db1e4be | ||
|
|
b8bbc37b8e | ||
|
|
40cbabc330 | ||
|
|
04a4e1b39a | ||
|
|
99f3160aa2 | ||
|
|
8cb72d8452 | ||
|
|
c9eb9f3eda | ||
|
|
64c3dcd732 | ||
|
|
d49ececcc5 | ||
|
|
90e1fadb1e | ||
|
|
071391ddff | ||
|
|
d70d46b4d5 | ||
|
|
3ef596b215 | ||
|
|
35c5518668 | ||
|
|
8b513537b7 | ||
|
|
b27f394995 | ||
|
|
3f9f556e1c | ||
|
|
1772d5c4b6 | ||
|
|
715d1dc02f | ||
|
|
6737f016f5 | ||
|
|
f2d2622172 | ||
|
|
72d6f97024 | ||
|
|
a0f0b4ff9e | ||
|
|
c27ef6ffbf | ||
|
|
f5499ff699 | ||
|
|
c4334d4e5f | ||
|
|
51e8f0440d | ||
|
|
5ec0311f84 | ||
|
|
556d563ba0 | ||
|
|
6a083b24c4 | ||
|
|
825929fdc8 | ||
|
|
941a03ed6c | ||
|
|
cf63619182 | ||
|
|
5c04d3c5ea | ||
|
|
46a47db2d8 | ||
|
|
21ef9a4567 | ||
|
|
6f0846b2af | ||
|
|
ecd78b3bdd | ||
|
|
d8afd1af88 | ||
|
|
7c1bc1f1a1 | ||
|
|
763fc89b29 | ||
|
|
47b33f2b17 | ||
|
|
9f0e16b045 | ||
|
|
2efedb1736 | ||
|
|
044116c14c | ||
|
|
b4bf11d648 | ||
|
|
6cc0a5a1a4 | ||
|
|
8f14de5108 | ||
|
|
8f6e5d73a2 | ||
|
|
ab9f5382b2 | ||
|
|
fd441d9303 | ||
|
|
e31bec3aff | ||
|
|
2a1c05a028 | ||
|
|
421bf33c0e | ||
|
|
9c7bacc65e | ||
|
|
3935c725c9 | ||
|
|
908ee0060f | ||
|
|
82e6fd7bb5 | ||
|
|
6b98b14179 | ||
|
|
1ecefd88f7 | ||
|
|
2e9e20ce7c | ||
|
|
fb60fbb217 | ||
|
|
4199e17da0 | ||
|
|
86b8bfcb1f | ||
|
|
dfd089132d | ||
|
|
3a10f58b28 | ||
|
|
9d55adbaf2 | ||
|
|
00be2be24f | ||
|
|
5b126c7e52 | ||
|
|
1943f3b53f | ||
|
|
4a0bef9afb | ||
|
|
dfd2a53129 | ||
|
|
aa4e855012 | ||
|
|
d6089e6309 | ||
|
|
038e6df8f0 | ||
|
|
2fd68bcac3 | ||
|
|
e468fecf12 | ||
|
|
fc31d8e5d1 | ||
|
|
115f357a07 | ||
|
|
ac04a1cac8 | ||
|
|
87a286ef07 | ||
|
|
622d8a4edb | ||
|
|
b44086f0dc | ||
|
|
0236e13187 | ||
|
|
a3d4a7253f | ||
|
|
e079f1b31a | ||
|
|
9a78a72eb3 | ||
|
|
862c2e8810 | ||
|
|
12cad4c418 | ||
|
|
89b9d3a7f7 | ||
|
|
57831d4880 | ||
|
|
052004d70e | ||
|
|
a765237441 | ||
|
|
ac470a6d07 | ||
|
|
7237d33be3 | ||
|
|
1610b480af | ||
|
|
287fa0a39c | ||
|
|
afa1a4303b | ||
|
|
28cedb1493 | ||
|
|
a280e25ee7 | ||
|
|
8464ca8931 | ||
|
|
44340f277d | ||
|
|
74bf99f9ea | ||
|
|
9caf820758 | ||
|
|
26c2598f56 | ||
|
|
6d9abf261c | ||
|
|
b16d0185dd | ||
|
|
ca51c2e93d | ||
|
|
da254975cd | ||
|
|
86bae6be3a | ||
|
|
fc8c7ef18d | ||
|
|
f654629c6a | ||
|
|
d4a87c561a | ||
|
|
d8872d48b3 | ||
|
|
ed16c2c18d | ||
|
|
0478a6ce3b | ||
|
|
68b60e82ba | ||
|
|
8edc0989e2 | ||
|
|
fbf3551bbe | ||
|
|
238d3122c4 | ||
|
|
ee22fba448 | ||
|
|
f8a2a28bff | ||
|
|
b3cfaf1420 | ||
|
|
2e9f701bb7 | ||
|
|
9aabc4ad6a | ||
|
|
5dc731bc77 | ||
|
|
32d05c9855 | ||
|
|
5a0d0c0b75 | ||
|
|
17d4a8fb26 | ||
|
|
348c1a7d5f | ||
|
|
49151dabf5 | ||
|
|
eb7c7cdcb6 | ||
|
|
ec95292209 | ||
|
|
4b84fb328c | ||
|
|
47d27c1f41 | ||
|
|
5267ad46da | ||
|
|
94bc880b7f | ||
|
|
bab3e0bc9b | ||
|
|
b3a324b6f5 | ||
|
|
a1117cd4ee | ||
|
|
6ece818d69 | ||
|
|
5df09d5e2a | ||
|
|
33450ce429 | ||
|
|
e2f0206d88 | ||
|
|
3767b2c7f9 | ||
|
|
1779f1f3da | ||
|
|
b9d1dca65d | ||
|
|
8e4d26163a | ||
|
|
53c1176cbf | ||
|
|
46d3e7884b | ||
|
|
a0290b0c1b | ||
|
|
b4ae706914 | ||
|
|
476bdac717 | ||
|
|
831627268d | ||
|
|
9b97dca601 | ||
|
|
4ea8c0802a | ||
|
|
9203870df5 | ||
|
|
e8088d6e38 | ||
|
|
59d9bcdd27 | ||
|
|
9d1b13ba73 | ||
|
|
dd1030139b | ||
|
|
30ca2117bb | ||
|
|
89024a8dc8 | ||
|
|
728c38396a | ||
|
|
d61cb98ac7 | ||
|
|
a7ceb61e27 | ||
|
|
74b915a790 | ||
|
|
01ea690421 | ||
|
|
17cc9284a0 | ||
|
|
498d0f0b8b | ||
|
|
89049e1a22 | ||
|
|
5e7254e8dc | ||
|
|
f8c2732fdc | ||
|
|
fec36eb298 | ||
|
|
2299a4156d | ||
|
|
32b82b9cb3 | ||
|
|
ba6039fc8b | ||
|
|
6885812d21 | ||
|
|
844025ec14 | ||
|
|
94bc91c554 | ||
|
|
044c16da4c | ||
|
|
cd4784c54a | ||
|
|
814aaa4a69 | ||
|
|
e3b3a4fefa | ||
|
|
3fcbb3010d | ||
|
|
7573a19dc9 | ||
|
|
3628d68d9a | ||
|
|
23872086fa | ||
|
|
bb349a03da | ||
|
|
82be426f78 | ||
|
|
9d2a633f5e | ||
|
|
1149d45589 | ||
|
|
9d7e19cebf | ||
|
|
b3023543d6 | ||
|
|
c229d2c3ce | ||
|
|
47ea383ddd | ||
|
|
f2a35f1114 | ||
|
|
147fc9a35a | ||
|
|
93a03f8fe4 | ||
|
|
230e3823a9 | ||
|
|
b14a0f24ae | ||
|
|
5295802720 | ||
|
|
fadd7f6eb4 | ||
|
|
011b76e4e7 | ||
|
|
f68cd2c5c0 | ||
|
|
6ac9789a1c | ||
|
|
2b0153807c | ||
|
|
34ab37f31e | ||
|
|
71af2628eb | ||
|
|
15f028abfb | ||
|
|
9bdd37bb63 | ||
|
|
d726c71141 | ||
|
|
1caa61f4c0 | ||
|
|
f3e3f08377 | ||
|
|
2ec8b7a804 | ||
|
|
9f7d137b05 | ||
|
|
7218f13783 | ||
|
|
fa31e7802c | ||
|
|
9b3b4494ba | ||
|
|
785d3748e1 | ||
|
|
5e0657ce55 | ||
|
|
700b06f9c5 | ||
|
|
b58bbf8eb4 | ||
|
|
2d1f522aaf | ||
|
|
0b2863dfab | ||
|
|
70907ead8a | ||
|
|
6dc4844c12 | ||
|
|
14bc1b6aac | ||
|
|
183ad2a34b | ||
|
|
d9758be3ae | ||
|
|
6b1b530443 | ||
|
|
1c20137b0e | ||
|
|
c4a6c933f8 | ||
|
|
31d9444264 | ||
|
|
8cb204e22e | ||
|
|
97aa72ec5b | ||
|
|
a68341eae6 | ||
|
|
aa08183439 | ||
|
|
7a5596b909 | ||
|
|
b9ffd50992 | ||
|
|
14f2a8f370 | ||
|
|
e7b16bfbc0 | ||
|
|
a16725ac17 | ||
|
|
2803a91673 | ||
|
|
cf2fce7666 | ||
|
|
1609abd166 | ||
|
|
88c74ae18d | ||
|
|
78e2b41e0c | ||
|
|
501f8b028b | ||
|
|
54401162bd | ||
|
|
7fde9327a2 | ||
|
|
bbbf59c74a | ||
|
|
c4ad66f745 | ||
|
|
69974d5651 | ||
|
|
ce3b6a04c2 | ||
|
|
37e2517dac | ||
|
|
d65ddead11 | ||
|
|
34034be0e3 | ||
|
|
d21481173e | ||
|
|
fa6ebadc7b | ||
|
|
a51fb24f36 | ||
|
|
c359b30763 | ||
|
|
95e3b156c0 | ||
|
|
b972a0d081 | ||
|
|
20749355da | ||
|
|
dad122199a | ||
|
|
9fb8fbcc65 | ||
|
|
78e7ea72dc | ||
|
|
4640060891 | ||
|
|
6efe4fb734 | ||
|
|
74986803db | ||
|
|
9b0a705055 | ||
|
|
163fc9e3a3 | ||
|
|
24bf7950d8 | ||
|
|
b6735bffe4 | ||
|
|
1d8fd480ca | ||
|
|
da2e2372aa | ||
|
|
f3b972e573 | ||
|
|
bf3bc3c7e9 | ||
|
|
38664487a0 | ||
|
|
de1111286c | ||
|
|
d89a12aa05 | ||
|
|
754acd7c26 | ||
|
|
c3e2f3b714 | ||
|
|
22ef3d3a46 | ||
|
|
7f3516f44f | ||
|
|
bfdb47a7ed | ||
|
|
f55f04ab4f | ||
|
|
01c9dbc1fd | ||
|
|
0aa807df19 | ||
|
|
48d44ece58 | ||
|
|
e58cb2b0db | ||
|
|
bffd9d9173 | ||
|
|
8688842984 | ||
|
|
cf29a8f2c8 | ||
|
|
1e00c89988 | ||
|
|
0eccb547b5 | ||
|
|
4789a7f6a9 | ||
|
|
0bf758afd4 | ||
|
|
6612550c06 | ||
|
|
d411159124 | ||
|
|
cf635a5e6f | ||
|
|
3a007e4f3d | ||
|
|
9faab960f6 | ||
|
|
5df8b1d183 | ||
|
|
ef5f910f19 | ||
|
|
fffbee80e8 | ||
|
|
6b30e167e1 | ||
|
|
8ec721259a | ||
|
|
9d7ce207b6 | ||
|
|
2d1f0c9f57 | ||
|
|
d3131d2f55 | ||
|
|
c10447df79 | ||
|
|
212ae76d76 | ||
|
|
cd48f7eff4 | ||
|
|
3513c6801e | ||
|
|
864529cbf6 | ||
|
|
58c0d3e12d | ||
|
|
a1493bfb4e | ||
|
|
b3e856df1d | ||
|
|
8ef2617eec | ||
|
|
1da7d81122 | ||
|
|
a103582346 | ||
|
|
7b61d05e88 | ||
|
|
6fc7c50f19 | ||
|
|
9d728ec3c5 | ||
|
|
9cd3358e4e | ||
|
|
4cd94370e8 | ||
|
|
52312dbd23 | ||
|
|
b2e8a1eaa2 | ||
|
|
506c17a093 | ||
|
|
69642fba52 | ||
|
|
7d647c981f | ||
|
|
9aec3b714e | ||
|
|
dd4648ed9a | ||
|
|
1cd0beb231 | ||
|
|
c96e4b7966 | ||
|
|
b7aab3c102 | ||
|
|
fcb1a657e3 | ||
|
|
9b2cb1e1c3 | ||
|
|
fb8b8d28da | ||
|
|
ad80153bbb | ||
|
|
9564b261d5 | ||
|
|
1e2a662fa6 | ||
|
|
51f7daaeaf | ||
|
|
f742a7ec4e | ||
|
|
e2c0d2a07b | ||
|
|
d112dc41b2 | ||
|
|
2322851ac4 | ||
|
|
aa084ea09a | ||
|
|
6520f9b7eb | ||
|
|
fd8d0a1746 | ||
|
|
af3ebacee6 | ||
|
|
55d7014301 | ||
|
|
b72d7fbeda | ||
|
|
ee15c14049 | ||
|
|
1756bdd033 | ||
|
|
0cffaf8dc5 | ||
|
|
55a93e7b47 | ||
|
|
5dc5bfb797 | ||
|
|
f101ee3c4f | ||
|
|
6319f41b2c | ||
|
|
6c718ada1b | ||
|
|
67acc38a1f | ||
|
|
dd1d8509f0 | ||
|
|
79f342439a | ||
|
|
13db64f0ec | ||
|
|
908ce3bbd9 | ||
|
|
df3313971d | ||
|
|
b175132854 | ||
|
|
4cb0655192 | ||
|
|
8b191bd2f7 | ||
|
|
f3106e3bbb | ||
|
|
7fcfbc3729 | ||
|
|
598468c2b7 | ||
|
|
84681d3878 | ||
|
|
c7b14cba4d | ||
|
|
d508127452 | ||
|
|
984c79e2d2 | ||
|
|
6cb296f952 | ||
|
|
db533fc166 | ||
|
|
02b0e79ba3 | ||
|
|
1b83dd0a8a | ||
|
|
9b982b408d | ||
|
|
9b03ab830d | ||
|
|
264da6798c | ||
|
|
f68b8afa8d | ||
|
|
63f9063255 | ||
|
|
6dad353e1c | ||
|
|
5446d8d4a2 | ||
|
|
ef7617d545 | ||
|
|
0fbb560e90 | ||
|
|
86b5c55855 | ||
|
|
768decde93 | ||
|
|
3cb4315193 | ||
|
|
69b079c86e | ||
|
|
9f3fc5eb9f | ||
|
|
15e595837b | ||
|
|
17e57bb28e | ||
|
|
4d0c77b973 | ||
|
|
f8b180ac44 | ||
|
|
cd30368da9 | ||
|
|
27ed57a648 | ||
|
|
e38b527ac2 | ||
|
|
113d9612db | ||
|
|
6b3daec23f | ||
|
|
e056a1d46d | ||
|
|
57026f6262 | ||
|
|
8ef77f50c3 | ||
|
|
93e21515e5 | ||
|
|
24caa3b97b | ||
|
|
c93b36fe79 | ||
|
|
0de9242a26 | ||
|
|
53fb52c6c0 | ||
|
|
afaa529ba6 | ||
|
|
43824bd621 | ||
|
|
3c97a4f5a1 | ||
|
|
711bf190d4 | ||
|
|
1049006cf9 | ||
|
|
76603d108d | ||
|
|
5bc3930230 | ||
|
|
e5edd851b3 | ||
|
|
dcad400758 | ||
|
|
a1aaea9c55 | ||
|
|
a4e4286e04 | ||
|
|
6dd7a6a171 | ||
|
|
8e554a87b0 | ||
|
|
f1b4c083a4 | ||
|
|
90af4e3b77 | ||
|
|
e8d76a513d | ||
|
|
29e03b88c7 | ||
|
|
ebbd870150 | ||
|
|
5bf402710f | ||
|
|
c0c54e5709 | ||
|
|
2053db4cfc | ||
|
|
754303e7c7 | ||
|
|
e33d05cfe5 | ||
|
|
ed8099bf1e | ||
|
|
524c9beee4 | ||
|
|
bf1b5c3951 | ||
|
|
3554872d9a | ||
|
|
86f42d56f2 | ||
|
|
f05bf0a6f6 | ||
|
|
943a2707d2 | ||
|
|
1442337e3c | ||
|
|
8dc8682078 | ||
|
|
36e9c6ac4d | ||
|
|
10ea9bf1e3 | ||
|
|
fe0f078353 | ||
|
|
39f3afd52c | ||
|
|
544bab0fe2 | ||
|
|
cdf0311d27 | ||
|
|
5610f423d0 |
@@ -1,8 +1,97 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Node.js dependencies
|
||||
/node_modules
|
||||
/jspm_packages
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# docker-compose env files
|
||||
.env
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
|
||||
# Vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Temporary folders
|
||||
tmp
|
||||
temp
|
||||
|
||||
# IDE and editor directories
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# secret key
|
||||
*.key
|
||||
*.key.pub
|
||||
*.key.pub
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
|
||||
# Your openai api key. (required)
|
||||
OPENAI_API_KEY=sk-xxxx
|
||||
|
||||
# Access passsword, separated by comma. (optional)
|
||||
# Access password, separated by comma. (optional)
|
||||
CODE=your-password
|
||||
|
||||
# You can start service behind a proxy
|
||||
# You can start service behind a proxy. (optional)
|
||||
PROXY_URL=http://localhost:7890
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
||||
# Google 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 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)
|
||||
@@ -47,3 +46,24 @@ ENABLE_BALANCE_QUERY=
|
||||
# If you want to disable parse settings from url, set this value to 1.
|
||||
DISABLE_FAST_LINK=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# 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.
|
||||
CUSTOM_MODELS=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# Change default model
|
||||
DEFAULT_MODEL=
|
||||
|
||||
# anthropic claude Api Key.(optional)
|
||||
ANTHROPIC_API_KEY=
|
||||
|
||||
### anthropic claude Api version. (optional)
|
||||
ANTHROPIC_API_VERSION=
|
||||
|
||||
### anthropic claude Api url (optional)
|
||||
ANTHROPIC_URL=
|
||||
|
||||
### (optional)
|
||||
WHITE_WEBDEV_ENDPOINTS=
|
||||
80
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: '🐛 Bug Report'
|
||||
description: 'Report an bug'
|
||||
title: '[Bug] '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '📦 Deployment Method'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Official installation package'
|
||||
- 'Vercel'
|
||||
- 'Zeabur'
|
||||
- 'Sealos'
|
||||
- 'Netlify'
|
||||
- 'Docker'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 Version'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '💻 Operating System'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Windows'
|
||||
- 'macOS'
|
||||
- 'Ubuntu'
|
||||
- 'Other Linux'
|
||||
- 'iOS'
|
||||
- 'iPad OS'
|
||||
- 'Android'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 System Version'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '🌐 Browser'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Chrome'
|
||||
- 'Edge'
|
||||
- 'Safari'
|
||||
- 'Firefox'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 Browser Version'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🐛 Bug Description'
|
||||
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📷 Recurrence Steps'
|
||||
description: A clear and concise description of how to recurrence.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🚦 Expected Behavior'
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 Additional Information'
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
80
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: '🐛 反馈缺陷'
|
||||
description: '反馈一个问题/缺陷'
|
||||
title: '[Bug] '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '📦 部署方式'
|
||||
multiple: true
|
||||
options:
|
||||
- '官方安装包'
|
||||
- 'Vercel'
|
||||
- 'Zeabur'
|
||||
- 'Sealos'
|
||||
- 'Netlify'
|
||||
- 'Docker'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 软件版本'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '💻 系统环境'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Windows'
|
||||
- 'macOS'
|
||||
- 'Ubuntu'
|
||||
- 'Other Linux'
|
||||
- 'iOS'
|
||||
- 'iPad OS'
|
||||
- 'Android'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 系统版本'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: '🌐 浏览器'
|
||||
multiple: true
|
||||
options:
|
||||
- 'Chrome'
|
||||
- 'Edge'
|
||||
- 'Safari'
|
||||
- 'Firefox'
|
||||
- 'Other'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: '📌 浏览器版本'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🐛 问题描述'
|
||||
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📷 复现步骤'
|
||||
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🚦 期望结果'
|
||||
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 补充信息'
|
||||
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。
|
||||
21
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: '🌠 Feature Request'
|
||||
description: 'Suggest an idea'
|
||||
title: '[Feature Request] '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🥰 Feature Description'
|
||||
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🧐 Proposed Solution'
|
||||
description: Describe the solution you'd like in a clear and concise manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 Additional Information'
|
||||
description: Add any other context about the problem here.
|
||||
21
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: '🌠 功能需求'
|
||||
description: '提出需求或建议'
|
||||
title: '[Feature Request] '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🥰 需求描述'
|
||||
description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '🧐 解决方案'
|
||||
description: 请清晰且简洁地描述您想要的解决方案。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: '📝 补充信息'
|
||||
description: 在这里添加关于问题的任何其他背景信息。
|
||||
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,146 +0,0 @@
|
||||
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
|
||||
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,53 +0,0 @@
|
||||
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
|
||||
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#### 💻 变更类型 | Change Type
|
||||
|
||||
<!-- For change type, change [ ] to [x]. -->
|
||||
|
||||
- [ ] feat <!-- 引入新功能 | Introduce new features -->
|
||||
- [ ] fix <!-- 修复 Bug | Fix a bug -->
|
||||
- [ ] refactor <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
|
||||
- [ ] perf <!-- 提升性能的代码变更 | A code change that improves performance -->
|
||||
- [ ] style <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
|
||||
- [ ] test <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
|
||||
- [ ] docs <!-- 仅文档更新 | Documentation only changes -->
|
||||
- [ ] ci <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
|
||||
- [ ] chore <!-- 其他不修改 src 或 test 文件的变更 | Other changes that don’t modify src or test files -->
|
||||
- [ ] build <!-- 进行架构变更 | Make architectural changes -->
|
||||
|
||||
#### 🔀 变更说明 | Description of Change
|
||||
|
||||
<!--
|
||||
感谢您的 Pull Request ,请提供此 Pull Request 的变更说明
|
||||
Thank you for your Pull Request. Please provide a description above.
|
||||
-->
|
||||
|
||||
#### 📝 补充信息 | Additional Information
|
||||
|
||||
<!--
|
||||
请添加与此 Pull Request 相关的补充信息
|
||||
Add any other context about the Pull Request here.
|
||||
-->
|
||||
4
.gitignore
vendored
@@ -43,4 +43,6 @@ dev
|
||||
.env
|
||||
|
||||
*.key
|
||||
*.key.pub
|
||||
*.key.pub
|
||||
|
||||
masks.json
|
||||
|
||||
@@ -43,7 +43,7 @@ COPY --from=builder /app/.next/server ./.next/server
|
||||
EXPOSE 3000
|
||||
|
||||
CMD if [ -n "$PROXY_URL" ]; then \
|
||||
export HOSTNAME="127.0.0.1"; \
|
||||
export HOSTNAME="0.0.0.0"; \
|
||||
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
||||
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
||||
port=$(echo $PROXY_URL | cut -d: -f3); \
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Zhang Yifei
|
||||
Copyright (c) 2023-2024 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
|
||||
|
||||
160
README.md
@@ -1,5 +1,8 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/head-cover.png" alt="icon"/>
|
||||
|
||||
<a href='#企业版'>
|
||||
<img src="./docs/images/ent.svg" alt="icon"/>
|
||||
</a>
|
||||
|
||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
||||
|
||||
@@ -14,27 +17,51 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
[![MacOS][MacOS-image]][download-url]
|
||||
[![Linux][Linux-image]][download-url]
|
||||
|
||||
[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)
|
||||
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
|
||||
|
||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||
|
||||
[web-url]: https://chatgpt.nextweb.fun
|
||||
[web-url]: https://app.nextchat.dev/
|
||||
[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)
|
||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](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) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||

|
||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||
|
||||
</div>
|
||||
|
||||
## Enterprise Edition
|
||||
|
||||
Meeting Your Company's Privatization and Customization Deployment Requirements:
|
||||
- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
|
||||
- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
|
||||
- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
|
||||
- **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
|
||||
- **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
|
||||
- **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
|
||||
- **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
|
||||
|
||||
For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||
|
||||
## 企业版
|
||||
|
||||
满足企业用户私有化部署和个性化定制需求:
|
||||
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
|
||||
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
|
||||
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
|
||||
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
|
||||
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
|
||||
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
|
||||
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
|
||||
|
||||
企业版咨询: **business@nextchat.dev**
|
||||
|
||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
||||
|
||||
## Features
|
||||
|
||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||
@@ -49,6 +76,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
- 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, 한국어, Indonesia
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||
@@ -57,10 +90,15 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
- [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)
|
||||
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
|
||||
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
|
||||
- [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
|
||||
- [ ] local knowledge base
|
||||
|
||||
## What's New
|
||||
|
||||
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
||||
- 🚀 v2.14.0 Now supports Artifacts & SD
|
||||
- 🚀 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!
|
||||
@@ -89,15 +127,21 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
- [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)
|
||||
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
|
||||
- [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
|
||||
- [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
|
||||
- [ ] 本地知识库
|
||||
|
||||
## 最新动态
|
||||
|
||||
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
||||
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
|
||||
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
|
||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
||||
- 🚀 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
|
||||
|
||||
@@ -180,7 +224,7 @@ Specify OpenAI organization ID.
|
||||
|
||||
### `AZURE_URL` (optional)
|
||||
|
||||
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
> Example: https://{azure-resource-url}/openai
|
||||
|
||||
Azure deploy url.
|
||||
|
||||
@@ -200,6 +244,58 @@ Google Gemini Pro Api Key.
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `ANTHROPIC_API_KEY` (optional)
|
||||
|
||||
anthropic claude Api Key.
|
||||
|
||||
### `ANTHROPIC_API_VERSION` (optional)
|
||||
|
||||
anthropic claude Api version.
|
||||
|
||||
### `ANTHROPIC_URL` (optional)
|
||||
|
||||
anthropic claude Api Url.
|
||||
|
||||
### `BAIDU_API_KEY` (optional)
|
||||
|
||||
Baidu Api Key.
|
||||
|
||||
### `BAIDU_SECRET_KEY` (optional)
|
||||
|
||||
Baidu Secret Key.
|
||||
|
||||
### `BAIDU_URL` (optional)
|
||||
|
||||
Baidu Api Url.
|
||||
|
||||
### `BYTEDANCE_API_KEY` (optional)
|
||||
|
||||
ByteDance Api Key.
|
||||
|
||||
### `BYTEDANCE_URL` (optional)
|
||||
|
||||
ByteDance Api Url.
|
||||
|
||||
### `ALIBABA_API_KEY` (optional)
|
||||
|
||||
Alibaba Cloud Api Key.
|
||||
|
||||
### `ALIBABA_URL` (optional)
|
||||
|
||||
Alibaba Cloud Api Url.
|
||||
|
||||
### `IFLYTEK_URL` (Optional)
|
||||
|
||||
iflytek Api Url.
|
||||
|
||||
### `IFLYTEK_API_KEY` (Optional)
|
||||
|
||||
iflytek Api Key.
|
||||
|
||||
### `IFLYTEK_API_SECRET` (Optional)
|
||||
|
||||
iflytek Api Secret.
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
@@ -216,7 +312,7 @@ If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do want users to query balance, set this value to 1, or you should set it to 0.
|
||||
If you do want users to query balance, set this value to 1.
|
||||
|
||||
### `DISABLE_FAST_LINK` (optional)
|
||||
|
||||
@@ -233,6 +329,36 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
|
||||
|
||||
User `-all` to disable all default models, `+all` to enable all default models.
|
||||
|
||||
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
|
||||
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
|
||||
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
||||
|
||||
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
||||
|
||||
### `DEFAULT_MODEL` (optional)
|
||||
|
||||
Change default model
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (optional)
|
||||
|
||||
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
|
||||
- Each address must be a complete endpoint
|
||||
> `https://xxxx/yyy`
|
||||
- Multiple addresses are connected by ', '
|
||||
|
||||
### `DEFAULT_INPUT_TEMPLATE` (optional)
|
||||
|
||||
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
||||
|
||||
### `STABILITY_API_KEY` (optional)
|
||||
|
||||
Stability API key.
|
||||
|
||||
### `STABILITY_URL` (optional)
|
||||
|
||||
Customize Stability API url.
|
||||
|
||||
## Requirements
|
||||
|
||||
NodeJS >= 18, Docker >= 20
|
||||
|
||||
128
README_CN.md
@@ -1,22 +1,34 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="预览"/>
|
||||
|
||||
<a href='#企业版'>
|
||||
<img src="./docs/images/ent.svg" alt="icon"/>
|
||||
</a>
|
||||
|
||||
<h1 align="center">NextChat</h1>
|
||||
|
||||
一键免费部署你的私人 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)
|
||||
[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 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)
|
||||
|
||||

|
||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](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) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
</div>
|
||||
|
||||
## 企业版
|
||||
|
||||
满足您公司私有化部署和定制需求
|
||||
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
|
||||
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
|
||||
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
|
||||
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
|
||||
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
|
||||
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
|
||||
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
|
||||
|
||||
企业版咨询: **business@nextchat.dev**
|
||||
|
||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
||||
|
||||
## 开始使用
|
||||
|
||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||
@@ -25,6 +37,12 @@
|
||||
3. 部署完毕后,即可开始使用;
|
||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 保持更新
|
||||
|
||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
||||
@@ -94,7 +112,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
### `AZURE_URL` (可选)
|
||||
|
||||
> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
> 形如:https://{azure-resource-url}/openai
|
||||
|
||||
Azure 部署地址。
|
||||
|
||||
@@ -106,14 +124,68 @@ Azure 密钥。
|
||||
|
||||
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
|
||||
|
||||
### `GOOGLE_API_KEY` (optional)
|
||||
### `GOOGLE_API_KEY` (可选)
|
||||
|
||||
Google Gemini Pro 密钥.
|
||||
|
||||
### `GOOGLE_URL` (optional)
|
||||
### `GOOGLE_URL` (可选)
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `ANTHROPIC_API_KEY` (可选)
|
||||
|
||||
anthropic claude Api Key.
|
||||
|
||||
### `ANTHROPIC_API_VERSION` (可选)
|
||||
|
||||
anthropic claude Api version.
|
||||
|
||||
### `ANTHROPIC_URL` (可选)
|
||||
|
||||
anthropic claude Api Url.
|
||||
|
||||
### `BAIDU_API_KEY` (可选)
|
||||
|
||||
Baidu Api Key.
|
||||
|
||||
### `BAIDU_SECRET_KEY` (可选)
|
||||
|
||||
Baidu Secret Key.
|
||||
|
||||
### `BAIDU_URL` (可选)
|
||||
|
||||
Baidu Api Url.
|
||||
|
||||
### `BYTEDANCE_API_KEY` (可选)
|
||||
|
||||
ByteDance Api Key.
|
||||
|
||||
### `BYTEDANCE_URL` (可选)
|
||||
|
||||
ByteDance Api Url.
|
||||
|
||||
### `ALIBABA_API_KEY` (可选)
|
||||
|
||||
阿里云(千问)Api Key.
|
||||
|
||||
### `ALIBABA_URL` (可选)
|
||||
|
||||
阿里云(千问)Api Url.
|
||||
|
||||
### `IFLYTEK_URL` (可选)
|
||||
|
||||
讯飞星火Api Url.
|
||||
|
||||
### `IFLYTEK_API_KEY` (可选)
|
||||
|
||||
讯飞星火Api Key.
|
||||
|
||||
### `IFLYTEK_API_SECRET` (可选)
|
||||
|
||||
讯飞星火Api Secret.
|
||||
|
||||
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||
@@ -130,6 +202,13 @@ Google Gemini Pro Api Url.
|
||||
|
||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
|
||||
|
||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
|
||||
- 每一个地址必须是一个完整的 endpoint
|
||||
> `https://xxxx/xxx`
|
||||
- 多个地址以`,`相连
|
||||
|
||||
### `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`。
|
||||
@@ -137,6 +216,31 @@ Google Gemini Pro Api Url.
|
||||
|
||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||
|
||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
||||
|
||||
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
||||
|
||||
|
||||
### `DEFAULT_MODEL` (可选)
|
||||
|
||||
更改默认模型
|
||||
|
||||
### `DEFAULT_INPUT_TEMPLATE` (可选)
|
||||
|
||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
||||
|
||||
### `STABILITY_API_KEY` (optional)
|
||||
|
||||
Stability API密钥
|
||||
|
||||
### `STABILITY_URL` (optional)
|
||||
|
||||
自定义的Stability API请求地址
|
||||
|
||||
|
||||
## 开发
|
||||
|
||||
点击下方按钮,开始二次开发:
|
||||
|
||||
310
README_JA.md
Normal file
@@ -0,0 +1,310 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/ent.svg" alt="プレビュー"/>
|
||||
|
||||
<h1 align="center">NextChat</h1>
|
||||
|
||||
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
|
||||
|
||||
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
|
||||
|
||||
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](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) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
## 企業版
|
||||
|
||||
あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
|
||||
- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
|
||||
- **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
|
||||
- **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
|
||||
- **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
|
||||
- **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
|
||||
- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
|
||||
- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
|
||||
|
||||
企業版のお問い合わせ: **business@nextchat.dev**
|
||||
|
||||
|
||||
## 始めに
|
||||
|
||||
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&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は一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## 更新を維持する
|
||||
|
||||
もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
|
||||
|
||||
以下の手順に従って再デプロイすることをお勧めします:
|
||||
|
||||
- 元のリポジトリを削除する
|
||||
- ページ右上の fork ボタンを使って、本プロジェクトを fork する
|
||||
- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
|
||||
|
||||
|
||||
### 自動更新を開く
|
||||
|
||||
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
|
||||
|
||||
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
### 手動でコードを更新する
|
||||
|
||||
手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
|
||||
|
||||
このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
|
||||
|
||||
|
||||
|
||||
## ページアクセスパスワードを設定する
|
||||
|
||||
> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
|
||||
|
||||
> **警告**:パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
|
||||
|
||||
このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
|
||||
|
||||
```
|
||||
code1,code2,code3
|
||||
```
|
||||
|
||||
この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
|
||||
|
||||
|
||||
## 環境変数
|
||||
|
||||
> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
|
||||
|
||||
### `OPENAI_API_KEY` (必須)
|
||||
|
||||
OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
|
||||
|
||||
### `CODE` (オプション)
|
||||
|
||||
アクセスパスワード。カンマで区切って複数設定可能。
|
||||
|
||||
**警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
|
||||
|
||||
### `BASE_URL` (オプション)
|
||||
|
||||
> デフォルト: `https://api.openai.com`
|
||||
|
||||
> 例: `http://your-openai-proxy.com`
|
||||
|
||||
OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
|
||||
|
||||
> SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
|
||||
|
||||
### `OPENAI_ORG_ID` (オプション)
|
||||
|
||||
OpenAI の組織 ID を指定します。
|
||||
|
||||
### `AZURE_URL` (オプション)
|
||||
|
||||
> 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
> `CUSTOM_MODELS` で `displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
|
||||
|
||||
Azure のデプロイ URL。
|
||||
|
||||
### `AZURE_API_KEY` (オプション)
|
||||
|
||||
Azure の API キー。
|
||||
|
||||
### `AZURE_API_VERSION` (オプション)
|
||||
|
||||
Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
|
||||
|
||||
### `GOOGLE_API_KEY` (オプション)
|
||||
|
||||
Google Gemini Pro API キー。
|
||||
|
||||
### `GOOGLE_URL` (オプション)
|
||||
|
||||
Google Gemini Pro API の URL。
|
||||
|
||||
### `ANTHROPIC_API_KEY` (オプション)
|
||||
|
||||
Anthropic Claude API キー。
|
||||
|
||||
### `ANTHROPIC_API_VERSION` (オプション)
|
||||
|
||||
Anthropic Claude API バージョン。
|
||||
|
||||
### `ANTHROPIC_URL` (オプション)
|
||||
|
||||
Anthropic Claude API の URL。
|
||||
|
||||
### `BAIDU_API_KEY` (オプション)
|
||||
|
||||
Baidu API キー。
|
||||
|
||||
### `BAIDU_SECRET_KEY` (オプション)
|
||||
|
||||
Baidu シークレットキー。
|
||||
|
||||
### `BAIDU_URL` (オプション)
|
||||
|
||||
Baidu API の URL。
|
||||
|
||||
### `BYTEDANCE_API_KEY` (オプション)
|
||||
|
||||
ByteDance API キー。
|
||||
|
||||
### `BYTEDANCE_URL` (オプション)
|
||||
|
||||
ByteDance API の URL。
|
||||
|
||||
### `ALIBABA_API_KEY` (オプション)
|
||||
|
||||
アリババ(千问)API キー。
|
||||
|
||||
### `ALIBABA_URL` (オプション)
|
||||
|
||||
アリババ(千问)API の URL。
|
||||
|
||||
### `HIDE_USER_API_KEY` (オプション)
|
||||
|
||||
ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `DISABLE_GPT4` (オプション)
|
||||
|
||||
ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `ENABLE_BALANCE_QUERY` (オプション)
|
||||
|
||||
バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `DISABLE_FAST_LINK` (オプション)
|
||||
|
||||
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
|
||||
|
||||
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
|
||||
- 各アドレスは完全なエンドポイントでなければなりません。
|
||||
> `https://xxxx/xxx`
|
||||
- 複数のアドレスは `,` で接続します。
|
||||
|
||||
### `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` のみを有効にすることを意味します。
|
||||
|
||||
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
||||
|
||||
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
||||
|
||||
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
||||
|
||||
### `DEFAULT_MODEL` (オプション)
|
||||
|
||||
デフォルトのモデルを変更します。
|
||||
|
||||
### `DEFAULT_INPUT_TEMPLATE` (オプション)
|
||||
|
||||
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
|
||||
|
||||
|
||||
## 開発
|
||||
|
||||
下のボタンをクリックして二次開発を開始してください:
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
```
|
||||
|
||||
|
||||
### ローカル開発
|
||||
|
||||
1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
|
||||
2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
|
||||
3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
|
||||
|
||||
|
||||
## デプロイ
|
||||
|
||||
### コンテナデプロイ(推奨)
|
||||
|
||||
> Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
|
||||
|
||||
> ⚠️ 注意:Docker バージョンは最新バージョンより 1~2 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
|
||||
|
||||
```shell
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=ページアクセスパスワード \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
プロキシを指定することもできます:
|
||||
|
||||
```shell
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=ページアクセスパスワード \
|
||||
--net=host \
|
||||
-e PROXY_URL=http://127.0.0.1:7890 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
|
||||
|
||||
```shell
|
||||
-e PROXY_URL="http://127.0.0.1:7890 user password"
|
||||
```
|
||||
|
||||
他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
|
||||
|
||||
|
||||
### ローカルデプロイ
|
||||
|
||||
コンソールで以下のコマンドを実行します:
|
||||
|
||||
```shell
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
⚠️ 注意:インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
|
||||
|
||||
|
||||
## 謝辞
|
||||
|
||||
### 寄付者
|
||||
|
||||
> 英語版をご覧ください。
|
||||
|
||||
### 貢献者
|
||||
|
||||
[プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
|
||||
### 関連プロジェクト
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
|
||||
|
||||
|
||||
## オープンソースライセンス
|
||||
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
70
app/api/[provider]/[...path]/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ApiPath } from "@/app/constant";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { handle as openaiHandler } from "../../openai";
|
||||
import { handle as azureHandler } from "../../azure";
|
||||
import { handle as googleHandler } from "../../google";
|
||||
import { handle as anthropicHandler } from "../../anthropic";
|
||||
import { handle as baiduHandler } from "../../baidu";
|
||||
import { handle as bytedanceHandler } from "../../bytedance";
|
||||
import { handle as alibabaHandler } from "../../alibaba";
|
||||
import { handle as moonshotHandler } from "../../moonshot";
|
||||
import { handle as stabilityHandler } from "../../stability";
|
||||
import { handle as iflytekHandler } from "../../iflytek";
|
||||
import { handle as proxyHandler } from "../../proxy";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { provider: string; path: string[] } },
|
||||
) {
|
||||
const apiPath = `/api/${params.provider}`;
|
||||
console.log(`[${params.provider} Route] params `, params);
|
||||
switch (apiPath) {
|
||||
case ApiPath.Azure:
|
||||
return azureHandler(req, { params });
|
||||
case ApiPath.Google:
|
||||
return googleHandler(req, { params });
|
||||
case ApiPath.Anthropic:
|
||||
return anthropicHandler(req, { params });
|
||||
case ApiPath.Baidu:
|
||||
return baiduHandler(req, { params });
|
||||
case ApiPath.ByteDance:
|
||||
return bytedanceHandler(req, { params });
|
||||
case ApiPath.Alibaba:
|
||||
return alibabaHandler(req, { params });
|
||||
// case ApiPath.Tencent: using "/api/tencent"
|
||||
case ApiPath.Moonshot:
|
||||
return moonshotHandler(req, { params });
|
||||
case ApiPath.Stability:
|
||||
return stabilityHandler(req, { params });
|
||||
case ApiPath.Iflytek:
|
||||
return iflytekHandler(req, { params });
|
||||
case ApiPath.OpenAI:
|
||||
return openaiHandler(req, { params });
|
||||
default:
|
||||
return proxyHandler(req, { params });
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
131
app/api/alibaba.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Alibaba,
|
||||
ALIBABA_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Alibaba Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Qwen);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Alibaba] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Alibaba, "");
|
||||
|
||||
let baseUrl = serverConfig.alibabaUrl || ALIBABA_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
"X-DashScope-SSE": req.headers.get("X-DashScope-SSE") ?? "disable",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Alibaba as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Alibaba] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
170
app/api/anthropic.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
ANTHROPIC_BASE_URL,
|
||||
Anthropic,
|
||||
ApiPath,
|
||||
DEFAULT_MODELS,
|
||||
ServiceProvider,
|
||||
ModelProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
|
||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Anthropic Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
console.log("[Anthropic Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + subpath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Claude);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Anthropic] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let authHeaderName = "x-api-key";
|
||||
let authValue =
|
||||
req.headers.get(authHeaderName) ||
|
||||
req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
|
||||
serverConfig.anthropicApiKey ||
|
||||
"";
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
|
||||
|
||||
let baseUrl =
|
||||
serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
// try rebuild url, when using cloudflare ai gateway in server
|
||||
const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}${path}`);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
[authHeaderName]: authValue,
|
||||
"anthropic-version":
|
||||
req.headers.get("anthropic-version") ||
|
||||
serverConfig.anthropicApiVersion ||
|
||||
Anthropic.Vision,
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Anthropic as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Anthropic] filter`, e);
|
||||
}
|
||||
}
|
||||
// console.log("[Anthropic request]", fetchOptions.headers, req.method);
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// console.log(
|
||||
// "[Anthropic response]",
|
||||
// res.status,
|
||||
// " ",
|
||||
// res.headers,
|
||||
// res.url,
|
||||
// );
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
73
app/api/artifacts/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import md5 from "spark-md5";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
|
||||
async function handle(req: NextRequest, res: NextResponse) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
const storeUrl = () =>
|
||||
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
|
||||
const storeHeaders = () => ({
|
||||
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
|
||||
});
|
||||
if (req.method === "POST") {
|
||||
const clonedBody = await req.text();
|
||||
const hashedCode = md5.hash(clonedBody).trim();
|
||||
const body: {
|
||||
key: string;
|
||||
value: string;
|
||||
expiration_ttl?: number;
|
||||
} = {
|
||||
key: hashedCode,
|
||||
value: clonedBody,
|
||||
};
|
||||
try {
|
||||
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
|
||||
if (ttl > 60) {
|
||||
body["expiration_ttl"] = ttl;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
const res = await fetch(`${storeUrl()}/bulk`, {
|
||||
headers: {
|
||||
...storeHeaders(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PUT",
|
||||
body: JSON.stringify([body]),
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log("save data", result);
|
||||
if (result?.success) {
|
||||
return NextResponse.json(
|
||||
{ code: 0, id: hashedCode, result },
|
||||
{ status: res.status },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: true, msg: "Save data error" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (req.method === "GET") {
|
||||
const id = req?.nextUrl?.searchParams?.get("id");
|
||||
const res = await fetch(`${storeUrl()}/values/${id}`, {
|
||||
headers: storeHeaders(),
|
||||
method: "GET",
|
||||
});
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: true, msg: "Invalid request" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
@@ -57,12 +57,50 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
if (!apiKey) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
const systemApiKey =
|
||||
modelProvider === ModelProvider.GeminiPro
|
||||
? serverConfig.googleApiKey
|
||||
: serverConfig.isAzure
|
||||
? serverConfig.azureApiKey
|
||||
: serverConfig.apiKey;
|
||||
// const systemApiKey =
|
||||
// modelProvider === ModelProvider.GeminiPro
|
||||
// ? serverConfig.googleApiKey
|
||||
// : serverConfig.isAzure
|
||||
// ? serverConfig.azureApiKey
|
||||
// : serverConfig.apiKey;
|
||||
|
||||
let systemApiKey: string | undefined;
|
||||
|
||||
switch (modelProvider) {
|
||||
case ModelProvider.Stability:
|
||||
systemApiKey = serverConfig.stabilityApiKey;
|
||||
break;
|
||||
case ModelProvider.GeminiPro:
|
||||
systemApiKey = serverConfig.googleApiKey;
|
||||
break;
|
||||
case ModelProvider.Claude:
|
||||
systemApiKey = serverConfig.anthropicApiKey;
|
||||
break;
|
||||
case ModelProvider.Doubao:
|
||||
systemApiKey = serverConfig.bytedanceApiKey;
|
||||
break;
|
||||
case ModelProvider.Ernie:
|
||||
systemApiKey = serverConfig.baiduApiKey;
|
||||
break;
|
||||
case ModelProvider.Qwen:
|
||||
systemApiKey = serverConfig.alibabaApiKey;
|
||||
break;
|
||||
case ModelProvider.Moonshot:
|
||||
systemApiKey = serverConfig.moonshotApiKey;
|
||||
break;
|
||||
case ModelProvider.Iflytek:
|
||||
systemApiKey =
|
||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||
break;
|
||||
case ModelProvider.GPT:
|
||||
default:
|
||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||
systemApiKey = serverConfig.azureApiKey;
|
||||
} else {
|
||||
systemApiKey = serverConfig.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (systemApiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||
|
||||
33
app/api/azure.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { ModelProvider } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { requestOpenai } from "./common";
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Azure Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
const authResult = auth(req, ModelProvider.GPT);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await requestOpenai(req);
|
||||
} catch (e) {
|
||||
console.error("[Azure] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
145
app/api/baidu.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
BAIDU_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
BAIDU_OATUH_URL,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { getAccessToken } from "@/app/utils/baidu";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Baidu Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Ernie);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
if (!serverConfig.baiduApiKey || !serverConfig.baiduSecretKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `missing BAIDU_API_KEY or BAIDU_SECRET_KEY in server env vars`,
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Baidu] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Baidu, "");
|
||||
|
||||
let baseUrl = serverConfig.baiduUrl || BAIDU_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const { access_token } = await getAccessToken(
|
||||
serverConfig.baiduApiKey as string,
|
||||
serverConfig.baiduSecretKey as string,
|
||||
);
|
||||
const fetchUrl = `${baseUrl}${path}?access_token=${access_token}`;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Baidu as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Baidu] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
129
app/api/bytedance.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
BYTEDANCE_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[ByteDance Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Doubao);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[ByteDance] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ByteDance, "");
|
||||
|
||||
let baseUrl = serverConfig.bytedanceUrl || BYTEDANCE_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.ByteDance as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[ByteDance] filter`, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
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";
|
||||
import {
|
||||
DEFAULT_MODELS,
|
||||
OPENAI_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { isModelAvailableInServer } from "../utils/model";
|
||||
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function requestOpenai(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const isAzure = req.nextUrl.pathname.includes("azure/deployments");
|
||||
|
||||
var authValue,
|
||||
authHeaderName = "";
|
||||
if (serverConfig.isAzure) {
|
||||
if (isAzure) {
|
||||
authValue =
|
||||
req.headers
|
||||
.get("Authorization")
|
||||
@@ -25,13 +32,10 @@ export async function requestOpenai(req: NextRequest) {
|
||||
authHeaderName = "Authorization";
|
||||
}
|
||||
|
||||
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
"/api/openai/",
|
||||
"",
|
||||
);
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
|
||||
|
||||
let baseUrl =
|
||||
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
|
||||
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
@@ -43,10 +47,6 @@ export async function requestOpenai(req: NextRequest) {
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
// 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(
|
||||
() => {
|
||||
@@ -55,17 +55,46 @@ export async function requestOpenai(req: NextRequest) {
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
if (serverConfig.isAzure) {
|
||||
if (!serverConfig.azureApiVersion) {
|
||||
return NextResponse.json({
|
||||
error: true,
|
||||
message: `missing AZURE_API_VERSION in server env vars`,
|
||||
});
|
||||
if (isAzure) {
|
||||
const azureApiVersion =
|
||||
req?.nextUrl?.searchParams?.get("api-version") ||
|
||||
serverConfig.azureApiVersion;
|
||||
baseUrl = baseUrl.split("/deployments").shift() as string;
|
||||
path = `${req.nextUrl.pathname.replaceAll(
|
||||
"/api/azure/",
|
||||
"",
|
||||
)}?api-version=${azureApiVersion}`;
|
||||
|
||||
// Forward compatibility:
|
||||
// if display_name(deployment_name) not set, and '{deploy-id}' in AZURE_URL
|
||||
// then using default '{deploy-id}'
|
||||
if (serverConfig.customModels && serverConfig.azureUrl) {
|
||||
const modelName = path.split("/")[1];
|
||||
let realDeployName = "";
|
||||
serverConfig.customModels
|
||||
.split(",")
|
||||
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
|
||||
.forEach((m) => {
|
||||
const [fullName, displayName] = m.split("=");
|
||||
const [_, providerName] = fullName.split("@");
|
||||
if (providerName === "azure" && !displayName) {
|
||||
const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
|
||||
"deployments/",
|
||||
);
|
||||
if (deployId) {
|
||||
realDeployName = deployId;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (realDeployName) {
|
||||
console.log("[Replace with DeployId", realDeployName);
|
||||
path = path.replaceAll(modelName, realDeployName);
|
||||
}
|
||||
}
|
||||
path = makeAzurePath(path, serverConfig.azureApiVersion);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}`;
|
||||
const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
|
||||
console.log("fetchUrl", fetchUrl);
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -87,17 +116,24 @@ export async function requestOpenai(req: NextRequest) {
|
||||
// #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) {
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.OpenAI as string,
|
||||
) ||
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Azure as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
@@ -116,12 +152,29 @@ export async function requestOpenai(req: NextRequest) {
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// Extract the OpenAI-Organization header from the response
|
||||
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
|
||||
|
||||
// Check if serverConfig.openaiOrgId is defined and not an empty string
|
||||
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
|
||||
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
|
||||
console.log("[Org ID]", openaiOrganizationHeader);
|
||||
} else {
|
||||
console.log("[Org ID] is not set up.");
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
|
||||
// Also, this is to prevent the header from being sent to the client
|
||||
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
|
||||
newHeaders.delete("OpenAI-Organization");
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -13,6 +13,7 @@ const DANGER_CONFIG = {
|
||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||
disableFastLink: serverConfig.disableFastLink,
|
||||
customModels: serverConfig.customModels,
|
||||
defaultModel: serverConfig.defaultModel,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { auth } from "./auth";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
|
||||
import {
|
||||
ApiPath,
|
||||
GEMINI_BASE_URL,
|
||||
Google,
|
||||
ModelProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
|
||||
async function handle(
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
{ params }: { params: { provider: string; path: string[] } },
|
||||
) {
|
||||
console.log("[Google Route] params ", params);
|
||||
|
||||
@@ -13,32 +21,6 @@ async function handle(
|
||||
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, {
|
||||
@@ -49,9 +31,9 @@ async function handle(
|
||||
const bearToken = req.headers.get("Authorization") ?? "";
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
|
||||
const key = token ? token : serverConfig.googleApiKey;
|
||||
const apiKey = token ? token : serverConfig.googleApiKey;
|
||||
|
||||
if (!key) {
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
@@ -62,8 +44,63 @@ async function handle(
|
||||
},
|
||||
);
|
||||
}
|
||||
try {
|
||||
const response = await request(req, apiKey);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Google] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}?key=${key}`;
|
||||
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",
|
||||
];
|
||||
|
||||
async function request(req: NextRequest, apiKey: string) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
|
||||
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
|
||||
}`;
|
||||
|
||||
console.log("[Fetch Url] ", fetchUrl);
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -95,22 +132,3 @@ async function handle(
|
||||
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",
|
||||
];
|
||||
131
app/api/iflytek.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Iflytek,
|
||||
IFLYTEK_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
// iflytek
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Iflytek Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Iflytek);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Iflytek] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// iflytek use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
|
||||
|
||||
let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Iflytek as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Iflytek] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
130
app/api/moonshot.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Moonshot,
|
||||
MOONSHOT_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Moonshot Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Moonshot);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Moonshot] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
|
||||
|
||||
let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Moonshot as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Moonshot] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ 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";
|
||||
import { auth } from "./auth";
|
||||
import { requestOpenai } from "./common";
|
||||
|
||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||
|
||||
@@ -13,14 +13,16 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
|
||||
if (config.disableGPT4) {
|
||||
remoteModelRes.data = remoteModelRes.data.filter(
|
||||
(m) => !m.id.startsWith("gpt-4"),
|
||||
(m) =>
|
||||
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
|
||||
m.id.startsWith("gpt-4o-mini"),
|
||||
);
|
||||
}
|
||||
|
||||
return remoteModelRes;
|
||||
}
|
||||
|
||||
async function handle(
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
@@ -70,27 +72,3 @@ async function handle(
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
75
app/api/proxy.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Proxy Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
// remove path params from searchParams
|
||||
req.nextUrl.searchParams.delete("path");
|
||||
req.nextUrl.searchParams.delete("provider");
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
const fetchUrl = `${req.headers.get(
|
||||
"x-base-url",
|
||||
)}/${subpath}?${req.nextUrl.searchParams.toString()}`;
|
||||
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
|
||||
const headers = new Headers(
|
||||
Array.from(req.headers.entries()).filter((item) => {
|
||||
if (
|
||||
item[0].indexOf("x-") > -1 ||
|
||||
item[0].indexOf("sec-") > -1 ||
|
||||
skipHeaders.includes(item[0])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const fetchOptions: RequestInit = {
|
||||
headers,
|
||||
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,
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
99
app/api/stability.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
|
||||
import { auth } from "@/app/api/auth";
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Stability] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
|
||||
|
||||
console.log("[Stability Proxy] ", path);
|
||||
console.log("[Stability Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const authResult = auth(req, ModelProvider.Stability);
|
||||
|
||||
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.stabilityApiKey;
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `missing STABILITY_API_KEY in server env vars`,
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}`;
|
||||
console.log("[Stability Url] ", fetchUrl);
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
|
||||
Accept: req.headers.get("Accept") || "application/json",
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
124
app/api/tencent/route.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
TENCENT_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
Tencent,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { getHeader } from "@/app/utils/tencent";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Tencent Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Hunyuan);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Tencent] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = baseUrl;
|
||||
|
||||
const body = await req.text();
|
||||
const headers = await getHeader(
|
||||
body,
|
||||
serverConfig.tencentSecretId as string,
|
||||
serverConfig.tencentSecretKey as string,
|
||||
);
|
||||
const fetchOptions: RequestInit = {
|
||||
headers,
|
||||
method: req.method,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { STORAGE_KEY } from "../../../constant";
|
||||
import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
|
||||
const config = getServerSideConfig();
|
||||
|
||||
const mergedAllowedWebDavEndpoints = [
|
||||
...internalAllowedWebDavEndpoints,
|
||||
...config.allowedWebDevEndpoints,
|
||||
].filter((domain) => Boolean(domain.trim()));
|
||||
|
||||
const normalizeUrl = (url: string) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
@@ -12,17 +29,52 @@ async function handle(
|
||||
|
||||
const requestUrl = new URL(req.url);
|
||||
let endpoint = requestUrl.searchParams.get("endpoint");
|
||||
if (!endpoint?.endsWith("/")) {
|
||||
endpoint += "/";
|
||||
}
|
||||
const endpointPath = params.path.join("/");
|
||||
let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
|
||||
|
||||
// only allow MKCOL, GET, PUT
|
||||
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
|
||||
// Validate the endpoint to prevent potential SSRF attacks
|
||||
if (
|
||||
!endpoint ||
|
||||
!mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
|
||||
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
|
||||
const normalizedEndpoint = normalizeUrl(endpoint as string);
|
||||
|
||||
return (
|
||||
normalizedEndpoint &&
|
||||
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
|
||||
normalizedEndpoint.pathname.startsWith(
|
||||
normalizedAllowedEndpoint.pathname,
|
||||
)
|
||||
);
|
||||
})
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
msg: "Invalid endpoint",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!endpoint?.endsWith("/")) {
|
||||
endpoint += "/";
|
||||
}
|
||||
|
||||
const endpointPath = params.path.join("/");
|
||||
const targetPath = `${endpoint}${endpointPath}`;
|
||||
|
||||
// only allow MKCOL, GET, PUT
|
||||
if (
|
||||
proxy_method !== "MKCOL" &&
|
||||
proxy_method !== "GET" &&
|
||||
proxy_method !== "PUT"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -31,14 +83,11 @@ async function handle(
|
||||
}
|
||||
|
||||
// for MKCOL request, only allow request ${folder}
|
||||
if (
|
||||
req.method == "MKCOL" &&
|
||||
!new URL(endpointPath).pathname.endsWith(folder)
|
||||
) {
|
||||
if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -47,14 +96,11 @@ async function handle(
|
||||
}
|
||||
|
||||
// for GET request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "GET" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -63,14 +109,11 @@ async function handle(
|
||||
}
|
||||
|
||||
// for PUT request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "PUT" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -78,9 +121,9 @@ async function handle(
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint + endpointPath}`;
|
||||
const targetUrl = targetPath;
|
||||
|
||||
const method = req.method;
|
||||
const method = proxy_method || req.method;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
@@ -90,22 +133,34 @@ async function handle(
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
redirect: "manual",
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
let fetchResult;
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
try {
|
||||
fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
} finally {
|
||||
console.log(
|
||||
"[Any Proxy]",
|
||||
targetUrl,
|
||||
{
|
||||
method: method,
|
||||
},
|
||||
{
|
||||
status: fetchResult?.status,
|
||||
statusText: fetchResult?.statusText,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const PUT = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -5,9 +5,23 @@ import {
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
import {
|
||||
ChatMessageTool,
|
||||
ChatMessage,
|
||||
ModelType,
|
||||
useAccessStore,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
import { ClaudeApi } from "./platforms/anthropic";
|
||||
import { ErnieApi } from "./platforms/baidu";
|
||||
import { DoubaoApi } from "./platforms/bytedance";
|
||||
import { QwenApi } from "./platforms/alibaba";
|
||||
import { HunyuanApi } from "./platforms/tencent";
|
||||
import { MoonshotApi } from "./platforms/moonshot";
|
||||
import { SparkApi } from "./platforms/iflytek";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
@@ -29,11 +43,15 @@ export interface RequestMessage {
|
||||
|
||||
export interface LLMConfig {
|
||||
model: string;
|
||||
providerName?: string;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
stream?: boolean;
|
||||
presence_penalty?: number;
|
||||
frequency_penalty?: number;
|
||||
size?: DalleRequestPayload["size"];
|
||||
quality?: DalleRequestPayload["quality"];
|
||||
style?: DalleRequestPayload["style"];
|
||||
}
|
||||
|
||||
export interface ChatOptions {
|
||||
@@ -44,6 +62,8 @@ export interface ChatOptions {
|
||||
onFinish: (message: string) => void;
|
||||
onError?: (err: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
onBeforeTool?: (tool: ChatMessageTool) => void;
|
||||
onAfterTool?: (tool: ChatMessageTool) => void;
|
||||
}
|
||||
|
||||
export interface LLMUsage {
|
||||
@@ -53,14 +73,17 @@ export interface LLMUsage {
|
||||
|
||||
export interface LLMModel {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
available: boolean;
|
||||
provider: LLMModelProvider;
|
||||
sorted: number;
|
||||
}
|
||||
|
||||
export interface LLMModelProvider {
|
||||
id: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
sorted: number;
|
||||
}
|
||||
|
||||
export abstract class LLMApi {
|
||||
@@ -94,11 +117,34 @@ export class ClientApi {
|
||||
public llm: LLMApi;
|
||||
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
if (provider === ModelProvider.GeminiPro) {
|
||||
this.llm = new GeminiProApi();
|
||||
return;
|
||||
switch (provider) {
|
||||
case ModelProvider.GeminiPro:
|
||||
this.llm = new GeminiProApi();
|
||||
break;
|
||||
case ModelProvider.Claude:
|
||||
this.llm = new ClaudeApi();
|
||||
break;
|
||||
case ModelProvider.Ernie:
|
||||
this.llm = new ErnieApi();
|
||||
break;
|
||||
case ModelProvider.Doubao:
|
||||
this.llm = new DoubaoApi();
|
||||
break;
|
||||
case ModelProvider.Qwen:
|
||||
this.llm = new QwenApi();
|
||||
break;
|
||||
case ModelProvider.Hunyuan:
|
||||
this.llm = new HunyuanApi();
|
||||
break;
|
||||
case ModelProvider.Moonshot:
|
||||
this.llm = new MoonshotApi();
|
||||
break;
|
||||
case ModelProvider.Iflytek:
|
||||
this.llm = new SparkApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
|
||||
config() {}
|
||||
@@ -147,39 +193,122 @@ export class ClientApi {
|
||||
}
|
||||
}
|
||||
|
||||
export function getBearerToken(
|
||||
apiKey: string,
|
||||
noBearer: boolean = false,
|
||||
): string {
|
||||
return validString(apiKey)
|
||||
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
|
||||
: "";
|
||||
}
|
||||
|
||||
export function validString(x: string): boolean {
|
||||
return x?.length > 0;
|
||||
}
|
||||
|
||||
export function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
const chatStore = useChatStore.getState();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
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;
|
||||
|
||||
const clientConfig = getClientConfig();
|
||||
|
||||
function getConfig() {
|
||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||
const isGoogle = modelConfig.providerName == ServiceProvider.Google;
|
||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
||||
const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
|
||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
: isAzure
|
||||
? accessStore.azureApiKey
|
||||
: isAnthropic
|
||||
? accessStore.anthropicApiKey
|
||||
: isByteDance
|
||||
? accessStore.bytedanceApiKey
|
||||
: isAlibaba
|
||||
? accessStore.alibabaApiKey
|
||||
: isMoonshot
|
||||
? accessStore.moonshotApiKey
|
||||
: isIflytek
|
||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||
: ""
|
||||
: accessStore.openaiApiKey;
|
||||
return {
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
isBaidu,
|
||||
isByteDance,
|
||||
isAlibaba,
|
||||
isMoonshot,
|
||||
isIflytek,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthHeader(): string {
|
||||
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
||||
}
|
||||
|
||||
const {
|
||||
isGoogle,
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
isBaidu,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
} = getConfig();
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
if (isGoogle && clientConfig?.isApp) return headers;
|
||||
// when using baidu api in app, not set auth header
|
||||
if (isBaidu && clientConfig?.isApp) return headers;
|
||||
|
||||
const authHeader = getAuthHeader();
|
||||
|
||||
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
} else if (isEnabledAccessControl && validString(accessStore.accessCode)) {
|
||||
headers["Authorization"] = getBearerToken(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
switch (provider) {
|
||||
case ServiceProvider.Google:
|
||||
return new ClientApi(ModelProvider.GeminiPro);
|
||||
case ServiceProvider.Anthropic:
|
||||
return new ClientApi(ModelProvider.Claude);
|
||||
case ServiceProvider.Baidu:
|
||||
return new ClientApi(ModelProvider.Ernie);
|
||||
case ServiceProvider.ByteDance:
|
||||
return new ClientApi(ModelProvider.Doubao);
|
||||
case ServiceProvider.Alibaba:
|
||||
return new ClientApi(ModelProvider.Qwen);
|
||||
case ServiceProvider.Tencent:
|
||||
return new ClientApi(ModelProvider.Hunyuan);
|
||||
case ServiceProvider.Moonshot:
|
||||
return new ClientApi(ModelProvider.Moonshot);
|
||||
case ServiceProvider.Iflytek:
|
||||
return new ClientApi(ModelProvider.Iflytek);
|
||||
default:
|
||||
return new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
}
|
||||
|
||||
268
app/client/platforms/alibaba.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
Alibaba,
|
||||
ALIBABA_BASE_URL,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
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 { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RequestInput {
|
||||
messages: {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | MultimodalContent[];
|
||||
}[];
|
||||
}
|
||||
interface RequestParam {
|
||||
result_format: string;
|
||||
incremental_output?: boolean;
|
||||
temperature: number;
|
||||
repetition_penalty?: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
interface RequestPayload {
|
||||
model: string;
|
||||
input: RequestInput;
|
||||
parameters: RequestParam;
|
||||
}
|
||||
|
||||
export class QwenApi implements LLMApi {
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.alibabaUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Alibaba)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res?.output?.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const requestPayload: RequestPayload = {
|
||||
model: modelConfig.model,
|
||||
input: {
|
||||
messages,
|
||||
},
|
||||
parameters: {
|
||||
result_format: "message",
|
||||
incremental_output: shouldStream,
|
||||
temperature: modelConfig.temperature,
|
||||
// max_tokens: modelConfig.max_tokens,
|
||||
top_p: modelConfig.top_p === 1 ? 0.99 : modelConfig.top_p, // qwen top_p is should be < 1
|
||||
},
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(Alibaba.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
"X-DashScope-SSE": shouldStream ? "enable" : "disable",
|
||||
},
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
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");
|
||||
if (responseText?.length === 0) {
|
||||
options.onError?.(new Error("empty response from server"));
|
||||
}
|
||||
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) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[Alibaba] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.output.choices as Array<{
|
||||
message: { content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.message?.content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
export { Alibaba };
|
||||
418
app/client/platforms/anthropic.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
usePluginStore,
|
||||
ChatMessageTool,
|
||||
} from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
|
||||
import Locale from "../../locales";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { RequestPayload } from "./openai";
|
||||
|
||||
export type MultiBlockContent = {
|
||||
type: "image" | "text";
|
||||
source?: {
|
||||
type: string;
|
||||
media_type: string;
|
||||
data: string;
|
||||
};
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type AnthropicMessage = {
|
||||
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
|
||||
content: string | MultiBlockContent[];
|
||||
};
|
||||
|
||||
export interface AnthropicChatRequest {
|
||||
model: string; // The model that will complete your prompt.
|
||||
messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
|
||||
max_tokens: number; // The maximum number of tokens to generate before stopping.
|
||||
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||
temperature?: number; // Amount of randomness injected into the response.
|
||||
top_p?: number; // Use nucleus sampling.
|
||||
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||
metadata?: object; // An object describing metadata about the request.
|
||||
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
model: string; // The model that will complete your prompt.
|
||||
prompt: string; // The prompt that you want Claude to complete.
|
||||
max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
|
||||
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||
temperature?: number; // Amount of randomness injected into the response.
|
||||
top_p?: number; // Use nucleus sampling.
|
||||
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||
metadata?: object; // An object describing metadata about the request.
|
||||
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
completion: string;
|
||||
stop_reason: "stop_sequence" | "max_tokens";
|
||||
model: string;
|
||||
}
|
||||
|
||||
export type ChatStreamResponse = ChatResponse & {
|
||||
stop?: string;
|
||||
log_id: string;
|
||||
};
|
||||
|
||||
const ClaudeMapper = {
|
||||
assistant: "assistant",
|
||||
user: "user",
|
||||
system: "user",
|
||||
} as const;
|
||||
|
||||
const keys = ["claude-2, claude-instant-1"];
|
||||
|
||||
export class ClaudeApi implements LLMApi {
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] claude response: ", res);
|
||||
|
||||
return res?.content?.[0]?.text;
|
||||
}
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
// try get base64image from local cache image_url
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = await preProcessImageContent(v.content);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const keys = ["system", "user"];
|
||||
|
||||
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i];
|
||||
const nextMessage = messages[i + 1];
|
||||
|
||||
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||
messages[i] = [
|
||||
message,
|
||||
{
|
||||
role: "assistant",
|
||||
content: ";",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = messages
|
||||
.flat()
|
||||
.filter((v) => {
|
||||
if (!v.content) return false;
|
||||
if (typeof v.content === "string" && !v.content.trim()) return false;
|
||||
return true;
|
||||
})
|
||||
.map((v) => {
|
||||
const { role, content } = v;
|
||||
const insideRole = ClaudeMapper[role] ?? "user";
|
||||
|
||||
if (!visionModel || typeof content === "string") {
|
||||
return {
|
||||
role: insideRole,
|
||||
content: getMessageTextContent(v),
|
||||
};
|
||||
}
|
||||
return {
|
||||
role: insideRole,
|
||||
content: content
|
||||
.filter((v) => v.image_url || v.text)
|
||||
.map(({ type, text, image_url }) => {
|
||||
if (type === "text") {
|
||||
return {
|
||||
type,
|
||||
text: text!,
|
||||
};
|
||||
}
|
||||
const { url = "" } = image_url || {};
|
||||
const colonIndex = url.indexOf(":");
|
||||
const semicolonIndex = url.indexOf(";");
|
||||
const comma = url.indexOf(",");
|
||||
|
||||
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
|
||||
const encodeType = url.slice(semicolonIndex + 1, comma);
|
||||
const data = url.slice(comma + 1);
|
||||
|
||||
return {
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: encodeType,
|
||||
media_type: mimeType,
|
||||
data,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
if (prompt[0]?.role === "assistant") {
|
||||
prompt.unshift({
|
||||
role: "user",
|
||||
content: ";",
|
||||
});
|
||||
}
|
||||
|
||||
const requestBody: AnthropicChatRequest = {
|
||||
messages: prompt,
|
||||
stream: shouldStream,
|
||||
|
||||
model: modelConfig.model,
|
||||
max_tokens: modelConfig.max_tokens,
|
||||
temperature: modelConfig.temperature,
|
||||
top_p: modelConfig.top_p,
|
||||
// top_k: modelConfig.top_k,
|
||||
top_k: 5,
|
||||
};
|
||||
|
||||
const path = this.path(Anthropic.ChatPath);
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
if (shouldStream) {
|
||||
let index = -1;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
path,
|
||||
requestBody,
|
||||
{
|
||||
...getHeaders(),
|
||||
"anthropic-version": accessStore.anthropicApiVersion,
|
||||
},
|
||||
// @ts-ignore
|
||||
tools.map((tool) => ({
|
||||
name: tool?.function?.name,
|
||||
description: tool?.function?.description,
|
||||
input_schema: tool?.function?.parameters,
|
||||
})),
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
let chunkJson:
|
||||
| undefined
|
||||
| {
|
||||
type: "content_block_delta" | "content_block_stop";
|
||||
content_block?: {
|
||||
type: "tool_use";
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
delta?: {
|
||||
type: "text_delta" | "input_json_delta";
|
||||
text?: string;
|
||||
partial_json?: string;
|
||||
};
|
||||
index: number;
|
||||
};
|
||||
chunkJson = JSON.parse(text);
|
||||
|
||||
if (chunkJson?.content_block?.type == "tool_use") {
|
||||
index += 1;
|
||||
const id = chunkJson?.content_block.id;
|
||||
const name = chunkJson?.content_block.name;
|
||||
runTools.push({
|
||||
id,
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
arguments: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
chunkJson?.delta?.type == "input_json_delta" &&
|
||||
chunkJson?.delta?.partial_json
|
||||
) {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] +=
|
||||
chunkJson?.delta?.partial_json;
|
||||
}
|
||||
return chunkJson?.delta?.text;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// reset index value
|
||||
index = -1;
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
{
|
||||
role: "assistant",
|
||||
content: toolCallMessage.tool_calls.map(
|
||||
(tool: ChatMessageTool) => ({
|
||||
type: "tool_use",
|
||||
id: tool.id,
|
||||
name: tool?.function?.name,
|
||||
input: tool?.function?.arguments
|
||||
? JSON.parse(tool?.function?.arguments)
|
||||
: {},
|
||||
}),
|
||||
),
|
||||
},
|
||||
// @ts-ignore
|
||||
...toolCallResult.map((result) => ({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: result.tool_call_id,
|
||||
content: result.content,
|
||||
},
|
||||
],
|
||||
})),
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const payload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getHeaders(), // get common headers
|
||||
"anthropic-version": accessStore.anthropicApiVersion,
|
||||
// do not send `anthropicApiKey` in browser!!!
|
||||
// Authorization: getAuthKey(accessStore.anthropicApiKey),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
controller.signal.onabort = () => options.onFinish("");
|
||||
|
||||
const res = await fetch(path, payload);
|
||||
const resJson = await res.json();
|
||||
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
async models() {
|
||||
// const provider = {
|
||||
// id: "anthropic",
|
||||
// providerName: "Anthropic",
|
||||
// providerType: "anthropic",
|
||||
// };
|
||||
|
||||
return [
|
||||
// {
|
||||
// name: "claude-instant-1.2",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-2.0",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-2.1",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-opus-20240229",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-sonnet-20240229",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-haiku-20240307",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
];
|
||||
}
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl: string = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.anthropicUrl;
|
||||
}
|
||||
|
||||
// if endpoint is empty, use default endpoint
|
||||
if (baseUrl.trim().length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
baseUrl = trimEnd(baseUrl, "/");
|
||||
|
||||
// try rebuild url, when using cloudflare ai gateway in client
|
||||
return cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
function trimEnd(s: string, end = " ") {
|
||||
if (end.length === 0) return s;
|
||||
|
||||
while (s.endsWith(end)) {
|
||||
s = s.slice(0, -end.length);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
281
app/client/platforms/baidu.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
Baidu,
|
||||
BAIDU_BASE_URL,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import { getAccessToken } from "@/app/utils/baidu";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
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 { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RequestPayload {
|
||||
messages: {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | MultimodalContent[];
|
||||
}[];
|
||||
stream?: boolean;
|
||||
model: string;
|
||||
temperature: number;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export class ErnieApi implements LLMApi {
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.baiduUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
// do not use proxy for baidubce api
|
||||
baseUrl = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Baidu)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
|
||||
role: v.role === "system" ? "user" : v.role,
|
||||
content: getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",
|
||||
if (messages.length % 2 === 0) {
|
||||
if (messages.at(0)?.role === "user") {
|
||||
messages.splice(1, 0, {
|
||||
role: "assistant",
|
||||
content: " ",
|
||||
});
|
||||
} else {
|
||||
messages.unshift({
|
||||
role: "user",
|
||||
content: " ",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: shouldStream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
};
|
||||
|
||||
console.log("[Request] Baidu payload: ", requestPayload);
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
let chatPath = this.path(Baidu.ChatPath(modelConfig.model));
|
||||
|
||||
// getAccessToken can not run in browser, because cors error
|
||||
if (!!getClientConfig()?.isApp) {
|
||||
const accessStore = useAccessStore.getState();
|
||||
if (accessStore.useCustomConfig) {
|
||||
if (accessStore.isValidBaidu()) {
|
||||
const { access_token } = await getAccessToken(
|
||||
accessStore.baiduApiKey,
|
||||
accessStore.baiduSecretKey,
|
||||
);
|
||||
chatPath = `${chatPath}${
|
||||
chatPath.includes("?") ? "&" : "?"
|
||||
}access_token=${access_token}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
console.log("[Response Animation] finished");
|
||||
if (responseText?.length === 0) {
|
||||
options.onError?.(new Error("empty response from server"));
|
||||
}
|
||||
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) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Baidu] request response content type: ", contentType);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const delta = json?.result;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = resJson?.result;
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
export { Baidu };
|
||||
255
app/client/platforms/bytedance.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
ByteDance,
|
||||
BYTEDANCE_BASE_URL,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
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 { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RequestPayload {
|
||||
messages: {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | MultimodalContent[];
|
||||
}[];
|
||||
stream?: boolean;
|
||||
model: string;
|
||||
temperature: number;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export class DoubaoApi implements LLMApi {
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.bytedanceUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ByteDance)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: shouldStream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(ByteDance.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let 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");
|
||||
if (responseText?.length === 0) {
|
||||
options.onError?.(new Error("empty response from server"));
|
||||
}
|
||||
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) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[ByteDance] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: { content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.delta?.content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
export { ByteDance };
|
||||
@@ -1,15 +1,52 @@
|
||||
import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ApiPath, 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 Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
import { preProcessImageContent } from "@/app/utils/chat";
|
||||
|
||||
export class GeminiProApi implements LLMApi {
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.googleUrl;
|
||||
}
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
if (baseUrl.length === 0) {
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
||||
}
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
let chatPath = [baseUrl, path].join("/");
|
||||
|
||||
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
|
||||
// if chatPath.startsWith('http') then add key in query string
|
||||
if (chatPath.startsWith("http") && accessStore.googleApiKey) {
|
||||
chatPath += `&key=${accessStore.googleApiKey}`;
|
||||
}
|
||||
return chatPath;
|
||||
}
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] gemini-pro response: ", res);
|
||||
|
||||
@@ -20,12 +57,18 @@ export class GeminiProApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
// const apiClient = this;
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const apiClient = this;
|
||||
let multimodal = false;
|
||||
const messages = options.messages.map((v) => {
|
||||
|
||||
// try get base64image from local cache image_url
|
||||
const _messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = await preProcessImageContent(v.content);
|
||||
_messages.push({ role: v.role, content });
|
||||
}
|
||||
const messages = _messages.map((v) => {
|
||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||
if (visionModel) {
|
||||
if (isVisionModel(options.config.model)) {
|
||||
const images = getMessageImages(v);
|
||||
if (images.length > 0) {
|
||||
multimodal = true;
|
||||
@@ -65,6 +108,9 @@ export class GeminiProApi implements LLMApi {
|
||||
// if (visionModel && messages.length > 1) {
|
||||
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
|
||||
// }
|
||||
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
@@ -86,47 +132,30 @@ export class GeminiProApi implements LLMApi {
|
||||
safetySettings: [
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
threshold: accessStore.googleSafetySettings,
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
threshold: accessStore.googleSafetySettings,
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
threshold: accessStore.googleSafetySettings,
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
threshold: accessStore.googleSafetySettings,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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);
|
||||
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
|
||||
const chatPath = this.path(Google.ChatPath(modelConfig.model));
|
||||
|
||||
// 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),
|
||||
@@ -139,15 +168,17 @@ export class GeminiProApi implements LLMApi {
|
||||
() => 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(""));
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
// animate response to make it looks smooth
|
||||
@@ -172,74 +203,83 @@ export class GeminiProApi implements LLMApi {
|
||||
// start animaion
|
||||
animateResponseText();
|
||||
|
||||
fetch(
|
||||
baseUrl.replace("generateContent", "streamGenerateContent"),
|
||||
chatPayload,
|
||||
)
|
||||
.then((response) => {
|
||||
const reader = response?.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let partialData = "";
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[Gemini] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
console.log("Stream complete");
|
||||
// options.onFinish(responseText + remainText);
|
||||
finished = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
partialData += decoder.decode(value, { stream: true });
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
let data = JSON.parse(ensureProperEnding(partialData));
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
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
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
return reader.read().then(processText);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const delta = apiClient.extractMessage(json);
|
||||
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
|
||||
const blockReason = json?.promptFeedback?.blockReason;
|
||||
if (blockReason) {
|
||||
// being blocked
|
||||
console.log(`[Google] [Safety Ratings] result:`, blockReason);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(baseUrl, chatPayload);
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
const resJson = await res.json();
|
||||
if (resJson?.promptFeedback?.blockReason) {
|
||||
@@ -251,7 +291,7 @@ export class GeminiProApi implements LLMApi {
|
||||
),
|
||||
);
|
||||
}
|
||||
const message = this.extractMessage(resJson);
|
||||
const message = apiClient.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -265,14 +305,4 @@ export class GeminiProApi implements LLMApi {
|
||||
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;
|
||||
}
|
||||
|
||||
240
app/client/platforms/iflytek.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
Iflytek,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel } 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 { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||
|
||||
export class SparkApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.iflytekUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Iflytek;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
console.log("[Request] Spark payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(Iflytek.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// Make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
|
||||
// Animate response text to make it look 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 animation
|
||||
animateResponseText();
|
||||
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Spark] request response content type: ", contentType);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
// Handle different error scenarios
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
extraInfo = Locale.Error.Unauthorized;
|
||||
}
|
||||
|
||||
options.onError?.(
|
||||
new Error(
|
||||
`Request failed with status ${res.status}: ${extraInfo}`,
|
||||
),
|
||||
);
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: { content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.delta?.content;
|
||||
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text);
|
||||
options.onError?.(new Error(`Failed to parse response: ${text}`));
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
options.onError?.(
|
||||
new Error(`Request failed with status ${res.status}: ${errorText}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
208
app/client/platforms/moonshot.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
Moonshot,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
|
||||
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 { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||
|
||||
export class MoonshotApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.moonshotUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Moonshot;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(Moonshot.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,30 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
Azure,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import {
|
||||
ChatMessageTool,
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||
import {
|
||||
preProcessImageContent,
|
||||
uploadImage,
|
||||
base64Image2Blob,
|
||||
stream,
|
||||
} from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
@@ -24,11 +41,11 @@ import {
|
||||
} 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,
|
||||
isDalle3 as _isDalle3,
|
||||
} from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
@@ -40,221 +57,273 @@ export interface OpenAIListModelResponse {
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RequestPayload {
|
||||
messages: {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | MultimodalContent[];
|
||||
}[];
|
||||
stream?: boolean;
|
||||
model: string;
|
||||
temperature: number;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export interface DalleRequestPayload {
|
||||
model: string;
|
||||
prompt: string;
|
||||
response_format: "url" | "b64_json";
|
||||
n: number;
|
||||
size: DalleSize;
|
||||
quality: DalleQuality;
|
||||
style: DalleStyle;
|
||||
}
|
||||
|
||||
export class ChatGPTApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
let baseUrl = "";
|
||||
|
||||
if (isAzure && !accessStore.isValidAzure()) {
|
||||
throw Error(
|
||||
"incomplete azure config, please check it in your settings page",
|
||||
);
|
||||
const isAzure = path.includes("deployments");
|
||||
if (accessStore.useCustomConfig) {
|
||||
if (isAzure && !accessStore.isValidAzure()) {
|
||||
throw Error(
|
||||
"incomplete azure config, please check it in your settings page",
|
||||
);
|
||||
}
|
||||
|
||||
baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
|
||||
}
|
||||
|
||||
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;
|
||||
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
|
||||
if (
|
||||
!baseUrl.startsWith("http") &&
|
||||
!isAzure &&
|
||||
!baseUrl.startsWith(ApiPath.OpenAI)
|
||||
) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
if (isAzure) {
|
||||
path = makeAzurePath(path, accessStore.azureApiVersion);
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
// try rebuild url, when using cloudflare ai gateway in client
|
||||
return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
async extractMessage(res: any) {
|
||||
if (res.error) {
|
||||
return "```\n" + JSON.stringify(res, null, 4) + "\n```";
|
||||
}
|
||||
// dalle3 model return url, using url create image message
|
||||
if (res.data) {
|
||||
let url = res.data?.at(0)?.url ?? "";
|
||||
const b64_json = res.data?.at(0)?.b64_json ?? "";
|
||||
if (!url && b64_json) {
|
||||
// uploadImage
|
||||
url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return res.choices?.at(0)?.message?.content ?? res;
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: visionModel ? v.content : getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
let requestPayload: RequestPayload | DalleRequestPayload;
|
||||
|
||||
// add max_tokens to vision model
|
||||
if (visionModel) {
|
||||
Object.defineProperty(requestPayload, "max_tokens", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: modelConfig.max_tokens,
|
||||
});
|
||||
const isDalle3 = _isDalle3(options.config.model);
|
||||
const isO1 = options.config.model.startsWith("o1");
|
||||
if (isDalle3) {
|
||||
const prompt = getMessageTextContent(
|
||||
options.messages.slice(-1)?.pop() as any,
|
||||
);
|
||||
requestPayload = {
|
||||
model: options.config.model,
|
||||
prompt,
|
||||
// URLs are only valid for 60 minutes after the image has been generated.
|
||||
response_format: "b64_json", // using b64_json, and save image in CacheStorage
|
||||
n: 1,
|
||||
size: options.config?.size ?? "1024x1024",
|
||||
quality: options.config?.quality ?? "standard",
|
||||
style: options.config?.style ?? "vivid",
|
||||
};
|
||||
} else {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = visionModel
|
||||
? await preProcessImageContent(v.content)
|
||||
: getMessageTextContent(v);
|
||||
if (!(isO1 && v.role === "system"))
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
|
||||
requestPayload = {
|
||||
messages,
|
||||
stream: !isO1 ? options.config.stream : false,
|
||||
model: modelConfig.model,
|
||||
temperature: !isO1 ? modelConfig.temperature : 1,
|
||||
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
|
||||
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
|
||||
top_p: !isO1 ? modelConfig.top_p : 1,
|
||||
// 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) {
|
||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(OpenaiPath.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
let chatPath = "";
|
||||
if (modelConfig.providerName === ServiceProvider.Azure) {
|
||||
// find model, and get displayName as deployName
|
||||
const { models: configModels, customModels: configCustomModels } =
|
||||
useAppConfig.getState();
|
||||
const {
|
||||
defaultModel,
|
||||
customModels: accessCustomModels,
|
||||
useCustomConfig,
|
||||
} = useAccessStore.getState();
|
||||
const models = collectModelsWithDefaultModel(
|
||||
configModels,
|
||||
[configCustomModels, accessCustomModels].join(","),
|
||||
defaultModel,
|
||||
);
|
||||
const model = models.find(
|
||||
(model) =>
|
||||
model.name === modelConfig.model &&
|
||||
model?.provider?.providerName === ServiceProvider.Azure,
|
||||
);
|
||||
chatPath = this.path(
|
||||
(isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
|
||||
(model?.displayName ?? model?.name) as string,
|
||||
useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chatPath = this.path(
|
||||
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
// console.log("getAsTools", tools, funcs);
|
||||
stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
);
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[OpenAI] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text) as {
|
||||
choices: Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
const delta = json.choices[0]?.delta?.content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
const message = await this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -341,20 +410,26 @@ export class ChatGPTApi implements LLMApi {
|
||||
});
|
||||
|
||||
const resJson = (await res.json()) as OpenAIListModelResponse;
|
||||
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
|
||||
const chatModels = resJson.data?.filter(
|
||||
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
|
||||
);
|
||||
console.log("[Models]", chatModels);
|
||||
|
||||
if (!chatModels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
//由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
|
||||
let seq = 1000; //同 Constant.ts 中的排序保持一致
|
||||
return chatModels.map((m) => ({
|
||||
name: m.id,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
sorted: 1,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
268
app/client/platforms/tencent.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
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 { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import mapKeys from "lodash-es/mapKeys";
|
||||
import mapValues from "lodash-es/mapValues";
|
||||
import isArray from "lodash-es/isArray";
|
||||
import isObject from "lodash-es/isObject";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
data: Array<{
|
||||
id: string;
|
||||
object: string;
|
||||
root: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RequestPayload {
|
||||
Messages: {
|
||||
Role: "system" | "user" | "assistant";
|
||||
Content: string | MultimodalContent[];
|
||||
}[];
|
||||
Stream?: boolean;
|
||||
Model: string;
|
||||
Temperature: number;
|
||||
TopP: number;
|
||||
}
|
||||
|
||||
function capitalizeKeys(obj: any): any {
|
||||
if (isArray(obj)) {
|
||||
return obj.map(capitalizeKeys);
|
||||
} else if (isObject(obj)) {
|
||||
return mapValues(
|
||||
mapKeys(obj, (value: any, key: string) =>
|
||||
key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
|
||||
),
|
||||
capitalizeKeys,
|
||||
);
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export class HunyuanApi implements LLMApi {
|
||||
path(): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.tencentUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl);
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.Choices?.at(0)?.Message?.Content ?? "";
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages = options.messages.map((v, index) => ({
|
||||
// "Messages 中 system 角色必须位于列表的最开始"
|
||||
role: index !== 0 && v.role === "system" ? "user" : v.role,
|
||||
content: visionModel ? v.content : getMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = capitalizeKeys({
|
||||
model: modelConfig.model,
|
||||
messages,
|
||||
temperature: modelConfig.temperature,
|
||||
top_p: modelConfig.top_p,
|
||||
stream: options.config.stream,
|
||||
});
|
||||
|
||||
console.log("[Request] Tencent payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path();
|
||||
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;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
console.log("[Response Animation] finished");
|
||||
if (responseText?.length === 0) {
|
||||
options.onError?.(new Error("empty response from server"));
|
||||
}
|
||||
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) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[Tencent] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.Choices as Array<{
|
||||
Delta: { Content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.Delta?.Content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,16 @@ interface ChatCommands {
|
||||
del?: Command;
|
||||
}
|
||||
|
||||
export const ChatCommandPrefix = ":";
|
||||
// Compatible with Chinese colon character ":"
|
||||
export const ChatCommandPrefix = /^[::]/;
|
||||
|
||||
export function useChatCommand(commands: ChatCommands = {}) {
|
||||
function extract(userInput: string) {
|
||||
return (
|
||||
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
||||
) as keyof ChatCommands;
|
||||
const match = userInput.match(ChatCommandPrefix);
|
||||
if (match) {
|
||||
return userInput.slice(1) as keyof ChatCommands;
|
||||
}
|
||||
return userInput as keyof ChatCommands;
|
||||
}
|
||||
|
||||
function search(userInput: string) {
|
||||
@@ -57,7 +60,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
|
||||
.filter((c) => c.startsWith(input))
|
||||
.map((c) => ({
|
||||
title: desc[c as keyof ChatCommands],
|
||||
content: ChatCommandPrefix + c,
|
||||
content: ":" + c,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
31
app/components/artifacts.module.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.artifacts {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 20px;
|
||||
background: var(--second);
|
||||
}
|
||||
&-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
&-content {
|
||||
flex-grow: 1;
|
||||
padding: 0 20px 20px 20px;
|
||||
background-color: var(--second);
|
||||
}
|
||||
}
|
||||
|
||||
.artifacts-iframe {
|
||||
width: 100%;
|
||||
border: var(--border-in-light);
|
||||
border-radius: 6px;
|
||||
background-color: var(--gray);
|
||||
}
|
||||
267
app/components/artifacts.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useWindowSize } from "@/app/utils";
|
||||
import { IconButton } from "./button";
|
||||
import { nanoid } from "nanoid";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import LoadingButtonIcon from "../icons/loading.svg";
|
||||
import ReloadButtonIcon from "../icons/reload.svg";
|
||||
import Locale from "../locales";
|
||||
import { Modal, showToast } from "./ui-lib";
|
||||
import { copyToClipboard, downloadAs } from "../utils";
|
||||
import { Path, ApiPath, REPO_URL } from "@/app/constant";
|
||||
import { Loading } from "./home";
|
||||
import styles from "./artifacts.module.scss";
|
||||
|
||||
type HTMLPreviewProps = {
|
||||
code: string;
|
||||
autoHeight?: boolean;
|
||||
height?: number | string;
|
||||
onLoad?: (title?: string) => void;
|
||||
};
|
||||
|
||||
export type HTMLPreviewHander = {
|
||||
reload: () => void;
|
||||
};
|
||||
|
||||
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
|
||||
function HTMLPreview(props, ref) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [frameId, setFrameId] = useState<string>(nanoid());
|
||||
const [iframeHeight, setIframeHeight] = useState(600);
|
||||
const [title, setTitle] = useState("");
|
||||
/*
|
||||
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
||||
* 1. using srcdoc
|
||||
* 2. using src with dataurl:
|
||||
* easy to share
|
||||
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (e: any) => {
|
||||
const { id, height, title } = e.data;
|
||||
setTitle(title);
|
||||
if (id == frameId) {
|
||||
setIframeHeight(height);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [frameId]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reload: () => {
|
||||
setFrameId(nanoid());
|
||||
},
|
||||
}));
|
||||
|
||||
const height = useMemo(() => {
|
||||
if (!props.autoHeight) return props.height || 600;
|
||||
if (typeof props.height === "string") {
|
||||
return props.height;
|
||||
}
|
||||
const parentHeight = props.height || 600;
|
||||
return iframeHeight + 40 > parentHeight
|
||||
? parentHeight
|
||||
: iframeHeight + 40;
|
||||
}, [props.autoHeight, props.height, iframeHeight]);
|
||||
|
||||
const srcDoc = useMemo(() => {
|
||||
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
|
||||
if (props.code.includes("<!DOCTYPE html>")) {
|
||||
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
|
||||
}
|
||||
return script + props.code;
|
||||
}, [props.code, frameId]);
|
||||
|
||||
const handleOnLoad = () => {
|
||||
if (props?.onLoad) {
|
||||
props.onLoad(title);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className={styles["artifacts-iframe"]}
|
||||
key={frameId}
|
||||
ref={iframeRef}
|
||||
sandbox="allow-forms allow-modals allow-scripts"
|
||||
style={{ height }}
|
||||
srcDoc={srcDoc}
|
||||
onLoad={handleOnLoad}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function ArtifactsShareButton({
|
||||
getCode,
|
||||
id,
|
||||
style,
|
||||
fileName,
|
||||
}: {
|
||||
getCode: () => string;
|
||||
id?: string;
|
||||
style?: any;
|
||||
fileName?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [name, setName] = useState(id);
|
||||
const [show, setShow] = useState(false);
|
||||
const shareUrl = useMemo(
|
||||
() => [location.origin, "#", Path.Artifacts, "/", name].join(""),
|
||||
[name],
|
||||
);
|
||||
const upload = (code: string) =>
|
||||
id
|
||||
? Promise.resolve({ id })
|
||||
: fetch(ApiPath.Artifacts, {
|
||||
method: "POST",
|
||||
body: code,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ id }) => {
|
||||
if (id) {
|
||||
return { id };
|
||||
}
|
||||
throw Error();
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast(Locale.Export.Artifacts.Error);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="window-action-button" style={style}>
|
||||
<IconButton
|
||||
icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
|
||||
bordered
|
||||
title={Locale.Export.Artifacts.Title}
|
||||
onClick={() => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
upload(getCode())
|
||||
.then((res) => {
|
||||
if (res?.id) {
|
||||
setShow(true);
|
||||
setName(res?.id);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{show && (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Export.Artifacts.Title}
|
||||
onClose={() => setShow(false)}
|
||||
actions={[
|
||||
<IconButton
|
||||
key="download"
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
text={Locale.Export.Download}
|
||||
onClick={() => {
|
||||
downloadAs(getCode(), `${fileName || name}.html`).then(() =>
|
||||
setShow(false),
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<IconButton
|
||||
key="copy"
|
||||
icon={<CopyIcon />}
|
||||
bordered
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl).then(() => setShow(false));
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<a target="_blank" href={shareUrl}>
|
||||
{shareUrl}
|
||||
</a>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Artifacts() {
|
||||
const { id } = useParams();
|
||||
const [code, setCode] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fileName, setFileName] = useState("");
|
||||
const previewRef = useRef<HTMLPreviewHander>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetch(`${ApiPath.Artifacts}?id=${id}`)
|
||||
.then((res) => {
|
||||
if (res.status > 300) {
|
||||
throw Error("can not get content");
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then(setCode)
|
||||
.catch((e) => {
|
||||
showToast(Locale.Export.Artifacts.Error);
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className={styles["artifacts"]}>
|
||||
<div className={styles["artifacts-header"]}>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton bordered icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
<IconButton
|
||||
bordered
|
||||
style={{ marginLeft: 20 }}
|
||||
icon={<ReloadButtonIcon />}
|
||||
shadow
|
||||
onClick={() => previewRef.current?.reload()}
|
||||
/>
|
||||
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
|
||||
<ArtifactsShareButton
|
||||
id={id}
|
||||
getCode={() => code}
|
||||
fileName={fileName}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["artifacts-content"]}>
|
||||
{loading && <Loading />}
|
||||
{code && (
|
||||
<HTMLPreview
|
||||
code={code}
|
||||
ref={previewRef}
|
||||
autoHeight={false}
|
||||
height={"100%"}
|
||||
onLoad={(title) => {
|
||||
setFileName(title as string);
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import styles from "./button.module.scss";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
@@ -16,6 +17,8 @@ export function IconButton(props: {
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
style?: CSSProperties;
|
||||
aria?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
@@ -31,9 +34,12 @@ export function IconButton(props: {
|
||||
role="button"
|
||||
tabIndex={props.tabIndex}
|
||||
autoFocus={props.autoFocus}
|
||||
style={props.style}
|
||||
aria-label={props.aria}
|
||||
>
|
||||
{props.icon && (
|
||||
<div
|
||||
aria-label={props.text || props.title}
|
||||
className={
|
||||
styles["icon-button-icon"] +
|
||||
` ${props.type === "primary" && "no-dark"}`
|
||||
@@ -44,7 +50,12 @@ export function IconButton(props: {
|
||||
)}
|
||||
|
||||
{props.text && (
|
||||
<div className={styles["icon-button-text"]}>{props.text}</div>
|
||||
<div
|
||||
aria-label={props.text || props.title}
|
||||
className={styles["icon-button-text"]}
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
@@ -40,12 +40,16 @@ export function ChatItem(props: {
|
||||
});
|
||||
}
|
||||
}, [props.selected]);
|
||||
|
||||
const { pathname: currentPath } = useLocation();
|
||||
return (
|
||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={`${styles["chat-item"]} ${
|
||||
props.selected && styles["chat-item-selected"]
|
||||
props.selected &&
|
||||
(currentPath === Path.Chat || currentPath === Path.Home) &&
|
||||
styles["chat-item-selected"]
|
||||
}`}
|
||||
onClick={props.onClick}
|
||||
ref={(ele) => {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: width ease 0.3s;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
height: 16px;
|
||||
width: var(--icon-width);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
transition: all ease 0.3s;
|
||||
@@ -345,6 +346,12 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-model-name {
|
||||
font-size: 12px;
|
||||
color: var(--black);
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
@@ -406,6 +413,21 @@
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat-message-tools {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
margin-top: 5px;
|
||||
.chat-message-tool {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
svg {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-item {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
@@ -609,6 +631,10 @@
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
@@ -619,4 +645,52 @@
|
||||
.chat-input-send {
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key-container {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-key-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shortcut-key-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.shortcut-key-title {
|
||||
font-size: 14px;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.shortcut-key-keys {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--border-in-light);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
background-color: var(--gray);
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.shortcut-key span {
|
||||
font-size: 12px;
|
||||
color: var(--black);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import DeleteIcon from "../icons/clear.svg";
|
||||
import PinIcon from "../icons/pin.svg";
|
||||
import EditIcon from "../icons/rename.svg";
|
||||
import ConfirmIcon from "../icons/confirm.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import CancelIcon from "../icons/cancel.svg";
|
||||
import ImageIcon from "../icons/image.svg";
|
||||
|
||||
@@ -37,6 +38,11 @@ import AutoIcon from "../icons/auto.svg";
|
||||
import BottomIcon from "../icons/bottom.svg";
|
||||
import StopIcon from "../icons/pause.svg";
|
||||
import RobotIcon from "../icons/robot.svg";
|
||||
import SizeIcon from "../icons/size.svg";
|
||||
import QualityIcon from "../icons/hd.svg";
|
||||
import StyleIcon from "../icons/palette.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
@@ -49,6 +55,7 @@ import {
|
||||
useAppConfig,
|
||||
DEFAULT_TOPIC,
|
||||
ModelType,
|
||||
usePluginStore,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@@ -59,12 +66,17 @@ import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
compressImage,
|
||||
isDalle3,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
} from "../utils";
|
||||
|
||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
|
||||
import { Prompt, usePromptStore } from "../store/prompt";
|
||||
import Locale from "../locales";
|
||||
|
||||
@@ -87,6 +99,7 @@ import {
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||
@@ -97,7 +110,9 @@ import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
import SpeechRecorder from "./chat/speechRecorder";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
@@ -177,7 +192,7 @@ function PromptToast(props: {
|
||||
|
||||
return (
|
||||
<div className={styles["prompt-toast"]} key="prompt-toast">
|
||||
{props.showToast && (
|
||||
{props.showToast && context.length > 0 && (
|
||||
<div
|
||||
className={styles["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
@@ -243,11 +258,11 @@ function useSubmitHandler() {
|
||||
};
|
||||
}
|
||||
|
||||
export type RenderPompt = Pick<Prompt, "title" | "content">;
|
||||
export type RenderPrompt = Pick<Prompt, "title" | "content">;
|
||||
|
||||
export function PromptHints(props: {
|
||||
prompts: RenderPompt[];
|
||||
onPromptSelect: (prompt: RenderPompt) => void;
|
||||
prompts: RenderPrompt[];
|
||||
onPromptSelect: (prompt: RenderPrompt) => void;
|
||||
}) {
|
||||
const noPrompts = props.prompts.length === 0;
|
||||
const [selectIndex, setSelectIndex] = useState(0);
|
||||
@@ -336,7 +351,7 @@ function ClearContextDivider() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatAction(props: {
|
||||
export function ChatAction(props: {
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
@@ -347,7 +362,7 @@ function ChatAction(props: {
|
||||
full: 16,
|
||||
icon: 16,
|
||||
});
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
function updateWidth() {
|
||||
if (!iconRef.current || !textRef.current) return;
|
||||
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
|
||||
@@ -361,22 +376,25 @@ function ChatAction(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles["chat-input-action"]} clickable group`}
|
||||
className={`${styles["chat-input-action"]} clickable`}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setTimeout(updateWidth, 1);
|
||||
}}
|
||||
onMouseEnter={updateWidth}
|
||||
onTouchStart={updateWidth}
|
||||
style={
|
||||
{
|
||||
"--icon-width": `${width.icon}px`,
|
||||
"--full-width": `${width.full}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles["text"]} transition-all duration-1000 w-0 group-hover:w-[60px]`}
|
||||
ref={textRef}
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className={styles["text"]} ref={textRef}>
|
||||
{props.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -421,13 +439,14 @@ export function ChatActions(props: {
|
||||
showPromptModal: () => void;
|
||||
scrollToBottom: () => void;
|
||||
showPromptHints: () => void;
|
||||
setUserInput: (text: string) => void;
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
const chatStore = useChatStore();
|
||||
const pluginStore = usePluginStore();
|
||||
|
||||
// switch themes
|
||||
const theme = config.theme;
|
||||
@@ -445,14 +464,51 @@ export function ChatActions(props: {
|
||||
|
||||
// switch model
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
const currentProviderName =
|
||||
chatStore.currentSession().mask.modelConfig?.providerName ||
|
||||
ServiceProvider.OpenAI;
|
||||
const allModels = useAllModels();
|
||||
const models = useMemo(
|
||||
() => allModels.filter((m) => m.available),
|
||||
[allModels],
|
||||
);
|
||||
const models = useMemo(() => {
|
||||
const filteredModels = allModels.filter((m) => m.available);
|
||||
const defaultModel = filteredModels.find((m) => m.isDefault);
|
||||
|
||||
if (defaultModel) {
|
||||
const arr = [
|
||||
defaultModel,
|
||||
...filteredModels.filter((m) => m !== defaultModel),
|
||||
];
|
||||
return arr;
|
||||
} else {
|
||||
return filteredModels;
|
||||
}
|
||||
}, [allModels]);
|
||||
const currentModelName = useMemo(() => {
|
||||
const model = models.find(
|
||||
(m) =>
|
||||
m.name == currentModel &&
|
||||
m?.provider?.providerName == currentProviderName,
|
||||
);
|
||||
return model?.displayName ?? "";
|
||||
}, [models, currentModel, currentProviderName]);
|
||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||
const [showUploadImage, setShowUploadImage] = useState(false);
|
||||
|
||||
const [showSizeSelector, setShowSizeSelector] = useState(false);
|
||||
const [showQualitySelector, setShowQualitySelector] = useState(false);
|
||||
const [showStyleSelector, setShowStyleSelector] = useState(false);
|
||||
const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
|
||||
const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
|
||||
const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
|
||||
const currentSize =
|
||||
chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
|
||||
const currentQuality =
|
||||
chatStore.currentSession().mask.modelConfig?.quality ?? "standard";
|
||||
const currentStyle =
|
||||
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const show = isVisionModel(currentModel);
|
||||
setShowUploadImage(show);
|
||||
@@ -465,11 +521,18 @@ export function ChatActions(props: {
|
||||
// switch to first available model
|
||||
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||
if (isUnavaliableModel && models.length > 0) {
|
||||
const nextModel = models[0].name as ModelType;
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.mask.modelConfig.model = nextModel),
|
||||
// show next model to default model if exist
|
||||
let nextModel = models.find((model) => model.isDefault) || models[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.model = nextModel.name;
|
||||
session.mask.modelConfig.providerName = nextModel?.provider
|
||||
?.providerName as ServiceProvider;
|
||||
});
|
||||
showToast(
|
||||
nextModel?.provider?.providerName == "ByteDance"
|
||||
? nextModel.displayName
|
||||
: nextModel.name,
|
||||
);
|
||||
showToast(nextModel);
|
||||
}
|
||||
}, [chatStore, currentModel, models]);
|
||||
|
||||
@@ -551,28 +614,162 @@ export function ChatActions(props: {
|
||||
|
||||
<ChatAction
|
||||
onClick={() => setShowModelSelector(true)}
|
||||
text={currentModel}
|
||||
text={currentModelName}
|
||||
icon={<RobotIcon />}
|
||||
/>
|
||||
|
||||
{showModelSelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={currentModel}
|
||||
defaultSelectedValue={`${currentModel}@${currentProviderName}`}
|
||||
items={models.map((m) => ({
|
||||
title: m.displayName,
|
||||
value: m.name,
|
||||
title: `${m.displayName}${
|
||||
m?.provider?.providerName
|
||||
? "(" + m?.provider?.providerName + ")"
|
||||
: ""
|
||||
}`,
|
||||
value: `${m.name}@${m?.provider?.providerName}`,
|
||||
}))}
|
||||
onClose={() => setShowModelSelector(false)}
|
||||
onSelection={(s) => {
|
||||
if (s.length === 0) return;
|
||||
const [model, providerName] = s[0].split("@");
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.model = s[0] as ModelType;
|
||||
session.mask.modelConfig.model = model as ModelType;
|
||||
session.mask.modelConfig.providerName =
|
||||
providerName as ServiceProvider;
|
||||
session.mask.syncGlobalConfig = false;
|
||||
});
|
||||
showToast(s[0]);
|
||||
if (providerName == "ByteDance") {
|
||||
const selectedModel = models.find(
|
||||
(m) =>
|
||||
m.name == model && m?.provider?.providerName == providerName,
|
||||
);
|
||||
showToast(selectedModel?.displayName ?? "");
|
||||
} else {
|
||||
showToast(model);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDalle3(currentModel) && (
|
||||
<ChatAction
|
||||
onClick={() => setShowSizeSelector(true)}
|
||||
text={currentSize}
|
||||
icon={<SizeIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSizeSelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={currentSize}
|
||||
items={dalle3Sizes.map((m) => ({
|
||||
title: m,
|
||||
value: m,
|
||||
}))}
|
||||
onClose={() => setShowSizeSelector(false)}
|
||||
onSelection={(s) => {
|
||||
if (s.length === 0) return;
|
||||
const size = s[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.size = size;
|
||||
});
|
||||
showToast(size);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDalle3(currentModel) && (
|
||||
<ChatAction
|
||||
onClick={() => setShowQualitySelector(true)}
|
||||
text={currentQuality}
|
||||
icon={<QualityIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showQualitySelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={currentQuality}
|
||||
items={dalle3Qualitys.map((m) => ({
|
||||
title: m,
|
||||
value: m,
|
||||
}))}
|
||||
onClose={() => setShowQualitySelector(false)}
|
||||
onSelection={(q) => {
|
||||
if (q.length === 0) return;
|
||||
const quality = q[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.quality = quality;
|
||||
});
|
||||
showToast(quality);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDalle3(currentModel) && (
|
||||
<ChatAction
|
||||
onClick={() => setShowStyleSelector(true)}
|
||||
text={currentStyle}
|
||||
icon={<StyleIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showStyleSelector && (
|
||||
<Selector
|
||||
defaultSelectedValue={currentStyle}
|
||||
items={dalle3Styles.map((m) => ({
|
||||
title: m,
|
||||
value: m,
|
||||
}))}
|
||||
onClose={() => setShowStyleSelector(false)}
|
||||
onSelection={(s) => {
|
||||
if (s.length === 0) return;
|
||||
const style = s[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.style = style;
|
||||
});
|
||||
showToast(style);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPlugins(currentProviderName, currentModel) && (
|
||||
<ChatAction
|
||||
onClick={() => {
|
||||
if (pluginStore.getAll().length == 0) {
|
||||
navigate(Path.Plugins);
|
||||
} else {
|
||||
setShowPluginSelector(true);
|
||||
}
|
||||
}}
|
||||
text={Locale.Plugin.Name}
|
||||
icon={<PluginIcon />}
|
||||
/>
|
||||
)}
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
multiple
|
||||
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
||||
items={pluginStore.getAll().map((item) => ({
|
||||
title: `${item?.title}@${item?.version}`,
|
||||
value: item?.id,
|
||||
}))}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onSelection={(s) => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.plugin = s as string[];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMobileScreen && (
|
||||
<ChatAction
|
||||
onClick={() => props.setShowShortcutKeyModal(true)}
|
||||
text={Locale.Chat.ShortcutKey.Title}
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -647,6 +844,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutKeyModal(props: { onClose: () => void }) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const shortcuts = [
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.newChat,
|
||||
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
|
||||
},
|
||||
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastCode,
|
||||
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastMessage,
|
||||
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.showShortcutKey,
|
||||
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Chat.ShortcutKey.Title}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
<IconButton
|
||||
type="primary"
|
||||
text={Locale.UI.Confirm}
|
||||
icon={<ConfirmIcon />}
|
||||
key="ok"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div className={styles["shortcut-key-container"]}>
|
||||
<div className={styles["shortcut-key-grid"]}>
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div key={index} className={styles["shortcut-key-item"]}>
|
||||
<div className={styles["shortcut-key-title"]}>
|
||||
{shortcut.title}
|
||||
</div>
|
||||
<div className={styles["shortcut-key-keys"]}>
|
||||
{shortcut.keys.map((key, i) => (
|
||||
<div key={i} className={styles["shortcut-key"]}>
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _Chat() {
|
||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
@@ -654,6 +912,7 @@ function _Chat() {
|
||||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
const fontSize = config.fontSize;
|
||||
const fontFamily = config.fontFamily;
|
||||
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
|
||||
@@ -680,7 +939,7 @@ function _Chat() {
|
||||
|
||||
// prompt hints
|
||||
const promptStore = usePromptStore();
|
||||
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
|
||||
const [promptHints, setPromptHints] = useState<RenderPrompt[]>([]);
|
||||
const onSearch = useDebouncedCallback(
|
||||
(text: string) => {
|
||||
const matchedPrompts = promptStore.search(text);
|
||||
@@ -733,7 +992,7 @@ function _Chat() {
|
||||
// clear search results
|
||||
if (n === 0) {
|
||||
setPromptHints([]);
|
||||
} else if (text.startsWith(ChatCommandPrefix)) {
|
||||
} else if (text.match(ChatCommandPrefix)) {
|
||||
setPromptHints(chatCommands.search(text));
|
||||
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
// check if need to trigger auto completion
|
||||
@@ -758,14 +1017,14 @@ function _Chat() {
|
||||
.onUserInput(userInput, attachImages)
|
||||
.then(() => setIsLoading(false));
|
||||
setAttachImages([]);
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
chatStore.setLastInput(userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
setAutoScroll(true);
|
||||
};
|
||||
|
||||
const onPromptSelect = (prompt: RenderPompt) => {
|
||||
const onPromptSelect = (prompt: RenderPrompt) => {
|
||||
setTimeout(() => {
|
||||
setPromptHints([]);
|
||||
|
||||
@@ -824,7 +1083,7 @@ function _Chat() {
|
||||
userInput.length <= 0 &&
|
||||
!(e.metaKey || e.altKey || e.ctrlKey)
|
||||
) {
|
||||
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
||||
setUserInput(chatStore.lastInput ?? "");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -1073,6 +1332,7 @@ function _Chat() {
|
||||
if (payload.url) {
|
||||
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
||||
}
|
||||
accessStore.update((access) => (access.useCustomConfig = true));
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -1100,11 +1360,13 @@ function _Chat() {
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
const handlePaste = useCallback(
|
||||
async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
if(!isVisionModel(currentModel)){return;}
|
||||
if (!isVisionModel(currentModel)) {
|
||||
return;
|
||||
}
|
||||
const items = (event.clipboardData || window.clipboardData).items;
|
||||
for (const item of items) {
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
@@ -1117,7 +1379,7 @@ function _Chat() {
|
||||
...(await new Promise<string[]>((res, rej) => {
|
||||
setUploading(true);
|
||||
const imagesData: string[] = [];
|
||||
compressImage(file, 256 * 1024)
|
||||
uploadImageRemote(file)
|
||||
.then((dataUrl) => {
|
||||
imagesData.push(dataUrl);
|
||||
setUploading(false);
|
||||
@@ -1159,7 +1421,7 @@ function _Chat() {
|
||||
const imagesData: string[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = event.target.files[i];
|
||||
compressImage(file, 256 * 1024)
|
||||
uploadImageRemote(file)
|
||||
.then((dataUrl) => {
|
||||
imagesData.push(dataUrl);
|
||||
if (
|
||||
@@ -1187,6 +1449,70 @@ function _Chat() {
|
||||
setAttachImages(images);
|
||||
}
|
||||
|
||||
// 快捷键 shortcut keys
|
||||
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
// 打开新聊天 command + shift + o
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "o"
|
||||
) {
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
chatStore.newSession();
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
}
|
||||
// 聚焦聊天输入 shift + esc
|
||||
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
// 复制最后一个代码块 command + shift + ;
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.code === "Semicolon"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const copyCodeButton =
|
||||
document.querySelectorAll<HTMLElement>(".copy-code-button");
|
||||
if (copyCodeButton.length > 0) {
|
||||
copyCodeButton[copyCodeButton.length - 1].click();
|
||||
}
|
||||
}
|
||||
// 复制最后一个回复 command + shift + c
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "c"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const lastNonUserMessage = messages
|
||||
.filter((message) => message.role !== "user")
|
||||
.pop();
|
||||
if (lastNonUserMessage) {
|
||||
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
|
||||
copyToClipboard(lastMessageContent);
|
||||
}
|
||||
}
|
||||
// 展示快捷键 command + /
|
||||
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
setShowShortcutKeyModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [messages, chatStore, navigate]);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1220,6 +1546,8 @@ function _Chat() {
|
||||
<IconButton
|
||||
icon={<RenameIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.EditMessage.Title}
|
||||
aria={Locale.Chat.EditMessage.Title}
|
||||
onClick={() => setIsEditingMessage(true)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1239,6 +1567,8 @@ function _Chat() {
|
||||
<IconButton
|
||||
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.FullScreen}
|
||||
aria={Locale.Chat.Actions.FullScreen}
|
||||
onClick={() => {
|
||||
config.update(
|
||||
(config) => (config.tightBorder = !config.tightBorder),
|
||||
@@ -1290,6 +1620,7 @@ function _Chat() {
|
||||
<div className={styles["chat-message-edit"]}>
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria={Locale.Chat.Actions.Edit}
|
||||
onClick={async () => {
|
||||
const newMessage = await showPrompt(
|
||||
Locale.Chat.Actions.Edit,
|
||||
@@ -1338,6 +1669,11 @@ function _Chat() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isUser && (
|
||||
<div className={styles["chat-model-name"]}>
|
||||
{message.model}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showActions && (
|
||||
<div className={styles["chat-message-actions"]}>
|
||||
@@ -1382,25 +1718,47 @@ function _Chat() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showTyping && (
|
||||
{message?.tools?.length == 0 && showTyping && (
|
||||
<div className={styles["chat-message-status"]}>
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)}
|
||||
{/*@ts-ignore*/}
|
||||
{message?.tools?.length > 0 && (
|
||||
<div className={styles["chat-message-tools"]}>
|
||||
{message?.tools?.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className={styles["chat-message-tool"]}
|
||||
>
|
||||
{tool.isError === false ? (
|
||||
<ConfirmIcon />
|
||||
) : tool.isError === true ? (
|
||||
<CloseIcon />
|
||||
) : (
|
||||
<LoadingButtonIcon />
|
||||
)}
|
||||
<span>{tool?.function?.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
<Markdown
|
||||
key={message.streaming ? "loading" : "done"}
|
||||
content={getMessageTextContent(message)}
|
||||
loading={
|
||||
(message.preview || message.streaming) &&
|
||||
message.content.length === 0 &&
|
||||
!isUser
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
// onContextMenu={(e) => onRightClick(e, message)} // hard to use
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen) return;
|
||||
setUserInput(getMessageTextContent(message));
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
fontFamily={fontFamily}
|
||||
parentRef={scrollRef}
|
||||
defaultShow={i >= messages.length - 6}
|
||||
/>
|
||||
@@ -1460,7 +1818,6 @@ function _Chat() {
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
uploading={uploading}
|
||||
setUserInput={setUserInput}
|
||||
showPromptHints={() => {
|
||||
// Click again to close
|
||||
if (promptHints.length > 0) {
|
||||
@@ -1472,6 +1829,7 @@ function _Chat() {
|
||||
setUserInput("/");
|
||||
onSearch("");
|
||||
}}
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
/>
|
||||
<label
|
||||
className={`${styles["chat-input-panel-inner"]} ${
|
||||
@@ -1496,6 +1854,7 @@ function _Chat() {
|
||||
autoFocus={autoFocus}
|
||||
style={{
|
||||
fontSize: config.fontSize,
|
||||
fontFamily: config.fontFamily,
|
||||
}}
|
||||
/>
|
||||
{attachImages.length != 0 && (
|
||||
@@ -1521,19 +1880,13 @@ function _Chat() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 absolute left-[30px] bottom-[32px]">
|
||||
<SpeechRecorder textUpdater={setUserInput}></SpeechRecorder>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 absolute right-[30px] bottom-[32px]">
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1548,6 +1901,10 @@ function _Chat() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showShortcutKeyModal && (
|
||||
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
||||
if (props.model) {
|
||||
return (
|
||||
<div className="no-dark">
|
||||
{props.model?.startsWith("gpt-4") ? (
|
||||
{props.model?.startsWith("gpt-4") ||
|
||||
props.model?.startsWith("chatgpt-4o") ? (
|
||||
<BlackBotIcon className="user-avatar" />
|
||||
) : (
|
||||
<BotIcon className="user-avatar" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { IconButton } from "./button";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
@@ -6,6 +8,7 @@ import { ISSUE_URL } from "../constant";
|
||||
import Locale from "../locales";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
import { useChatStore } from "../store/chat";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -28,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
try {
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
useChatStore.getState().clearAllData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ 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 { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ClientApi } from "../client/api";
|
||||
import { type ClientApi, getClientApi } from "../client/api";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
@@ -312,12 +312,7 @@ export function PreviewActions(props: {
|
||||
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);
|
||||
}
|
||||
const api: ClientApi = getClientApi(config.modelConfig.providerName);
|
||||
|
||||
api
|
||||
.share(msgs)
|
||||
@@ -546,7 +541,7 @@ export function ImagePreviewer(props: {
|
||||
<div>
|
||||
<div className={styles["main-title"]}>NextChat</div>
|
||||
<div className={styles["sub-title"]}>
|
||||
github.com/Yidadaa/ChatGPT-Next-Web
|
||||
github.com/ChatGPTNextWeb/ChatGPT-Next-Web
|
||||
</div>
|
||||
<div className={styles["icons"]}>
|
||||
<ExportAvatar avatar={config.avatar} />
|
||||
@@ -588,6 +583,7 @@ export function ImagePreviewer(props: {
|
||||
<Markdown
|
||||
content={getMessageTextContent(m)}
|
||||
fontSize={config.fontSize}
|
||||
fontFamily={config.fontFamily}
|
||||
defaultShow
|
||||
/>
|
||||
{getMessageImages(m).length == 1 && (
|
||||
|
||||
@@ -137,12 +137,18 @@
|
||||
position: relative;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 18px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.sidebar-title-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
|
||||
@@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import { getCSSVar, useMobileScreen } from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { ModelProvider, Path, SlotID } from "../constant";
|
||||
import { Path, SlotID } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import { getISOLang, getLang } from "../locales";
|
||||
@@ -27,7 +27,7 @@ import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ClientApi } from "../client/api";
|
||||
import { type ClientApi, getClientApi } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
@@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
@@ -55,6 +59,21 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const SearchChat = dynamic(
|
||||
async () => (await import("./search-chat")).SearchChatPage,
|
||||
{
|
||||
loading: () => <Loading noLogo />,
|
||||
},
|
||||
);
|
||||
|
||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
export function useSwitchTheme() {
|
||||
const config = useAppConfig();
|
||||
|
||||
@@ -122,11 +141,23 @@ const loadAsyncGoogleFont = () => {
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
export function WindowContent(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
||||
{props?.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Screen() {
|
||||
const config = useAppConfig();
|
||||
const location = useLocation();
|
||||
const isArtifact = location.pathname.includes(Path.Artifacts);
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isAuth = location.pathname === Path.Auth;
|
||||
const isSd = location.pathname === Path.Sd;
|
||||
const isSdNew = location.pathname === Path.SdNew;
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldTightBorder =
|
||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||
@@ -135,34 +166,42 @@ function Screen() {
|
||||
loadAsyncGoogleFont();
|
||||
}, []);
|
||||
|
||||
if (isArtifact) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/artifacts/:id" element={<Artifacts />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
const renderContent = () => {
|
||||
if (isAuth) return <AuthPage />;
|
||||
if (isSd) return <Sd />;
|
||||
if (isSdNew) return <Sd />;
|
||||
return (
|
||||
<>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
<WindowContent>
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={<Chat />} />
|
||||
<Route path={Path.NewChat} element={<NewChat />} />
|
||||
<Route path={Path.Masks} element={<MaskPage />} />
|
||||
<Route path={Path.Plugins} element={<PluginPage />} />
|
||||
<Route path={Path.SearchChat} element={<SearchChat />} />
|
||||
<Route path={Path.Chat} element={<Chat />} />
|
||||
<Route path={Path.Settings} element={<Settings />} />
|
||||
</Routes>
|
||||
</WindowContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles.container +
|
||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
||||
}`
|
||||
}
|
||||
className={`${styles.container} ${
|
||||
shouldTightBorder ? styles["tight-container"] : styles.container
|
||||
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -170,12 +209,8 @@ function Screen() {
|
||||
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);
|
||||
}
|
||||
const api: ClientApi = getClientApi(config.modelConfig.providerName);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const models = await api.llm.models();
|
||||
|
||||
@@ -9,6 +9,7 @@ interface InputRangeProps {
|
||||
min: string;
|
||||
max: string;
|
||||
step: string;
|
||||
aria: string;
|
||||
}
|
||||
|
||||
export function InputRange({
|
||||
@@ -19,11 +20,13 @@ export function InputRange({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
aria,
|
||||
}: InputRangeProps) {
|
||||
return (
|
||||
<div className={styles["input-range"] + ` ${className ?? ""}`}>
|
||||
{title || value}
|
||||
<input
|
||||
aria-label={aria}
|
||||
type="range"
|
||||
title={title}
|
||||
value={value}
|
||||
|
||||
@@ -6,13 +6,21 @@ import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import { copyToClipboard, useWindowSize } from "../utils";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
import Locale from "../locales";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import ReloadButtonIcon from "../icons/reload.svg";
|
||||
import React from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { showImageModal } from "./ui-lib";
|
||||
import { showImageModal, FullScreen } from "./ui-lib";
|
||||
import {
|
||||
ArtifactsShareButton,
|
||||
HTMLPreview,
|
||||
HTMLPreviewHander,
|
||||
} from "./artifacts";
|
||||
import { useChatStore } from "../store";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -62,27 +70,59 @@ export function Mermaid(props: { code: string }) {
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const refText = ref.current?.innerText;
|
||||
const previewRef = useRef<HTMLPreviewHander>(null);
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
const [htmlCode, setHtmlCode] = useState("");
|
||||
const { height } = useWindowSize();
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
|
||||
const renderMermaid = useDebouncedCallback(() => {
|
||||
const renderArtifacts = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||
if (mermaidDom) {
|
||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||
}
|
||||
const htmlDom = ref.current.querySelector("code.language-html");
|
||||
const refText = ref.current.querySelector("code")?.innerText;
|
||||
if (htmlDom) {
|
||||
setHtmlCode((htmlDom as HTMLElement).innerText);
|
||||
} else if (refText?.startsWith("<!DOCTYPE")) {
|
||||
setHtmlCode(refText);
|
||||
}
|
||||
}, 600);
|
||||
|
||||
const enableArtifacts = session.mask?.enableArtifacts !== false;
|
||||
|
||||
//Wrap the paragraph for plain-text
|
||||
useEffect(() => {
|
||||
setTimeout(renderMermaid, 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refText]);
|
||||
if (ref.current) {
|
||||
const codeElements = ref.current.querySelectorAll(
|
||||
"code",
|
||||
) as NodeListOf<HTMLElement>;
|
||||
const wrapLanguages = [
|
||||
"",
|
||||
"md",
|
||||
"markdown",
|
||||
"text",
|
||||
"txt",
|
||||
"plaintext",
|
||||
"tex",
|
||||
"latex",
|
||||
];
|
||||
codeElements.forEach((codeElement) => {
|
||||
let languageClass = codeElement.className.match(/language-(\w+)/);
|
||||
let name = languageClass ? languageClass[1] : "";
|
||||
if (wrapLanguages.includes(name)) {
|
||||
codeElement.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
});
|
||||
setTimeout(renderArtifacts, 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
<pre ref={ref}>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
@@ -95,6 +135,69 @@ export function PreCode(props: { children: any }) {
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
{htmlCode.length > 0 && enableArtifacts && (
|
||||
<FullScreen className="no-dark html" right={70}>
|
||||
<ArtifactsShareButton
|
||||
style={{ position: "absolute", right: 20, top: 10 }}
|
||||
getCode={() => htmlCode}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ position: "absolute", right: 120, top: 10 }}
|
||||
bordered
|
||||
icon={<ReloadButtonIcon />}
|
||||
shadow
|
||||
onClick={() => previewRef.current?.reload()}
|
||||
/>
|
||||
<HTMLPreview
|
||||
ref={previewRef}
|
||||
code={htmlCode}
|
||||
autoHeight={!document.fullscreenElement}
|
||||
height={!document.fullscreenElement ? 600 : height}
|
||||
/>
|
||||
</FullScreen>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomCode(props: { children: any; className?: string }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const codeHeight = ref.current.scrollHeight;
|
||||
setShowToggle(codeHeight > 400);
|
||||
ref.current.scrollTop = ref.current.scrollHeight;
|
||||
}
|
||||
}, [props.children]);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<code
|
||||
className={props?.className}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? "400px" : "none",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</code>
|
||||
{showToggle && collapsed && (
|
||||
<div
|
||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -116,11 +219,45 @@ function escapeDollarNumber(text: string) {
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(
|
||||
() => escapeDollarNumber(props.content),
|
||||
[props.content],
|
||||
function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||
return text.replace(
|
||||
pattern,
|
||||
(match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock;
|
||||
} else if (squareBracket) {
|
||||
return `$$${squareBracket}$$`;
|
||||
} else if (roundBracket) {
|
||||
return `$${roundBracket}$`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function tryWrapHtmlCode(text: string) {
|
||||
// try add wrap html code (fixed: html codeblock include 2 newline)
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
(match, quoteStart, lang, newLine, doctype) => {
|
||||
return !quoteStart ? "\n```html\n" + doctype : match;
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
||||
}, [props.content]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
@@ -137,6 +274,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
code: CustomCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || "";
|
||||
@@ -158,6 +296,7 @@ export function Markdown(
|
||||
content: string;
|
||||
loading?: boolean;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
parentRef?: RefObject<HTMLDivElement>;
|
||||
defaultShow?: boolean;
|
||||
} & React.DOMAttributes<HTMLDivElement>,
|
||||
@@ -169,6 +308,7 @@ export function Markdown(
|
||||
className="markdown-body"
|
||||
style={{
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
fontFamily: props.fontFamily || "inherit",
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
|
||||
@@ -127,6 +127,8 @@ export function MaskConfig(props: {
|
||||
onClose={() => setShowPicker(false)}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
aria-label={Locale.Mask.Config.Avatar}
|
||||
onClick={() => setShowPicker(true)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
@@ -139,6 +141,7 @@ export function MaskConfig(props: {
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Mask.Config.Name}>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Name}
|
||||
type="text"
|
||||
value={props.mask.name}
|
||||
onInput={(e) =>
|
||||
@@ -153,6 +156,7 @@ export function MaskConfig(props: {
|
||||
subTitle={Locale.Mask.Config.HideContext.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.HideContext.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.hideContext}
|
||||
onChange={(e) => {
|
||||
@@ -163,12 +167,29 @@ export function MaskConfig(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Artifacts.Title}
|
||||
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Artifacts.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableArtifacts !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableArtifacts = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Share.Title}
|
||||
subTitle={Locale.Mask.Config.Share.SubTitle}
|
||||
>
|
||||
<IconButton
|
||||
aria={Locale.Mask.Config.Share.Title}
|
||||
icon={<CopyIcon />}
|
||||
text={Locale.Mask.Config.Share.Action}
|
||||
onClick={copyMaskLink}
|
||||
@@ -182,6 +203,7 @@ export function MaskConfig(props: {
|
||||
subTitle={Locale.Mask.Config.Sync.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Sync.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.syncGlobalConfig}
|
||||
onChange={async (e) => {
|
||||
@@ -404,7 +426,7 @@ export function MaskPage() {
|
||||
const maskStore = useMaskStore();
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const [filterLang, setFilterLang] = useState<Lang>();
|
||||
const filterLang = maskStore.language;
|
||||
|
||||
const allMasks = maskStore
|
||||
.getAll()
|
||||
@@ -511,9 +533,9 @@ export function MaskPage() {
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (value === Locale.Settings.Lang.All) {
|
||||
setFilterLang(undefined);
|
||||
maskStore.setLanguage(undefined);
|
||||
} else {
|
||||
setFilterLang(value as Lang);
|
||||
maskStore.setLanguage(value as Lang);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -227,7 +227,7 @@ export function MessageSelector(props: {
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
<input type="checkbox" checked={isSelected} readOnly></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ServiceProvider } from "@/app/constant";
|
||||
import { ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
@@ -10,25 +11,26 @@ export function ModelConfigList(props: {
|
||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||
}) {
|
||||
const allModels = useAllModels();
|
||||
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem title={Locale.Settings.Model}>
|
||||
<Select
|
||||
value={props.modelConfig.model}
|
||||
aria-label={Locale.Settings.Model}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.model = ModalConfigValidator.model(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
const [model, providerName] = e.currentTarget.value.split("@");
|
||||
props.updateConfig((config) => {
|
||||
config.model = ModalConfigValidator.model(model);
|
||||
config.providerName = providerName as ServiceProvider;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={v.name} key={i}>
|
||||
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
|
||||
{v.displayName}({v.provider?.providerName})
|
||||
</option>
|
||||
))}
|
||||
@@ -39,6 +41,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.Temperature.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.Temperature.Title}
|
||||
value={props.modelConfig.temperature?.toFixed(1)}
|
||||
min="0"
|
||||
max="1" // lets limit it to 0-1
|
||||
@@ -58,6 +61,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.TopP.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.TopP.Title}
|
||||
value={(props.modelConfig.top_p ?? 1).toFixed(1)}
|
||||
min="0"
|
||||
max="1"
|
||||
@@ -77,6 +81,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.MaxTokens.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.MaxTokens.Title}
|
||||
type="number"
|
||||
min={1024}
|
||||
max={512000}
|
||||
@@ -92,13 +97,14 @@ export function ModelConfigList(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
{props.modelConfig.model.startsWith("gemini") ? null : (
|
||||
{props.modelConfig?.providerName == ServiceProvider.Google ? null : (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.PresencePenalty.Title}
|
||||
subTitle={Locale.Settings.PresencePenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.PresencePenalty.Title}
|
||||
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
@@ -120,6 +126,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.FrequencyPenalty.Title}
|
||||
value={props.modelConfig.frequency_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
@@ -141,6 +148,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.InjectSystemPrompts.Title}
|
||||
type="checkbox"
|
||||
checked={props.modelConfig.enableInjectSystemPrompts}
|
||||
onChange={(e) =>
|
||||
@@ -158,6 +166,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.InputTemplate.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.InputTemplate.Title}
|
||||
type="text"
|
||||
value={props.modelConfig.template}
|
||||
onChange={(e) =>
|
||||
@@ -174,6 +183,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.HistoryCount.Title}
|
||||
title={props.modelConfig.historyMessageCount.toString()}
|
||||
value={props.modelConfig.historyMessageCount}
|
||||
min="0"
|
||||
@@ -192,6 +202,7 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.CompressThreshold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.CompressThreshold.Title}
|
||||
type="number"
|
||||
min={500}
|
||||
max={4000}
|
||||
@@ -207,6 +218,7 @@ export function ModelConfigList(props: {
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
|
||||
<input
|
||||
aria-label={Locale.Memory.Title}
|
||||
type="checkbox"
|
||||
checked={props.modelConfig.sendMemory}
|
||||
onChange={(e) =>
|
||||
|
||||
16
app/components/plugin.module.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.plugin-title {
|
||||
font-weight: bolder;
|
||||
font-size: 16px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.plugin-content {
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
pre code {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
393
app/components/plugin.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import OpenAPIClientAxios from "openapi-client-axios";
|
||||
import yaml from "js-yaml";
|
||||
import { PLUGINS_REPO_URL } from "../constant";
|
||||
import { IconButton } from "./button";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import styles from "./mask.module.scss";
|
||||
import pluginStyles from "./plugin.module.scss";
|
||||
|
||||
import EditIcon from "../icons/edit.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import ConfirmIcon from "../icons/confirm.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
|
||||
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
|
||||
import {
|
||||
PasswordInput,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
showConfirm,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function PluginPage() {
|
||||
const navigate = useNavigate();
|
||||
const pluginStore = usePluginStore();
|
||||
|
||||
const allPlugins = pluginStore.getAll();
|
||||
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
|
||||
|
||||
// refactored already, now it accurate
|
||||
const onSearch = (text: string) => {
|
||||
setSearchText(text);
|
||||
if (text.length > 0) {
|
||||
const result = allPlugins.filter(
|
||||
(m) => m?.title.toLowerCase().includes(text.toLowerCase()),
|
||||
);
|
||||
setSearchPlugins(result);
|
||||
} else {
|
||||
setSearchPlugins(allPlugins);
|
||||
}
|
||||
};
|
||||
|
||||
const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
|
||||
const editingPlugin = pluginStore.get(editingPluginId);
|
||||
const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
|
||||
const closePluginModal = () => setEditingPluginId(undefined);
|
||||
|
||||
const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
|
||||
const content = e.target.innerText;
|
||||
try {
|
||||
const api = new OpenAPIClientAxios({
|
||||
definition: yaml.load(content) as any,
|
||||
});
|
||||
api
|
||||
.init()
|
||||
.then(() => {
|
||||
if (content != editingPlugin.content) {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.content = content;
|
||||
const tool = FunctionToolService.add(plugin, true);
|
||||
plugin.title = tool.api.definition.info.title;
|
||||
plugin.version = tool.api.definition.info.version;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
showToast(Locale.Plugin.EditModal.Error);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(Locale.Plugin.EditModal.Error);
|
||||
}
|
||||
}, 100).bind(null, editingPlugin);
|
||||
|
||||
const [loadUrl, setLoadUrl] = useState<string>("");
|
||||
const loadFromUrl = (loadUrl: string) =>
|
||||
fetch(loadUrl)
|
||||
.catch((e) => {
|
||||
const p = new URL(loadUrl);
|
||||
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
|
||||
headers: {
|
||||
"X-Base-URL": p.origin,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((content) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(content), null, " ");
|
||||
} catch (e) {
|
||||
return content;
|
||||
}
|
||||
})
|
||||
.then((content) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.content = content;
|
||||
const tool = FunctionToolService.add(plugin, true);
|
||||
plugin.title = tool.api.definition.info.title;
|
||||
plugin.version = tool.api.definition.info.version;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast(Locale.Plugin.EditModal.Error);
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={styles["mask-page"]}>
|
||||
<div className="window-header">
|
||||
<div className="window-header-title">
|
||||
<div className="window-header-main-title">
|
||||
{Locale.Plugin.Page.Title}
|
||||
</div>
|
||||
<div className="window-header-submai-title">
|
||||
{Locale.Plugin.Page.SubTitle(plugins.length)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<a
|
||||
href={PLUGINS_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconButton icon={<GithubIcon />} bordered />
|
||||
</a>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
bordered
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["mask-page-body"]}>
|
||||
<div className={styles["mask-filter"]}>
|
||||
<input
|
||||
type="text"
|
||||
className={styles["search-bar"]}
|
||||
placeholder={Locale.Plugin.Page.Search}
|
||||
autoFocus
|
||||
onInput={(e) => onSearch(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles["mask-create"]}
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Plugin.Page.Create}
|
||||
bordered
|
||||
onClick={() => {
|
||||
const createdPlugin = pluginStore.create();
|
||||
setEditingPluginId(createdPlugin.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{plugins.length == 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
margin: "60px auto",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{Locale.Plugin.Page.Find}
|
||||
<a
|
||||
href={PLUGINS_REPO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ marginLeft: 16 }}
|
||||
>
|
||||
<IconButton icon={<GithubIcon />} bordered />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{plugins.map((m) => (
|
||||
<div className={styles["mask-item"]} key={m.id}>
|
||||
<div className={styles["mask-header"]}>
|
||||
<div className={styles["mask-icon"]}></div>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>
|
||||
{m.title}@<small>{m.version}</small>
|
||||
</div>
|
||||
<div className={styles["mask-info"] + " one-line"}>
|
||||
{Locale.Plugin.Item.Info(
|
||||
FunctionToolService.add(m).length,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["mask-actions"]}>
|
||||
{m.builtin ? (
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text={Locale.Plugin.Item.View}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
)}
|
||||
{!m.builtin && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
text={Locale.Plugin.Item.Delete}
|
||||
onClick={async () => {
|
||||
if (
|
||||
await showConfirm(Locale.Plugin.Item.DeleteConfirm)
|
||||
) {
|
||||
pluginStore.delete(m.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingPlugin && (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
|
||||
onClose={closePluginModal}
|
||||
actions={[
|
||||
<IconButton
|
||||
icon={<ConfirmIcon />}
|
||||
text={Locale.UI.Confirm}
|
||||
key="export"
|
||||
bordered
|
||||
onClick={() => setEditingPluginId("")}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<List>
|
||||
<ListItem title={Locale.Plugin.EditModal.Auth}>
|
||||
<select
|
||||
value={editingPlugin?.authType}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.authType = e.target.value;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">{Locale.Plugin.Auth.None}</option>
|
||||
<option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
|
||||
<option value="basic">{Locale.Plugin.Auth.Basic}</option>
|
||||
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
|
||||
</select>
|
||||
</ListItem>
|
||||
{["bearer", "basic", "custom"].includes(
|
||||
editingPlugin.authType as string,
|
||||
) && (
|
||||
<ListItem title={Locale.Plugin.Auth.Location}>
|
||||
<select
|
||||
value={editingPlugin?.authLocation}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.authLocation = e.target.value;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="header">
|
||||
{Locale.Plugin.Auth.LocationHeader}
|
||||
</option>
|
||||
<option value="query">
|
||||
{Locale.Plugin.Auth.LocationQuery}
|
||||
</option>
|
||||
<option value="body">
|
||||
{Locale.Plugin.Auth.LocationBody}
|
||||
</option>
|
||||
</select>
|
||||
</ListItem>
|
||||
)}
|
||||
{editingPlugin.authType == "custom" && (
|
||||
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
|
||||
<input
|
||||
type="text"
|
||||
value={editingPlugin?.authHeader}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.authHeader = e.target.value;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
{["bearer", "basic", "custom"].includes(
|
||||
editingPlugin.authType as string,
|
||||
) && (
|
||||
<ListItem title={Locale.Plugin.Auth.Token}>
|
||||
<PasswordInput
|
||||
type="text"
|
||||
value={editingPlugin?.authToken}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.authToken = e.currentTarget.value;
|
||||
});
|
||||
}}
|
||||
></PasswordInput>
|
||||
</ListItem>
|
||||
)}
|
||||
{!getClientConfig()?.isApp && (
|
||||
<ListItem
|
||||
title={Locale.Plugin.Auth.Proxy}
|
||||
subTitle={Locale.Plugin.Auth.ProxyDescription}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingPlugin?.usingProxy}
|
||||
style={{ minWidth: 16 }}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.usingProxy = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
<List>
|
||||
<ListItem title={Locale.Plugin.EditModal.Content}>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<input
|
||||
type="text"
|
||||
style={{ minWidth: 200, marginRight: 20 }}
|
||||
onInput={(e) => setLoadUrl(e.currentTarget.value)}
|
||||
></input>
|
||||
<IconButton
|
||||
icon={<ReloadIcon />}
|
||||
text={Locale.Plugin.EditModal.Load}
|
||||
bordered
|
||||
onClick={() => loadFromUrl(loadUrl)}
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
subTitle={
|
||||
<div
|
||||
className={`markdown-body ${pluginStyles["plugin-content"]}`}
|
||||
dir="auto"
|
||||
>
|
||||
<pre>
|
||||
<code
|
||||
contentEditable={true}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: editingPlugin.content,
|
||||
}}
|
||||
onBlur={onChangePlugin}
|
||||
></code>
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
></ListItem>
|
||||
{editingPluginTool?.tools.map((tool, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
title={tool?.function?.name}
|
||||
subTitle={tool?.function?.description}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
2
app/components/sd/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./sd";
|
||||
export * from "./sd-panel";
|
||||
45
app/components/sd/sd-panel.module.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.ctrl-param-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 40px;
|
||||
padding: 10px 0;
|
||||
animation: slide-in ease 0.6s;
|
||||
flex-direction: column;
|
||||
|
||||
.ctrl-param-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ctrl-param-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: bolder;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ctrl-param-item-sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
margin-top: 3px;
|
||||
}
|
||||
textarea {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-models {
|
||||
button {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
320
app/components/sd/sd-panel.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import styles from "./sd-panel.module.scss";
|
||||
import React from "react";
|
||||
import { Select } from "@/app/components/ui-lib";
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import Locale from "@/app/locales";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
|
||||
export const params = [
|
||||
{
|
||||
name: Locale.SdPanel.Prompt,
|
||||
value: "prompt",
|
||||
type: "textarea",
|
||||
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.ModelVersion,
|
||||
value: "model",
|
||||
type: "select",
|
||||
default: "sd3-medium",
|
||||
support: ["sd3"],
|
||||
options: [
|
||||
{ name: "SD3 Medium", value: "sd3-medium" },
|
||||
{ name: "SD3 Large", value: "sd3-large" },
|
||||
{ name: "SD3 Large Turbo", value: "sd3-large-turbo" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.NegativePrompt,
|
||||
value: "negative_prompt",
|
||||
type: "textarea",
|
||||
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.AspectRatio,
|
||||
value: "aspect_ratio",
|
||||
type: "select",
|
||||
default: "1:1",
|
||||
options: [
|
||||
{ name: "1:1", value: "1:1" },
|
||||
{ name: "16:9", value: "16:9" },
|
||||
{ name: "21:9", value: "21:9" },
|
||||
{ name: "2:3", value: "2:3" },
|
||||
{ name: "3:2", value: "3:2" },
|
||||
{ name: "4:5", value: "4:5" },
|
||||
{ name: "5:4", value: "5:4" },
|
||||
{ name: "9:16", value: "9:16" },
|
||||
{ name: "9:21", value: "9:21" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.ImageStyle,
|
||||
value: "style",
|
||||
type: "select",
|
||||
default: "3d-model",
|
||||
support: ["core"],
|
||||
options: [
|
||||
{ name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
|
||||
{ name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
|
||||
{ name: Locale.SdPanel.Styles.Anime, value: "anime" },
|
||||
{ name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
|
||||
{ name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
|
||||
{ name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
|
||||
{ name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
|
||||
{ name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
|
||||
{ name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
|
||||
{ name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
|
||||
{ name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
|
||||
{
|
||||
name: Locale.SdPanel.Styles.ModelingCompound,
|
||||
value: "modeling-compound",
|
||||
},
|
||||
{ name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
|
||||
{ name: Locale.SdPanel.Styles.Origami, value: "origami" },
|
||||
{ name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
|
||||
{ name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
|
||||
{ name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Seed",
|
||||
value: "seed",
|
||||
type: "number",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 4294967294,
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.OutFormat,
|
||||
value: "output_format",
|
||||
type: "select",
|
||||
default: "png",
|
||||
options: [
|
||||
{ name: "PNG", value: "png" },
|
||||
{ name: "JPEG", value: "jpeg" },
|
||||
{ name: "WebP", value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sdCommonParams = (model: string, data: any) => {
|
||||
return params.filter((item) => {
|
||||
return !(item.support && !item.support.includes(model));
|
||||
});
|
||||
};
|
||||
|
||||
export const models = [
|
||||
{
|
||||
name: "Stable Image Ultra",
|
||||
value: "ultra",
|
||||
params: (data: any) => sdCommonParams("ultra", data),
|
||||
},
|
||||
{
|
||||
name: "Stable Image Core",
|
||||
value: "core",
|
||||
params: (data: any) => sdCommonParams("core", data),
|
||||
},
|
||||
{
|
||||
name: "Stable Diffusion 3",
|
||||
value: "sd3",
|
||||
params: (data: any) => {
|
||||
return sdCommonParams("sd3", data).filter((item) => {
|
||||
return !(
|
||||
data.model === "sd3-large-turbo" && item.value == "negative_prompt"
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function ControlParamItem(props: {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
required?: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
||||
<div className={styles["ctrl-param-item-header"]}>
|
||||
<div className={styles["ctrl-param-item-title"]}>
|
||||
<div>
|
||||
{props.title}
|
||||
{props.required && <span style={{ color: "red" }}>*</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.children}
|
||||
{props.subTitle && (
|
||||
<div className={styles["ctrl-param-item-sub-title"]}>
|
||||
{props.subTitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ControlParam(props: {
|
||||
columns: any[];
|
||||
data: any;
|
||||
onChange: (field: string, val: any) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{props.columns?.map((item) => {
|
||||
let element: null | JSX.Element;
|
||||
switch (item.type) {
|
||||
case "textarea":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<textarea
|
||||
rows={item.rows || 3}
|
||||
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
|
||||
placeholder={item.placeholder}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
value={props.data[item.value]}
|
||||
></textarea>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
case "select":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<Select
|
||||
aria-label={item.name}
|
||||
value={props.data[item.value]}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
{item.options.map((opt: any) => {
|
||||
return (
|
||||
<option value={opt.value} key={opt.value}>
|
||||
{opt.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
case "number":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<input
|
||||
aria-label={item.name}
|
||||
type="number"
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
value={props.data[item.value] || 0}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, parseInt(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<input
|
||||
aria-label={item.name}
|
||||
type="text"
|
||||
value={props.data[item.value]}
|
||||
style={{ maxWidth: "100%", width: "100%" }}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ControlParamItem>
|
||||
);
|
||||
}
|
||||
return <div key={item.value}>{element}</div>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getModelParamBasicData = (
|
||||
columns: any[],
|
||||
data: any,
|
||||
clearText?: boolean,
|
||||
) => {
|
||||
const newParams: any = {};
|
||||
columns.forEach((item: any) => {
|
||||
if (clearText && ["text", "textarea", "number"].includes(item.type)) {
|
||||
newParams[item.value] = item.default || "";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
newParams[item.value] = data[item.value] || item.default || "";
|
||||
}
|
||||
});
|
||||
return newParams;
|
||||
};
|
||||
|
||||
export const getParams = (model: any, params: any) => {
|
||||
return models.find((m) => m.value === model.value)?.params(params) || [];
|
||||
};
|
||||
|
||||
export function SdPanel() {
|
||||
const sdStore = useSdStore();
|
||||
const currentModel = sdStore.currentModel;
|
||||
const setCurrentModel = sdStore.setCurrentModel;
|
||||
const params = sdStore.currentParams;
|
||||
const setParams = sdStore.setCurrentParams;
|
||||
|
||||
const handleValueChange = (field: string, val: any) => {
|
||||
setParams({
|
||||
...params,
|
||||
[field]: val,
|
||||
});
|
||||
};
|
||||
const handleModelChange = (model: any) => {
|
||||
setCurrentModel(model);
|
||||
setParams(getModelParamBasicData(model.params({}), params));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ControlParamItem title={Locale.SdPanel.AIModel}>
|
||||
<div className={styles["ai-models"]}>
|
||||
{models.map((item) => {
|
||||
return (
|
||||
<IconButton
|
||||
text={item.name}
|
||||
key={item.value}
|
||||
type={currentModel.value == item.value ? "primary" : null}
|
||||
shadow
|
||||
onClick={() => handleModelChange(item)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ControlParamItem>
|
||||
<ControlParam
|
||||
columns={getParams?.(currentModel, params) as any[]}
|
||||
data={params}
|
||||
onChange={handleValueChange}
|
||||
></ControlParam>
|
||||
</>
|
||||
);
|
||||
}
|
||||
140
app/components/sd/sd-sidebar.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import GithubIcon from "@/app/icons/github.svg";
|
||||
import SDIcon from "@/app/icons/sd.svg";
|
||||
import ReturnIcon from "@/app/icons/return.svg";
|
||||
import HistoryIcon from "@/app/icons/history.svg";
|
||||
import Locale from "@/app/locales";
|
||||
|
||||
import { Path, REPO_URL } from "@/app/constant";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dynamic from "next/dynamic";
|
||||
import {
|
||||
SideBarContainer,
|
||||
SideBarBody,
|
||||
SideBarHeader,
|
||||
SideBarTail,
|
||||
useDragSideBar,
|
||||
useHotKey,
|
||||
} from "@/app/components/sidebar";
|
||||
|
||||
import { getParams, getModelParamBasicData } from "./sd-panel";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
import { showToast } from "@/app/components/ui-lib";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
|
||||
const SdPanel = dynamic(
|
||||
async () => (await import("@/app/components/sd")).SdPanel,
|
||||
{
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
useHotKey();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const sdStore = useSdStore();
|
||||
const currentModel = sdStore.currentModel;
|
||||
const params = sdStore.currentParams;
|
||||
const setParams = sdStore.setCurrentParams;
|
||||
|
||||
const handleSubmit = () => {
|
||||
const columns = getParams?.(currentModel, params);
|
||||
const reqParams: any = {};
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const item = columns[i];
|
||||
reqParams[item.value] = params[item.value] ?? null;
|
||||
if (item.required) {
|
||||
if (!reqParams[item.value]) {
|
||||
showToast(Locale.SdPanel.ParamIsRequired(item.name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let data: any = {
|
||||
model: currentModel.value,
|
||||
model_name: currentModel.name,
|
||||
status: "wait",
|
||||
params: reqParams,
|
||||
created_at: new Date().toLocaleString(),
|
||||
img_data: "",
|
||||
};
|
||||
sdStore.sendTask(data, () => {
|
||||
setParams(getModelParamBasicData(columns, params, true));
|
||||
navigate(Path.SdNew);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SideBarContainer
|
||||
onDragStart={onDragStart}
|
||||
shouldNarrow={shouldNarrow}
|
||||
{...props}
|
||||
>
|
||||
{isMobileScreen ? (
|
||||
<div
|
||||
className="window-header"
|
||||
data-tauri-drag-region
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
>
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.ReturnHome}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SDIcon width={50} height={50} />
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<HistoryIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.History}
|
||||
onClick={() => navigate(Path.SdNew)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SideBarHeader
|
||||
title={
|
||||
<IconButton
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.ReturnHome}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
/>
|
||||
}
|
||||
logo={<SDIcon width={38} height={"100%"} />}
|
||||
></SideBarHeader>
|
||||
)}
|
||||
<SideBarBody>
|
||||
<SdPanel />
|
||||
</SideBarBody>
|
||||
<SideBarTail
|
||||
primaryAction={
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
text={Locale.SdPanel.Submit}
|
||||
type="primary"
|
||||
shadow
|
||||
onClick={handleSubmit}
|
||||
></IconButton>
|
||||
}
|
||||
/>
|
||||
</SideBarContainer>
|
||||
);
|
||||
}
|
||||
53
app/components/sd/sd.module.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.sd-img-list{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.sd-img-item{
|
||||
width: 48%;
|
||||
.sd-img-item-info{
|
||||
flex:1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
p{
|
||||
margin: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.line-1{
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.pre-img{
|
||||
display: flex;
|
||||
width: 130px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--second);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.img{
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
&:hover{
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
&:not(:last-child){
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.sd-img-list{
|
||||
.sd-img-item{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
336
app/components/sd/sd.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import chatStyles from "@/app/components/chat.module.scss";
|
||||
import styles from "@/app/components/sd/sd.module.scss";
|
||||
import homeStyles from "@/app/components/home.module.scss";
|
||||
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import ReturnIcon from "@/app/icons/return.svg";
|
||||
import Locale from "@/app/locales";
|
||||
import { Path } from "@/app/constant";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
copyToClipboard,
|
||||
getMessageTextContent,
|
||||
useMobileScreen,
|
||||
} from "@/app/utils";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useAppConfig } from "@/app/store";
|
||||
import MinIcon from "@/app/icons/min.svg";
|
||||
import MaxIcon from "@/app/icons/max.svg";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { ChatAction } from "@/app/components/chat";
|
||||
import DeleteIcon from "@/app/icons/clear.svg";
|
||||
import CopyIcon from "@/app/icons/copy.svg";
|
||||
import PromptIcon from "@/app/icons/prompt.svg";
|
||||
import ResetIcon from "@/app/icons/reload.svg";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
import LoadingIcon from "@/app/icons/three-dots.svg";
|
||||
import ErrorIcon from "@/app/icons/delete.svg";
|
||||
import SDIcon from "@/app/icons/sd.svg";
|
||||
import { Property } from "csstype";
|
||||
import {
|
||||
showConfirm,
|
||||
showImageModal,
|
||||
showModal,
|
||||
} from "@/app/components/ui-lib";
|
||||
import { removeImage } from "@/app/utils/chat";
|
||||
import { SideBar } from "./sd-sidebar";
|
||||
import { WindowContent } from "@/app/components/home";
|
||||
import { params } from "./sd-panel";
|
||||
|
||||
function getSdTaskStatus(item: any) {
|
||||
let s: string;
|
||||
let color: Property.Color | undefined = undefined;
|
||||
switch (item.status) {
|
||||
case "success":
|
||||
s = Locale.Sd.Status.Success;
|
||||
color = "green";
|
||||
break;
|
||||
case "error":
|
||||
s = Locale.Sd.Status.Error;
|
||||
color = "red";
|
||||
break;
|
||||
case "wait":
|
||||
s = Locale.Sd.Status.Wait;
|
||||
color = "yellow";
|
||||
break;
|
||||
case "running":
|
||||
s = Locale.Sd.Status.Running;
|
||||
color = "blue";
|
||||
break;
|
||||
default:
|
||||
s = item.status.toUpperCase();
|
||||
}
|
||||
return (
|
||||
<p className={styles["line-1"]} title={item.error} style={{ color: color }}>
|
||||
<span>
|
||||
{Locale.Sd.Status.Name}: {s}
|
||||
</span>
|
||||
{item.status === "error" && (
|
||||
<span
|
||||
className="clickable"
|
||||
onClick={() => {
|
||||
showModal({
|
||||
title: Locale.Sd.Detail,
|
||||
children: (
|
||||
<div style={{ color: color, userSelect: "text" }}>
|
||||
{item.error}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
- {item.error}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sd() {
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const clientConfig = useMemo(() => getClientConfig(), []);
|
||||
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
||||
const config = useAppConfig();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const sdStore = useSdStore();
|
||||
const [sdImages, setSdImages] = useState(sdStore.draw);
|
||||
const isSd = location.pathname === Path.Sd;
|
||||
|
||||
useEffect(() => {
|
||||
setSdImages(sdStore.draw);
|
||||
}, [sdStore.currentId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
|
||||
<WindowContent>
|
||||
<div className={chatStyles.chat} key={"1"}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
{isMobileScreen && (
|
||||
<div className="window-actions">
|
||||
<div className={"window-action-button"}>
|
||||
<IconButton
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.ChatList}
|
||||
onClick={() => navigate(Path.Sd)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`window-header-title ${chatStyles["chat-body-title"]}`}
|
||||
>
|
||||
<div className={`window-header-main-title`}>Stability AI</div>
|
||||
<div className="window-header-sub-title">
|
||||
{Locale.Sd.SubTitle(sdImages.length || 0)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="window-actions">
|
||||
{showMaxIcon && (
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
aria={Locale.Chat.Actions.FullScreen}
|
||||
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
bordered
|
||||
onClick={() => {
|
||||
config.update(
|
||||
(config) => (config.tightBorder = !config.tightBorder),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isMobileScreen && <SDIcon width={50} height={50} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={chatStyles["chat-body"]} ref={scrollRef}>
|
||||
<div className={styles["sd-img-list"]}>
|
||||
{sdImages.length > 0 ? (
|
||||
sdImages.map((item: any) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ display: "flex" }}
|
||||
className={styles["sd-img-item"]}
|
||||
>
|
||||
{item.status === "success" ? (
|
||||
<img
|
||||
className={styles["img"]}
|
||||
src={item.img_data}
|
||||
alt={item.id}
|
||||
onClick={(e) =>
|
||||
showImageModal(
|
||||
item.img_data,
|
||||
true,
|
||||
isMobileScreen
|
||||
? { width: "100%", height: "fit-content" }
|
||||
: { maxWidth: "100%", maxHeight: "100%" },
|
||||
isMobileScreen
|
||||
? { width: "100%", height: "fit-content" }
|
||||
: { width: "100%", height: "100%" },
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : item.status === "error" ? (
|
||||
<div className={styles["pre-img"]}>
|
||||
<ErrorIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles["pre-img"]}>
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{ marginLeft: "10px" }}
|
||||
className={styles["sd-img-item-info"]}
|
||||
>
|
||||
<p className={styles["line-1"]}>
|
||||
{Locale.SdPanel.Prompt}:{" "}
|
||||
<span
|
||||
className="clickable"
|
||||
title={item.params.prompt}
|
||||
onClick={() => {
|
||||
showModal({
|
||||
title: Locale.Sd.Detail,
|
||||
children: (
|
||||
<div style={{ userSelect: "text" }}>
|
||||
{item.params.prompt}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.params.prompt}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
{Locale.SdPanel.AIModel}: {item.model_name}
|
||||
</p>
|
||||
{getSdTaskStatus(item)}
|
||||
<p>{item.created_at}</p>
|
||||
<div className={chatStyles["chat-message-actions"]}>
|
||||
<div className={chatStyles["chat-input-actions"]}>
|
||||
<ChatAction
|
||||
text={Locale.Sd.Actions.Params}
|
||||
icon={<PromptIcon />}
|
||||
onClick={() => {
|
||||
showModal({
|
||||
title: Locale.Sd.GenerateParams,
|
||||
children: (
|
||||
<div style={{ userSelect: "text" }}>
|
||||
{Object.keys(item.params).map((key) => {
|
||||
let label = key;
|
||||
let value = item.params[key];
|
||||
switch (label) {
|
||||
case "prompt":
|
||||
label = Locale.SdPanel.Prompt;
|
||||
break;
|
||||
case "negative_prompt":
|
||||
label =
|
||||
Locale.SdPanel.NegativePrompt;
|
||||
break;
|
||||
case "aspect_ratio":
|
||||
label = Locale.SdPanel.AspectRatio;
|
||||
break;
|
||||
case "seed":
|
||||
label = "Seed";
|
||||
value = value || 0;
|
||||
break;
|
||||
case "output_format":
|
||||
label = Locale.SdPanel.OutFormat;
|
||||
value = value?.toUpperCase();
|
||||
break;
|
||||
case "style":
|
||||
label = Locale.SdPanel.ImageStyle;
|
||||
value = params
|
||||
.find(
|
||||
(item) =>
|
||||
item.value === "style",
|
||||
)
|
||||
?.options?.find(
|
||||
(item) => item.value === value,
|
||||
)?.name;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{ margin: "10px" }}
|
||||
>
|
||||
<strong>{label}: </strong>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ChatAction
|
||||
text={Locale.Sd.Actions.Copy}
|
||||
icon={<CopyIcon />}
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
getMessageTextContent({
|
||||
role: "user",
|
||||
content: item.params.prompt,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ChatAction
|
||||
text={Locale.Sd.Actions.Retry}
|
||||
icon={<ResetIcon />}
|
||||
onClick={() => {
|
||||
const reqData = {
|
||||
model: item.model,
|
||||
model_name: item.model_name,
|
||||
status: "wait",
|
||||
params: { ...item.params },
|
||||
created_at: new Date().toLocaleString(),
|
||||
img_data: "",
|
||||
};
|
||||
sdStore.sendTask(reqData);
|
||||
}}
|
||||
/>
|
||||
<ChatAction
|
||||
text={Locale.Sd.Actions.Delete}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (
|
||||
await showConfirm(Locale.Sd.Danger.Delete)
|
||||
) {
|
||||
// remove img_data + remove item in list
|
||||
removeImage(item.img_data).finally(() => {
|
||||
sdStore.draw = sdImages.filter(
|
||||
(i: any) => i.id !== item.id,
|
||||
);
|
||||
sdStore.getNextId();
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div>{Locale.Sd.EmptyRecord}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WindowContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
167
app/components/search-chat.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import styles from "./mask.module.scss";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IconButton } from "./button";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import Locale from "../locales";
|
||||
import { Path } from "../constant";
|
||||
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
type Item = {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
export function SearchChatPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const sessions = chatStore.sessions;
|
||||
const selectSession = chatStore.selectSession;
|
||||
|
||||
const [searchResults, setSearchResults] = useState<Item[]>([]);
|
||||
|
||||
const previousValueRef = useRef<string>("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const doSearch = useCallback((text: string) => {
|
||||
const lowerCaseText = text.toLowerCase();
|
||||
const results: Item[] = [];
|
||||
|
||||
sessions.forEach((session, index) => {
|
||||
const fullTextContents: string[] = [];
|
||||
|
||||
session.messages.forEach((message) => {
|
||||
const content = message.content as string;
|
||||
if (!content.toLowerCase || content === "") return;
|
||||
const lowerCaseContent = content.toLowerCase();
|
||||
|
||||
// full text search
|
||||
let pos = lowerCaseContent.indexOf(lowerCaseText);
|
||||
while (pos !== -1) {
|
||||
const start = Math.max(0, pos - 35);
|
||||
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
|
||||
fullTextContents.push(content.substring(start, end));
|
||||
pos = lowerCaseContent.indexOf(
|
||||
lowerCaseText,
|
||||
pos + lowerCaseText.length,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (fullTextContents.length > 0) {
|
||||
results.push({
|
||||
id: index,
|
||||
name: session.topic,
|
||||
content: fullTextContents.join("... "), // concat content with...
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// sort by length of matching content
|
||||
results.sort((a, b) => b.content.length - a.content.length);
|
||||
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (searchInputRef.current) {
|
||||
const currentValue = searchInputRef.current.value;
|
||||
if (currentValue !== previousValueRef.current) {
|
||||
if (currentValue.length > 0) {
|
||||
const result = doSearch(currentValue);
|
||||
setSearchResults(result);
|
||||
}
|
||||
previousValueRef.current = currentValue;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Cleanup the interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, [doSearch]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={styles["mask-page"]}>
|
||||
{/* header */}
|
||||
<div className="window-header">
|
||||
<div className="window-header-title">
|
||||
<div className="window-header-main-title">
|
||||
{Locale.SearchChat.Page.Title}
|
||||
</div>
|
||||
<div className="window-header-submai-title">
|
||||
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
bordered
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["mask-page-body"]}>
|
||||
<div className={styles["mask-filter"]}>
|
||||
{/**搜索输入框 */}
|
||||
<input
|
||||
type="text"
|
||||
className={styles["search-bar"]}
|
||||
placeholder={Locale.SearchChat.Page.Search}
|
||||
autoFocus
|
||||
ref={searchInputRef}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const searchText = e.currentTarget.value;
|
||||
if (searchText.length > 0) {
|
||||
const result = doSearch(searchText);
|
||||
setSearchResults(result);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{searchResults.map((item) => (
|
||||
<div
|
||||
className={styles["mask-item"]}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
navigate(Path.Chat);
|
||||
selectSession(item.id);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{/** 搜索匹配的文本 */}
|
||||
<div className={styles["mask-header"]}>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>{item.name}</div>
|
||||
{item.content.slice(0, 70)}
|
||||
</div>
|
||||
</div>
|
||||
{/** 操作按钮 */}
|
||||
<div className={styles["mask-actions"]}>
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text={Locale.SearchChat.Item.View}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
@@ -10,8 +10,8 @@ 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 DiscoveryIcon from "../icons/discovery.svg";
|
||||
|
||||
import Locale from "../locales";
|
||||
|
||||
@@ -23,19 +23,20 @@ import {
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
PLUGINS,
|
||||
REPO_URL,
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showConfirm, showToast } from "./ui-lib";
|
||||
import { showConfirm, Selector } from "./ui-lib";
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
function useHotKey() {
|
||||
export function useHotKey() {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,7 +55,7 @@ function useHotKey() {
|
||||
});
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
export function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||
|
||||
const config = useAppConfig();
|
||||
@@ -127,25 +128,21 @@ function useDragSideBar() {
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
export function SideBarContainer(props: {
|
||||
children: React.ReactNode;
|
||||
onDragStart: (e: MouseEvent) => void;
|
||||
shouldNarrow: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const isIOSMobile = useMemo(
|
||||
() => isIOS() && isMobileScreen,
|
||||
[isMobileScreen],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
const { children, className, onDragStart, shouldNarrow } = props;
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
className={`${styles.sidebar} ${className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
style={{
|
||||
@@ -153,43 +150,130 @@ export function SideBar(props: { className?: string }) {
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<div className={styles["sidebar-logo"] + " no-dark"}>
|
||||
<ChatGptIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PluginIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Plugin.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
className={styles["sidebar-drag"]}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideBarHeader(props: {
|
||||
title?: string | React.ReactNode;
|
||||
subTitle?: string | React.ReactNode;
|
||||
logo?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { title, subTitle, logo, children } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div className={styles["sidebar-title-container"]}>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
||||
</div>
|
||||
{children}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideBarBody(props: {
|
||||
children: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}) {
|
||||
const { onClick, children } = props;
|
||||
return (
|
||||
<div className={styles["sidebar-body"]} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideBarTail(props: {
|
||||
primaryAction?: React.ReactNode;
|
||||
secondaryAction?: React.ReactNode;
|
||||
}) {
|
||||
const { primaryAction, secondaryAction } = props;
|
||||
|
||||
return (
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
|
||||
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
useHotKey();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const chatStore = useChatStore();
|
||||
|
||||
return (
|
||||
<SideBarContainer
|
||||
onDragStart={onDragStart}
|
||||
shouldNarrow={shouldNarrow}
|
||||
{...props}
|
||||
>
|
||||
<SideBarHeader
|
||||
title="NextChat"
|
||||
subTitle="Build your own AI assistant."
|
||||
logo={<ChatGptIcon />}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
icon={<DiscoveryIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => setShowPluginSelector(true)}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
items={[
|
||||
{
|
||||
title: "👇 Please select the plugin you need to use",
|
||||
value: "-",
|
||||
disable: true,
|
||||
},
|
||||
...PLUGINS.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.path,
|
||||
};
|
||||
}),
|
||||
]}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onSelection={(s) => {
|
||||
navigate(s[0], { state: { fromHome: true } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SideBarHeader>
|
||||
<SideBarBody
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
navigate(Path.Home);
|
||||
@@ -197,32 +281,41 @@ export function SideBar(props: { className?: string }) {
|
||||
}}
|
||||
>
|
||||
<ChatList narrow={shouldNarrow} />
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<Link to={Path.Settings}>
|
||||
<IconButton icon={<SettingsIcon />} shadow />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</SideBarBody>
|
||||
<SideBarTail
|
||||
primaryAction={
|
||||
<>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<Link to={Path.Settings}>
|
||||
<IconButton
|
||||
aria={Locale.Settings.Title}
|
||||
icon={<SettingsIcon />}
|
||||
shadow
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton
|
||||
aria={Locale.Export.MessageFromChatGPT}
|
||||
icon={<GithubIcon />}
|
||||
shadow
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||
@@ -236,15 +329,8 @@ export function SideBar(props: { className?: string }) {
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</SideBarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,19 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical{
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
.list-header{
|
||||
.list-item-title{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.list-item-sub-title{
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
@@ -291,7 +304,12 @@
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
|
||||
.selector-item-disabled{
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&-content {
|
||||
min-width: 300px;
|
||||
.list {
|
||||
max-height: 90vh;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -13,7 +13,15 @@ import MinIcon from "../icons/min.svg";
|
||||
import Locale from "../locales";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import React, { HTMLProps, useEffect, useState } from "react";
|
||||
import React, {
|
||||
CSSProperties,
|
||||
HTMLProps,
|
||||
MouseEvent,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
export function Popover(props: {
|
||||
@@ -42,16 +50,21 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
|
||||
}
|
||||
|
||||
export function ListItem(props: {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
title?: string;
|
||||
subTitle?: string | JSX.Element;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
icon?: JSX.Element;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={styles["list-item"] + ` ${props.className || ""}`}
|
||||
className={
|
||||
styles["list-item"] +
|
||||
` ${props.vertical ? styles["vertical"] : ""} ` +
|
||||
` ${props.className || ""}`
|
||||
}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className={styles["list-header"]}>
|
||||
@@ -252,9 +265,10 @@ export function Input(props: InputProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
||||
export function PasswordInput(
|
||||
props: HTMLProps<HTMLInputElement> & { aria?: string },
|
||||
) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
function changeVisibility() {
|
||||
setVisible(!visible);
|
||||
}
|
||||
@@ -262,6 +276,7 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
||||
return (
|
||||
<div className={"password-input-container"}>
|
||||
<IconButton
|
||||
aria={props.aria}
|
||||
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
|
||||
onClick={changeVisibility}
|
||||
className={"password-eye"}
|
||||
@@ -420,17 +435,25 @@ export function showPrompt(content: any, value = "", rows = 3) {
|
||||
});
|
||||
}
|
||||
|
||||
export function showImageModal(img: string) {
|
||||
export function showImageModal(
|
||||
img: string,
|
||||
defaultMax?: boolean,
|
||||
style?: CSSProperties,
|
||||
boxStyle?: CSSProperties,
|
||||
) {
|
||||
showModal({
|
||||
title: Locale.Export.Image.Modal,
|
||||
defaultMax: defaultMax,
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
|
||||
<img
|
||||
src={img}
|
||||
alt="preview"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
style={
|
||||
style ?? {
|
||||
maxWidth: "100%",
|
||||
}
|
||||
}
|
||||
></img>
|
||||
</div>
|
||||
),
|
||||
@@ -442,27 +465,56 @@ export function Selector<T>(props: {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
value: T;
|
||||
disable?: boolean;
|
||||
}>;
|
||||
defaultSelectedValue?: T;
|
||||
defaultSelectedValue?: T[] | T;
|
||||
onSelection?: (selection: T[]) => void;
|
||||
onClose?: () => void;
|
||||
multiple?: boolean;
|
||||
}) {
|
||||
const [selectedValues, setSelectedValues] = useState<T[]>(
|
||||
Array.isArray(props.defaultSelectedValue)
|
||||
? props.defaultSelectedValue
|
||||
: props.defaultSelectedValue !== undefined
|
||||
? [props.defaultSelectedValue]
|
||||
: [],
|
||||
);
|
||||
|
||||
const handleSelection = (e: MouseEvent, value: T) => {
|
||||
if (props.multiple) {
|
||||
e.stopPropagation();
|
||||
const newSelectedValues = selectedValues.includes(value)
|
||||
? selectedValues.filter((v) => v !== value)
|
||||
: [...selectedValues, value];
|
||||
setSelectedValues(newSelectedValues);
|
||||
props.onSelection?.(newSelectedValues);
|
||||
} else {
|
||||
setSelectedValues([value]);
|
||||
props.onSelection?.([value]);
|
||||
props.onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
const selected = selectedValues.includes(item.value);
|
||||
return (
|
||||
<ListItem
|
||||
className={styles["selector-item"]}
|
||||
className={`${styles["selector-item"]} ${
|
||||
item.disable && styles["selector-item-disabled"]
|
||||
}`}
|
||||
key={i}
|
||||
title={item.title}
|
||||
subTitle={item.subTitle}
|
||||
onClick={() => {
|
||||
props.onSelection?.([item.value]);
|
||||
props.onClose?.();
|
||||
onClick={(e) => {
|
||||
if (item.disable) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
handleSelection(e, item.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selected ? (
|
||||
@@ -485,3 +537,38 @@ export function Selector<T>(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function FullScreen(props: any) {
|
||||
const { children, right = 10, top = 10, ...rest } = props;
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
ref.current?.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleScreenChange = (e: any) => {
|
||||
if (e.target === ref.current) {
|
||||
setFullScreen(!!document.fullscreenElement);
|
||||
}
|
||||
};
|
||||
document.addEventListener("fullscreenchange", handleScreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleScreenChange);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={ref} style={{ position: "relative" }} {...rest}>
|
||||
<div style={{ position: "absolute", right, top }}>
|
||||
<IconButton
|
||||
icon={fullScreen ? <MinIcon /> : <MaxIcon />}
|
||||
onClick={toggleFullscreen}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import tauriConfig from "../../src-tauri/tauri.conf.json";
|
||||
import { DEFAULT_INPUT_TEMPLATE } from "../constant";
|
||||
|
||||
export const getBuildConfig = () => {
|
||||
if (typeof process === "undefined") {
|
||||
@@ -38,6 +39,7 @@ export const getBuildConfig = () => {
|
||||
...commitInfo,
|
||||
buildMode,
|
||||
isApp,
|
||||
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
|
||||
export function getClientConfig() {
|
||||
if (typeof document !== "undefined") {
|
||||
// client side
|
||||
return JSON.parse(queryMeta("config")) as BuildConfig;
|
||||
return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
|
||||
}
|
||||
|
||||
if (typeof process !== "undefined") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import md5 from "spark-md5";
|
||||
import { DEFAULT_MODELS } from "../constant";
|
||||
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
@@ -21,6 +21,11 @@ declare global {
|
||||
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
|
||||
DEFAULT_MODEL?: string; // to control default model in every new chat window
|
||||
|
||||
// stability only
|
||||
STABILITY_URL?: string;
|
||||
STABILITY_API_KEY?: string;
|
||||
|
||||
// azure only
|
||||
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
||||
@@ -33,6 +38,41 @@ declare global {
|
||||
|
||||
// google tag manager
|
||||
GTM_ID?: string;
|
||||
|
||||
// anthropic only
|
||||
ANTHROPIC_URL?: string;
|
||||
ANTHROPIC_API_KEY?: string;
|
||||
ANTHROPIC_API_VERSION?: string;
|
||||
|
||||
// baidu only
|
||||
BAIDU_URL?: string;
|
||||
BAIDU_API_KEY?: string;
|
||||
BAIDU_SECRET_KEY?: string;
|
||||
|
||||
// bytedance only
|
||||
BYTEDANCE_URL?: string;
|
||||
BYTEDANCE_API_KEY?: string;
|
||||
|
||||
// alibaba only
|
||||
ALIBABA_URL?: string;
|
||||
ALIBABA_API_KEY?: string;
|
||||
|
||||
// tencent only
|
||||
TENCENT_URL?: string;
|
||||
TENCENT_SECRET_KEY?: string;
|
||||
TENCENT_SECRET_ID?: string;
|
||||
|
||||
// moonshot only
|
||||
MOONSHOT_URL?: string;
|
||||
MOONSHOT_API_KEY?: string;
|
||||
|
||||
// iflytek only
|
||||
IFLYTEK_URL?: string;
|
||||
IFLYTEK_API_KEY?: string;
|
||||
IFLYTEK_API_SECRET?: string;
|
||||
|
||||
// custom template for preprocessing user input
|
||||
DEFAULT_INPUT_TEMPLATE?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +90,22 @@ const ACCESS_CODES = (function getAccessCodes(): Set<string> {
|
||||
}
|
||||
})();
|
||||
|
||||
function getApiKey(keys?: string) {
|
||||
const apiKeyEnvVar = keys ?? "";
|
||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
const apiKey = apiKeys[randomIndex];
|
||||
if (apiKey) {
|
||||
console.log(
|
||||
`[Server Config] using ${randomIndex + 1} of ${
|
||||
apiKeys.length
|
||||
} api key - ${apiKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
export const getServerSideConfig = () => {
|
||||
if (typeof process === "undefined") {
|
||||
throw Error(
|
||||
@@ -59,40 +115,106 @@ export const getServerSideConfig = () => {
|
||||
|
||||
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||
let defaultModel = process.env.DEFAULT_MODEL ?? "";
|
||||
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
|
||||
customModels += DEFAULT_MODELS.filter(
|
||||
(m) =>
|
||||
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
|
||||
!m.name.startsWith("gpt-4o-mini"),
|
||||
)
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
if (
|
||||
(defaultModel.startsWith("gpt-4") ||
|
||||
defaultModel.startsWith("chatgpt-4o")) &&
|
||||
!defaultModel.startsWith("gpt-4o-mini")
|
||||
)
|
||||
defaultModel = "";
|
||||
}
|
||||
|
||||
const isStability = !!process.env.STABILITY_API_KEY;
|
||||
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
const isTencent = !!process.env.TENCENT_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`,
|
||||
);
|
||||
const isBaidu = !!process.env.BAIDU_API_KEY;
|
||||
const isBytedance = !!process.env.BYTEDANCE_API_KEY;
|
||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||
const isIflytek = !!process.env.IFLYTEK_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`,
|
||||
// );
|
||||
|
||||
const allowedWebDevEndpoints = (
|
||||
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
|
||||
).split(",");
|
||||
|
||||
return {
|
||||
baseUrl: process.env.BASE_URL,
|
||||
apiKey,
|
||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||
|
||||
isStability,
|
||||
stabilityUrl: process.env.STABILITY_URL,
|
||||
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||
|
||||
isAzure,
|
||||
azureUrl: process.env.AZURE_URL,
|
||||
azureApiKey: process.env.AZURE_API_KEY,
|
||||
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
||||
azureApiVersion: process.env.AZURE_API_VERSION,
|
||||
|
||||
isGoogle,
|
||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
||||
googleApiKey: getApiKey(process.env.GOOGLE_API_KEY),
|
||||
googleUrl: process.env.GOOGLE_URL,
|
||||
|
||||
isAnthropic,
|
||||
anthropicApiKey: getApiKey(process.env.ANTHROPIC_API_KEY),
|
||||
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
|
||||
anthropicUrl: process.env.ANTHROPIC_URL,
|
||||
|
||||
isBaidu,
|
||||
baiduUrl: process.env.BAIDU_URL,
|
||||
baiduApiKey: getApiKey(process.env.BAIDU_API_KEY),
|
||||
baiduSecretKey: process.env.BAIDU_SECRET_KEY,
|
||||
|
||||
isBytedance,
|
||||
bytedanceApiKey: getApiKey(process.env.BYTEDANCE_API_KEY),
|
||||
bytedanceUrl: process.env.BYTEDANCE_URL,
|
||||
|
||||
isAlibaba,
|
||||
alibabaUrl: process.env.ALIBABA_URL,
|
||||
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
|
||||
|
||||
isTencent,
|
||||
tencentUrl: process.env.TENCENT_URL,
|
||||
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
|
||||
tencentSecretId: process.env.TENCENT_SECRET_ID,
|
||||
|
||||
isMoonshot,
|
||||
moonshotUrl: process.env.MOONSHOT_URL,
|
||||
moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
|
||||
|
||||
isIflytek,
|
||||
iflytekUrl: process.env.IFLYTEK_URL,
|
||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||
|
||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
gaId: process.env.GA_ID || DEFAULT_GA_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
code: process.env.CODE,
|
||||
@@ -106,5 +228,7 @@ export const getServerSideConfig = () => {
|
||||
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
defaultModel,
|
||||
allowedWebDevEndpoints,
|
||||
};
|
||||
};
|
||||
|
||||
519
app/constant.ts
@@ -1,6 +1,9 @@
|
||||
export const OWNER = "Yidadaa";
|
||||
import path from "path";
|
||||
|
||||
export const OWNER = "ChatGPTNextWeb";
|
||||
export const REPO = "ChatGPT-Next-Web";
|
||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
|
||||
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`;
|
||||
@@ -8,23 +11,57 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
|
||||
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 STABILITY_BASE_URL = "https://api.stability.ai";
|
||||
|
||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||
|
||||
export const BAIDU_BASE_URL = "https://aip.baidubce.com";
|
||||
export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`;
|
||||
|
||||
export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
|
||||
|
||||
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
||||
|
||||
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||
|
||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||
|
||||
export const CACHE_URL_PREFIX = "/api/cache";
|
||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
Settings = "/settings",
|
||||
NewChat = "/new-chat",
|
||||
Masks = "/masks",
|
||||
Plugins = "/plugins",
|
||||
Auth = "/auth",
|
||||
Sd = "/sd",
|
||||
SdNew = "/sd-new",
|
||||
Artifacts = "/artifacts",
|
||||
SearchChat = "/search-chat",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
Azure = "/api/azure",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
Google = "/api/google",
|
||||
Baidu = "/api/baidu",
|
||||
ByteDance = "/api/bytedance",
|
||||
Alibaba = "/api/alibaba",
|
||||
Tencent = "/api/tencent",
|
||||
Moonshot = "/api/moonshot",
|
||||
Iflytek = "/api/iflytek",
|
||||
Stability = "/api/stability",
|
||||
Artifacts = "/api/artifacts",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@@ -39,12 +76,14 @@ export enum FileName {
|
||||
|
||||
export enum StoreKey {
|
||||
Chat = "chat-next-web-store",
|
||||
Plugin = "chat-next-web-plugin",
|
||||
Access = "access-control",
|
||||
Config = "app-config",
|
||||
Mask = "mask-store",
|
||||
Prompt = "prompt-store",
|
||||
Update = "chat-update",
|
||||
Sync = "sync",
|
||||
SdList = "sd-list",
|
||||
}
|
||||
|
||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||
@@ -67,229 +106,385 @@ export enum ServiceProvider {
|
||||
OpenAI = "OpenAI",
|
||||
Azure = "Azure",
|
||||
Google = "Google",
|
||||
Anthropic = "Anthropic",
|
||||
Baidu = "Baidu",
|
||||
ByteDance = "ByteDance",
|
||||
Alibaba = "Alibaba",
|
||||
Tencent = "Tencent",
|
||||
Moonshot = "Moonshot",
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
}
|
||||
|
||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
|
||||
export enum GoogleSafetySettingsThreshold {
|
||||
BLOCK_NONE = "BLOCK_NONE",
|
||||
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
|
||||
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
|
||||
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
Stability = "Stability",
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
Ernie = "Ernie",
|
||||
Doubao = "Doubao",
|
||||
Qwen = "Qwen",
|
||||
Hunyuan = "Hunyuan",
|
||||
Moonshot = "Moonshot",
|
||||
Iflytek = "Iflytek",
|
||||
}
|
||||
|
||||
export const Stability = {
|
||||
GeneratePath: "v2beta/stable-image/generate",
|
||||
ExampleEndpoint: "https://api.stability.ai",
|
||||
};
|
||||
|
||||
export const Anthropic = {
|
||||
ChatPath: "v1/messages",
|
||||
ChatPath1: "v1/complete",
|
||||
ExampleEndpoint: "https://api.anthropic.com",
|
||||
Vision: "2023-06-01",
|
||||
};
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
ImagePath: "v1/images/generations",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
ListModelPath: "v1/models",
|
||||
};
|
||||
|
||||
export const Azure = {
|
||||
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
||||
ChatPath: (deployName: string, apiVersion: string) =>
|
||||
`deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
|
||||
// https://<your_resource_name>.openai.azure.com/openai/deployments/<your_deployment_name>/images/generations?api-version=<api_version>
|
||||
ImagePath: (deployName: string, apiVersion: string) =>
|
||||
`deployments/${deployName}/images/generations?api-version=${apiVersion}`,
|
||||
ExampleEndpoint: "https://{resource-url}/openai",
|
||||
};
|
||||
|
||||
export const Google = {
|
||||
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
||||
ChatPath: "v1beta/models/gemini-pro:generateContent",
|
||||
VisionChatPath: "v1beta/models/gemini-pro-vision:generateContent",
|
||||
ChatPath: (modelName: string) =>
|
||||
`v1beta/models/${modelName}:streamGenerateContent`,
|
||||
};
|
||||
|
||||
// /api/openai/v1/chat/completions
|
||||
export const Baidu = {
|
||||
ExampleEndpoint: BAIDU_BASE_URL,
|
||||
ChatPath: (modelName: string) => {
|
||||
let endpoint = modelName;
|
||||
if (modelName === "ernie-4.0-8k") {
|
||||
endpoint = "completions_pro";
|
||||
}
|
||||
if (modelName === "ernie-4.0-8k-preview-0518") {
|
||||
endpoint = "completions_adv_pro";
|
||||
}
|
||||
if (modelName === "ernie-3.5-8k") {
|
||||
endpoint = "completions";
|
||||
}
|
||||
if (modelName === "ernie-speed-8k") {
|
||||
endpoint = "ernie_speed";
|
||||
}
|
||||
return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
|
||||
},
|
||||
};
|
||||
|
||||
export const ByteDance = {
|
||||
ExampleEndpoint: "https://ark.cn-beijing.volces.com/api/",
|
||||
ChatPath: "api/v3/chat/completions",
|
||||
};
|
||||
|
||||
export const Alibaba = {
|
||||
ExampleEndpoint: ALIBABA_BASE_URL,
|
||||
ChatPath: "v1/services/aigc/text-generation/generation",
|
||||
};
|
||||
|
||||
export const Tencent = {
|
||||
ExampleEndpoint: TENCENT_BASE_URL,
|
||||
};
|
||||
|
||||
export const Moonshot = {
|
||||
ExampleEndpoint: MOONSHOT_BASE_URL,
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const Iflytek = {
|
||||
ExampleEndpoint: IFLYTEK_BASE_URL,
|
||||
ChatPath: "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 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 inline: \\(x^2\\)
|
||||
Latex block: $$e=mc^2$$
|
||||
`;
|
||||
|
||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
||||
export const SUMMARIZE_MODEL = "gpt-4o-mini";
|
||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
||||
|
||||
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
default: "2021-09",
|
||||
"gpt-4-turbo": "2023-12",
|
||||
"gpt-4-turbo-2024-04-09": "2023-12",
|
||||
"gpt-4-turbo-preview": "2023-12",
|
||||
"gpt-4-1106-preview": "2023-04",
|
||||
"gpt-4-0125-preview": "2023-12",
|
||||
"gpt-4o": "2023-10",
|
||||
"gpt-4o-2024-05-13": "2023-10",
|
||||
"gpt-4o-2024-08-06": "2023-10",
|
||||
"chatgpt-4o-latest": "2023-10",
|
||||
"gpt-4o-mini": "2023-10",
|
||||
"gpt-4o-mini-2024-07-18": "2023-10",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
"o1-mini": "2023-10",
|
||||
"o1-preview": "2023-10",
|
||||
// After improvements,
|
||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||
"gemini-pro": "2023-12",
|
||||
"gemini-pro-vision": "2023-12",
|
||||
};
|
||||
|
||||
const openaiModels = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-4",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"chatgpt-4o-latest",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-1106-preview",
|
||||
"dall-e-3",
|
||||
"o1-mini",
|
||||
"o1-preview"
|
||||
];
|
||||
|
||||
const googleModels = [
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-flash-latest",
|
||||
"gemini-pro-vision",
|
||||
];
|
||||
|
||||
const anthropicModels = [
|
||||
"claude-instant-1.2",
|
||||
"claude-2.0",
|
||||
"claude-2.1",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
];
|
||||
|
||||
const baiduModels = [
|
||||
"ernie-4.0-turbo-8k",
|
||||
"ernie-4.0-8k",
|
||||
"ernie-4.0-8k-preview",
|
||||
"ernie-4.0-8k-preview-0518",
|
||||
"ernie-4.0-8k-latest",
|
||||
"ernie-3.5-8k",
|
||||
"ernie-3.5-8k-0205",
|
||||
"ernie-speed-128k",
|
||||
"ernie-speed-8k",
|
||||
"ernie-lite-8k",
|
||||
"ernie-tiny-8k",
|
||||
];
|
||||
|
||||
const bytedanceModels = [
|
||||
"Doubao-lite-4k",
|
||||
"Doubao-lite-32k",
|
||||
"Doubao-lite-128k",
|
||||
"Doubao-pro-4k",
|
||||
"Doubao-pro-32k",
|
||||
"Doubao-pro-128k",
|
||||
];
|
||||
|
||||
const alibabaModes = [
|
||||
"qwen-turbo",
|
||||
"qwen-plus",
|
||||
"qwen-max",
|
||||
"qwen-max-0428",
|
||||
"qwen-max-0403",
|
||||
"qwen-max-0107",
|
||||
"qwen-max-longcontext",
|
||||
];
|
||||
|
||||
const tencentModels = [
|
||||
"hunyuan-pro",
|
||||
"hunyuan-standard",
|
||||
"hunyuan-lite",
|
||||
"hunyuan-role",
|
||||
"hunyuan-functioncall",
|
||||
"hunyuan-code",
|
||||
"hunyuan-vision",
|
||||
];
|
||||
|
||||
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
|
||||
|
||||
const iflytekModels = [
|
||||
"general",
|
||||
"generalv3",
|
||||
"pro-128k",
|
||||
"generalv3.5",
|
||||
"4.0Ultra",
|
||||
];
|
||||
|
||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||
export const DEFAULT_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
...openaiModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++, // Global sequence sort(index)
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
})),
|
||||
...openaiModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
id: "azure",
|
||||
providerName: "Azure",
|
||||
providerType: "azure",
|
||||
sorted: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
})),
|
||||
...googleModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "google",
|
||||
providerName: "Google",
|
||||
providerType: "google",
|
||||
sorted: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gemini-pro-vision",
|
||||
})),
|
||||
...anthropicModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "google",
|
||||
providerName: "Google",
|
||||
providerType: "google",
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
sorted: 4,
|
||||
},
|
||||
},
|
||||
})),
|
||||
...baiduModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "baidu",
|
||||
providerName: "Baidu",
|
||||
providerType: "baidu",
|
||||
sorted: 5,
|
||||
},
|
||||
})),
|
||||
...bytedanceModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "bytedance",
|
||||
providerName: "ByteDance",
|
||||
providerType: "bytedance",
|
||||
sorted: 6,
|
||||
},
|
||||
})),
|
||||
...alibabaModes.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "alibaba",
|
||||
providerName: "Alibaba",
|
||||
providerType: "alibaba",
|
||||
sorted: 7,
|
||||
},
|
||||
})),
|
||||
...tencentModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "tencent",
|
||||
providerName: "Tencent",
|
||||
providerType: "tencent",
|
||||
sorted: 8,
|
||||
},
|
||||
})),
|
||||
...moonshotModes.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "moonshot",
|
||||
providerName: "Moonshot",
|
||||
providerType: "moonshot",
|
||||
sorted: 9,
|
||||
},
|
||||
})),
|
||||
...iflytekModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "iflytek",
|
||||
providerName: "Iflytek",
|
||||
providerType: "iflytek",
|
||||
sorted: 10,
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
export const MAX_RENDER_MSG_COUNT = 45;
|
||||
|
||||
// some famous webdav endpoints
|
||||
export const internalAllowedWebDavEndpoints = [
|
||||
"https://dav.jianguoyun.com/dav/",
|
||||
"https://dav.dropdav.com/",
|
||||
"https://dav.box.com/dav",
|
||||
"https://nanao.teracloud.jp/dav/",
|
||||
"https://bora.teracloud.jp/dav/",
|
||||
"https://webdav.4shared.com/",
|
||||
"https://dav.idrivesync.com",
|
||||
"https://webdav.yandex.com",
|
||||
"https://app.koofr.net/dav/Koofr",
|
||||
];
|
||||
|
||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
||||
export const PLUGINS = [
|
||||
{ name: "Plugins", path: Path.Plugins },
|
||||
{ name: "Stable Diffusion", path: Path.Sd },
|
||||
{ name: "Search Chat", path: Path.SearchChat },
|
||||
];
|
||||
|
||||
8
app/global.d.ts
vendored
@@ -21,10 +21,16 @@ declare interface Window {
|
||||
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
||||
writeTextFile(path: string, data: string): Promise<void>;
|
||||
};
|
||||
notification:{
|
||||
notification: {
|
||||
requestPermission(): Promise<Permission>;
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
http: {
|
||||
fetch<T>(
|
||||
url: string,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<Response<T>>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
7
app/icons/discovery.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path
|
||||
d="M11.307 9.739L15 9l-.739 3.693a2 2 0 0 1-1.568 1.569L9 15l.739-3.693a2 2 0 0 1 1.568-1.568" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
4
app/icons/hd.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-badge-hd" viewBox="0 0 16 16">
|
||||
<path d="M7.396 11V5.001H6.209v2.44H3.687V5H2.5v6h1.187V8.43h2.522V11zM8.5 5.001V11h2.188c1.811 0 2.685-1.107 2.685-3.015 0-1.894-.86-2.984-2.684-2.984zm1.187.967h.843c1.112 0 1.622.686 1.622 2.04 0 1.353-.505 2.02-1.622 2.02h-.843z"/>
|
||||
<path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
10
app/icons/history.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.81836 6.72729V14H13.0911" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981"
|
||||
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#333" stroke-width="4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 660 B |
4
app/icons/palette.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-palette" viewBox="0 0 16 16">
|
||||
<path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
|
||||
<path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
12
app/icons/sd.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.21em" height="1em" viewBox="0 0 256 213">
|
||||
<defs>
|
||||
<linearGradient id="logosStabilityAiIcon0" x1="50%" x2="50%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#9d39ff" />
|
||||
<stop offset="100%" stop-color="#a380ff" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#logosStabilityAiIcon0)"
|
||||
d="M72.418 212.45c49.478 0 81.658-26.205 81.658-65.626c0-30.572-19.572-49.998-54.569-58.043l-22.469-6.74c-19.71-4.424-31.215-9.738-28.505-23.312c2.255-11.292 9.002-17.667 24.69-17.667c49.872 0 68.35 17.667 68.35 17.667V16.237S123.583 0 73.223 0C25.757 0 0 24.424 0 62.236c0 30.571 17.85 48.35 54.052 56.798q3.802.95 3.885.976q8.26 2.556 22.293 6.755c18.504 4.425 23.262 9.121 23.262 23.2c0 12.872-13.374 20.19-31.074 20.19C21.432 170.154 0 144.36 0 144.36v47.078s13.402 21.01 72.418 21.01" />
|
||||
<path fill="#e80000"
|
||||
d="M225.442 209.266c17.515 0 30.558-12.67 30.558-29.812c0-17.515-12.67-29.813-30.558-29.813c-17.515 0-30.185 12.298-30.185 29.813s12.67 29.812 30.185 29.812" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
app/icons/shortcutkey.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 734 B |
1
app/icons/size.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V39C4 40.1046 4.89543 41 6 41H42C43.1046 41 44 40.1046 44 39V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#333" stroke-width="4"/><path d="M30 30V18L38 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 30V18L18 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 20V21" stroke="#333" stroke-width="4" stroke-linecap="round"/><path d="M24 27V28" stroke="#333" stroke-width="4" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@@ -1,11 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.9375 5.3125C5.9375 3.06884 7.75634 1.25 10 1.25C12.2437 1.25 14.0625 3.06884 14.0625 5.3125V8.75C14.0625 10.9937 12.2437 12.8125 10 12.8125C7.75634 12.8125 5.9375 10.9937 5.9375 8.75V5.3125Z"
|
||||
fill="auto" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M3.35938 7.8125C3.79085 7.8125 4.14062 8.16228 4.14062 8.59375V8.75C4.14062 11.986 6.76396 14.6094 10 14.6094C13.236 14.6094 15.8594 11.986 15.8594 8.75V8.59375C15.8594 8.16228 16.2092 7.8125 16.6406 7.8125C17.0721 7.8125 17.4219 8.16228 17.4219 8.59375V8.75C17.4219 12.849 14.099 16.1719 10 16.1719C5.90101 16.1719 2.57812 12.849 2.57812 8.75V8.59375C2.57812 8.16228 2.9279 7.8125 3.35938 7.8125Z"
|
||||
fill="auto" />
|
||||
<path
|
||||
d="M9.21875 15.4688C9.21875 15.0373 9.56853 14.6875 10 14.6875C10.4315 14.6875 10.7812 15.0373 10.7812 15.4688V17.9688C10.7812 18.4002 10.4315 18.75 10 18.75C9.56853 18.75 9.21875 18.4002 9.21875 17.9688V15.4688Z"
|
||||
fill="auto" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
1
app/icons/zoom.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -3,30 +3,31 @@ import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/highlight.scss";
|
||||
import { getClientConfig } from "./config/client";
|
||||
import { type Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { getServerSideConfig } from "./config/server";
|
||||
import { GoogleTagManager } from "@next/third-parties/google";
|
||||
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "NextChat",
|
||||
description: "Your personal ChatGPT Chat Bot.",
|
||||
viewport: {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
},
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
|
||||
],
|
||||
appleWebApp: {
|
||||
title: "NextChat",
|
||||
statusBarStyle: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
@@ -36,7 +37,11 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||
<link rel="manifest" href="/site.webmanifest"></link>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" crossOrigin="use-credentials"></link>
|
||||
<script src="/serviceWorkerRegister.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -51,6 +56,11 @@ export default function RootLayout({
|
||||
<GoogleTagManager gtmId={serverConfig.gtmId} />
|
||||
</>
|
||||
)}
|
||||
{serverConfig?.gaId && (
|
||||
<>
|
||||
<GoogleAnalytics gaId={serverConfig.gaId} />
|
||||
</>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,274 +1,535 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ar: PartialLocaleType = {
|
||||
WIP: "قريبًا...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.",
|
||||
Unauthorized: isApp
|
||||
? "تم اكتشاف مفتاح API غير صالح، يرجى الذهاب إلى [الإعدادات](/#/settings) للتحقق من صحة مفتاح API."
|
||||
: "كلمة المرور غير صحيحة أو فارغة، يرجى الذهاب إلى [تسجيل الدخول](/#/auth) لإدخال كلمة مرور صحيحة، أو أدخل مفتاح OpenAI API الخاص بك في [الإعدادات](/#/settings).",
|
||||
},
|
||||
Auth: {
|
||||
Title: "تحتاج إلى رمز الوصول",
|
||||
Tips: "يرجى إدخال رمز الوصول أدناه",
|
||||
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
|
||||
Input: "رمز الوصول",
|
||||
Title: "تحتاج إلى كلمة مرور",
|
||||
Tips: "قام المشرف بتفعيل التحقق بكلمة المرور، يرجى إدخال رمز الوصول أدناه",
|
||||
SubTips: "أو إدخال مفتاح API الخاص بـ OpenAI أو Google",
|
||||
Input: "أدخل رمز الوصول هنا",
|
||||
Confirm: "تأكيد",
|
||||
Later: "لاحقًا",
|
||||
Later: "في وقت لاحق",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} رسائل`,
|
||||
ChatItemCount: (count: number) => `${count} محادثة`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`,
|
||||
SubTitle: (count: number) => `إجمالي ${count} محادثة`,
|
||||
EditMessage: {
|
||||
Title: "تحرير سجل الرسائل",
|
||||
Topic: {
|
||||
Title: "موضوع الدردشة",
|
||||
SubTitle: "تغيير موضوع الدردشة الحالي",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "الانتقال إلى قائمة الدردشة",
|
||||
CompressedHistory: "ملخص ضغط ذاكرة التاريخ",
|
||||
Export: "تصدير جميع الرسائل كـ Markdown",
|
||||
ChatList: "عرض قائمة الرسائل",
|
||||
CompressedHistory: "عرض التاريخ المضغوط",
|
||||
Export: "تصدير سجل الدردشة",
|
||||
Copy: "نسخ",
|
||||
Stop: "توقف",
|
||||
Stop: "إيقاف",
|
||||
Retry: "إعادة المحاولة",
|
||||
Pin: "تثبيت",
|
||||
PinToastContent: "تم تثبيت 1 محادثة في الإشعارات المسبقة",
|
||||
PinToastAction: "عرض",
|
||||
Delete: "حذف",
|
||||
Edit: "تحرير",
|
||||
},
|
||||
Commands: {
|
||||
new: "دردشة جديدة",
|
||||
newm: "إنشاء دردشة من القناع",
|
||||
next: "الدردشة التالية",
|
||||
prev: "الدردشة السابقة",
|
||||
clear: "مسح السياق",
|
||||
del: "حذف الدردشة",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "توقف",
|
||||
ToBottom: "إلى آخر",
|
||||
Stop: "إيقاف الاستجابة",
|
||||
ToBottom: "الانتقال إلى الأحدث",
|
||||
Theme: {
|
||||
auto: "تلقائي",
|
||||
light: "نمط فاتح",
|
||||
dark: "نمط داكن",
|
||||
auto: "موضوع تلقائي",
|
||||
light: "الوضع الفاتح",
|
||||
dark: "الوضع الداكن",
|
||||
},
|
||||
Prompt: "الاقتراحات",
|
||||
Masks: "الأقنعة",
|
||||
Clear: "مسح السياق",
|
||||
Settings: "الإعدادات",
|
||||
Prompt: "الأوامر السريعة",
|
||||
Masks: "جميع الأقنعة",
|
||||
Clear: "مسح الدردشة",
|
||||
Settings: "إعدادات الدردشة",
|
||||
UploadImage: "تحميل صورة",
|
||||
},
|
||||
Rename: "إعادة تسمية الدردشة",
|
||||
Typing: "كتابة...",
|
||||
Typing: "يكتب…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = ` اضغط على ${submitKey} للإرسال`;
|
||||
var inputHints = `${submitKey} إرسال`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += "، Shift + Enter للإنشاء";
|
||||
inputHints += "، Shift + Enter لإدراج سطر جديد";
|
||||
}
|
||||
return inputHints + "، / للبحث في الاقتراحات";
|
||||
return inputHints + "، / لتفعيل الإكمال التلقائي، : لتفعيل الأوامر";
|
||||
},
|
||||
Send: "إرسال",
|
||||
Config: {
|
||||
Reset: "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
SaveAs: "حفظ كأقنعة",
|
||||
Reset: "مسح الذاكرة",
|
||||
SaveAs: "حفظ كقناع",
|
||||
},
|
||||
IsContext: "الإشعارات المسبقة",
|
||||
},
|
||||
Export: {
|
||||
Title: "تصدير الرسائل",
|
||||
Title: "مشاركة سجل الدردشة",
|
||||
Copy: "نسخ الكل",
|
||||
Download: "تنزيل",
|
||||
MessageFromYou: "رسالة منك",
|
||||
MessageFromChatGPT: "رسالة من ChatGPT",
|
||||
Download: "تحميل الملف",
|
||||
Share: "مشاركة على ShareGPT",
|
||||
MessageFromYou: "المستخدم",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "صيغة التصدير",
|
||||
SubTitle: "Markdown أو صورة PNG",
|
||||
Title: "تنسيق التصدير",
|
||||
SubTitle: "يمكنك تصدير النص كـ Markdown أو صورة PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "تضمين السياق",
|
||||
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا",
|
||||
Title: "تضمين سياق القناع",
|
||||
SubTitle: "هل تريد عرض سياق القناع في الرسائل",
|
||||
},
|
||||
Steps: {
|
||||
Select: "تحديد",
|
||||
Select: "اختيار",
|
||||
Preview: "معاينة",
|
||||
},
|
||||
Image: {
|
||||
Toast: "يتم إنشاء لقطة الشاشة",
|
||||
Modal: "اضغط مطولاً أو انقر بزر الماوس الأيمن لحفظ الصورة",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "بحث",
|
||||
Search: "بحث في الرسائل",
|
||||
All: "تحديد الكل",
|
||||
Latest: "تحديد أحدث",
|
||||
Clear: "مسح",
|
||||
Latest: "أحدث الرسائل",
|
||||
Clear: "مسح التحديد",
|
||||
},
|
||||
Memory: {
|
||||
Title: "اقتراحات الذاكرة",
|
||||
EmptyContent: "لا شيء حتى الآن.",
|
||||
Send: "إرسال الذاكرة",
|
||||
Copy: "نسخ الذاكرة",
|
||||
Reset: "إعادة التعيين",
|
||||
ResetConfirm:
|
||||
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
|
||||
Title: "ملخص التاريخ",
|
||||
EmptyContent: "محتوى المحادثة قصير جداً، لا حاجة للتلخيص",
|
||||
Send: "ضغط تلقائي لسجل الدردشة كـ سياق",
|
||||
Copy: "نسخ الملخص",
|
||||
Reset: "[غير مستخدم]",
|
||||
ResetConfirm: "تأكيد مسح ملخص التاريخ؟",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "دردشة جديدة",
|
||||
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟",
|
||||
DeleteToast: "تم حذف الدردشة",
|
||||
Revert: "التراجع",
|
||||
DeleteChat: "تأكيد حذف المحادثة المحددة؟",
|
||||
DeleteToast: "تم حذف المحادثة",
|
||||
Revert: "تراجع",
|
||||
},
|
||||
Settings: {
|
||||
Title: "الإعدادات",
|
||||
SubTitle: "جميع الإعدادات",
|
||||
SubTitle: "جميع خيارات الإعدادات",
|
||||
|
||||
Lang: {
|
||||
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
|
||||
All: "كل اللغات",
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "إعادة تعيين جميع الإعدادات",
|
||||
SubTitle: "إعادة تعيين جميع عناصر الإعدادات إلى القيم الافتراضية",
|
||||
Action: "إعادة التعيين الآن",
|
||||
Confirm: "تأكيد إعادة تعيين جميع الإعدادات؟",
|
||||
},
|
||||
Clear: {
|
||||
Title: "مسح جميع البيانات",
|
||||
SubTitle: "مسح جميع الدردشات وبيانات الإعدادات",
|
||||
Action: "مسح الآن",
|
||||
Confirm: "تأكيد مسح جميع الدردشات وبيانات الإعدادات؟",
|
||||
},
|
||||
},
|
||||
Avatar: "الصورة الرمزية",
|
||||
Lang: {
|
||||
Name: "Language", // انتبه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة، اتركها كما هي "Language"
|
||||
All: "جميع اللغات",
|
||||
},
|
||||
Avatar: "الصورة الشخصية",
|
||||
FontSize: {
|
||||
Title: "حجم الخط",
|
||||
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
|
||||
SubTitle: "حجم الخط في محتوى الدردشة",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "خط الدردشة",
|
||||
SubTitle: "خط محتوى الدردشة، اتركه فارغًا لتطبيق الخط الافتراضي العالمي",
|
||||
Placeholder: "اسم الخط",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "حقن تلميحات النظام",
|
||||
Title: "حقن الرسائل النصية النظامية",
|
||||
SubTitle:
|
||||
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب",
|
||||
"فرض إضافة رسالة نظامية تحاكي ChatGPT في بداية قائمة الرسائل لكل طلب",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "نموذج الإدخال",
|
||||
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج",
|
||||
Title: "معالجة الإدخال من قبل المستخدم",
|
||||
SubTitle: "سيتم ملء آخر رسالة من المستخدم في هذا القالب",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => ` الإصدار: ${x}`,
|
||||
IsLatest: "أحدث إصدار",
|
||||
CheckUpdate: "التحقق من التحديث",
|
||||
IsChecking: "جارٍ التحقق من التحديث...",
|
||||
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`,
|
||||
GoToUpdate: "التحديث",
|
||||
Version: (x: string) => `الإصدار الحالي: ${x}`,
|
||||
IsLatest: "أنت على أحدث إصدار",
|
||||
CheckUpdate: "التحقق من التحديثات",
|
||||
IsChecking: "جارٍ التحقق من التحديثات...",
|
||||
FoundUpdate: (x: string) => `تم العثور على إصدار جديد: ${x}`,
|
||||
GoToUpdate: "انتقل للتحديث",
|
||||
},
|
||||
SendKey: "مفتاح الإرسال",
|
||||
SendKey: "زر الإرسال",
|
||||
Theme: "السمة",
|
||||
TightBorder: "حدود ضيقة",
|
||||
TightBorder: "وضع بدون حدود",
|
||||
SendPreviewBubble: {
|
||||
Title: "عرض معاينة الـ Send",
|
||||
SubTitle: "معاينة Markdown في فقاعة",
|
||||
Title: "فقاعة المعاينة",
|
||||
SubTitle: "معاينة محتوى Markdown في فقاعة المعاينة",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "توليد العنوان تلقائيًا",
|
||||
SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "بيانات السحابة",
|
||||
NotSyncYet: "لم يتم التزامن بعد",
|
||||
Success: "تم التزامن بنجاح",
|
||||
Fail: "فشل التزامن",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "تكوين التزامن السحابي",
|
||||
Check: "التحقق من التوفر",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "نوع التزامن",
|
||||
SubTitle: "اختر خادم التزامن المفضل",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "تفعيل الوكيل",
|
||||
SubTitle: "يجب تفعيل الوكيل عند التزامن عبر المتصفح لتجنب قيود CORS",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "عنوان الوكيل",
|
||||
SubTitle: "ينطبق فقط على الوكيل المتاح في هذا المشروع",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "عنوان WebDAV",
|
||||
UserName: "اسم المستخدم",
|
||||
Password: "كلمة المرور",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "رابط UpStash Redis REST",
|
||||
UserName: "اسم النسخ الاحتياطي",
|
||||
Password: "رمز UpStash Redis REST",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "بيانات محلية",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} دردشة، ${overview.message} رسالة، ${overview.prompt} إشعار، ${overview.mask} قناع`;
|
||||
},
|
||||
ImportFailed: "فشل الاستيراد",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "شاشة تظهر الأقنعة",
|
||||
SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة",
|
||||
Title: "صفحة بدء القناع",
|
||||
SubTitle: "عرض صفحة بدء القناع عند بدء دردشة جديدة",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "إخفاء الأقنعة المدمجة",
|
||||
SubTitle: "إخفاء الأقنعة المدمجة في قائمة الأقنعة",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "تعطيل الاكتمال التلقائي",
|
||||
SubTitle: "اكتب / لتشغيل الاكتمال التلقائي",
|
||||
Title: "تعطيل الإكمال التلقائي للإشعارات",
|
||||
SubTitle: "استخدم / في بداية مربع النص لتفعيل الإكمال التلقائي",
|
||||
},
|
||||
List: "قائمة الاقتراحات",
|
||||
ListCount: (builtin: number, custom: number) => `
|
||||
${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخدم`,
|
||||
Edit: "تعديل",
|
||||
List: "قائمة الإشعارات المخصصة",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`مدمج ${builtin} إشعار، مخصص ${custom} إشعار`,
|
||||
Edit: "تحرير",
|
||||
Modal: {
|
||||
Title: "قائمة الاقتراحات",
|
||||
Add: "إضافة واحدة",
|
||||
Search: "البحث في الاقتراحات",
|
||||
Title: "قائمة الإشعارات",
|
||||
Add: "جديد",
|
||||
Search: "بحث عن إشعارات",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "تحرير الاقتراح",
|
||||
Title: "تحرير الإشعارات",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "عدد الرسائل المرفقة",
|
||||
SubTitle: "عدد الرسائل المرسلة المرفقة في كل طلب",
|
||||
Title: "عدد الرسائل التاريخية المرفقة",
|
||||
SubTitle: "عدد الرسائل التاريخية المرفقة مع كل طلب",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "حد الضغط للتاريخ",
|
||||
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
||||
Title: "عتبة ضغط طول الرسائل التاريخية",
|
||||
SubTitle:
|
||||
"عندما يتجاوز طول الرسائل التاريخية غير المضغوطة هذه القيمة، سيتم الضغط",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "رصيد الحساب",
|
||||
Title: "التحقق من الرصيد",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`;
|
||||
return `تم استخدام $${used} هذا الشهر، إجمالي الاشتراك $${total}`;
|
||||
},
|
||||
IsChecking: "جارٍ التحقق...",
|
||||
Check: "التحقق",
|
||||
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
||||
Check: "إعادة التحقق",
|
||||
NoAccess: "أدخل مفتاح API أو كلمة مرور للوصول إلى الرصيد",
|
||||
},
|
||||
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "كلمة المرور للوصول",
|
||||
SubTitle: "قام المشرف بتمكين الوصول المشفر",
|
||||
Placeholder: "أدخل كلمة المرور للوصول",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "واجهة مخصصة",
|
||||
SubTitle: "هل تستخدم خدمة Azure أو OpenAI مخصصة",
|
||||
},
|
||||
Provider: {
|
||||
Title: "موفر الخدمة النموذجية",
|
||||
SubTitle: "التبديل بين مقدمي الخدمة المختلفين",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح API",
|
||||
SubTitle: "استخدم مفتاح OpenAI مخصص لتجاوز قيود كلمة المرور",
|
||||
Placeholder: "مفتاح OpenAI API",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "يجب أن يحتوي على http(s):// بخلاف العنوان الافتراضي",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح الواجهة",
|
||||
SubTitle: "استخدم مفتاح Azure مخصص لتجاوز قيود كلمة المرور",
|
||||
Placeholder: "مفتاح Azure API",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "مثال:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "إصدار الواجهة (azure api version)",
|
||||
SubTitle: "اختر إصدارًا معينًا",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح الواجهة",
|
||||
SubTitle: "استخدم مفتاح Anthropic مخصص لتجاوز قيود كلمة المرور",
|
||||
Placeholder: "مفتاح Anthropic API",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "مثال:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "إصدار الواجهة (claude api version)",
|
||||
SubTitle: "اختر إصدار API محدد",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح API",
|
||||
SubTitle: "احصل على مفتاح API الخاص بك من Google AI",
|
||||
Placeholder: "أدخل مفتاح Google AI Studio API",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "عنوان النهاية",
|
||||
SubTitle: "مثال:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "إصدار API (مخصص لـ gemini-pro)",
|
||||
SubTitle: "اختر إصدار API معين",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "مستوى تصفية الأمان من Google",
|
||||
SubTitle: "تعيين مستوى تصفية المحتوى",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح API",
|
||||
SubTitle: "استخدم مفتاح Baidu API مخصص",
|
||||
Placeholder: "مفتاح Baidu API",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "المفتاح السري",
|
||||
SubTitle: "استخدم مفتاح Baidu Secret مخصص",
|
||||
Placeholder: "مفتاح Baidu Secret",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "لا يدعم التخصيص، انتقل إلى .env للتكوين",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح الواجهة",
|
||||
SubTitle: "استخدم مفتاح ByteDance API مخصص",
|
||||
Placeholder: "مفتاح ByteDance API",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "مثال:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "مفتاح الواجهة",
|
||||
SubTitle: "استخدم مفتاح Alibaba Cloud API مخصص",
|
||||
Placeholder: "مفتاح Alibaba Cloud API",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "عنوان الواجهة",
|
||||
SubTitle: "مثال:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "اسم النموذج المخصص",
|
||||
SubTitle: "أضف خيارات نموذج مخصص، مفصولة بفواصل إنجليزية",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "النموذج",
|
||||
Temperature: {
|
||||
Title: "الحرارة",
|
||||
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية",
|
||||
Title: "العشوائية (temperature)",
|
||||
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",
|
||||
},
|
||||
TopP: {
|
||||
Title: "عينات النواة (top_p)",
|
||||
SubTitle: "مشابه للعشوائية ولكن لا تغيره مع العشوائية",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "الحد الأقصى للرموز",
|
||||
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة",
|
||||
Title: "حد أقصى للرموز لكل رد (max_tokens)",
|
||||
SubTitle: "أقصى عدد للرموز في تفاعل واحد",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "تأثير الوجود",
|
||||
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة",
|
||||
Title: "تجدد الموضوع (presence_penalty)",
|
||||
SubTitle: "كلما زادت القيمة، زادت احتمالية التوسع في مواضيع جديدة",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "تأثير التكرار",
|
||||
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر",
|
||||
Title: "عقوبة التكرار (frequency_penalty)",
|
||||
SubTitle: "كلما زادت القيمة، زادت احتمالية تقليل تكرار الكلمات",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "محادثة جديدة",
|
||||
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟",
|
||||
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.",
|
||||
DefaultTopic: "دردشة جديدة",
|
||||
BotHello: "كيف يمكنني مساعدتك؟",
|
||||
Error: "حدث خطأ، يرجى المحاولة مرة أخرى لاحقًا",
|
||||
Prompt: {
|
||||
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content,
|
||||
History: (content: string) =>
|
||||
"هذا ملخص للدردشة السابقة كنقطة انطلاق: " + content,
|
||||
Topic:
|
||||
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.",
|
||||
"استخدم أربع إلى خمس كلمات لإرجاع ملخص مختصر لهذه الجملة، بدون شرح، بدون علامات ترقيم، بدون كلمات تعبيرية، بدون نص إضافي، بدون تنسيق عريض، إذا لم يكن هناك موضوع، يرجى العودة إلى 'دردشة عامة'",
|
||||
Summarize:
|
||||
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.",
|
||||
"قم بتلخيص محتوى الدردشة باختصار، لاستخدامه كإشارة سياقية لاحقة، اجعلها في حدود 200 كلمة",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "تم النسخ إلى الحافظة",
|
||||
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة",
|
||||
Success: "تم الكتابة إلى الحافظة",
|
||||
Failed: "فشل النسخ، يرجى منح أذونات الحافظة",
|
||||
},
|
||||
Download: {
|
||||
Success: "تم تنزيل المحتوى إلى مجلدك.",
|
||||
Failed: "فشل التنزيل.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`,
|
||||
Edit: "الاقتراحات السياقية والذاكرة",
|
||||
Add: "إضافة اقتراح",
|
||||
Clear: "مسح السياق",
|
||||
Revert: "التراجع",
|
||||
Toast: (x: any) => `يحتوي على ${x} إشعارات مخصصة`,
|
||||
Edit: "إعدادات الدردشة الحالية",
|
||||
Add: "إضافة دردشة جديدة",
|
||||
Clear: "تم مسح السياق",
|
||||
Revert: "استعادة السياق",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "المكوّن الإضافي",
|
||||
Name: "الإضافات",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "أنت مساعد ي",
|
||||
Sysmessage: "أنت مساعد",
|
||||
},
|
||||
Mask: {
|
||||
Name: "الأقنعة",
|
||||
SearchChat: {
|
||||
Name: "بحث",
|
||||
Page: {
|
||||
Title: "قالب الاقتراح",
|
||||
SubTitle: (count: number) => `${count} قوالب الاقتراح`,
|
||||
Search: "البحث في القوالب",
|
||||
Create: "إنشاء",
|
||||
Title: "البحث في سجلات الدردشة",
|
||||
Search: "أدخل كلمات البحث",
|
||||
NoResult: "لم يتم العثور على نتائج",
|
||||
NoData: "لا توجد بيانات",
|
||||
Loading: "جارٍ التحميل",
|
||||
|
||||
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} اقتراحات`,
|
||||
View: "عرض",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "القناع",
|
||||
Page: {
|
||||
Title: "أقنعة الأدوار المخصصة",
|
||||
SubTitle: (count: number) => `${count} تعريف لدور مخصص`,
|
||||
Search: "بحث عن قناع الدور",
|
||||
Create: "إنشاء جديد",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `يحتوي على ${count} محادثات مخصصة`,
|
||||
Chat: "الدردشة",
|
||||
View: "عرض",
|
||||
Edit: "تعديل",
|
||||
Edit: "تحرير",
|
||||
Delete: "حذف",
|
||||
DeleteConfirm: "تأكيد الحذف؟",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) => `
|
||||
تعديل قالب الاقتراح ${readonly ? "(للقراءة فقط)" : ""}`,
|
||||
Download: "تنزيل",
|
||||
Clone: "استنساخ",
|
||||
Title: (readonly: boolean) =>
|
||||
`تحرير القناع المخصص ${readonly ? " (للقراءة فقط)" : ""}`,
|
||||
Download: "تنزيل القناع المخصص",
|
||||
Clone: "استنساخ القناع",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "صورة الروبوت",
|
||||
Name: "اسم الروبوت",
|
||||
Avatar: "صورة الدور",
|
||||
Name: "اسم الدور",
|
||||
Sync: {
|
||||
Title: "استخدام الإعدادات العامة",
|
||||
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة",
|
||||
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟",
|
||||
Title: "استخدام الإعدادات العالمية",
|
||||
SubTitle: "هل تستخدم الدردشة الحالية الإعدادات العالمية للنموذج",
|
||||
Confirm:
|
||||
"ستتم الكتابة فوق الإعدادات المخصصة للدردشة الحالية تلقائيًا، تأكيد تفعيل الإعدادات العالمية؟",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "إخفاء اقتراحات السياق",
|
||||
SubTitle: "عدم عرض اقتراحات السياق في الدردشة",
|
||||
Title: "إخفاء المحادثات المخصصة",
|
||||
SubTitle: "بعد الإخفاء، لن تظهر المحادثات المخصصة في واجهة الدردشة",
|
||||
},
|
||||
Share: {
|
||||
Title: "مشاركة هذا القناع",
|
||||
SubTitle: "إنشاء رابط مباشر لهذا القناع",
|
||||
Action: "نسخ الرابط",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "العودة",
|
||||
Skip: "ابدأ فقط",
|
||||
Title: "اختيار قناع",
|
||||
SubTitle: "دردشة مع الروح وراء القناع",
|
||||
More: "المزيد",
|
||||
Skip: "بدء الآن",
|
||||
NotShow: "عدم العرض مرة أخرى",
|
||||
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.",
|
||||
ConfirmNoShow:
|
||||
"تأكيد إلغاء العرض؟ بعد الإلغاء، يمكنك إعادة تفعيله في الإعدادات في أي وقت.",
|
||||
Title: "اختر قناعًا",
|
||||
SubTitle: "ابدأ الآن وتفاعل مع الأفكار خلف القناع",
|
||||
More: "عرض الكل",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "تم الكشف عن رمز وصول في الرابط، هل تريد تعبئته تلقائيًا؟",
|
||||
Settings: "تم الكشف عن إعدادات مسبقة في الرابط، هل تريد تعبئتها تلقائيًا؟",
|
||||
},
|
||||
|
||||
UI: {
|
||||
@@ -276,9 +537,16 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
||||
Cancel: "إلغاء",
|
||||
Close: "إغلاق",
|
||||
Create: "إنشاء",
|
||||
Edit: "تعديل",
|
||||
Edit: "تحرير",
|
||||
Export: "تصدير",
|
||||
Import: "استيراد",
|
||||
Sync: "مزامنة",
|
||||
Config: "تكوين",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "فقط الرسائل بعد مسح السياق سيتم عرضها",
|
||||
},
|
||||
Model: "النموذج",
|
||||
Messages: "الرسائل",
|
||||
Topic: "الموضوع",
|
||||
|
||||
@@ -1,332 +1,564 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "./index";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const bn: PartialLocaleType = {
|
||||
WIP: "শীঘ্রই আসছে...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"অননুমোদিত অ্যাক্সেস, অনুগ্রহ করে [অথোরাইজশন](/#/auth) পৃষ্ঠায় অ্যাক্সেস কোড ইনপুট করুন।",
|
||||
Unauthorized: isApp
|
||||
? "অবৈধ API কী সনাক্ত করা হয়েছে, অনুগ্রহ করে [সেটিংস](/#/settings) পৃষ্ঠায় যান এবং নিশ্চিত করুন যে API কী সঠিকভাবে কনফিগার করা হয়েছে।"
|
||||
: "অ্যাক্সেস পাসওয়ার্ড সঠিক নয় বা খালি, অনুগ্রহ করে [লগইন](/#/auth) পৃষ্ঠায় যান এবং সঠিক অ্যাক্সেস পাসওয়ার্ড প্রবেশ করান, অথবা [সেটিংস](/#/settings) পৃষ্ঠায় আপনার OpenAI API কী প্রবেশ করান।",
|
||||
},
|
||||
Auth: {
|
||||
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
|
||||
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
|
||||
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
|
||||
Input: "অ্যাক্সেস কোড",
|
||||
Title: "পাসওয়ার্ড প্রয়োজন",
|
||||
Tips: "অ্যাডমিন পাসওয়ার্ড প্রমাণীকরণ চালু করেছেন, নিচে অ্যাক্সেস কোড প্রবেশ করুন",
|
||||
SubTips: "অথবা আপনার OpenAI অথবা Google API কী প্রবেশ করান",
|
||||
Input: "এখানে অ্যাক্সেস কোড লিখুন",
|
||||
Confirm: "নিশ্চিত করুন",
|
||||
Later: "পরে",
|
||||
Later: "পরে বলুন",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} টি বার্তা`,
|
||||
ChatItemCount: (count: number) => `${count} টি চ্যাট`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} টি বার্তা`,
|
||||
SubTitle: (count: number) => `মোট ${count} টি চ্যাট`,
|
||||
EditMessage: {
|
||||
Title: "বার্তাগুলি সম্পাদনা করুন",
|
||||
Topic: {
|
||||
Title: "চ্যাটের বিষয়",
|
||||
SubTitle: "বর্তমান চ্যাটের বিষয় পরিবর্তন করুন",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "চ্যাট তালিকায় যান",
|
||||
CompressedHistory: "সংক্ষিপ্ত ইতিহাস মেমোরি প্রম্পট",
|
||||
Export: "সমস্ত বার্তা মার্কডাউন হিসাবে রপ্তানি করুন",
|
||||
Copy: "কপি",
|
||||
Stop: "বন্ধ করুন",
|
||||
ChatList: "বার্তা তালিকা দেখুন",
|
||||
CompressedHistory: "সংকুচিত ইতিহাস দেখুন",
|
||||
Export: "চ্যাট ইতিহাস রপ্তানী করুন",
|
||||
Copy: "অনুলিপি করুন",
|
||||
Stop: "থামান",
|
||||
Retry: "পুনরায় চেষ্টা করুন",
|
||||
Pin: "পিন করুন",
|
||||
PinToastContent: "পিন করা হয়েছে ২টি বার্তা প্রম্পটে",
|
||||
PinToastContent: "1 টি চ্যাট পূর্বনির্ধারিত প্রম্পটে পিন করা হয়েছে",
|
||||
PinToastAction: "দেখুন",
|
||||
Delete: "মুছে ফেলুন",
|
||||
Edit: "সম্পাদন করুন",
|
||||
Edit: "সম্পাদনা করুন",
|
||||
},
|
||||
Commands: {
|
||||
new: "নতুন চ্যাট শুরু করুন",
|
||||
newm: "মাস্ক সহ নতুন চ্যাট শুরু করুন",
|
||||
new: "নতুন চ্যাট",
|
||||
newm: "মাস্ক থেকে নতুন চ্যাট",
|
||||
next: "পরবর্তী চ্যাট",
|
||||
prev: "পূর্ববর্তী চ্যাট",
|
||||
clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন",
|
||||
del: "চ্যাট মুছুন",
|
||||
clear: "প্রসঙ্গ পরিষ্কার করুন",
|
||||
del: "চ্যাট মুছে ফেলুন",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "বন্ধ করুন",
|
||||
ToBottom: "সর্বশেষতম দিকে",
|
||||
Stop: "প্রতিক্রিয়া থামান",
|
||||
ToBottom: "সর্বশেষে স্ক্রোল করুন",
|
||||
Theme: {
|
||||
auto: "অটো",
|
||||
light: "হালকা থিম",
|
||||
dark: "ডার্ক থিম",
|
||||
auto: "স্বয়ংক্রিয় থিম",
|
||||
light: "আলোর মোড",
|
||||
dark: "অন্ধকার মোড",
|
||||
},
|
||||
Prompt: "প্রম্পটগুলিতে",
|
||||
Masks: "মাস্কগুলি",
|
||||
Clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন",
|
||||
Settings: "সেটিংস",
|
||||
Prompt: "সংক্ষিপ্ত নির্দেশনা",
|
||||
Masks: "সমস্ত মাস্ক",
|
||||
Clear: "চ্যাট পরিষ্কার করুন",
|
||||
Settings: "চ্যাট সেটিংস",
|
||||
UploadImage: "চিত্র আপলোড করুন",
|
||||
},
|
||||
Rename: "চ্যাট পুনঃনামকরণ করুন",
|
||||
Typing: "টাইপিং...",
|
||||
Rename: "চ্যাট নাম পরিবর্তন করুন",
|
||||
Typing: "লিখছে…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} to send`;
|
||||
var inputHints = `${submitKey} পাঠান`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter to wrap";
|
||||
inputHints += ",Shift + Enter নতুন লাইন";
|
||||
}
|
||||
return inputHints + ", / to search prompts, : to use commands";
|
||||
return inputHints + ",/ পূর্ণতা সক্রিয় করুন,: কমান্ড সক্রিয় করুন";
|
||||
},
|
||||
Send: "প্রেরণ করুন",
|
||||
Send: "পাঠান",
|
||||
Config: {
|
||||
Reset: "ডিফল্টে রিসেট করুন",
|
||||
Reset: "মেমরি মুছে ফেলুন",
|
||||
SaveAs: "মাস্ক হিসাবে সংরক্ষণ করুন",
|
||||
},
|
||||
IsContext: "পূর্বনির্ধারিত প্রম্পট",
|
||||
},
|
||||
Export: {
|
||||
Title: "বার্তা রপ্তানিকরণ",
|
||||
Copy: "সমস্তটি কপি করুন",
|
||||
Download: "ডাউনলোড করুন",
|
||||
MessageFromYou: "আপনার বার্তা",
|
||||
MessageFromChatGPT: "চ্যাটজিপিটির বার্তা",
|
||||
Share: "শেয়ার করুন শেয়ারজিপিটি তে",
|
||||
Title: "চ্যাট ইতিহাস শেয়ার করুন",
|
||||
Copy: "সবকিছু কপি করুন",
|
||||
Download: "ফাইল ডাউনলোড করুন",
|
||||
Share: "ShareGPT তে শেয়ার করুন",
|
||||
MessageFromYou: "ব্যবহারকারী",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "রপ্তানি ফরম্যাট",
|
||||
SubTitle: "মার্কডাউন বা পিএনজি চিত্র",
|
||||
Title: "রপ্তানির ফর্ম্যাট",
|
||||
SubTitle: "Markdown টেক্সট বা PNG চিত্র রপ্তানি করা যাবে",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "মাস্ক অন্তর্ভুক্ত করুন",
|
||||
SubTitle: "মাস্কগুলি সংরক্ষণ করবেন না কি",
|
||||
Title: "মাস্ক প্রসঙ্গ অন্তর্ভুক্ত করুন",
|
||||
SubTitle: "বার্তায় মাস্ক প্রসঙ্গ প্রদর্শন করা হবে কি না",
|
||||
},
|
||||
Steps: {
|
||||
Select: "নির্বাচন করুন",
|
||||
Preview: "প্রিভিউ করুন",
|
||||
Preview: "পূর্বরূপ দেখুন",
|
||||
},
|
||||
Image: {
|
||||
Toast: "স্ক্রীনশট তৈরি করা হচ্ছে",
|
||||
Modal: "ছবি সংরক্ষণ করতে দীর্ঘ প্রেস করুন অথবা রাইট ক্লিক করুন",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "অনুসন্ধান করুন",
|
||||
All: "সমস্তটি নির্বাচন করুন",
|
||||
Latest: "সর্বশেষতমটি নির্বাচন করুন",
|
||||
Clear: "পরিষ্কার করুন",
|
||||
Search: "বার্তা অনুসন্ধান করুন",
|
||||
All: "সবকিছু নির্বাচন করুন",
|
||||
Latest: "সর্বশেষ কিছু",
|
||||
Clear: "নির্বাচন পরিষ্কার করুন",
|
||||
},
|
||||
Memory: {
|
||||
Title: "মেমোরি প্রম্পট",
|
||||
EmptyContent: "এখনও কিছুই নেই।",
|
||||
Send: "মেমোরি প্রেরণ করুন",
|
||||
Copy: "মেমোরি কপি করুন",
|
||||
Reset: "পুনরায় নিশ্চিত করুন",
|
||||
ResetConfirm:
|
||||
"রিসেট করলে বর্তমান চ্যাট ইতিহাস এবং ঐতিহাসিক মেমোরি মুছে যাবে। পুনরায় নির্দিষ্ট করতে চান তা নিশ্চিত করতে চান?",
|
||||
Title: "ইতিহাস সারাংশ",
|
||||
EmptyContent: "চ্যাটের বিষয়বস্তু খুব সংক্ষিপ্ত, সারাংশ প্রয়োজন নেই",
|
||||
Send: "অটোমেটিক চ্যাট ইতিহাস সংকুচিত করুন এবং প্রসঙ্গ হিসেবে পাঠান",
|
||||
Copy: "সারাংশ কপি করুন",
|
||||
Reset: "[unused]",
|
||||
ResetConfirm: "ইতিহাস সারাংশ মুছে ফেলার নিশ্চিত করুন?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "নতুন চ্যাট",
|
||||
DeleteChat: "নির্বাচিত সংলাপটি মুছতে নিশ্চিত করুন?",
|
||||
DeleteToast: "চ্যাটটি মুছেছেন",
|
||||
Revert: "পুনরায়",
|
||||
DeleteChat: "নির্বাচিত চ্যাট মুছে ফেলার নিশ্চিত করুন?",
|
||||
DeleteToast: "চ্যাট মুছে ফেলা হয়েছে",
|
||||
Revert: "পূর্বাবস্থায় ফেরান",
|
||||
},
|
||||
Settings: {
|
||||
Title: "সেটিংস",
|
||||
SubTitle: "সমস্ত সেটিংস",
|
||||
SubTitle: "সমস্ত সেটিংস অপশন",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "সমস্ত সেটিংস পুনঃনির্দেশ দিন",
|
||||
SubTitle: "সকল সেটিংস ডিফল্টে পুনঃনির্দেশ দিতে",
|
||||
Action: "পুনঃনির্দেশ দিন",
|
||||
Confirm: "সমস্ত সেটিংস ডিফল্টে পুনঃনির্দেশ করতে নিশ্চিত করতে?",
|
||||
Title: "সমস্ত সেটিংস পুনরায় সেট করুন",
|
||||
SubTitle: "সমস্ত সেটিংস বিকল্পগুলিকে ডিফল্ট মানে পুনরায় সেট করুন",
|
||||
Action: "এখনই পুনরায় সেট করুন",
|
||||
Confirm: "সমস্ত সেটিংস পুনরায় সেট করার নিশ্চিত করুন?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "সমস্ত তথ্য মুছুন",
|
||||
SubTitle: "সমস্ত বার্তা এবং সেটিংস মুছুন",
|
||||
Action: "মুছুন",
|
||||
Confirm: "সমস্ত বার্তা এবং সেটিংস মুছে ফেলতে নিশ্চিত করতে?",
|
||||
Title: "সমস্ত তথ্য মুছে ফেলুন",
|
||||
SubTitle: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলুন",
|
||||
Action: "এখনই মুছে ফেলুন",
|
||||
Confirm: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলানোর নিশ্চিত করুন?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "বাংলা", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "সমস্ত ভাষা",
|
||||
},
|
||||
Avatar: "অবতার",
|
||||
Avatar: "অভিনেতা",
|
||||
FontSize: {
|
||||
Title: "ফন্ট সাইজ",
|
||||
SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন",
|
||||
SubTitle: "চ্যাট কনটেন্টের ফন্ট সাইজ",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "চ্যাট ফন্ট",
|
||||
SubTitle:
|
||||
"চ্যাট সামগ্রীর ফন্ট, বিশ্বব্যাপী ডিফল্ট ফন্ট প্রয়োগ করতে খালি রাখুন",
|
||||
Placeholder: "ফন্টের নাম",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "حقن تلميحات النظام",
|
||||
Title: "সিস্টেম-লেভেল প্রম্পট যোগ করুন",
|
||||
SubTitle:
|
||||
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب",
|
||||
"প্রত্যেক বার্তায় একটি সিস্টেম প্রম্পট যোগ করুন যা ChatGPT এর অনুকরণ করবে",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "ইনপুট টেমপ্লেট",
|
||||
SubTitle: "নতুনতম বার্তা এই টেমপ্লেটে পূরণ হবে",
|
||||
Title: "ব্যবহারকারীর ইনপুট প্রিপ্রসেসিং",
|
||||
SubTitle: "ব্যবহারকারীর সর্বশেষ বার্তা এই টেমপ্লেটে পূরণ করা হবে",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
IsLatest: "Latest version",
|
||||
CheckUpdate: "Check Update",
|
||||
IsChecking: "Checking update...",
|
||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||
GoToUpdate: "Update",
|
||||
Version: (x: string) => `বর্তমান সংস্করণ: ${x}`,
|
||||
IsLatest: "এটি সর্বশেষ সংস্করণ",
|
||||
CheckUpdate: "আপডেট পরীক্ষা করুন",
|
||||
IsChecking: "আপডেট পরীক্ষা করা হচ্ছে...",
|
||||
FoundUpdate: (x: string) => `নতুন সংস্করণ পাওয়া গিয়েছে: ${x}`,
|
||||
GoToUpdate: "আপডেট করতে যান",
|
||||
},
|
||||
SendKey: "প্রেরণ চাবি",
|
||||
SendKey: "পাঠানোর কী",
|
||||
Theme: "থিম",
|
||||
TightBorder: "সঙ্গতি সীমা",
|
||||
TightBorder: "বর্ডার-বিহীন মোড",
|
||||
SendPreviewBubble: {
|
||||
Title: "প্রিভিউ বুলবুল প্রেরণ করুন",
|
||||
SubTitle: "বুলবুলে মার্কডাউন প্রিভিউ করুন",
|
||||
Title: "প্রিভিউ বুদবুদ",
|
||||
SubTitle: "প্রিভিউ বুদবুদে Markdown কনটেন্ট প্রিভিউ করুন",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন",
|
||||
SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "ক্লাউড ডেটা",
|
||||
NotSyncYet: "এখনো সিঙ্ক করা হয়নি",
|
||||
Success: "সিঙ্ক সফল",
|
||||
Fail: "সিঙ্ক ব্যর্থ",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "ক্লাউড সিঙ্ক কনফিগার করুন",
|
||||
Check: "পরীক্ষা করুন",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "সিঙ্ক টাইপ",
|
||||
SubTitle: "পছন্দসই সিঙ্ক সার্ভার নির্বাচন করুন",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "প্রক্সি সক্রিয় করুন",
|
||||
SubTitle:
|
||||
"ব্রাউজারে সিঙ্ক করার সময়, ক্রস-অরিজিন সীমাবদ্ধতা এড়াতে প্রক্সি সক্রিয় করতে হবে",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "প্রক্সি ঠিকানা",
|
||||
SubTitle:
|
||||
"এটি শুধুমাত্র প্রকল্পের সাথে সরবরাহিত ক্রস-অরিজিন প্রক্সির জন্য প্রযোজ্য",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "WebDAV ঠিকানা",
|
||||
UserName: "ব্যবহারকারীর নাম",
|
||||
Password: "পাসওয়ার্ড",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST URL",
|
||||
UserName: "ব্যাকআপ নাম",
|
||||
Password: "UpStash Redis REST টোকেন",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "স্থানীয় ডেটা",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} বার চ্যাট, ${overview.message} বার্তা, ${overview.prompt} প্রম্পট, ${overview.mask} মাস্ক`;
|
||||
},
|
||||
ImportFailed: "আমদানি ব্যর্থ",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "মাস্ক স্প্ল্যাশ স্ক্রিন",
|
||||
SubTitle:
|
||||
"নতুন চ্যাট শুরু করার আগে মাস্ক স্প্ল্যাশ স্ক্রিন প্রদর্শন করুন",
|
||||
Title: "মাস্ক লঞ্চ পেজ",
|
||||
SubTitle: "নতুন চ্যাট শুরু করার সময় মাস্ক লঞ্চ পেজ প্রদর্শন করুন",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "মূলত মাস্ক গোপন করুন",
|
||||
SubTitle: "মাস্ক তালিকা থেকে মূলত মাস্কগুলি লুকান",
|
||||
Title: "ইনবিল্ট মাস্ক লুকান",
|
||||
SubTitle: "সমস্ত মাস্ক তালিকায় ইনবিল্ট মাস্ক লুকান",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "অটো-সম্পূর্ণতা নিষ্ক্রিয় করুন",
|
||||
SubTitle: "অটো-সম্পূর্ণতা চালু করতে / ইনপুট করুন",
|
||||
Title: "প্রম্পট অটো-কমপ্লিশন নিষ্ক্রিয় করুন",
|
||||
SubTitle: "ইনপুট বক্সের শুরুতে / টাইপ করলে অটো-কমপ্লিশন সক্রিয় হবে",
|
||||
},
|
||||
List: "প্রম্পট তালিকা",
|
||||
List: "স্বনির্ধারিত প্রম্পট তালিকা",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} built-in, ${custom} user-defined`,
|
||||
Edit: "সম্পাদন করুন",
|
||||
`ইনবিল্ট ${builtin} টি, ব্যবহারকারী সংজ্ঞায়িত ${custom} টি`,
|
||||
Edit: "সম্পাদনা করুন",
|
||||
Modal: {
|
||||
Title: "প্রম্পট তালিকা",
|
||||
Add: "একটি যোগ করুন",
|
||||
Search: "সন্ধান প্রম্পট",
|
||||
Add: "নতুন করুন",
|
||||
Search: "প্রম্পট অনুসন্ধান করুন",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "সম্পাদন করুন প্রম্পট",
|
||||
Title: "প্রম্পট সম্পাদনা করুন",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "সংযুক্ত বার্তা সংখ্যা",
|
||||
SubTitle: "প্রতি অনুরোধে প্রেরণ করা গেলে প্রেরণ করা হবে",
|
||||
Title: "সংযুক্ত ইতিহাস বার্তার সংখ্যা",
|
||||
SubTitle: "প্রতিটি অনুরোধে সংযুক্ত ইতিহাস বার্তার সংখ্যা",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "ইতিহাস সঙ্কুচিত করার সীমা",
|
||||
Title: "ইতিহাস বার্তা দৈর্ঘ্য সংকুচিত থ্রেশহোল্ড",
|
||||
SubTitle:
|
||||
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
|
||||
"যখন সংকুচিত ইতিহাস বার্তা এই মান ছাড়িয়ে যায়, তখন সংকুচিত করা হবে",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "একাউন্ট ব্যালেন্স",
|
||||
Title: "ব্যালেন্স চেক",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন $${total}`;
|
||||
return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন মোট $${total}`;
|
||||
},
|
||||
IsChecking: "চেক করা হচ্ছে...",
|
||||
Check: "চেক",
|
||||
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
|
||||
IsChecking: "পরীক্ষা করা হচ্ছে…",
|
||||
Check: "পুনরায় পরীক্ষা করুন",
|
||||
NoAccess: "ব্যালেন্স দেখতে API কী অথবা অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
|
||||
},
|
||||
|
||||
Model: "মডেল",
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "অ্যাক্সেস পাসওয়ার্ড",
|
||||
SubTitle: "অ্যাডমিন এনক্রিপ্টেড অ্যাক্সেস সক্রিয় করেছেন",
|
||||
Placeholder: "অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "স্বনির্ধারিত ইন্টারফেস",
|
||||
SubTitle: "স্বনির্ধারিত Azure বা OpenAI সার্ভিস ব্যবহার করবেন কি?",
|
||||
},
|
||||
Provider: {
|
||||
Title: "মডেল পরিষেবা প্রদানকারী",
|
||||
SubTitle: "বিভিন্ন পরিষেবা প্রদানকারীতে স্যুইচ করুন",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API কী",
|
||||
SubTitle:
|
||||
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত OpenAI কী ব্যবহার করুন",
|
||||
Placeholder: "OpenAI API কী",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "ডিফল্ট ঠিকানা বাদে, http(s):// অন্তর্ভুক্ত করতে হবে",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "ইন্টারফেস কী",
|
||||
SubTitle:
|
||||
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Azure কী ব্যবহার করুন",
|
||||
Placeholder: "Azure API কী",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "উদাহরণ:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "ইন্টারফেস সংস্করণ (azure api version)",
|
||||
SubTitle: "নির্দিষ্ট সংস্করণ নির্বাচন করুন",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "ইন্টারফেস কী",
|
||||
SubTitle:
|
||||
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Anthropic কী ব্যবহার করুন",
|
||||
Placeholder: "Anthropic API কী",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "উদাহরণ:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "ইন্টারফেস সংস্করণ (claude api version)",
|
||||
SubTitle: "নির্দিষ্ট API সংস্করণ প্রবেশ করুন",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API কী",
|
||||
SubTitle: "Google AI থেকে আপনার API কী পান",
|
||||
Placeholder: "আপনার Google AI Studio API কী প্রবেশ করুন",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "টার্মিনাল ঠিকানা",
|
||||
SubTitle: "উদাহরণ:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "API সংস্করণ (শুধুমাত্র gemini-pro)",
|
||||
SubTitle: "একটি নির্দিষ্ট API সংস্করণ নির্বাচন করুন",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Google সেফটি ফিল্টার স্তর",
|
||||
SubTitle: "বিষয়বস্তু ফিল্টার স্তর সেট করুন",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "API কী",
|
||||
SubTitle: "স্বনির্ধারিত Baidu API কী ব্যবহার করুন",
|
||||
Placeholder: "Baidu API কী",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "সিক্রেট কী",
|
||||
SubTitle: "স্বনির্ধারিত Baidu সিক্রেট কী ব্যবহার করুন",
|
||||
Placeholder: "Baidu সিক্রেট কী",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "স্বনির্ধারিত সমর্থিত নয়, .env কনফিগারেশনে চলে যান",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "ইন্টারফেস কী",
|
||||
SubTitle: "স্বনির্ধারিত ByteDance API কী ব্যবহার করুন",
|
||||
Placeholder: "ByteDance API কী",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "উদাহরণ:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "ইন্টারফেস কী",
|
||||
SubTitle: "স্বনির্ধারিত আলিবাবা ক্লাউড API কী ব্যবহার করুন",
|
||||
Placeholder: "Alibaba Cloud API কী",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "ইন্টারফেস ঠিকানা",
|
||||
SubTitle: "উদাহরণ:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "স্বনির্ধারিত মডেল নাম",
|
||||
SubTitle:
|
||||
"স্বনির্ধারিত মডেল বিকল্পগুলি যুক্ত করুন, ইংরেজি কমা দ্বারা আলাদা করুন",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "মডেল (model)",
|
||||
Temperature: {
|
||||
Title: "তাপমাত্রা",
|
||||
SubTitle: "আরতি মান বেশি করলে বেশি এলোমেলো আউটপুট হবে",
|
||||
Title: "যাদুকরিতা (temperature)",
|
||||
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",
|
||||
},
|
||||
TopP: {
|
||||
Title: "শীর্ষ পি",
|
||||
SubTitle: "তাপমাত্রা সঙ্গে এই মান পরিবর্তন করবেন না",
|
||||
Title: "নিউক্লিয়ার স্যাম্পলিং (top_p)",
|
||||
SubTitle: "যাদুকরিতা মত, কিন্তু একসাথে পরিবর্তন করবেন না",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "সর্বাধিক টোকেন",
|
||||
SubTitle: "ইনপুট টোকেন এবং উৎপাদিত টোকেনের সর্বাধিক দৈর্ঘ্য",
|
||||
Title: "একটি উত্তর সীমা (max_tokens)",
|
||||
SubTitle: "প্রতি ইন্টারঅ্যাকশনে সর্বাধিক টোকেন সংখ্যা",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "উপস্থিতির জরিমানা",
|
||||
SubTitle: "আরতি মান বেশি করলে নতুন বিষয়গুলি সম্ভাব্যতা বাড়াতে পারে",
|
||||
Title: "বিষয়বস্তু তাজা (presence_penalty)",
|
||||
SubTitle: "মান বাড়ালে নতুন বিষয়ে প্রসারিত হওয়ার সম্ভাবনা বেশি",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "ফ্রিকুয়েন্সি জরিমানা",
|
||||
SubTitle:
|
||||
"আরতি মান বাড়ালে একই লাইন পুনরায় ব্যাবহার করার সম্ভাবনা হ্রাস পায়",
|
||||
Title: "ফ্রিকোয়েন্সি পেনাল্টি (frequency_penalty)",
|
||||
SubTitle: "মান বাড়ালে পুনরাবৃত্তি শব্দ কমানোর সম্ভাবনা বেশি",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "নতুন সংলাপ",
|
||||
BotHello: "হ্যালো! আজকে আপনাকে কিভাবে সাহায্য করতে পারি?",
|
||||
Error: "কিছু নিয়ে ভুল হয়েছে, পরে আবার চেষ্টা করুন।",
|
||||
DefaultTopic: "নতুন চ্যাট",
|
||||
BotHello: "আপনার জন্য কিছু করতে পারি?",
|
||||
Error: "একটি ত্রুটি ঘটেছে, পরে আবার চেষ্টা করুন",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"এটি চ্যাট ইতিহাসের সংক্ষিপ্ত সংকলনের মতো: " + content,
|
||||
"এটি পূর্বের চ্যাটের সারাংশ হিসেবে ব্যবহৃত হবে: " + content,
|
||||
Topic:
|
||||
"আমাদের সংলাপটির চার থেকে পাঁচ শব্দের একটি শিরোনাম তৈরি করুন যা আমাদের আলাপের সংক্ষিপ্তসার হিসাবে যোগ হবে না, যেমন অভিবৃত্তি, বিন্যাস, উদ্ধৃতি, পূর্বচালক চিহ্ন, পূর্বরোবক্তির যেকোনো চিহ্ন বা অতিরিক্ত পাঠ। মেয়াদশেষ উদ্ধৃতি চেষ্টা করুন।",
|
||||
"চার থেকে পাঁচটি শব্দ ব্যবহার করে এই বাক্যের সংক্ষিপ্ত থিম দিন, ব্যাখ্যা, বিরাম চিহ্ন, ভাষা, অতিরিক্ত টেক্সট বা বোল্ড না ব্যবহার করুন। যদি কোনো থিম না থাকে তবে সরাসরি 'বেকার' বলুন",
|
||||
Summarize:
|
||||
"২০০ শব্দের লম্বা হয়ে মুহূর্তে আলোচনা সংক্ষেপের রপ্তানি করুন, যেটি ভবিষ্যতের প্রম্পট হিসাবে ব্যবহার করবেন।",
|
||||
"আলোচনার বিষয়বস্তু সংক্ষিপ্তভাবে সারাংশ করুন, পরবর্তী কনটেক্সট প্রম্পট হিসেবে ব্যবহারের জন্য, ২০০ শব্দের মধ্যে সীমাবদ্ধ রাখুন",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "ক্লিপবোর্ডে কপি করা হয়েছে",
|
||||
Failed: "কপি ব্যর্থ, অনুমতি প্রদান করার জন্য অনুমতি প্রদান করুন",
|
||||
Success: "ক্লিপবোর্ডে লেখা হয়েছে",
|
||||
Failed: "কপি ব্যর্থ হয়েছে, দয়া করে ক্লিপবোর্ড অনুমতি প্রদান করুন",
|
||||
},
|
||||
Download: {
|
||||
Success: "বিষয়বস্তু আপনার ডিরেক্টরিতে ডাউনলোড করা হয়েছে।",
|
||||
Failed: "ডাউনলোড ব্যর্থ হয়েছে।",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||
Toast: (x: any) => `${x}টি পূর্বনির্ধারিত প্রম্পট অন্তর্ভুক্ত`,
|
||||
Edit: "বর্তমান চ্যাট সেটিংস",
|
||||
Add: "একটি প্রম্পট যোগ করুন",
|
||||
Clear: "সঙ্গতি পরিস্কার করুন",
|
||||
Revert: "পূর্ববর্তী অবস্থানে ফিরে যান",
|
||||
Add: "একটি নতুন চ্যাট যোগ করুন",
|
||||
Clear: "কনটেক্সট পরিষ্কার করা হয়েছে",
|
||||
Revert: "কনটেক্সট পুনরুদ্ধার করুন",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "প্লাগইন",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "আপনি একটি সহকারী যা",
|
||||
Sysmessage: "আপনি একজন সহকারী",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "অনুসন্ধান",
|
||||
Page: {
|
||||
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
|
||||
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
|
||||
NoResult: "কোন ফলাফল পাওয়া যায়নি",
|
||||
NoData: "কোন তথ্য নেই",
|
||||
Loading: "লোড হচ্ছে",
|
||||
|
||||
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
|
||||
},
|
||||
Item: {
|
||||
View: "দেখুন",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "মাস্ক",
|
||||
Page: {
|
||||
Title: "প্রম্পট টেমপ্লেট",
|
||||
SubTitle: (count: number) => `${count} টি প্রম্পট টেমপ্লেট`,
|
||||
Search: "টেমপ্লেট অনুসন্ধান করুন",
|
||||
Create: "তৈরি করুন",
|
||||
Title: "পূর্বনির্ধারিত চরিত্র মাস্ক",
|
||||
SubTitle: (count: number) => `${count}টি পূর্বনির্ধারিত চরিত্র সংজ্ঞা`,
|
||||
Search: "চরিত্র মাস্ক অনুসন্ধান করুন",
|
||||
Create: "নতুন তৈরি করুন",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} প্রম্পট`,
|
||||
Info: (count: number) => `ভিতরে ${count}টি পূর্বনির্ধারিত চ্যাট রয়েছে`,
|
||||
Chat: "চ্যাট",
|
||||
View: "দেখুন",
|
||||
Edit: "সম্পাদন করুন",
|
||||
Edit: "সম্পাদনা করুন",
|
||||
Delete: "মুছে ফেলুন",
|
||||
DeleteConfirm: "মুছে ফেলতে নিশ্চিত করুন?",
|
||||
DeleteConfirm: "মুছে ফেলার জন্য নিশ্চিত করুন?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`প্রম্পট টেমপ্লেট সম্পাদন করুন ${readonly ? "(readonly)" : ""}`,
|
||||
Download: "ডাউনলোড করুন",
|
||||
Clone: "ক্লোন করুন",
|
||||
`পূর্বনির্ধারিত মাস্ক সম্পাদনা ${readonly ? "(পঠনযোগ্য)" : ""}`,
|
||||
Download: "পূর্বনির্ধারিত ডাউনলোড করুন",
|
||||
Clone: "পূর্বনির্ধারিত ক্লোন করুন",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "বট অবতার",
|
||||
Name: "বটের নাম",
|
||||
Avatar: "চরিত্রের চিত্র",
|
||||
Name: "চরিত্রের নাম",
|
||||
Sync: {
|
||||
Title: "গ্লোবাল কনফিগ ব্যবহার করুন",
|
||||
SubTitle: "এই চ্যাটে গ্লোবাল কনফিগ ব্যবহার করুন",
|
||||
Title: "গ্লোবাল সেটিংস ব্যবহার করুন",
|
||||
SubTitle: "বর্তমান চ্যাট গ্লোবাল মডেল সেটিংস ব্যবহার করছে কি না",
|
||||
Confirm:
|
||||
"গ্লোবাল কনফিগ দ্বারা কাস্টম কনফিগ ওভাররাইড করতে নিশ্চিত করতে?",
|
||||
"বর্তমান চ্যাটের কাস্টম সেটিংস স্বয়ংক্রিয়ভাবে ওভাররাইট হবে, গ্লোবাল সেটিংস সক্রিয় করতে নিশ্চিত?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "সংশ্লিষ্টতা প্রম্পটগুলি লুকান",
|
||||
SubTitle: "চ্যাটে সংশ্লিষ্টতা প্রম্পটগুলি দেখাবেন না",
|
||||
Title: "পূর্বনির্ধারিত চ্যাট লুকান",
|
||||
SubTitle:
|
||||
"লুকানোর পরে পূর্বনির্ধারিত চ্যাট চ্যাট ইন্টারফেসে প্রদর্শিত হবে না",
|
||||
},
|
||||
Share: {
|
||||
Title: "এই মাস্কটি শেয়ার করুন",
|
||||
SubTitle: "এই মাস্কের একটি লিঙ্ক তৈরি করুন",
|
||||
Title: "এই মাস্ক শেয়ার করুন",
|
||||
SubTitle: "এই মাস্কের সরাসরি লিঙ্ক তৈরি করুন",
|
||||
Action: "লিঙ্ক কপি করুন",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "ফিরে যান",
|
||||
Skip: "শুরু করুন",
|
||||
Title: "মাস্ক নির্বাচন করুন",
|
||||
SubTitle: "মাস্কের পিছনে আত্মার সঙ্গে চ্যাট করুন",
|
||||
More: "আরো খুঁজুন",
|
||||
NotShow: "এখনও দেখাবেন না",
|
||||
Skip: "ডাইরেক্ট শুরু করুন",
|
||||
NotShow: "আবার প্রদর্শন করবেন না",
|
||||
ConfirmNoShow:
|
||||
"নিষ্ক্রিয় করতে নিশ্চিত করুন? পরে আপনি এটি সেটিংসে সক্ষম করতে পারবেন।",
|
||||
"নিশ্চিত যে নিষ্ক্রিয় করবেন? নিষ্ক্রিয় করার পরে সেটিংসে পুনরায় সক্রিয় করা যাবে।",
|
||||
Title: "একটি মাস্ক নির্বাচন করুন",
|
||||
SubTitle: "এখন শুরু করুন, মাস্কের পিছনের চিন্তা প্রতিক্রিয়া করুন",
|
||||
More: "সব দেখুন",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "লিঙ্কে অ্যাক্সেস কোড ইতিমধ্যে অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
|
||||
Settings:
|
||||
"লিঙ্কে প্রাক-নির্ধারিত সেটিংস অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "নিশ্চিত করুন",
|
||||
Cancel: "বাতিল করুন",
|
||||
Close: "বন্ধ করুন",
|
||||
Create: "তৈরি করুন",
|
||||
Edit: "সম্পাদন করুন",
|
||||
Create: "নতুন তৈরি করুন",
|
||||
Edit: "সম্পাদনা করুন",
|
||||
Export: "রপ্তানি করুন",
|
||||
Import: "আমদানি করুন",
|
||||
Sync: "সিঙ্ক",
|
||||
Config: "কনফিগারেশন",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "শুধুমাত্র কনটেক্সট পরিষ্কার করার পরে বার্তাগুলি প্রদর্শিত হবে",
|
||||
},
|
||||
Model: "মডেল",
|
||||
Messages: "বার্তা",
|
||||
Topic: "টপিক",
|
||||
Topic: "থিম",
|
||||
Time: "সময়",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ShortcutKeyModal } from "../components/chat";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
@@ -42,6 +43,7 @@ const cn = {
|
||||
PinToastAction: "查看",
|
||||
Delete: "删除",
|
||||
Edit: "编辑",
|
||||
FullScreen: "全屏",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
@@ -80,6 +82,14 @@ const cn = {
|
||||
SaveAs: "存为面具",
|
||||
},
|
||||
IsContext: "预设提示词",
|
||||
ShortcutKey: {
|
||||
Title: "键盘快捷方式",
|
||||
newChat: "打开新聊天",
|
||||
focusInput: "聚焦输入框",
|
||||
copyLastMessage: "复制最后一个回复",
|
||||
copyLastCode: "复制最后一个代码块",
|
||||
showShortcutKey: "显示快捷方式",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "分享聊天记录",
|
||||
@@ -104,6 +114,10 @@ const cn = {
|
||||
Toast: "正在生成截图",
|
||||
Modal: "长按或右键保存图片",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "分享页面",
|
||||
Error: "分享失败",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "搜索消息",
|
||||
@@ -128,6 +142,7 @@ const cn = {
|
||||
Settings: {
|
||||
Title: "设置",
|
||||
SubTitle: "所有设置选项",
|
||||
ShowPassword: "显示密码",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
@@ -152,6 +167,11 @@ const cn = {
|
||||
Title: "字体大小",
|
||||
SubTitle: "聊天内容的字体大小",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "聊天字体",
|
||||
SubTitle: "聊天内容的字体,若置空则应用全局默认字体",
|
||||
Placeholder: "字体名称",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "注入系统级提示信息",
|
||||
SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示",
|
||||
@@ -313,6 +333,23 @@ const cn = {
|
||||
SubTitle: "选择指定的部分版本",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制",
|
||||
Placeholder: "Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "接口版本 (claude api version)",
|
||||
SubTitle: "选择一个特定的 API 版本输入",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API 密钥",
|
||||
@@ -329,6 +366,102 @@ const cn = {
|
||||
Title: "API 版本(仅适用于 gemini-pro)",
|
||||
SubTitle: "选择一个特定的 API 版本",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Google 安全过滤级别",
|
||||
SubTitle: "设置内容过滤级别",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自定义 Baidu API Key",
|
||||
Placeholder: "Baidu API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Secret Key",
|
||||
SubTitle: "使用自定义 Baidu Secret Key",
|
||||
Placeholder: "Baidu Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "不支持自定义前往.env配置",
|
||||
},
|
||||
},
|
||||
Tencent: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自定义腾讯云API Key",
|
||||
Placeholder: "Tencent API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Secret Key",
|
||||
SubTitle: "使用自定义腾讯云Secret Key",
|
||||
Placeholder: "Tencent Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "不支持自定义前往.env配置",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 ByteDance API Key",
|
||||
Placeholder: "ByteDance API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义阿里云API Key",
|
||||
Placeholder: "Alibaba Cloud API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Moonshot: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义月之暗面API Key",
|
||||
Placeholder: "Moonshot API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 Stability API Key",
|
||||
Placeholder: "Stability API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Iflytek: {
|
||||
ApiKey: {
|
||||
Title: "ApiKey",
|
||||
SubTitle: "从讯飞星火控制台获取的 APIKey",
|
||||
Placeholder: "APIKey",
|
||||
},
|
||||
ApiSecret: {
|
||||
Title: "ApiSecret",
|
||||
SubTitle: "从讯飞星火控制台获取的 APISecret",
|
||||
Placeholder: "APISecret",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "自定义模型名",
|
||||
@@ -371,8 +504,8 @@ const cn = {
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "已写入剪切板",
|
||||
Failed: "复制失败,请赋予剪切板权限",
|
||||
Success: "已写入剪贴板",
|
||||
Failed: "复制失败,请赋予剪贴板权限",
|
||||
},
|
||||
Download: {
|
||||
Success: "内容已下载到您的目录。",
|
||||
@@ -385,12 +518,67 @@ const cn = {
|
||||
Clear: "上下文已清除",
|
||||
Revert: "恢复上下文",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "插件",
|
||||
Discovery: {
|
||||
Name: "发现",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "你是一个助手",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "搜索",
|
||||
Page: {
|
||||
Title: "搜索聊天记录",
|
||||
Search: "输入搜索关键词",
|
||||
NoResult: "没有找到结果",
|
||||
NoData: "没有数据",
|
||||
Loading: "加载中",
|
||||
|
||||
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
|
||||
},
|
||||
Item: {
|
||||
View: "查看",
|
||||
},
|
||||
},
|
||||
Plugin: {
|
||||
Name: "插件",
|
||||
Page: {
|
||||
Title: "插件",
|
||||
SubTitle: (count: number) => `${count} 个插件`,
|
||||
Search: "搜索插件",
|
||||
Create: "新建",
|
||||
Find: "您可以在Github上找到优秀的插件:",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} 方法`,
|
||||
View: "查看",
|
||||
Edit: "编辑",
|
||||
Delete: "删除",
|
||||
DeleteConfirm: "确认删除?",
|
||||
},
|
||||
Auth: {
|
||||
None: "不需要授权",
|
||||
Basic: "Basic",
|
||||
Bearer: "Bearer",
|
||||
Custom: "自定义",
|
||||
CustomHeader: "自定义参数名称",
|
||||
Token: "Token",
|
||||
Proxy: "使用代理",
|
||||
ProxyDescription: "使用代理解决 CORS 错误",
|
||||
Location: "位置",
|
||||
LocationHeader: "Header",
|
||||
LocationQuery: "Query",
|
||||
LocationBody: "Body",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
|
||||
Download: "下载",
|
||||
Auth: "授权方式",
|
||||
Content: "OpenAPI Schema",
|
||||
Load: "从网页加载",
|
||||
Method: "方法",
|
||||
Error: "格式错误",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "面具",
|
||||
Page: {
|
||||
@@ -425,6 +613,10 @@ const cn = {
|
||||
Title: "隐藏预设对话",
|
||||
SubTitle: "隐藏后预设对话不会出现在聊天界面",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "启用Artifacts",
|
||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||
},
|
||||
Share: {
|
||||
Title: "分享此面具",
|
||||
SubTitle: "生成此面具的直达链接",
|
||||
@@ -467,6 +659,61 @@ const cn = {
|
||||
Topic: "主题",
|
||||
Time: "时间",
|
||||
},
|
||||
SdPanel: {
|
||||
Prompt: "画面提示",
|
||||
NegativePrompt: "否定提示",
|
||||
PleaseInput: (name: string) => `请输入${name}`,
|
||||
AspectRatio: "横纵比",
|
||||
ImageStyle: "图像风格",
|
||||
OutFormat: "输出格式",
|
||||
AIModel: "AI模型",
|
||||
ModelVersion: "模型版本",
|
||||
Submit: "提交生成",
|
||||
ParamIsRequired: (name: string) => `${name}不能为空`,
|
||||
Styles: {
|
||||
D3Model: "3D模型",
|
||||
AnalogFilm: "模拟电影",
|
||||
Anime: "动漫",
|
||||
Cinematic: "电影风格",
|
||||
ComicBook: "漫画书",
|
||||
DigitalArt: "数字艺术",
|
||||
Enhance: "增强",
|
||||
FantasyArt: "幻想艺术",
|
||||
Isometric: "等角",
|
||||
LineArt: "线描",
|
||||
LowPoly: "低多边形",
|
||||
ModelingCompound: "建模材料",
|
||||
NeonPunk: "霓虹朋克",
|
||||
Origami: "折纸",
|
||||
Photographic: "摄影",
|
||||
PixelArt: "像素艺术",
|
||||
TileTexture: "贴图",
|
||||
},
|
||||
},
|
||||
Sd: {
|
||||
SubTitle: (count: number) => `共 ${count} 条绘画`,
|
||||
Actions: {
|
||||
Params: "查看参数",
|
||||
Copy: "复制提示词",
|
||||
Delete: "删除",
|
||||
Retry: "重试",
|
||||
ReturnHome: "返回首页",
|
||||
History: "查看历史",
|
||||
},
|
||||
EmptyRecord: "暂无绘画记录",
|
||||
Status: {
|
||||
Name: "状态",
|
||||
Success: "成功",
|
||||
Error: "失败",
|
||||
Wait: "等待中",
|
||||
Running: "运行中",
|
||||
},
|
||||
Danger: {
|
||||
Delete: "确认删除?",
|
||||
},
|
||||
GenerateParams: "生成参数",
|
||||
Detail: "详情",
|
||||
},
|
||||
};
|
||||
|
||||
type DeepPartial<T> = T extends object
|
||||
|
||||
@@ -1,233 +1,562 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const cs: PartialLocaleType = {
|
||||
WIP: "V přípravě...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Neoprávněný přístup, zadejte přístupový kód na [stránce](/#/auth) nastavení.",
|
||||
Unauthorized: isApp
|
||||
? "Byl zjištěn neplatný API Key, prosím přejděte na stránku [Nastavení](/#/settings) a zkontrolujte, zda je API Key správně nakonfigurován."
|
||||
: "Heslo je nesprávné nebo prázdné, prosím přejděte na stránku [Přihlášení](/#/auth) a zadejte správné heslo, nebo na stránku [Nastavení](/#/settings) a zadejte svůj vlastní OpenAI API Key.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Potřebné heslo",
|
||||
Tips: "Administrátor povolil ověření heslem, prosím zadejte přístupový kód níže",
|
||||
SubTips: "nebo zadejte svůj OpenAI nebo Google API klíč",
|
||||
Input: "Zadejte přístupový kód zde",
|
||||
Confirm: "Potvrdit",
|
||||
Later: "Později",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} zpráv`,
|
||||
ChatItemCount: (count: number) => `${count} konverzací`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} zpráv s ChatGPT`,
|
||||
SubTitle: (count: number) => `Celkem ${count} konverzací`,
|
||||
EditMessage: {
|
||||
Title: "Upravit zprávy",
|
||||
Topic: {
|
||||
Title: "Téma konverzace",
|
||||
SubTitle: "Změnit aktuální téma konverzace",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Přejít na seznam chatů",
|
||||
CompressedHistory: "Pokyn z komprimované paměti historie",
|
||||
Export: "Exportovat všechny zprávy jako Markdown",
|
||||
ChatList: "Zobrazit seznam zpráv",
|
||||
CompressedHistory: "Zobrazit komprimovanou historii Prompt",
|
||||
Export: "Exportovat konverzace",
|
||||
Copy: "Kopírovat",
|
||||
Stop: "Zastavit",
|
||||
Retry: "Zopakovat",
|
||||
Retry: "Zkusit znovu",
|
||||
Pin: "Připnout",
|
||||
PinToastContent: "1 konverzace byla připnuta k přednastaveným promptům",
|
||||
PinToastAction: "Zobrazit",
|
||||
Delete: "Smazat",
|
||||
Edit: "Upravit",
|
||||
},
|
||||
Rename: "Přejmenovat chat",
|
||||
Typing: "Píše...",
|
||||
Commands: {
|
||||
new: "Nová konverzace",
|
||||
newm: "Nová konverzace z masky",
|
||||
next: "Další konverzace",
|
||||
prev: "Předchozí konverzace",
|
||||
clear: "Vymazat kontext",
|
||||
del: "Smazat konverzaci",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Zastavit odpověď",
|
||||
ToBottom: "Přejít na nejnovější",
|
||||
Theme: {
|
||||
auto: "Automatické téma",
|
||||
light: "Světelný režim",
|
||||
dark: "Tmavý režim",
|
||||
},
|
||||
Prompt: "Rychlé příkazy",
|
||||
Masks: "Všechny masky",
|
||||
Clear: "Vymazat konverzaci",
|
||||
Settings: "Nastavení konverzace",
|
||||
UploadImage: "Nahrát obrázek",
|
||||
},
|
||||
Rename: "Přejmenovat konverzaci",
|
||||
Typing: "Píše se…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} pro odeslání`;
|
||||
var inputHints = `${submitKey} odeslat`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter pro řádkování";
|
||||
inputHints += ",Shift + Enter pro nový řádek";
|
||||
}
|
||||
return inputHints + ", / pro vyhledávání pokynů";
|
||||
return inputHints + ",/ pro doplnění, : pro příkaz";
|
||||
},
|
||||
Send: "Odeslat",
|
||||
Config: {
|
||||
Reset: "Obnovit výchozí",
|
||||
SaveAs: "Uložit jako Masku",
|
||||
Reset: "Vymazat paměť",
|
||||
SaveAs: "Uložit jako masku",
|
||||
},
|
||||
IsContext: "Přednastavené prompty",
|
||||
},
|
||||
Export: {
|
||||
Title: "Všechny zprávy",
|
||||
Title: "Sdílet konverzace",
|
||||
Copy: "Kopírovat vše",
|
||||
Download: "Stáhnout",
|
||||
MessageFromYou: "Zpráva od vás",
|
||||
MessageFromChatGPT: "Zpráva z ChatGPT",
|
||||
Download: "Stáhnout soubor",
|
||||
Share: "Sdílet na ShareGPT",
|
||||
MessageFromYou: "Uživatel",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "Formát exportu",
|
||||
SubTitle: "Lze exportovat jako Markdown text nebo PNG obrázek",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Zahrnout kontext masky",
|
||||
SubTitle: "Zobrazit kontext masky ve zprávách",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Vybrat",
|
||||
Preview: "Náhled",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Generování screenshotu",
|
||||
Modal: "Dlouhým stiskem nebo pravým tlačítkem myši uložte obrázek",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Hledat zprávy",
|
||||
All: "Vybrat vše",
|
||||
Latest: "Několik posledních",
|
||||
Clear: "Zrušit výběr",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Pokyn z paměti",
|
||||
EmptyContent: "Zatím nic.",
|
||||
Send: "Odeslat paměť",
|
||||
Copy: "Kopírovat paměť",
|
||||
Reset: "Obnovit relaci",
|
||||
ResetConfirm:
|
||||
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
|
||||
Title: "Historie shrnutí",
|
||||
EmptyContent: "Obsah konverzace je příliš krátký, není třeba shrnovat",
|
||||
Send: "Automaticky komprimovat konverzace a odeslat jako kontext",
|
||||
Copy: "Kopírovat shrnutí",
|
||||
Reset: "[nepoužívá se]",
|
||||
ResetConfirm: "Opravdu chcete vymazat historii shrnutí?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Nový chat",
|
||||
DeleteChat: "Potvrzujete smazání vybrané konverzace?",
|
||||
DeleteToast: "Chat smazán",
|
||||
Revert: "Zvrátit",
|
||||
NewChat: "Nová konverzace",
|
||||
DeleteChat: "Opravdu chcete smazat vybranou konverzaci?",
|
||||
DeleteToast: "Konverzace byla smazána",
|
||||
Revert: "Vrátit",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Nastavení",
|
||||
SubTitle: "Všechna nastavení",
|
||||
SubTitle: "Všechny možnosti nastavení",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Obnovit všechna nastavení",
|
||||
SubTitle: "Obnovit všechny nastavení na výchozí hodnoty",
|
||||
Action: "Okamžitě obnovit",
|
||||
Confirm: "Opravdu chcete obnovit všechna nastavení?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Smazat všechna data",
|
||||
SubTitle: "Smazat všechny chaty a nastavení",
|
||||
Action: "Okamžitě smazat",
|
||||
Confirm: "Opravdu chcete smazat všechny chaty a nastavení?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Language", // POZOR: pokud chcete přidat nový překlad, prosím, nechte tuto hodnotu jako `Language`
|
||||
All: "Všechny jazyky",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
Avatar: "Profilový obrázek",
|
||||
FontSize: {
|
||||
Title: "Velikost písma",
|
||||
SubTitle: "Nastavení velikosti písma obsahu chatu",
|
||||
SubTitle: "Velikost písma pro obsah chatu",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Chatové Písmo",
|
||||
SubTitle:
|
||||
"Písmo obsahu chatu, ponechejte prázdné pro použití globálního výchozího písma",
|
||||
Placeholder: "Název Písma",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Vložit systémové prompty",
|
||||
Title: "Vložit systémové výzvy",
|
||||
SubTitle:
|
||||
"Vynutit přidání simulovaného systémového promptu ChatGPT na začátek seznamu zpráv každého požadavku",
|
||||
"Automaticky přidat systémovou výzvu simulující ChatGPT na začátek seznamu zpráv pro každý požadavek",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Předzpracování uživatelského vstupu",
|
||||
SubTitle: "Nejnovější zpráva uživatele bude vyplněna do této šablony",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Verze: ${x}`,
|
||||
IsLatest: "Aktuální verze",
|
||||
Version: (x: string) => `Aktuální verze: ${x}`,
|
||||
IsLatest: "Jste na nejnovější verzi",
|
||||
CheckUpdate: "Zkontrolovat aktualizace",
|
||||
IsChecking: "Kontrola aktualizace...",
|
||||
IsChecking: "Kontrola aktualizací...",
|
||||
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
|
||||
GoToUpdate: "Aktualizovat",
|
||||
GoToUpdate: "Přejít na aktualizaci",
|
||||
},
|
||||
SendKey: "Odeslat klíč",
|
||||
SendKey: "Klávesa pro odeslání",
|
||||
Theme: "Téma",
|
||||
TightBorder: "Těsné ohraničení",
|
||||
TightBorder: "Režim bez okrajů",
|
||||
SendPreviewBubble: {
|
||||
Title: "Odesílat chatovací bublinu s náhledem",
|
||||
SubTitle: "Zobrazit v náhledu bubliny",
|
||||
Title: "Náhledová bublina",
|
||||
SubTitle: "Náhled Markdown obsahu v náhledové bublině",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Automatické generování názvu",
|
||||
SubTitle: "Generovat vhodný název na základě obsahu konverzace",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Data na cloudu",
|
||||
NotSyncYet: "Ještě nebylo synchronizováno",
|
||||
Success: "Synchronizace úspěšná",
|
||||
Fail: "Synchronizace selhala",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Nastavení cloudové synchronizace",
|
||||
Check: "Zkontrolovat dostupnost",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Typ synchronizace",
|
||||
SubTitle: "Vyberte oblíbený synchronizační server",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Povolit proxy",
|
||||
SubTitle:
|
||||
"Při synchronizaci v prohlížeči musí být proxy povolena, aby se předešlo problémům s CORS",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Adresa proxy",
|
||||
SubTitle: "Pouze pro interní proxy",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "WebDAV adresa",
|
||||
UserName: "Uživatelské jméno",
|
||||
Password: "Heslo",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST URL",
|
||||
UserName: "Název zálohy",
|
||||
Password: "UpStash Redis REST Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Lokální data",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} konverzací, ${overview.message} zpráv, ${overview.prompt} promptů, ${overview.mask} masek`;
|
||||
},
|
||||
ImportFailed: "Import selhal",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Úvodní obrazovka Masek",
|
||||
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek",
|
||||
Title: "Úvodní stránka masky",
|
||||
SubTitle: "Při zahájení nové konverzace zobrazit úvodní stránku masky",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Skrýt vestavěné masky",
|
||||
SubTitle: "Skrýt vestavěné masky v seznamu všech masek",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Deaktivovat automatické dokončování",
|
||||
SubTitle: "Zadejte / pro spuštění automatického dokončování",
|
||||
Title: "Zakázat automatické doplňování promptů",
|
||||
SubTitle:
|
||||
"Automatické doplňování se aktivuje zadáním / na začátku textového pole",
|
||||
},
|
||||
List: "Seznam pokynů",
|
||||
List: "Seznam vlastních promptů",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} vestavěných, ${custom} uživatelských`,
|
||||
`Vestavěné ${builtin} položek, uživatelsky definované ${custom} položek`,
|
||||
Edit: "Upravit",
|
||||
Modal: {
|
||||
Title: "Seznam pokynů",
|
||||
Add: "Přidat pokyn",
|
||||
Search: "Hledat pokyny",
|
||||
Title: "Seznam promptů",
|
||||
Add: "Nový",
|
||||
Search: "Hledat prompty",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Editovat pokyn",
|
||||
Title: "Upravit prompt",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Počet připojených zpráv",
|
||||
SubTitle: "Počet odeslaných připojených zpráv na žádost",
|
||||
Title: "Počet historických zpráv",
|
||||
SubTitle: "Počet historických zpráv zahrnutých v každém požadavku",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Práh pro kompresi historie",
|
||||
Title: "Prahová hodnota komprese historických zpráv",
|
||||
SubTitle:
|
||||
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
|
||||
"Když nekomprimované historické zprávy překročí tuto hodnotu, dojde ke kompresi",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Stav účtu",
|
||||
Title: "Kontrola zůstatku",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Použito tento měsíc $${used}, předplaceno $${total}`;
|
||||
return `Tento měsíc použito $${used}, celkový předplatný objem $${total}`;
|
||||
},
|
||||
IsChecking: "Kontroluji...",
|
||||
Check: "Zkontrolovat",
|
||||
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
|
||||
IsChecking: "Probíhá kontrola…",
|
||||
Check: "Znovu zkontrolovat",
|
||||
NoAccess: "Zadejte API Key nebo přístupové heslo pro zobrazení zůstatku",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Přístupový kód",
|
||||
SubTitle: "Administrátor aktivoval šifrovaný přístup",
|
||||
Placeholder: "Zadejte přístupový kód",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Vlastní rozhraní",
|
||||
SubTitle: "Použít vlastní Azure nebo OpenAI službu",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Poskytovatel modelu",
|
||||
SubTitle: "Přepnout mezi různými poskytovateli",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle:
|
||||
"Použijte vlastní OpenAI Key k obejití přístupového omezení",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle: "Kromě výchozí adresy musí obsahovat http(s)://",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Rozhraní klíč",
|
||||
SubTitle: "Použijte vlastní Azure Key k obejití přístupového omezení",
|
||||
Placeholder: "Azure API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle: "Příklad:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Verze rozhraní (azure api version)",
|
||||
SubTitle: "Vyberte konkrétní verzi",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Rozhraní klíč",
|
||||
SubTitle:
|
||||
"Použijte vlastní Anthropic Key k obejití přístupového omezení",
|
||||
Placeholder: "Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle: "Příklad:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Verze rozhraní (claude api version)",
|
||||
SubTitle: "Vyberte konkrétní verzi API",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API klíč",
|
||||
SubTitle: "Získejte svůj API klíč od Google AI",
|
||||
Placeholder: "Zadejte svůj Google AI Studio API klíč",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Konečná adresa",
|
||||
SubTitle: "Příklad:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "Verze API (pouze pro gemini-pro)",
|
||||
SubTitle: "Vyberte konkrétní verzi API",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Úroveň bezpečnostního filtrování Google",
|
||||
SubTitle: "Nastavit úroveň filtrování obsahu",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Použijte vlastní Baidu API Key",
|
||||
Placeholder: "Baidu API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Secret Key",
|
||||
SubTitle: "Použijte vlastní Baidu Secret Key",
|
||||
Placeholder: "Baidu Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle:
|
||||
"Nepodporuje vlastní nastavení, přejděte na .env konfiguraci",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "Rozhraní klíč",
|
||||
SubTitle: "Použijte vlastní ByteDance API Key",
|
||||
Placeholder: "ByteDance API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle: "Příklad:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Rozhraní klíč",
|
||||
SubTitle: "Použijte vlastní Alibaba Cloud API Key",
|
||||
Placeholder: "Alibaba Cloud API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresa rozhraní",
|
||||
SubTitle: "Příklad:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Vlastní názvy modelů",
|
||||
SubTitle: "Přidejte možnosti vlastních modelů, oddělené čárkami",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Model (model)",
|
||||
Temperature: {
|
||||
Title: "Teplota",
|
||||
SubTitle: "Větší hodnota činí výstup náhodnějším",
|
||||
Title: "Náhodnost (temperature)",
|
||||
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Jádrové vzorkování (top_p)",
|
||||
SubTitle: "Podobné náhodnosti, ale neměňte spolu s náhodností",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Max. počet tokenů",
|
||||
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů",
|
||||
Title: "Omezení odpovědi (max_tokens)",
|
||||
SubTitle: "Maximální počet Tokenů použitých v jednom interakci",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Přítomnostní korekce",
|
||||
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
|
||||
Title: "Čerstvost témat (presence_penalty)",
|
||||
SubTitle:
|
||||
"Čím vyšší hodnota, tím větší pravděpodobnost rozšíření na nová témata",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Frekvenční penalizace",
|
||||
Title: "Penalizace frekvence (frequency_penalty)",
|
||||
SubTitle:
|
||||
"Větší hodnota snižující pravděpodobnost opakování stejného řádku",
|
||||
"Čím vyšší hodnota, tím větší pravděpodobnost snížení opakování slov",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nová konverzace",
|
||||
BotHello: "Ahoj! Jak mohu dnes pomoci?",
|
||||
Error: "Něco se pokazilo, zkuste to prosím později.",
|
||||
DefaultTopic: "Nový chat",
|
||||
BotHello: "Jak vám mohu pomoci?",
|
||||
Error: "Došlo k chybě, zkuste to prosím znovu později.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " +
|
||||
content,
|
||||
"Toto je shrnutí historie chatu jako kontext: " + content,
|
||||
Topic:
|
||||
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.",
|
||||
"Použijte čtyři až pět slov pro stručné téma této věty, bez vysvětlení, interpunkce, citoslovcí, nadbytečného textu, bez tučného písma. Pokud téma neexistuje, vraťte pouze 'neformální chat'.",
|
||||
Summarize:
|
||||
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.",
|
||||
"Stručně shrňte obsah konverzace jako kontextový prompt pro budoucí použití, do 200 slov",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Zkopírováno do schránky",
|
||||
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
|
||||
},
|
||||
Download: {
|
||||
Success: "Obsah byl stažen do vašeho adresáře.",
|
||||
Failed: "Stahování selhalo.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Použití ${x} kontextových pokynů`,
|
||||
Edit: "Kontextové a paměťové pokyny",
|
||||
Add: "Přidat pokyn",
|
||||
Toast: (x: any) => `Obsahuje ${x} přednastavených promptů`,
|
||||
Edit: "Nastavení aktuální konverzace",
|
||||
Add: "Přidat novou konverzaci",
|
||||
Clear: "Kontext byl vymazán",
|
||||
Revert: "Obnovit kontext",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Jste asistent, který",
|
||||
Sysmessage: "Jste asistent",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "Hledat",
|
||||
Page: {
|
||||
Title: "Hledat v historii chatu",
|
||||
Search: "Zadejte hledané klíčové slovo",
|
||||
NoResult: "Nebyly nalezeny žádné výsledky",
|
||||
NoData: "Žádná data",
|
||||
Loading: "Načítání",
|
||||
|
||||
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
|
||||
},
|
||||
Item: {
|
||||
View: "Zobrazit",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Maska",
|
||||
Page: {
|
||||
Title: "Šablona pokynu",
|
||||
SubTitle: (count: number) => `${count} šablon pokynů`,
|
||||
Search: "Hledat v šablonách",
|
||||
Create: "Vytvořit",
|
||||
Title: "Přednastavené role masky",
|
||||
SubTitle: (count: number) => `${count} definovaných rolí`,
|
||||
Search: "Hledat role masky",
|
||||
Create: "Nový",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} pokynů`,
|
||||
Info: (count: number) => `Obsahuje ${count} přednastavených konverzací`,
|
||||
Chat: "Chat",
|
||||
View: "Zobrazit",
|
||||
Edit: "Upravit",
|
||||
Delete: "Smazat",
|
||||
DeleteConfirm: "Potvrdit smazání?",
|
||||
DeleteConfirm: "Opravdu chcete smazat?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`,
|
||||
Download: "Stáhnout",
|
||||
Clone: "Duplikovat",
|
||||
`Upravit přednastavenou masku ${readonly ? " (jen pro čtení)" : ""}`,
|
||||
Download: "Stáhnout přednastavení",
|
||||
Clone: "Klonovat přednastavení",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar Bota",
|
||||
Name: "Jméno Bota",
|
||||
Avatar: "Profilový obrázek",
|
||||
Name: "Název role",
|
||||
Sync: {
|
||||
Title: "Použít globální nastavení",
|
||||
SubTitle: "Použít globální modelová nastavení pro aktuální konverzaci",
|
||||
Confirm:
|
||||
"Vaše vlastní nastavení konverzace bude automaticky přepsáno, opravdu chcete použít globální nastavení?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Skrýt přednastavené konverzace",
|
||||
SubTitle:
|
||||
"Po skrytí se přednastavené konverzace nebudou zobrazovat v chatovém rozhraní",
|
||||
},
|
||||
Share: {
|
||||
Title: "Sdílet tuto masku",
|
||||
SubTitle: "Generovat přímý odkaz na tuto masku",
|
||||
Action: "Kopírovat odkaz",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Zpět",
|
||||
Skip: "Přeskočit",
|
||||
Title: "Vyberte Masku",
|
||||
SubTitle: "Chatovat s duší za Maskou",
|
||||
More: "Najít více",
|
||||
NotShow: "Nezobrazovat znovu",
|
||||
ConfirmNoShow: "Potvrdit zakázání?Můžete jej povolit později v nastavení.",
|
||||
Skip: "Začít hned",
|
||||
NotShow: "Zobrazit už nikdy",
|
||||
ConfirmNoShow:
|
||||
"Opravdu chcete zakázat? Zakázání můžete kdykoli znovu povolit v nastavení.",
|
||||
Title: "Vyberte masku",
|
||||
SubTitle: "Začněte nyní a konfrontujte se s myslí za maskou",
|
||||
More: "Zobrazit vše",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Byl detekován přístupový kód v odkazu, chcete jej automaticky vyplnit?",
|
||||
Settings:
|
||||
"Byla detekována přednastavená nastavení v odkazu, chcete je automaticky vyplnit?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Potvrdit",
|
||||
Cancel: "Zrušit",
|
||||
Close: "Zavřít",
|
||||
Create: "Vytvořit",
|
||||
Create: "Nový",
|
||||
Edit: "Upravit",
|
||||
Export: "Exportovat",
|
||||
Import: "Importovat",
|
||||
Sync: "Synchronizovat",
|
||||
Config: "Konfigurovat",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Pouze zprávy po vymazání kontextu budou zobrazeny",
|
||||
},
|
||||
Model: "Model",
|
||||
Messages: "Zprávy",
|
||||
Topic: "Téma",
|
||||
|
||||
@@ -1,235 +1,580 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const de: PartialLocaleType = {
|
||||
WIP: "In Bearbeitung...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der [Einstellungsseite](/#/auth) ein.",
|
||||
Unauthorized: isApp
|
||||
? "Ungültiger API-Schlüssel erkannt. Bitte gehen Sie zur [Einstellungen](/#/settings) Seite, um zu überprüfen, ob der API-Schlüssel korrekt konfiguriert ist."
|
||||
: "Das Passwort ist falsch oder leer. Bitte gehen Sie zur [Login](/#/auth) Seite, um das richtige Passwort einzugeben, oder fügen Sie Ihren OpenAI API-Schlüssel auf der [Einstellungen](/#/settings) Seite hinzu.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Passwort erforderlich",
|
||||
Tips: "Der Administrator hat die Passwortüberprüfung aktiviert. Bitte geben Sie den Zugangscode unten ein.",
|
||||
SubTips: "Oder geben Sie Ihren OpenAI oder Google API-Schlüssel ein.",
|
||||
Input: "Geben Sie hier den Zugangscode ein",
|
||||
Confirm: "Bestätigen",
|
||||
Later: "Später",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} Nachrichten`,
|
||||
ChatItemCount: (count: number) => `${count} Gespräche`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "Zur Chat-Liste gehen",
|
||||
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
|
||||
Export: "Alle Nachrichten als Markdown exportieren",
|
||||
Copy: "Kopieren",
|
||||
Stop: "Stop",
|
||||
Retry: "Wiederholen",
|
||||
Delete: "Delete",
|
||||
SubTitle: (count: number) => `Insgesamt ${count} Gespräche`,
|
||||
EditMessage: {
|
||||
Title: "Nachricht bearbeiten",
|
||||
Topic: {
|
||||
Title: "Chat-Thema",
|
||||
SubTitle: "Ändern Sie das aktuelle Chat-Thema",
|
||||
},
|
||||
},
|
||||
Rename: "Chat umbenennen",
|
||||
Typing: "Tippen...",
|
||||
Actions: {
|
||||
ChatList: "Nachrichtliste anzeigen",
|
||||
CompressedHistory: "Komprimierte Historie anzeigen",
|
||||
Export: "Chatverlauf exportieren",
|
||||
Copy: "Kopieren",
|
||||
Stop: "Stoppen",
|
||||
Retry: "Erneut versuchen",
|
||||
Pin: "Anheften",
|
||||
PinToastContent: "1 Gespräch an den voreingestellten Prompt angeheftet",
|
||||
PinToastAction: "Ansehen",
|
||||
Delete: "Löschen",
|
||||
Edit: "Bearbeiten",
|
||||
},
|
||||
Commands: {
|
||||
new: "Neues Gespräch",
|
||||
newm: "Neues Gespräch aus Maske erstellen",
|
||||
next: "Nächstes Gespräch",
|
||||
prev: "Vorheriges Gespräch",
|
||||
clear: "Kontext löschen",
|
||||
del: "Gespräch löschen",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Antwort stoppen",
|
||||
ToBottom: "Zum neuesten Beitrag",
|
||||
Theme: {
|
||||
auto: "Automatisches Thema",
|
||||
light: "Helles Thema",
|
||||
dark: "Dunkles Thema",
|
||||
},
|
||||
Prompt: "Schnellbefehle",
|
||||
Masks: "Alle Masken",
|
||||
Clear: "Chat löschen",
|
||||
Settings: "Gesprächseinstellungen",
|
||||
UploadImage: "Bild hochladen",
|
||||
},
|
||||
Rename: "Gespräch umbenennen",
|
||||
Typing: "Tippt…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} um zu Senden`;
|
||||
var inputHints = `${submitKey} senden`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
|
||||
inputHints += ",Shift + Enter für Zeilenumbruch";
|
||||
}
|
||||
return inputHints + ", / zum Durchsuchen von Prompts";
|
||||
return inputHints + ",/ für Autovervollständigung, : für Befehle";
|
||||
},
|
||||
Send: "Senden",
|
||||
Config: {
|
||||
Reset: "Reset to Default",
|
||||
SaveAs: "Save as Mask",
|
||||
Reset: "Erinnerung löschen",
|
||||
SaveAs: "Als Maske speichern",
|
||||
},
|
||||
IsContext: "Voreingestellter Prompt",
|
||||
},
|
||||
Export: {
|
||||
Title: "Alle Nachrichten",
|
||||
Title: "Chatverlauf teilen",
|
||||
Copy: "Alles kopieren",
|
||||
Download: "Herunterladen",
|
||||
MessageFromYou: "Deine Nachricht",
|
||||
MessageFromChatGPT: "Nachricht von ChatGPT",
|
||||
Download: "Datei herunterladen",
|
||||
Share: "Auf ShareGPT teilen",
|
||||
MessageFromYou: "Benutzer",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "Exportformat",
|
||||
SubTitle: "Kann als Markdown-Text oder PNG-Bild exportiert werden",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Maske Kontext einbeziehen",
|
||||
SubTitle: "Soll der Maskenkontext in den Nachrichten angezeigt werden?",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Auswählen",
|
||||
Preview: "Vorschau",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Screenshot wird erstellt",
|
||||
Modal: "Lang drücken oder Rechtsklick, um Bild zu speichern",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Nachrichten suchen",
|
||||
All: "Alles auswählen",
|
||||
Latest: "Neueste",
|
||||
Clear: "Auswahl aufheben",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Gedächtnis-Prompt",
|
||||
EmptyContent: "Noch nichts.",
|
||||
Send: "Gedächtnis senden",
|
||||
Copy: "Gedächtnis kopieren",
|
||||
Reset: "Sitzung zurücksetzen",
|
||||
ResetConfirm:
|
||||
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
|
||||
Title: "Historische Zusammenfassung",
|
||||
EmptyContent:
|
||||
"Gesprächsinhalte sind zu kurz, keine Zusammenfassung erforderlich",
|
||||
Send: "Chatverlauf automatisch komprimieren und als Kontext senden",
|
||||
Copy: "Zusammenfassung kopieren",
|
||||
Reset: "[nicht verwendet]",
|
||||
ResetConfirm: "Zusammenfassung löschen bestätigen?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Neuer Chat",
|
||||
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
|
||||
DeleteToast: "Chat gelöscht",
|
||||
Revert: "Zurücksetzen",
|
||||
NewChat: "Neues Gespräch",
|
||||
DeleteChat: "Bestätigen Sie das Löschen des ausgewählten Gesprächs?",
|
||||
DeleteToast: "Gespräch gelöscht",
|
||||
Revert: "Rückgängig machen",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Einstellungen",
|
||||
SubTitle: "Alle Einstellungen",
|
||||
SubTitle: "Alle Einstellungsmöglichkeiten",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Alle Einstellungen zurücksetzen",
|
||||
SubTitle: "Setzt alle Einstellungen auf die Standardwerte zurück",
|
||||
Action: "Jetzt zurücksetzen",
|
||||
Confirm: "Bestätigen Sie das Zurücksetzen aller Einstellungen?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Alle Daten löschen",
|
||||
SubTitle: "Löscht alle Chats und Einstellungsdaten",
|
||||
Action: "Jetzt löschen",
|
||||
Confirm:
|
||||
"Bestätigen Sie das Löschen aller Chats und Einstellungsdaten?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Sprache", // ACHTUNG: Wenn Sie eine neue Übersetzung hinzufügen möchten, übersetzen Sie diesen Wert bitte nicht, lassen Sie ihn als `Sprache`
|
||||
All: "Alle Sprachen",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Schriftgröße",
|
||||
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
|
||||
SubTitle: "Schriftgröße des Chat-Inhalts",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Chat-Schriftart",
|
||||
SubTitle:
|
||||
"Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden",
|
||||
Placeholder: "Schriftartname",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "System-Prompts einfügen",
|
||||
Title: "Systemweite Eingabeaufforderungen einfügen",
|
||||
SubTitle:
|
||||
"Erzwingt das Hinzufügen eines simulierten systemweiten Prompts von ChatGPT am Anfang der Nachrichtenliste bei jeder Anfrage",
|
||||
"Fügt jeder Nachricht am Anfang der Nachrichtenliste eine simulierte ChatGPT-Systemaufforderung hinzu",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Benutzer-Eingabeverarbeitung",
|
||||
SubTitle:
|
||||
"Die neueste Nachricht des Benutzers wird in diese Vorlage eingefügt",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
IsLatest: "Neueste Version",
|
||||
CheckUpdate: "Update prüfen",
|
||||
IsChecking: "Update wird geprüft...",
|
||||
Version: (x: string) => `Aktuelle Version: ${x}`,
|
||||
IsLatest: "Bereits die neueste Version",
|
||||
CheckUpdate: "Auf Updates überprüfen",
|
||||
IsChecking: "Überprüfe auf Updates...",
|
||||
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
|
||||
GoToUpdate: "Aktualisieren",
|
||||
GoToUpdate: "Zum Update gehen",
|
||||
},
|
||||
SendKey: "Senden-Taste",
|
||||
Theme: "Erscheinungsbild",
|
||||
TightBorder: "Enger Rahmen",
|
||||
SendKey: "Sende-Taste",
|
||||
Theme: "Thema",
|
||||
TightBorder: "Randloser Modus",
|
||||
SendPreviewBubble: {
|
||||
Title: "Vorschau-Bubble senden",
|
||||
SubTitle: "Preview markdown in bubble",
|
||||
Title: "Vorschau-Bubble",
|
||||
SubTitle: "Markdown-Inhalt in der Vorschau-Bubble anzeigen",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Titel automatisch generieren",
|
||||
SubTitle:
|
||||
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Cloud-Daten",
|
||||
NotSyncYet: "Noch nicht synchronisiert",
|
||||
Success: "Synchronisation erfolgreich",
|
||||
Fail: "Synchronisation fehlgeschlagen",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Cloud-Synchronisation konfigurieren",
|
||||
Check: "Verfügbarkeit überprüfen",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Synchronisationstyp",
|
||||
SubTitle: "Wählen Sie den bevorzugten Synchronisationsserver aus",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Proxy aktivieren",
|
||||
SubTitle:
|
||||
"Beim Synchronisieren im Browser muss ein Proxy aktiviert werden, um Cross-Origin-Beschränkungen zu vermeiden",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Proxy-Adresse",
|
||||
SubTitle: "Nur für projektinterne Cross-Origin-Proxy",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "WebDAV-Adresse",
|
||||
UserName: "Benutzername",
|
||||
Password: "Passwort",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST-Url",
|
||||
UserName: "Sicherungsname",
|
||||
Password: "UpStash Redis REST-Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Lokale Daten",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} Chats, ${overview.message} Nachrichten, ${overview.prompt} Eingabeaufforderungen, ${overview.mask} Masken`;
|
||||
},
|
||||
ImportFailed: "Import fehlgeschlagen",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Mask Splash Screen",
|
||||
SubTitle: "Show a mask splash screen before starting new chat",
|
||||
Title: "Masken-Startseite",
|
||||
SubTitle:
|
||||
"Zeige die Masken-Startseite beim Erstellen eines neuen Chats",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Eingebaute Masken ausblenden",
|
||||
SubTitle: "Blendet eingebaute Masken in allen Maskenlisten aus",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Autovervollständigung deaktivieren",
|
||||
SubTitle: "Autovervollständigung mit / starten",
|
||||
Title: "Automatische Eingabeaufforderung deaktivieren",
|
||||
SubTitle:
|
||||
"Geben Sie am Anfang des Eingabefelds / ein, um die automatische Vervollständigung auszulösen",
|
||||
},
|
||||
List: "Prompt-Liste",
|
||||
List: "Benutzerdefinierte Eingabeaufforderungsliste",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} integriert, ${custom} benutzerdefiniert`,
|
||||
`Eingebaut ${builtin} Stück, Benutzerdefiniert ${custom} Stück`,
|
||||
Edit: "Bearbeiten",
|
||||
Modal: {
|
||||
Title: "Prompt List",
|
||||
Add: "Add One",
|
||||
Search: "Search Prompts",
|
||||
Title: "Eingabeaufforderungsliste",
|
||||
Add: "Neu erstellen",
|
||||
Search: "Eingabeaufforderungen suchen",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Edit Prompt",
|
||||
Title: "Eingabeaufforderung bearbeiten",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Anzahl der angehängten Nachrichten",
|
||||
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
|
||||
Title: "Anzahl der historischen Nachrichten",
|
||||
SubTitle:
|
||||
"Anzahl der historischen Nachrichten, die bei jeder Anfrage mitgesendet werden",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Schwellenwert für Verlaufskomprimierung",
|
||||
Title: "Komprimierungsschwelle für historische Nachrichtenlänge",
|
||||
SubTitle:
|
||||
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
|
||||
"Wenn die unkomprimierten historischen Nachrichten diesen Wert überschreiten, wird komprimiert",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Kontostand",
|
||||
Title: "Guthabenabfrage",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
|
||||
return `In diesem Monat verwendet $${used}, Abonnement insgesamt $${total}`;
|
||||
},
|
||||
IsChecking: "Wird überprüft...",
|
||||
Check: "Erneut prüfen",
|
||||
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
|
||||
IsChecking: "Wird überprüft…",
|
||||
Check: "Erneut überprüfen",
|
||||
NoAccess:
|
||||
"Geben Sie API-Schlüssel oder Zugangspasswort ein, um das Guthaben einzusehen",
|
||||
},
|
||||
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Zugangscode",
|
||||
SubTitle:
|
||||
"Der Administrator hat die verschlüsselte Zugriffskontrolle aktiviert",
|
||||
Placeholder: "Geben Sie den Zugangscode ein",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Benutzerdefinierte Schnittstelle",
|
||||
SubTitle: "Benutzerdefinierte Azure- oder OpenAI-Dienste verwenden",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Modellanbieter",
|
||||
SubTitle: "Wechseln Sie zu verschiedenen Anbietern",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API-Schlüssel",
|
||||
SubTitle:
|
||||
"Verwenden Sie benutzerdefinierten OpenAI-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
|
||||
Placeholder: "OpenAI API-Schlüssel",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle: "Neben der Standardadresse muss http(s):// enthalten sein",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Schnittstellenschlüssel",
|
||||
SubTitle:
|
||||
"Verwenden Sie benutzerdefinierten Azure-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
|
||||
Placeholder: "Azure API-Schlüssel",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle: "Beispiel:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Schnittstellenversion (azure api version)",
|
||||
SubTitle: "Wählen Sie eine spezifische Teilversion aus",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Schnittstellenschlüssel",
|
||||
SubTitle:
|
||||
"Verwenden Sie benutzerdefinierten Anthropic-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
|
||||
Placeholder: "Anthropic API-Schlüssel",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle: "Beispiel:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Schnittstellenversion (claude api version)",
|
||||
SubTitle: "Wählen Sie eine spezifische API-Version aus",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API-Schlüssel",
|
||||
SubTitle: "Holen Sie sich Ihren API-Schlüssel von Google AI",
|
||||
Placeholder: "Geben Sie Ihren Google AI Studio API-Schlüssel ein",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpunktadresse",
|
||||
SubTitle: "Beispiel:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "API-Version (nur für gemini-pro)",
|
||||
SubTitle: "Wählen Sie eine spezifische API-Version aus",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Google Sicherheitsfilterstufe",
|
||||
SubTitle: "Inhaltfilterstufe einstellen",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "API-Schlüssel",
|
||||
SubTitle: "Verwenden Sie benutzerdefinierten Baidu API-Schlüssel",
|
||||
Placeholder: "Baidu API-Schlüssel",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Geheimschlüssel",
|
||||
SubTitle: "Verwenden Sie benutzerdefinierten Baidu Geheimschlüssel",
|
||||
Placeholder: "Baidu Geheimschlüssel",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle:
|
||||
"Keine benutzerdefinierten Adressen unterstützen, konfigurieren Sie in .env",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "Schnittstellenschlüssel",
|
||||
SubTitle: "Verwenden Sie benutzerdefinierten ByteDance API-Schlüssel",
|
||||
Placeholder: "ByteDance API-Schlüssel",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle: "Beispiel:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Schnittstellenschlüssel",
|
||||
SubTitle:
|
||||
"Verwenden Sie benutzerdefinierten Alibaba Cloud API-Schlüssel",
|
||||
Placeholder: "Alibaba Cloud API-Schlüssel",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Schnittstellenadresse",
|
||||
SubTitle: "Beispiel:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Benutzerdefinierter Modellname",
|
||||
SubTitle:
|
||||
"Fügen Sie benutzerdefinierte Modelloptionen hinzu, getrennt durch Kommas",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Modell",
|
||||
Temperature: {
|
||||
Title: "Temperature", //Temperatur
|
||||
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
|
||||
Title: "Zufälligkeit (temperature)",
|
||||
SubTitle: "Je höher der Wert, desto zufälliger die Antwort",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Kern-Sampling (top_p)",
|
||||
SubTitle:
|
||||
"Ähnlich der Zufälligkeit, aber nicht zusammen mit Zufälligkeit ändern",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Max Tokens", //Maximale Token
|
||||
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
|
||||
Title: "Maximale Token-Anzahl pro Antwort",
|
||||
SubTitle: "Maximale Anzahl der Tokens pro Interaktion",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Presence Penalty", //Anwesenheitsstrafe
|
||||
Title: "Themenfrische (presence_penalty)",
|
||||
SubTitle:
|
||||
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
|
||||
"Je höher der Wert, desto wahrscheinlicher wird auf neue Themen eingegangen",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Frequency Penalty", // HäufigkeitStrafe
|
||||
Title: "Häufigkeitsstrafe (frequency_penalty)",
|
||||
SubTitle:
|
||||
"Ein größerer Wert, der die Wahrscheinlichkeit verringert, dass dieselbe Zeile wiederholt wird",
|
||||
"Je höher der Wert, desto wahrscheinlicher werden wiederholte Wörter reduziert",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Neues Gespräch",
|
||||
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
|
||||
DefaultTopic: "Neuer Chat",
|
||||
BotHello: "Wie kann ich Ihnen helfen?",
|
||||
Error:
|
||||
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
|
||||
"Ein Fehler ist aufgetreten, bitte versuchen Sie es später noch einmal",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
|
||||
"Dies ist eine Zusammenfassung des bisherigen Chats als Hintergrundinformation: " +
|
||||
content,
|
||||
Topic:
|
||||
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
|
||||
"Geben Sie ein kurzes Thema in vier bis fünf Wörtern zurück, ohne Erklärungen, ohne Satzzeichen, ohne Füllwörter, ohne zusätzliche Texte und ohne Fettdruck. Wenn kein Thema vorhanden ist, geben Sie bitte „Allgemeines Gespräch“ zurück.",
|
||||
Summarize:
|
||||
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
|
||||
"Fassen Sie den Gesprächsinhalt zusammen, um als Kontextaufforderung für den nächsten Schritt zu dienen, halten Sie es unter 200 Zeichen",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "In die Zwischenablage kopiert",
|
||||
Success: "In die Zwischenablage geschrieben",
|
||||
Failed:
|
||||
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
|
||||
"Kopieren fehlgeschlagen, bitte erlauben Sie Zugriff auf die Zwischenablage",
|
||||
},
|
||||
Download: {
|
||||
Success: "Inhalt wurde in Ihrem Verzeichnis heruntergeladen.",
|
||||
Failed: "Download fehlgeschlagen.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
|
||||
Edit: "Kontext- und Gedächtnis-Prompts",
|
||||
Add: "Hinzufügen",
|
||||
Toast: (x: any) => `Beinhaltet ${x} vordefinierte Eingabeaufforderungen`,
|
||||
Edit: "Aktuelle Gesprächseinstellungen",
|
||||
Add: "Neues Gespräch hinzufügen",
|
||||
Clear: "Kontext gelöscht",
|
||||
Revert: "Kontext wiederherstellen",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
Name: "Plugins",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Du bist ein Assistent, der",
|
||||
Sysmessage: "Du bist ein Assistent",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
SearchChat: {
|
||||
Name: "Suche",
|
||||
Page: {
|
||||
Title: "Prompt Template",
|
||||
SubTitle: (count: number) => `${count} prompt templates`,
|
||||
Search: "Search Templates",
|
||||
Create: "Create",
|
||||
Title: "Chatverlauf durchsuchen",
|
||||
Search: "Suchbegriff eingeben",
|
||||
NoResult: "Keine Ergebnisse gefunden",
|
||||
NoData: "Keine Daten",
|
||||
Loading: "Laden",
|
||||
|
||||
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompts`,
|
||||
Chat: "Chat",
|
||||
View: "View",
|
||||
Edit: "Edit",
|
||||
Delete: "Delete",
|
||||
DeleteConfirm: "Confirm to delete?",
|
||||
View: "Ansehen",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masken",
|
||||
Page: {
|
||||
Title: "Vordefinierte Rollenmasken",
|
||||
SubTitle: (count: number) =>
|
||||
`${count} vordefinierte Rollenbeschreibungen`,
|
||||
Search: "Rollenmasken suchen",
|
||||
Create: "Neu erstellen",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `Beinhaltet ${count} vordefinierte Gespräche`,
|
||||
Chat: "Gespräch",
|
||||
View: "Anzeigen",
|
||||
Edit: "Bearbeiten",
|
||||
Delete: "Löschen",
|
||||
DeleteConfirm: "Bestätigen Sie das Löschen?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
|
||||
Download: "Download",
|
||||
Clone: "Clone",
|
||||
`Vordefinierte Maske bearbeiten ${readonly ? "(Nur lesen)" : ""}`,
|
||||
Download: "Vorgabe herunterladen",
|
||||
Clone: "Vorgabe klonen",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Bot Avatar",
|
||||
Name: "Bot Name",
|
||||
Avatar: "Rollen-Avatar",
|
||||
Name: "Rollenname",
|
||||
Sync: {
|
||||
Title: "Globale Einstellungen verwenden",
|
||||
SubTitle:
|
||||
"Soll das aktuelle Gespräch die globalen Modelleinstellungen verwenden?",
|
||||
Confirm:
|
||||
"Die benutzerdefinierten Einstellungen des aktuellen Gesprächs werden automatisch überschrieben. Bestätigen Sie, dass Sie die globalen Einstellungen aktivieren möchten?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Vordefinierte Gespräche ausblenden",
|
||||
SubTitle:
|
||||
"Nach dem Ausblenden werden vordefinierte Gespräche nicht mehr im Chat angezeigt",
|
||||
},
|
||||
Share: {
|
||||
Title: "Diese Maske teilen",
|
||||
SubTitle: "Generieren Sie einen Direktlink zu dieser Maske",
|
||||
Action: "Link kopieren",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Return",
|
||||
Skip: "Skip",
|
||||
Title: "Pick a Mask",
|
||||
SubTitle: "Chat with the Soul behind the Mask",
|
||||
More: "Find More",
|
||||
NotShow: "Not Show Again",
|
||||
ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
|
||||
Return: "Zurück",
|
||||
Skip: "Direkt beginnen",
|
||||
NotShow: "Nicht mehr anzeigen",
|
||||
ConfirmNoShow:
|
||||
"Bestätigen Sie die Deaktivierung? Nach der Deaktivierung können Sie jederzeit in den Einstellungen wieder aktivieren.",
|
||||
Title: "Wählen Sie eine Maske aus",
|
||||
SubTitle:
|
||||
"Starten Sie jetzt und lassen Sie sich von den Gedanken hinter der Maske inspirieren",
|
||||
More: "Alle anzeigen",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Ein Zugangscode wurde im Link gefunden. Möchten Sie diesen automatisch einfügen?",
|
||||
Settings:
|
||||
"Vordefinierte Einstellungen wurden im Link gefunden. Möchten Sie diese automatisch einfügen?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Confirm",
|
||||
Cancel: "Cancel",
|
||||
Close: "Close",
|
||||
Create: "Create",
|
||||
Edit: "Edit",
|
||||
Confirm: "Bestätigen",
|
||||
Cancel: "Abbrechen",
|
||||
Close: "Schließen",
|
||||
Create: "Neu erstellen",
|
||||
Edit: "Bearbeiten",
|
||||
Export: "Exportieren",
|
||||
Import: "Importieren",
|
||||
Sync: "Synchronisieren",
|
||||
Config: "Konfigurieren",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Nur Nachrichten nach dem Löschen des Kontexts werden angezeigt",
|
||||
},
|
||||
Model: "Modell",
|
||||
Messages: "Nachrichten",
|
||||
Topic: "Thema",
|
||||
|
||||
@@ -44,6 +44,7 @@ const en: LocaleType = {
|
||||
PinToastAction: "View",
|
||||
Delete: "Delete",
|
||||
Edit: "Edit",
|
||||
FullScreen: "FullScreen",
|
||||
},
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
@@ -82,6 +83,14 @@ const en: LocaleType = {
|
||||
SaveAs: "Save as Mask",
|
||||
},
|
||||
IsContext: "Contextual Prompt",
|
||||
ShortcutKey: {
|
||||
Title: "Keyboard Shortcuts",
|
||||
newChat: "Open New Chat",
|
||||
focusInput: "Focus Input Field",
|
||||
copyLastMessage: "Copy Last Reply",
|
||||
copyLastCode: "Copy Last Code Block",
|
||||
showShortcutKey: "Show Shortcuts",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Export Messages",
|
||||
@@ -106,6 +115,10 @@ const en: LocaleType = {
|
||||
Toast: "Capturing Image...",
|
||||
Modal: "Long press or right click to save image",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "Share Artifacts",
|
||||
Error: "Share Error",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Search",
|
||||
@@ -131,6 +144,7 @@ const en: LocaleType = {
|
||||
Settings: {
|
||||
Title: "Settings",
|
||||
SubTitle: "All Settings",
|
||||
ShowPassword: "ShowPassword",
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Reset All Settings",
|
||||
@@ -154,6 +168,12 @@ const en: LocaleType = {
|
||||
Title: "Font Size",
|
||||
SubTitle: "Adjust font size of chat content",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Chat Font Family",
|
||||
SubTitle:
|
||||
"Font Family of the chat content, leave empty to apply global default font",
|
||||
Placeholder: "Font Family Name",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Inject System Prompts",
|
||||
SubTitle: "Inject a global system prompt for every request",
|
||||
@@ -296,7 +316,7 @@ const en: LocaleType = {
|
||||
|
||||
Endpoint: {
|
||||
Title: "OpenAI Endpoint",
|
||||
SubTitle: "Must starts with http(s):// or use /api/openai as default",
|
||||
SubTitle: "Must start with http(s):// or use /api/openai as default",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
@@ -316,6 +336,116 @@ const en: LocaleType = {
|
||||
SubTitle: "Check your api version from azure console",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Anthropic API Key",
|
||||
SubTitle:
|
||||
"Use a custom Anthropic Key to bypass password access restrictions",
|
||||
Placeholder: "Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "API Version (claude api version)",
|
||||
SubTitle: "Select and input a specific API version",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "Baidu API Key",
|
||||
SubTitle: "Use a custom Baidu API Key",
|
||||
Placeholder: "Baidu API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Baidu Secret Key",
|
||||
SubTitle: "Use a custom Baidu Secret Key",
|
||||
Placeholder: "Baidu Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "not supported, configure in .env",
|
||||
},
|
||||
},
|
||||
Tencent: {
|
||||
ApiKey: {
|
||||
Title: "Tencent API Key",
|
||||
SubTitle: "Use a custom Tencent API Key",
|
||||
Placeholder: "Tencent API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Tencent Secret Key",
|
||||
SubTitle: "Use a custom Tencent Secret Key",
|
||||
Placeholder: "Tencent Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "not supported, configure in .env",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "ByteDance API Key",
|
||||
SubTitle: "Use a custom ByteDance API Key",
|
||||
Placeholder: "ByteDance API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Alibaba API Key",
|
||||
SubTitle: "Use a custom Alibaba Cloud API Key",
|
||||
Placeholder: "Alibaba Cloud API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Moonshot: {
|
||||
ApiKey: {
|
||||
Title: "Moonshot API Key",
|
||||
SubTitle: "Use a custom Moonshot API Key",
|
||||
Placeholder: "Moonshot API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "Stability API Key",
|
||||
SubTitle: "Use a custom Stability API Key",
|
||||
Placeholder: "Stability API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Iflytek: {
|
||||
ApiKey: {
|
||||
Title: "Iflytek API Key",
|
||||
SubTitle: "Use a Iflytek API Key",
|
||||
Placeholder: "Iflytek API Key",
|
||||
},
|
||||
ApiSecret: {
|
||||
Title: "Iflytek API Secret",
|
||||
SubTitle: "Use a Iflytek API Secret",
|
||||
Placeholder: "Iflytek API Secret",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Custom Models",
|
||||
SubTitle: "Custom model options, seperated by comma",
|
||||
@@ -329,13 +459,17 @@ const en: LocaleType = {
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example:",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "API Version (specific to gemini-pro)",
|
||||
SubTitle: "Select a specific API version",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Google Safety Settings",
|
||||
SubTitle: "Select a safety filtering level",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -391,12 +525,68 @@ const en: LocaleType = {
|
||||
Clear: "Context Cleared",
|
||||
Revert: "Revert",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
Discovery: {
|
||||
Name: "Discovery",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "You are an assistant that",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "Search",
|
||||
Page: {
|
||||
Title: "Search Chat History",
|
||||
Search: "Enter search query to search chat history",
|
||||
NoResult: "No results found",
|
||||
NoData: "No data",
|
||||
Loading: "Loading...",
|
||||
|
||||
SubTitle: (count: number) => `Found ${count} results`,
|
||||
},
|
||||
Item: {
|
||||
View: "View",
|
||||
},
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
Page: {
|
||||
Title: "Plugins",
|
||||
SubTitle: (count: number) => `${count} plugins`,
|
||||
Search: "Search Plugin",
|
||||
Create: "Create",
|
||||
Find: "You can find awesome plugins on github: ",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} method`,
|
||||
View: "View",
|
||||
Edit: "Edit",
|
||||
Delete: "Delete",
|
||||
DeleteConfirm: "Confirm to delete?",
|
||||
},
|
||||
Auth: {
|
||||
None: "None",
|
||||
Basic: "Basic",
|
||||
Bearer: "Bearer",
|
||||
Custom: "Custom",
|
||||
CustomHeader: "Parameter Name",
|
||||
Token: "Token",
|
||||
Proxy: "Using Proxy",
|
||||
ProxyDescription: "Using proxies to solve CORS error",
|
||||
Location: "Location",
|
||||
LocationHeader: "Header",
|
||||
LocationQuery: "Query",
|
||||
LocationBody: "Body",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
|
||||
Download: "Download",
|
||||
Auth: "Authentication Type",
|
||||
Content: "OpenAPI Schema",
|
||||
Load: "Load From URL",
|
||||
Method: "Method",
|
||||
Error: "OpenAPI Schema Error",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
@@ -431,6 +621,10 @@ const en: LocaleType = {
|
||||
Title: "Hide Context Prompts",
|
||||
SubTitle: "Do not show in-context prompts in chat",
|
||||
},
|
||||
Artifacts: {
|
||||
Title: "Enable Artifacts",
|
||||
SubTitle: "Can render HTML page when enable artifacts.",
|
||||
},
|
||||
Share: {
|
||||
Title: "Share This Mask",
|
||||
SubTitle: "Generate a link to this mask",
|
||||
@@ -468,11 +662,65 @@ const en: LocaleType = {
|
||||
Topic: "Topic",
|
||||
Time: "Time",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Detected access code from url, confirm to apply? ",
|
||||
Settings: "Detected settings from url, confirm to apply?",
|
||||
},
|
||||
SdPanel: {
|
||||
Prompt: "Prompt",
|
||||
NegativePrompt: "Negative Prompt",
|
||||
PleaseInput: (name: string) => `Please input ${name}`,
|
||||
AspectRatio: "Aspect Ratio",
|
||||
ImageStyle: "Image Style",
|
||||
OutFormat: "Output Format",
|
||||
AIModel: "AI Model",
|
||||
ModelVersion: "Model Version",
|
||||
Submit: "Submit",
|
||||
ParamIsRequired: (name: string) => `${name} is required`,
|
||||
Styles: {
|
||||
D3Model: "3d-model",
|
||||
AnalogFilm: "analog-film",
|
||||
Anime: "anime",
|
||||
Cinematic: "cinematic",
|
||||
ComicBook: "comic-book",
|
||||
DigitalArt: "digital-art",
|
||||
Enhance: "enhance",
|
||||
FantasyArt: "fantasy-art",
|
||||
Isometric: "isometric",
|
||||
LineArt: "line-art",
|
||||
LowPoly: "low-poly",
|
||||
ModelingCompound: "modeling-compound",
|
||||
NeonPunk: "neon-punk",
|
||||
Origami: "origami",
|
||||
Photographic: "photographic",
|
||||
PixelArt: "pixel-art",
|
||||
TileTexture: "tile-texture",
|
||||
},
|
||||
},
|
||||
Sd: {
|
||||
SubTitle: (count: number) => `${count} images`,
|
||||
Actions: {
|
||||
Params: "See Params",
|
||||
Copy: "Copy Prompt",
|
||||
Delete: "Delete",
|
||||
Retry: "Retry",
|
||||
ReturnHome: "Return Home",
|
||||
History: "History",
|
||||
},
|
||||
EmptyRecord: "No images yet",
|
||||
Status: {
|
||||
Name: "Status",
|
||||
Success: "Success",
|
||||
Error: "Error",
|
||||
Wait: "Waiting",
|
||||
Running: "Running",
|
||||
},
|
||||
Danger: {
|
||||
Delete: "Confirm to delete?",
|
||||
},
|
||||
GenerateParams: "Generate Params",
|
||||
Detail: "Detail",
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -1,239 +1,580 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const es: PartialLocaleType = {
|
||||
WIP: "En construcción...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Acceso no autorizado, por favor ingrese el código de acceso en la [página](/#/auth) de configuración.",
|
||||
Unauthorized: isApp
|
||||
? "Se detectó una clave API inválida. Por favor, ve a la página de [Configuración](/#/settings) para verificar si la clave API está configurada correctamente."
|
||||
: "La contraseña de acceso es incorrecta o está vacía. Por favor, ve a la página de [Iniciar sesión](/#/auth) para ingresar la contraseña correcta, o en la página de [Configuración](/#/settings) para introducir tu propia clave API de OpenAI.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Se requiere contraseña",
|
||||
Tips: "El administrador ha habilitado la verificación de contraseña. Introduce el código de acceso a continuación",
|
||||
SubTips: "O ingresa tu clave API de OpenAI o Google",
|
||||
Input: "Introduce el código de acceso aquí",
|
||||
Confirm: "Confirmar",
|
||||
Later: "Más tarde",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} mensajes`,
|
||||
ChatItemCount: (count: number) => `${count} conversaciones`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} mensajes con ChatGPT`,
|
||||
SubTitle: (count: number) => `Total de ${count} conversaciones`,
|
||||
EditMessage: {
|
||||
Title: "Editar registro de mensajes",
|
||||
Topic: {
|
||||
Title: "Tema de la conversación",
|
||||
SubTitle: "Cambiar el tema de la conversación actual",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Ir a la lista de chats",
|
||||
CompressedHistory: "Historial de memoria comprimido",
|
||||
Export: "Exportar todos los mensajes como Markdown",
|
||||
ChatList: "Ver lista de mensajes",
|
||||
CompressedHistory: "Ver historial de Prompts comprimidos",
|
||||
Export: "Exportar historial de chat",
|
||||
Copy: "Copiar",
|
||||
Stop: "Detener",
|
||||
Retry: "Reintentar",
|
||||
Delete: "Delete",
|
||||
Pin: "Fijar",
|
||||
PinToastContent:
|
||||
"Se ha fijado 1 conversación a los prompts predeterminados",
|
||||
PinToastAction: "Ver",
|
||||
Delete: "Eliminar",
|
||||
Edit: "Editar",
|
||||
},
|
||||
Rename: "Renombrar chat",
|
||||
Typing: "Escribiendo...",
|
||||
Commands: {
|
||||
new: "Nueva conversación",
|
||||
newm: "Nueva conversación desde la máscara",
|
||||
next: "Siguiente conversación",
|
||||
prev: "Conversación anterior",
|
||||
clear: "Limpiar contexto",
|
||||
del: "Eliminar conversación",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Detener respuesta",
|
||||
ToBottom: "Ir al más reciente",
|
||||
Theme: {
|
||||
auto: "Tema automático",
|
||||
light: "Modo claro",
|
||||
dark: "Modo oscuro",
|
||||
},
|
||||
Prompt: "Comandos rápidos",
|
||||
Masks: "Todas las máscaras",
|
||||
Clear: "Limpiar chat",
|
||||
Settings: "Configuración de conversación",
|
||||
UploadImage: "Subir imagen",
|
||||
},
|
||||
Rename: "Renombrar conversación",
|
||||
Typing: "Escribiendo…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `Escribe algo y presiona ${submitKey} para enviar`;
|
||||
var inputHints = `${submitKey} para enviar`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", presiona Shift + Enter para nueva línea";
|
||||
inputHints += ",Shift + Enter para nueva línea";
|
||||
}
|
||||
return inputHints;
|
||||
return (
|
||||
inputHints + ",/ para activar autocompletado,: para activar comandos"
|
||||
);
|
||||
},
|
||||
Send: "Enviar",
|
||||
Config: {
|
||||
Reset: "Reset to Default",
|
||||
SaveAs: "Save as Mask",
|
||||
Reset: "Borrar memoria",
|
||||
SaveAs: "Guardar como máscara",
|
||||
},
|
||||
IsContext: "Prompt predeterminado",
|
||||
},
|
||||
Export: {
|
||||
Title: "Todos los mensajes",
|
||||
Title: "Compartir historial de chat",
|
||||
Copy: "Copiar todo",
|
||||
Download: "Descargar",
|
||||
MessageFromYou: "Mensaje de ti",
|
||||
MessageFromChatGPT: "Mensaje de ChatGPT",
|
||||
Download: "Descargar archivo",
|
||||
Share: "Compartir en ShareGPT",
|
||||
MessageFromYou: "Usuario",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "Formato de exportación",
|
||||
SubTitle: "Puedes exportar como texto Markdown o imagen PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Incluir contexto de máscara",
|
||||
SubTitle: "Mostrar contexto de máscara en los mensajes",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Seleccionar",
|
||||
Preview: "Vista previa",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Generando captura de pantalla",
|
||||
Modal: "Mantén presionado o haz clic derecho para guardar la imagen",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Buscar mensajes",
|
||||
All: "Seleccionar todo",
|
||||
Latest: "Últimos mensajes",
|
||||
Clear: "Limpiar selección",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Historial de memoria",
|
||||
EmptyContent: "Aún no hay nada.",
|
||||
Copy: "Copiar todo",
|
||||
Send: "Send Memory",
|
||||
Reset: "Reset Session",
|
||||
ResetConfirm:
|
||||
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
|
||||
Title: "Resumen histórico",
|
||||
EmptyContent:
|
||||
"El contenido de la conversación es demasiado corto para resumir",
|
||||
Send: "Comprimir automáticamente el historial de chat y enviarlo como contexto",
|
||||
Copy: "Copiar resumen",
|
||||
Reset: "[no usado]",
|
||||
ResetConfirm: "¿Confirmar para borrar el resumen histórico?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Nuevo chat",
|
||||
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
|
||||
DeleteToast: "Chat Deleted",
|
||||
Revert: "Revert",
|
||||
NewChat: "Nueva conversación",
|
||||
DeleteChat: "¿Confirmar la eliminación de la conversación seleccionada?",
|
||||
DeleteToast: "Conversación eliminada",
|
||||
Revert: "Deshacer",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Configuración",
|
||||
SubTitle: "Todas las configuraciones",
|
||||
SubTitle: "Todas las opciones de configuración",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Restablecer todas las configuraciones",
|
||||
SubTitle:
|
||||
"Restablecer todas las configuraciones a los valores predeterminados",
|
||||
Action: "Restablecer ahora",
|
||||
Confirm: "¿Confirmar el restablecimiento de todas las configuraciones?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Eliminar todos los datos",
|
||||
SubTitle: "Eliminar todos los chats y datos de configuración",
|
||||
Action: "Eliminar ahora",
|
||||
Confirm:
|
||||
"¿Confirmar la eliminación de todos los chats y datos de configuración?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Language", // ATENCIÓN: si deseas agregar una nueva traducción, por favor no traduzcas este valor, déjalo como `Language`
|
||||
All: "Todos los idiomas",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Tamaño de fuente",
|
||||
SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
|
||||
SubTitle: "Tamaño de la fuente del contenido del chat",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Fuente del Chat",
|
||||
SubTitle:
|
||||
"Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global",
|
||||
Placeholder: "Nombre de la Fuente",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Inyectar Prompts del Sistema",
|
||||
Title: "Inyectar mensajes del sistema",
|
||||
SubTitle:
|
||||
"Agregar forzosamente un prompt de sistema simulado de ChatGPT al comienzo de la lista de mensajes en cada solicitud",
|
||||
"Forzar la adición de un mensaje del sistema simulado de ChatGPT al principio de cada lista de mensajes",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Preprocesamiento de entrada del usuario",
|
||||
SubTitle: "El último mensaje del usuario se rellenará en esta plantilla",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Versión: ${x}`,
|
||||
IsLatest: "Última versión",
|
||||
Version: (x: string) => `Versión actual: ${x}`,
|
||||
IsLatest: "Ya estás en la última versión",
|
||||
CheckUpdate: "Buscar actualizaciones",
|
||||
IsChecking: "Buscando actualizaciones...",
|
||||
FoundUpdate: (x: string) => `Se encontró una nueva versión: ${x}`,
|
||||
GoToUpdate: "Actualizar",
|
||||
FoundUpdate: (x: string) => `Nueva versión encontrada: ${x}`,
|
||||
GoToUpdate: "Ir a actualizar",
|
||||
},
|
||||
SendKey: "Tecla de envío",
|
||||
SendKey: "Tecla de enviar",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Borde ajustado",
|
||||
TightBorder: "Modo sin borde",
|
||||
SendPreviewBubble: {
|
||||
Title: "Enviar burbuja de vista previa",
|
||||
SubTitle: "Preview markdown in bubble",
|
||||
Title: "Vista previa del globo",
|
||||
SubTitle:
|
||||
"Previsualiza el contenido Markdown en un globo de vista previa",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Generar título automáticamente",
|
||||
SubTitle: "Generar un título adecuado basado en el contenido del chat",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Datos en la nube",
|
||||
NotSyncYet: "Aún no se ha sincronizado",
|
||||
Success: "Sincronización exitosa",
|
||||
Fail: "Sincronización fallida",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Configurar sincronización en la nube",
|
||||
Check: "Verificar disponibilidad",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Tipo de sincronización",
|
||||
SubTitle: "Selecciona el servidor de sincronización preferido",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Habilitar proxy",
|
||||
SubTitle:
|
||||
"Debes habilitar el proxy para sincronizar en el navegador y evitar restricciones de CORS",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Dirección del proxy",
|
||||
SubTitle: "Solo para el proxy CORS incluido en este proyecto",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Dirección WebDAV",
|
||||
UserName: "Nombre de usuario",
|
||||
Password: "Contraseña",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "URL de REST de UpStash Redis",
|
||||
UserName: "Nombre de respaldo",
|
||||
Password: "Token de REST de UpStash Redis",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Datos locales",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} conversaciones, ${overview.message} mensajes, ${overview.prompt} prompts, ${overview.mask} máscaras`;
|
||||
},
|
||||
ImportFailed: "Importación fallida",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Mask Splash Screen",
|
||||
SubTitle: "Show a mask splash screen before starting new chat",
|
||||
Title: "Pantalla de inicio de máscara",
|
||||
SubTitle:
|
||||
"Mostrar la pantalla de inicio de la máscara al iniciar un nuevo chat",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Ocultar máscaras integradas",
|
||||
SubTitle:
|
||||
"Ocultar las máscaras integradas en todas las listas de máscaras",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Desactivar autocompletado",
|
||||
SubTitle: "Escribe / para activar el autocompletado",
|
||||
Title: "Deshabilitar autocompletado de prompts",
|
||||
SubTitle:
|
||||
"Escribe / al principio del campo de entrada para activar el autocompletado",
|
||||
},
|
||||
List: "Lista de autocompletado",
|
||||
List: "Lista de prompts personalizados",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} incorporado, ${custom} definido por el usuario`,
|
||||
`Integrados ${builtin}, definidos por el usuario ${custom}`,
|
||||
Edit: "Editar",
|
||||
Modal: {
|
||||
Title: "Prompt List",
|
||||
Add: "Add One",
|
||||
Search: "Search Prompts",
|
||||
Title: "Lista de prompts",
|
||||
Add: "Nuevo",
|
||||
Search: "Buscar prompts",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Edit Prompt",
|
||||
Title: "Editar prompt",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Cantidad de mensajes adjuntos",
|
||||
SubTitle: "Número de mensajes enviados adjuntos por solicitud",
|
||||
Title: "Número de mensajes históricos adjuntos",
|
||||
SubTitle: "Número de mensajes históricos enviados con cada solicitud",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Umbral de compresión de historial",
|
||||
Title: "Umbral de compresión de mensajes históricos",
|
||||
SubTitle:
|
||||
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
|
||||
"Cuando los mensajes históricos no comprimidos superan este valor, se realizará la compresión",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo de la cuenta",
|
||||
Title: "Consulta de saldo",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Usado $${used}, subscription $${total}`;
|
||||
return `Saldo usado este mes: $${used}, total suscrito: $${total}`;
|
||||
},
|
||||
IsChecking: "Comprobando...",
|
||||
Check: "Comprobar de nuevo",
|
||||
NoAccess: "Introduzca la clave API para comprobar el saldo",
|
||||
IsChecking: "Verificando…",
|
||||
Check: "Revisar de nuevo",
|
||||
NoAccess:
|
||||
"Introduce la clave API o la contraseña de acceso para ver el saldo",
|
||||
},
|
||||
|
||||
Model: "Modelo",
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Contraseña de acceso",
|
||||
SubTitle: "El administrador ha habilitado el acceso encriptado",
|
||||
Placeholder: "Introduce la contraseña de acceso",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Interfaz personalizada",
|
||||
SubTitle: "¿Usar servicios personalizados de Azure u OpenAI?",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Proveedor de modelos",
|
||||
SubTitle: "Cambiar entre diferentes proveedores",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "Clave API",
|
||||
SubTitle:
|
||||
"Usa una clave API de OpenAI personalizada para omitir la restricción de acceso por contraseña",
|
||||
Placeholder: "Clave API de OpenAI",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle:
|
||||
"Debe incluir http(s):// además de la dirección predeterminada",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Clave de interfaz",
|
||||
SubTitle:
|
||||
"Usa una clave de Azure personalizada para omitir la restricción de acceso por contraseña",
|
||||
Placeholder: "Clave API de Azure",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle: "Ejemplo:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versión de la interfaz (versión de api de azure)",
|
||||
SubTitle: "Selecciona una versión específica",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Clave de interfaz",
|
||||
SubTitle:
|
||||
"Usa una clave de Anthropic personalizada para omitir la restricción de acceso por contraseña",
|
||||
Placeholder: "Clave API de Anthropic",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle: "Ejemplo:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versión de la interfaz (versión de claude api)",
|
||||
SubTitle: "Selecciona una versión específica de la API",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "Clave API",
|
||||
SubTitle: "Obtén tu clave API de Google AI",
|
||||
Placeholder: "Introduce tu clave API de Google AI Studio",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle: "Ejemplo:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "Versión de la API (solo para gemini-pro)",
|
||||
SubTitle: "Selecciona una versión específica de la API",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Nivel de filtrado de seguridad de Google",
|
||||
SubTitle: "Configura el nivel de filtrado de contenido",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "Clave API",
|
||||
SubTitle: "Usa una clave API de Baidu personalizada",
|
||||
Placeholder: "Clave API de Baidu",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Clave secreta",
|
||||
SubTitle: "Usa una clave secreta de Baidu personalizada",
|
||||
Placeholder: "Clave secreta de Baidu",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle:
|
||||
"No admite personalización, dirígete a .env para configurarlo",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "Clave de interfaz",
|
||||
SubTitle: "Usa una clave API de ByteDance personalizada",
|
||||
Placeholder: "Clave API de ByteDance",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle: "Ejemplo:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Clave de interfaz",
|
||||
SubTitle: "Usa una clave API de Alibaba Cloud personalizada",
|
||||
Placeholder: "Clave API de Alibaba Cloud",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Dirección del endpoint",
|
||||
SubTitle: "Ejemplo:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Nombre del modelo personalizado",
|
||||
SubTitle:
|
||||
"Agrega opciones de modelos personalizados, separados por comas",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Modelo (model)",
|
||||
Temperature: {
|
||||
Title: "Temperatura",
|
||||
SubTitle: "Un valor mayor genera una salida más aleatoria",
|
||||
Title: "Aleatoriedad (temperature)",
|
||||
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Muestreo por núcleo (top_p)",
|
||||
SubTitle: "Similar a la aleatoriedad, pero no cambies ambos a la vez",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Máximo de tokens",
|
||||
SubTitle: "Longitud máxima de tokens de entrada y tokens generados",
|
||||
Title: "Límite de tokens por respuesta (max_tokens)",
|
||||
SubTitle: "Número máximo de tokens utilizados en una sola interacción",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalización de presencia",
|
||||
Title: "Novedad de temas (presence_penalty)",
|
||||
SubTitle:
|
||||
"Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas",
|
||||
"Cuanto mayor sea el valor, más probable es que se amplíen a nuevos temas",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalización de frecuencia",
|
||||
Title: "Penalización de frecuencia (frequency_penalty)",
|
||||
SubTitle:
|
||||
"Un valor mayor que disminuye la probabilidad de repetir la misma línea",
|
||||
"Cuanto mayor sea el valor, más probable es que se reduzcan las palabras repetidas",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nueva conversación",
|
||||
BotHello: "¡Hola! ¿Cómo puedo ayudarte hoy?",
|
||||
Error: "Algo salió mal, por favor intenta nuevamente más tarde.",
|
||||
DefaultTopic: "Nuevo chat",
|
||||
BotHello: "¿En qué puedo ayudarte?",
|
||||
Error: "Hubo un error, inténtalo de nuevo más tarde",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Este es un resumen del historial del chat entre la IA y el usuario como recapitulación: " +
|
||||
content,
|
||||
"Este es un resumen del chat histórico como referencia: " + content,
|
||||
Topic:
|
||||
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
|
||||
"Devuelve un tema breve de esta frase en cuatro a cinco palabras, sin explicación, sin puntuación, sin muletillas, sin texto adicional, sin negritas. Si no hay tema, devuelve 'charlas casuales'",
|
||||
Summarize:
|
||||
"Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
|
||||
"Resume brevemente el contenido de la conversación para usar como un prompt de contexto, manteniéndolo dentro de 200 palabras",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Copiado al portapapeles",
|
||||
Failed:
|
||||
"La copia falló, por favor concede permiso para acceder al portapapeles",
|
||||
Failed: "Error al copiar, por favor otorga permisos al portapapeles",
|
||||
},
|
||||
Download: {
|
||||
Success: "Contenido descargado en tu directorio.",
|
||||
Failed: "Error al descargar.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||
Edit: "Contextual and Memory Prompts",
|
||||
Add: "Add One",
|
||||
Toast: (x: any) => `Contiene ${x} prompts predefinidos`,
|
||||
Edit: "Configuración del chat actual",
|
||||
Add: "Agregar una conversación",
|
||||
Clear: "Contexto borrado",
|
||||
Revert: "Restaurar contexto",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
Name: "Complemento",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Eres un asistente que",
|
||||
Sysmessage: "Eres un asistente",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
SearchChat: {
|
||||
Name: "Buscar",
|
||||
Page: {
|
||||
Title: "Prompt Template",
|
||||
SubTitle: (count: number) => `${count} prompt templates`,
|
||||
Search: "Search Templates",
|
||||
Create: "Create",
|
||||
Title: "Buscar en el historial de chat",
|
||||
Search: "Ingrese la palabra clave de búsqueda",
|
||||
NoResult: "No se encontraron resultados",
|
||||
NoData: "Sin datos",
|
||||
Loading: "Cargando",
|
||||
|
||||
SubTitle: (count: number) => `Se encontraron ${count} resultados`,
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompts`,
|
||||
View: "Ver",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Máscara",
|
||||
Page: {
|
||||
Title: "Máscaras de rol predefinidas",
|
||||
SubTitle: (count: number) => `${count} definiciones de rol predefinidas`,
|
||||
Search: "Buscar máscara de rol",
|
||||
Create: "Crear nuevo",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `Contiene ${count} conversaciones predefinidas`,
|
||||
Chat: "Chat",
|
||||
View: "View",
|
||||
Edit: "Edit",
|
||||
Delete: "Delete",
|
||||
DeleteConfirm: "Confirm to delete?",
|
||||
View: "Ver",
|
||||
Edit: "Editar",
|
||||
Delete: "Eliminar",
|
||||
DeleteConfirm: "¿Confirmar eliminación?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
|
||||
Download: "Download",
|
||||
Clone: "Clone",
|
||||
`Editar máscara predefinida ${readonly ? "(solo lectura)" : ""}`,
|
||||
Download: "Descargar predefinido",
|
||||
Clone: "Clonar predefinido",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Bot Avatar",
|
||||
Name: "Bot Name",
|
||||
Avatar: "Avatar del rol",
|
||||
Name: "Nombre del rol",
|
||||
Sync: {
|
||||
Title: "Usar configuración global",
|
||||
SubTitle:
|
||||
"¿Usar la configuración global del modelo para la conversación actual?",
|
||||
Confirm:
|
||||
"La configuración personalizada de la conversación actual se sobrescribirá automáticamente, ¿confirmar habilitar la configuración global?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Ocultar conversaciones predefinidas",
|
||||
SubTitle:
|
||||
"Las conversaciones predefinidas ocultas no aparecerán en la interfaz de chat",
|
||||
},
|
||||
Share: {
|
||||
Title: "Compartir esta máscara",
|
||||
SubTitle: "Generar un enlace directo a esta máscara",
|
||||
Action: "Copiar enlace",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Return",
|
||||
Skip: "Skip",
|
||||
Title: "Pick a Mask",
|
||||
SubTitle: "Chat with the Soul behind the Mask",
|
||||
More: "Find More",
|
||||
NotShow: "Not Show Again",
|
||||
ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
|
||||
Return: "Regresar",
|
||||
Skip: "Comenzar ahora",
|
||||
NotShow: "No mostrar más",
|
||||
ConfirmNoShow:
|
||||
"¿Confirmar desactivación? Puedes reactivar en la configuración en cualquier momento.",
|
||||
Title: "Selecciona una máscara",
|
||||
SubTitle: "Comienza ahora y colisiona con la mente detrás de la máscara",
|
||||
More: "Ver todo",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Detectado un código de acceso en el enlace, ¿deseas autocompletarlo?",
|
||||
Settings:
|
||||
"Detectada configuración predefinida en el enlace, ¿deseas autocompletarla?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Confirm",
|
||||
Cancel: "Cancel",
|
||||
Close: "Close",
|
||||
Create: "Create",
|
||||
Edit: "Edit",
|
||||
Confirm: "Confirmar",
|
||||
Cancel: "Cancelar",
|
||||
Close: "Cerrar",
|
||||
Create: "Crear",
|
||||
Edit: "Editar",
|
||||
Export: "Exportar",
|
||||
Import: "Importar",
|
||||
Sync: "Sincronizar",
|
||||
Config: "Configurar",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Solo se mostrarán los mensajes después de borrar el contexto",
|
||||
},
|
||||
Model: "Modelo",
|
||||
Messages: "Mensajes",
|
||||
Topic: "Tema",
|
||||
Time: "Time",
|
||||
Time: "Hora",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,294 +1,559 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const fr: PartialLocaleType = {
|
||||
WIP: "Prochainement...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Accès non autorisé, veuillez saisir le code d'accès dans la [page](/#/auth) des paramètres.",
|
||||
Unauthorized: isApp
|
||||
? "Clé API invalide détectée. Veuillez vérifier si la clé API est correctement configurée dans la page [Paramètres](/#/settings)."
|
||||
: "Le mot de passe d'accès est incorrect ou manquant. Veuillez entrer le mot de passe d'accès correct sur la page [Connexion](/#/auth) ou entrer votre propre clé API OpenAI sur la page [Paramètres](/#/settings).",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Mot de passe requis",
|
||||
Tips: "L'administrateur a activé la vérification par mot de passe. Veuillez entrer le code d'accès ci-dessous",
|
||||
SubTips: "Ou entrez votre clé API OpenAI ou Google",
|
||||
Input: "Entrez le code d'accès ici",
|
||||
Confirm: "Confirmer",
|
||||
Later: "Plus tard",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} messages en total`,
|
||||
ChatItemCount: (count: number) => `${count} conversations`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} messages échangés avec ChatGPT`,
|
||||
SubTitle: (count: number) => `Total de ${count} conversations`,
|
||||
EditMessage: {
|
||||
Title: "Modifier l'historique des messages",
|
||||
Topic: {
|
||||
Title: "Sujet de la discussion",
|
||||
SubTitle: "Modifier le sujet de la discussion actuel",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Aller à la liste de discussion",
|
||||
CompressedHistory: "Mémoire d'historique compressée Prompt",
|
||||
Export: "Exporter tous les messages en tant que Markdown",
|
||||
ChatList: "Voir la liste des messages",
|
||||
CompressedHistory: "Voir l'historique des prompts compressés",
|
||||
Export: "Exporter l'historique de la discussion",
|
||||
Copy: "Copier",
|
||||
Stop: "Arrêter",
|
||||
Retry: "Réessayer",
|
||||
Delete: "Supprimer",
|
||||
Pin: "Épingler",
|
||||
PinToastContent: "Épingler 2 messages à des messages contextuels",
|
||||
PinToastContent: "1 conversation épinglée aux prompts prédéfinis",
|
||||
PinToastAction: "Voir",
|
||||
Delete: "Supprimer",
|
||||
Edit: "Modifier",
|
||||
},
|
||||
Commands: {
|
||||
new: "Commencer une nouvelle conversation",
|
||||
newm: "Démarrer une nouvelle conversation avec un assistant",
|
||||
next: "Conversation suivante",
|
||||
prev: "Conversation précédente",
|
||||
new: "Nouvelle discussion",
|
||||
newm: "Créer une discussion à partir du masque",
|
||||
next: "Discussion suivante",
|
||||
prev: "Discussion précédente",
|
||||
clear: "Effacer le contexte",
|
||||
del: "Supprimer la Conversation",
|
||||
del: "Supprimer la discussion",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Stop",
|
||||
ToBottom: "Au dernier",
|
||||
Stop: "Arrêter la réponse",
|
||||
ToBottom: "Aller au plus récent",
|
||||
Theme: {
|
||||
auto: "Auto",
|
||||
light: "Thème clair",
|
||||
dark: "Thème sombre",
|
||||
auto: "Thème automatique",
|
||||
light: "Mode clair",
|
||||
dark: "Mode sombre",
|
||||
},
|
||||
Prompt: "Instructions",
|
||||
Masks: "Assistants",
|
||||
Clear: "Effacer le contexte",
|
||||
Settings: "Réglages",
|
||||
Prompt: "Commandes rapides",
|
||||
Masks: "Tous les masques",
|
||||
Clear: "Effacer la discussion",
|
||||
Settings: "Paramètres de la discussion",
|
||||
UploadImage: "Télécharger une image",
|
||||
},
|
||||
Rename: "Renommer la conversation",
|
||||
Rename: "Renommer la discussion",
|
||||
Typing: "En train d'écrire…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `Appuyez sur ${submitKey} pour envoyer`;
|
||||
var inputHints = `${submitKey} pour envoyer`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter pour insérer un saut de ligne";
|
||||
inputHints += ",Shift + Enter pour passer à la ligne";
|
||||
}
|
||||
return inputHints + ", / pour rechercher des prompts";
|
||||
return inputHints + ",/ pour compléter, : pour déclencher des commandes";
|
||||
},
|
||||
Send: "Envoyer",
|
||||
Config: {
|
||||
Reset: "Restaurer les paramètres par défaut",
|
||||
SaveAs: "Enregistrer en tant que masque",
|
||||
Reset: "Effacer la mémoire",
|
||||
SaveAs: "Enregistrer comme masque",
|
||||
},
|
||||
IsContext: "Prompt prédéfini",
|
||||
},
|
||||
Export: {
|
||||
Title: "Tous les messages",
|
||||
Copy: "Tout sélectionner",
|
||||
Download: "Télécharger",
|
||||
MessageFromYou: "Message de votre part",
|
||||
MessageFromChatGPT: "Message de ChatGPT",
|
||||
Title: "Partager l'historique des discussions",
|
||||
Copy: "Tout copier",
|
||||
Download: "Télécharger le fichier",
|
||||
Share: "Partager sur ShareGPT",
|
||||
MessageFromYou: "Utilisateur",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "Format d'exportation",
|
||||
SubTitle: "Vous pouvez exporter en texte Markdown ou en image PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Inclure le contexte du masque",
|
||||
SubTitle: "Afficher le contexte du masque dans les messages",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Sélectionner",
|
||||
Preview: "Aperçu",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Génération de la capture d'écran",
|
||||
Modal:
|
||||
"Appuyez longuement ou faites un clic droit pour enregistrer l'image",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Rechercher des messages",
|
||||
All: "Tout sélectionner",
|
||||
Latest: "Derniers messages",
|
||||
Clear: "Effacer la sélection",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Prompt mémoire",
|
||||
EmptyContent: "Rien encore.",
|
||||
Send: "Envoyer la mémoire",
|
||||
Copy: "Copier la mémoire",
|
||||
Reset: "Réinitialiser la session",
|
||||
ResetConfirm:
|
||||
"La réinitialisation supprimera l'historique de la conversation actuelle ainsi que la mémoire de l'historique. Êtes-vous sûr de vouloir procéder à la réinitialisation?",
|
||||
Title: "Résumé historique",
|
||||
EmptyContent: "Le contenu de la discussion est trop court pour être résumé",
|
||||
Send: "Compresser automatiquement l'historique des discussions et l'envoyer comme contexte",
|
||||
Copy: "Copier le résumé",
|
||||
Reset: "[unused]",
|
||||
ResetConfirm: "Confirmer la suppression du résumé historique ?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Nouvelle discussion",
|
||||
DeleteChat: "Confirmer la suppression de la conversation sélectionnée ?",
|
||||
DeleteToast: "Conversation supprimée",
|
||||
Revert: "Revenir en arrière",
|
||||
DeleteChat: "Confirmer la suppression de la discussion sélectionnée ?",
|
||||
DeleteToast: "Discussion supprimée",
|
||||
Revert: "Annuler",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Paramètres",
|
||||
SubTitle: "Toutes les configurations",
|
||||
SubTitle: "Toutes les options de configuration",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Restaurer les paramètres",
|
||||
SubTitle: "Restaurer les paramètres par défaut",
|
||||
Action: "Reinitialiser",
|
||||
Confirm: "Confirmer la réinitialisation des paramètres?",
|
||||
Title: "Réinitialiser tous les paramètres",
|
||||
SubTitle:
|
||||
"Réinitialiser toutes les options de configuration aux valeurs par défaut",
|
||||
Action: "Réinitialiser maintenant",
|
||||
Confirm: "Confirmer la réinitialisation de tous les paramètres ?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Supprimer toutes les données",
|
||||
Title: "Effacer toutes les données",
|
||||
SubTitle:
|
||||
"Effacer toutes les données, y compris les conversations et les paramètres",
|
||||
Action: "Supprimer",
|
||||
Confirm: "Confirmer la suppression de toutes les données?",
|
||||
"Effacer toutes les discussions et les données de configuration",
|
||||
Action: "Effacer maintenant",
|
||||
Confirm:
|
||||
"Confirmer l'effacement de toutes les discussions et données de configuration ?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language`
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Toutes les langues",
|
||||
},
|
||||
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Taille des polices",
|
||||
SubTitle: "Ajuste la taille de police du contenu de la conversation",
|
||||
Title: "Taille de la police",
|
||||
SubTitle: "Taille de la police pour le contenu des discussions",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Police de Chat",
|
||||
SubTitle:
|
||||
"Police du contenu du chat, laissez vide pour appliquer la police par défaut globale",
|
||||
Placeholder: "Nom de la Police",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Injecter des invites système",
|
||||
SubTitle:
|
||||
"Ajoute de force une invite système simulée de ChatGPT au début de la liste des messages pour chaque demande",
|
||||
"Ajouter de manière forcée une invite système simulée de ChatGPT au début de chaque liste de messages",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Template",
|
||||
SubTitle: "Le message le plus récent sera ajouté à ce template.",
|
||||
Title: "Prétraitement des entrées utilisateur",
|
||||
SubTitle:
|
||||
"Le dernier message de l'utilisateur sera intégré dans ce modèle",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version : ${x}`,
|
||||
IsLatest: "Dernière version",
|
||||
CheckUpdate: "Vérifier la mise à jour",
|
||||
IsChecking: "Vérification de la mise à jour...",
|
||||
FoundUpdate: (x: string) => `Nouvelle version disponible : ${x}`,
|
||||
GoToUpdate: "Mise à jour",
|
||||
Version: (x: string) => `Version actuelle : ${x}`,
|
||||
IsLatest: "Vous avez la dernière version",
|
||||
CheckUpdate: "Vérifier les mises à jour",
|
||||
IsChecking: "Vérification des mises à jour en cours...",
|
||||
FoundUpdate: (x: string) => `Nouvelle version trouvée : ${x}`,
|
||||
GoToUpdate: "Aller à la mise à jour",
|
||||
},
|
||||
SendKey: "Clé d'envoi",
|
||||
SendKey: "Touche d'envoi",
|
||||
Theme: "Thème",
|
||||
TightBorder: "Bordure serrée",
|
||||
TightBorder: "Mode sans bordure",
|
||||
SendPreviewBubble: {
|
||||
Title: "Aperçu de l'envoi dans une bulle",
|
||||
SubTitle: "Aperçu du Markdown dans une bulle",
|
||||
Title: "Bulle d'aperçu",
|
||||
SubTitle: "Aperçu du contenu Markdown dans la bulle d'aperçu",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Génération automatique de titres",
|
||||
SubTitle:
|
||||
"Générer un titre approprié en fonction du contenu de la discussion",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Données cloud",
|
||||
NotSyncYet: "Pas encore synchronisé",
|
||||
Success: "Synchronisation réussie",
|
||||
Fail: "Échec de la synchronisation",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Configurer la synchronisation cloud",
|
||||
Check: "Vérifier la disponibilité",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Type de synchronisation",
|
||||
SubTitle: "Choisissez le serveur de synchronisation préféré",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Activer le proxy",
|
||||
SubTitle:
|
||||
"Lors de la synchronisation dans le navigateur, le proxy doit être activé pour éviter les restrictions de domaine croisé",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Adresse du proxy",
|
||||
SubTitle:
|
||||
"Uniquement pour le proxy de domaine croisé fourni par le projet",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Adresse WebDAV",
|
||||
UserName: "Nom d'utilisateur",
|
||||
Password: "Mot de passe",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "URL REST Redis UpStash",
|
||||
UserName: "Nom de sauvegarde",
|
||||
Password: "Token REST Redis UpStash",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Données locales",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} discussions, ${overview.message} messages, ${overview.prompt} invites, ${overview.mask} masques`;
|
||||
},
|
||||
ImportFailed: "Échec de l'importation",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Écran de masque",
|
||||
Title: "Page de démarrage du masque",
|
||||
SubTitle:
|
||||
"Afficher un écran de masque avant de démarrer une nouvelle discussion",
|
||||
"Afficher la page de démarrage du masque lors de la création d'une nouvelle discussion",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Masquer Les Assistants Intégrés",
|
||||
SubTitle: "Masquer les assistants intégrés par défaut",
|
||||
Title: "Masquer les masques intégrés",
|
||||
SubTitle:
|
||||
"Masquer les masques intégrés dans toutes les listes de masques",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Désactiver la saisie semi-automatique",
|
||||
SubTitle: "Appuyez sur / pour activer la saisie semi-automatique",
|
||||
Title: "Désactiver la complétion automatique des invites",
|
||||
SubTitle:
|
||||
"Saisir / au début de la zone de texte pour déclencher la complétion automatique",
|
||||
},
|
||||
List: "Liste de prompts",
|
||||
List: "Liste des invites personnalisées",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} intégré, ${custom} personnalisé`,
|
||||
`${builtin} intégrées, ${custom} définies par l'utilisateur`,
|
||||
Edit: "Modifier",
|
||||
Modal: {
|
||||
Title: "Liste de prompts",
|
||||
Add: "Ajouter un élément",
|
||||
Search: "Rechercher des prompts",
|
||||
Title: "Liste des invites",
|
||||
Add: "Créer",
|
||||
Search: "Rechercher des invites",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Modifier le prompt",
|
||||
Title: "Modifier les invites",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Nombre de messages joints",
|
||||
SubTitle: "Nombre de messages envoyés attachés par demande",
|
||||
Title: "Nombre de messages historiques",
|
||||
SubTitle: "Nombre de messages historiques envoyés avec chaque demande",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Seuil de compression de l'historique",
|
||||
Title: "Seuil de compression des messages historiques",
|
||||
SubTitle:
|
||||
"Comprimera si la longueur des messages non compressés dépasse cette valeur",
|
||||
"Compresser les messages historiques lorsque leur longueur dépasse cette valeur",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Solde du compte",
|
||||
Title: "Vérification du solde",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Épuisé ce mois-ci $${used}, abonnement $${total}`;
|
||||
return `Utilisé ce mois-ci : $${used}, Total d'abonnement : $${total}`;
|
||||
},
|
||||
IsChecking: "Vérification en cours…",
|
||||
Check: "Re-vérifier",
|
||||
NoAccess:
|
||||
"Entrez la clé API ou le mot de passe d'accès pour vérifier le solde",
|
||||
},
|
||||
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Mot de passe d'accès",
|
||||
SubTitle: "L'administrateur a activé l'accès sécurisé",
|
||||
Placeholder: "Veuillez entrer le mot de passe d'accès",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Interface personnalisée",
|
||||
SubTitle: "Utiliser un service Azure ou OpenAI personnalisé",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Fournisseur de modèle",
|
||||
SubTitle: "Changer de fournisseur de service",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "Clé API",
|
||||
SubTitle:
|
||||
"Utiliser une clé OpenAI personnalisée pour contourner les restrictions d'accès par mot de passe",
|
||||
Placeholder: "Clé API OpenAI",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Doit inclure http(s):// en dehors de l'adresse par défaut",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Clé d'interface",
|
||||
SubTitle:
|
||||
"Utiliser une clé Azure personnalisée pour contourner les restrictions d'accès par mot de passe",
|
||||
Placeholder: "Clé API Azure",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Exemple :",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Version de l'interface (version API azure)",
|
||||
SubTitle: "Choisissez une version spécifique",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Clé d'interface",
|
||||
SubTitle:
|
||||
"Utiliser une clé Anthropic personnalisée pour contourner les restrictions d'accès par mot de passe",
|
||||
Placeholder: "Clé API Anthropic",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Exemple :",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Version de l'interface (version API claude)",
|
||||
SubTitle: "Choisissez une version spécifique de l'API",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "Clé API",
|
||||
SubTitle: "Obtenez votre clé API Google AI",
|
||||
Placeholder: "Entrez votre clé API Google AI Studio",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Exemple :",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "Version de l'API (pour gemini-pro uniquement)",
|
||||
SubTitle: "Choisissez une version spécifique de l'API",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Niveau de filtrage de sécurité Google",
|
||||
SubTitle: "Définir le niveau de filtrage du contenu",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "Clé API",
|
||||
SubTitle: "Utiliser une clé API Baidu personnalisée",
|
||||
Placeholder: "Clé API Baidu",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Clé secrète",
|
||||
SubTitle: "Utiliser une clé secrète Baidu personnalisée",
|
||||
Placeholder: "Clé secrète Baidu",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle:
|
||||
"Non pris en charge pour les configurations personnalisées dans .env",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "Clé d'interface",
|
||||
SubTitle: "Utiliser une clé API ByteDance personnalisée",
|
||||
Placeholder: "Clé API ByteDance",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Exemple :",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Clé d'interface",
|
||||
SubTitle: "Utiliser une clé API Alibaba Cloud personnalisée",
|
||||
Placeholder: "Clé API Alibaba Cloud",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Adresse de l'interface",
|
||||
SubTitle: "Exemple :",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Nom du modèle personnalisé",
|
||||
SubTitle:
|
||||
"Ajouter des options de modèles personnalisés, séparées par des virgules",
|
||||
},
|
||||
IsChecking: "Vérification...",
|
||||
Check: "Vérifier",
|
||||
NoAccess: "Entrez la clé API pour vérifier le solde",
|
||||
},
|
||||
|
||||
Model: "Modèle",
|
||||
Temperature: {
|
||||
Title: "Température",
|
||||
SubTitle: "Une valeur plus élevée rendra les réponses plus aléatoires",
|
||||
Title: "Aléatoire (temperature)",
|
||||
SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
Title: "Échantillonnage par noyau (top_p)",
|
||||
SubTitle:
|
||||
"Ne modifiez pas à moins que vous ne sachiez ce que vous faites",
|
||||
"Semblable à l'aléatoire, mais ne pas modifier en même temps que l'aléatoire",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Limite de Tokens",
|
||||
SubTitle: "Longueur maximale des tokens d'entrée et des tokens générés",
|
||||
Title: "Limite de réponse unique (max_tokens)",
|
||||
SubTitle: "Nombre maximal de tokens utilisés pour une interaction unique",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Pénalité de présence",
|
||||
Title: "Nouveauté du sujet (presence_penalty)",
|
||||
SubTitle:
|
||||
"Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets",
|
||||
"Plus la valeur est élevée, plus il est probable d'élargir aux nouveaux sujets",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Pénalité de fréquence",
|
||||
Title: "Pénalité de fréquence (frequency_penalty)",
|
||||
SubTitle:
|
||||
"Une valeur plus élevée diminuant la probabilité de répéter la même ligne",
|
||||
"Plus la valeur est élevée, plus il est probable de réduire les répétitions",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nouvelle conversation",
|
||||
BotHello: "Bonjour ! Comment puis-je vous aider aujourd'hui ?",
|
||||
Error: "Quelque chose s'est mal passé, veuillez réessayer plus tard.",
|
||||
DefaultTopic: "Nouvelle discussion",
|
||||
BotHello: "Comment puis-je vous aider ?",
|
||||
Error: "Une erreur est survenue, veuillez réessayer plus tard",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Ceci est un résumé de l'historique des discussions entre l'IA et l'utilisateur : " +
|
||||
content,
|
||||
"Voici le résumé de la discussion précédente : " + content,
|
||||
Topic:
|
||||
"Veuillez générer un titre de quatre à cinq mots résumant notre conversation sans introduction, ponctuation, guillemets, points, symboles ou texte supplémentaire. Supprimez les guillemets inclus.",
|
||||
"Utilisez quatre à cinq mots pour retourner le sujet succinct de cette phrase, sans explication, sans ponctuation, sans interjections, sans texte superflu, sans gras. Si aucun sujet, retournez simplement « discussion informelle »",
|
||||
Summarize:
|
||||
"Résumez brièvement nos discussions en 200 mots ou moins pour les utiliser comme prompt de contexte futur.",
|
||||
"Faites un résumé succinct de la discussion, à utiliser comme prompt de contexte ultérieur, en moins de 200 mots",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Copié dans le presse-papiers",
|
||||
Failed:
|
||||
"La copie a échoué, veuillez accorder l'autorisation d'accès au presse-papiers",
|
||||
Failed: "Échec de la copie, veuillez autoriser l'accès au presse-papiers",
|
||||
},
|
||||
Download: {
|
||||
Success: "Le contenu a été téléchargé dans votre répertoire.",
|
||||
Failed: "Échec du téléchargement.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Avec ${x} contextes de prompts`,
|
||||
Edit: "Contextes et mémoires de prompts",
|
||||
Add: "Ajouter un prompt",
|
||||
Toast: (x: any) => `Contient ${x} invites prédéfinies`,
|
||||
Edit: "Paramètres de la discussion actuelle",
|
||||
Add: "Ajouter une discussion",
|
||||
Clear: "Contexte effacé",
|
||||
Revert: "Restaurer le contexte",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Extension",
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Eres un asistente que",
|
||||
Sysmessage: "Vous êtes un assistant",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "Recherche",
|
||||
Page: {
|
||||
Title: "Rechercher dans l'historique des discussions",
|
||||
Search: "Entrez le mot-clé de recherche",
|
||||
NoResult: "Aucun résultat trouvé",
|
||||
NoData: "Aucune donnée",
|
||||
Loading: "Chargement",
|
||||
|
||||
SubTitle: (count: number) => `${count} résultats trouvés`,
|
||||
},
|
||||
Item: {
|
||||
View: "Voir",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masque",
|
||||
Page: {
|
||||
Title: "Modèle de prompt",
|
||||
SubTitle: (count: number) => `${count} modèles de prompts`,
|
||||
Search: "Rechercher des modèles",
|
||||
Title: "Masques de rôle prédéfinis",
|
||||
SubTitle: (count: number) => `${count} définitions de rôle prédéfinies`,
|
||||
Search: "Rechercher des masques de rôle",
|
||||
Create: "Créer",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompts`,
|
||||
Info: (count: number) => `Contient ${count} discussions prédéfinies`,
|
||||
Chat: "Discussion",
|
||||
View: "Vue",
|
||||
View: "Voir",
|
||||
Edit: "Modifier",
|
||||
Delete: "Supprimer",
|
||||
DeleteConfirm: "Confirmer la suppression?",
|
||||
DeleteConfirm: "Confirmer la suppression ?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Modifier le modèle de prompt ${readonly ? "(en lecture seule)" : ""}`,
|
||||
Download: "Télécharger",
|
||||
Clone: "Dupliquer",
|
||||
`Modifier le masque prédéfini ${readonly ? " (lecture seule)" : ""}`,
|
||||
Download: "Télécharger le masque",
|
||||
Clone: "Cloner le masque",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar de lassistant",
|
||||
Name: "Nom de lassistant",
|
||||
Avatar: "Avatar du rôle",
|
||||
Name: "Nom du rôle",
|
||||
Sync: {
|
||||
Title: "Utiliser la configuration globale",
|
||||
SubTitle: "Utiliser la configuration globale dans cette conversation",
|
||||
Confirm: "Voulez-vous definir votre configuration personnalisée ?",
|
||||
Title: "Utiliser les paramètres globaux",
|
||||
SubTitle:
|
||||
"Cette discussion utilise-t-elle les paramètres du modèle globaux ?",
|
||||
Confirm:
|
||||
"Les paramètres personnalisés de cette discussion seront automatiquement remplacés. Confirmer l'activation des paramètres globaux ?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Masquer les invites contextuelles",
|
||||
SubTitle: "Ne pas afficher les instructions contextuelles dans le chat",
|
||||
Title: "Masquer les discussions prédéfinies",
|
||||
SubTitle:
|
||||
"Les discussions prédéfinies ne seront pas affichées dans l'interface de discussion après masquage",
|
||||
},
|
||||
Share: {
|
||||
Title: "Partager ce masque",
|
||||
SubTitle: "Générer un lien vers ce masque",
|
||||
SubTitle: "Générer un lien direct pour ce masque",
|
||||
Action: "Copier le lien",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Retour",
|
||||
Skip: "Passer",
|
||||
Title: "Choisir un assitant",
|
||||
SubTitle: "Discutez avec l'âme derrière le masque",
|
||||
More: "En savoir plus",
|
||||
NotShow: "Ne pas afficher à nouveau",
|
||||
Skip: "Commencer directement",
|
||||
NotShow: "Ne plus afficher",
|
||||
ConfirmNoShow:
|
||||
"Confirmez-vous vouloir désactiver cela? Vous pouvez le réactiver plus tard dans les paramètres.",
|
||||
"Confirmer la désactivation ? Vous pourrez réactiver cette option à tout moment dans les paramètres.",
|
||||
Title: "Choisir un masque",
|
||||
SubTitle: "Commencez maintenant, rencontrez les pensées derrière le masque",
|
||||
More: "Voir tout",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Code d'accès détecté dans le lien, souhaitez-vous le remplir automatiquement ?",
|
||||
Settings:
|
||||
"Paramètres prédéfinis détectés dans le lien, souhaitez-vous les remplir automatiquement ?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
@@ -296,9 +561,17 @@ const fr: PartialLocaleType = {
|
||||
Cancel: "Annuler",
|
||||
Close: "Fermer",
|
||||
Create: "Créer",
|
||||
Edit: "Éditer",
|
||||
Edit: "Modifier",
|
||||
Export: "Exporter",
|
||||
Import: "Importer",
|
||||
Sync: "Synchroniser",
|
||||
Config: "Configurer",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title:
|
||||
"Seuls les messages après avoir effacé le contexte seront affichés",
|
||||
},
|
||||
Model: "Modèle",
|
||||
Messages: "Messages",
|
||||
Topic: "Sujet",
|
||||
|
||||
@@ -1,228 +1,261 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "./index";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const id: PartialLocaleType = {
|
||||
WIP: "Coming Soon...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
|
||||
Unauthorized: isApp
|
||||
? "API Key tidak valid terdeteksi, silakan periksa apakah API Key telah dikonfigurasi dengan benar di halaman [Pengaturan](/#/settings)."
|
||||
: "Kata sandi akses tidak benar atau kosong, silakan masukkan kata sandi akses yang benar di halaman [Masuk](/#/auth), atau masukkan OpenAI API Key Anda di halaman [Pengaturan](/#/settings).",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Diperlukan Kode Akses",
|
||||
Tips: "Masukkan kode akses di bawah",
|
||||
SubTips: "Atau masukkan kunci API OpenAI Anda",
|
||||
Input: "Kode Akses",
|
||||
Title: "Kebutuhan Kata Sandi",
|
||||
Tips: "Administrator telah mengaktifkan verifikasi kata sandi, silakan masukkan kode akses di bawah ini",
|
||||
SubTips: "Atau masukkan kunci API OpenAI atau Google Anda",
|
||||
Input: "Masukkan kode akses di sini",
|
||||
Confirm: "Konfirmasi",
|
||||
Later: "Nanti",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} pesan`,
|
||||
ChatItemCount: (count: number) => `${count} percakapan`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} pesan`,
|
||||
SubTitle: (count: number) => `Total ${count} percakapan`,
|
||||
EditMessage: {
|
||||
Title: "Edit Riwayat Pesan",
|
||||
Topic: {
|
||||
Title: "Topik Obrolan",
|
||||
SubTitle: "Ubah topik obrolan saat ini",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Buka Daftar Chat",
|
||||
CompressedHistory: "Ekspor Riwayat Terkompresi",
|
||||
Export: "Ekspor Semua Pesan sebagai Markdown",
|
||||
ChatList: "Lihat daftar pesan",
|
||||
CompressedHistory: "Lihat riwayat Prompt yang dikompresi",
|
||||
Export: "Ekspor riwayat obrolan",
|
||||
Copy: "Salin",
|
||||
Stop: "Berhenti",
|
||||
Retry: "Coba Lagi",
|
||||
Pin: "Pin",
|
||||
PinToastContent: "2 pesan telah ditandai",
|
||||
Retry: "Coba lagi",
|
||||
Pin: "Sematkan",
|
||||
PinToastContent: "1 percakapan telah disematkan ke prompt default",
|
||||
PinToastAction: "Lihat",
|
||||
Delete: "Hapus",
|
||||
Edit: "Edit",
|
||||
},
|
||||
Commands: {
|
||||
new: "Mulai Chat Baru",
|
||||
newm: "Mulai Chat Baru dengan Masks",
|
||||
next: "Chat Selanjutnya",
|
||||
prev: "Chat Sebelumnya",
|
||||
clear: "Bersihkan Percakapan",
|
||||
del: "Hapus Chat",
|
||||
new: "Obrolan Baru",
|
||||
newm: "Buat Obrolan Baru dari Masker",
|
||||
next: "Obrolan Berikutnya",
|
||||
prev: "Obrolan Sebelumnya",
|
||||
clear: "Hapus Konteks",
|
||||
del: "Hapus Obrolan",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Berhenti",
|
||||
ToBottom: "Ke Bagian Bawah",
|
||||
Stop: "Hentikan Respons",
|
||||
ToBottom: "Gulir ke bawah",
|
||||
Theme: {
|
||||
auto: "Otomatis",
|
||||
light: "Tema Terang",
|
||||
dark: "Tema Gelap",
|
||||
auto: "Tema Otomatis",
|
||||
light: "Mode Terang",
|
||||
dark: "Mode Gelap",
|
||||
},
|
||||
Prompt: "Prompts",
|
||||
Masks: "Masks",
|
||||
Clear: "Bersihkan Percakapan",
|
||||
Settings: "Pengaturan",
|
||||
Prompt: "Perintah Cepat",
|
||||
Masks: "Semua Masker",
|
||||
Clear: "Hapus Obrolan",
|
||||
Settings: "Pengaturan Obrolan",
|
||||
UploadImage: "Unggah Gambar",
|
||||
},
|
||||
Rename: "Ubah Nama Chat",
|
||||
Typing: "Mengetik...",
|
||||
Rename: "Ganti Nama Obrolan",
|
||||
Typing: "Sedang Mengetik…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} untuk mengirim`;
|
||||
var inputHints = `${submitKey} kirim`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter untuk membalut";
|
||||
inputHints += ",Shift + Enter untuk baris baru";
|
||||
}
|
||||
return (
|
||||
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
|
||||
);
|
||||
return inputHints + ",/ untuk melengkapi, : untuk memicu perintah";
|
||||
},
|
||||
Send: "Kirim",
|
||||
Config: {
|
||||
Reset: "Reset ke Default",
|
||||
SaveAs: "Simpan sebagai Masks",
|
||||
Reset: "Hapus Memori",
|
||||
SaveAs: "Simpan sebagai Masker",
|
||||
},
|
||||
IsContext: "Prompt Default",
|
||||
},
|
||||
Export: {
|
||||
Title: "Ekspor Pesan",
|
||||
Title: "Bagikan Riwayat Obrolan",
|
||||
Copy: "Salin Semua",
|
||||
Download: "Unduh",
|
||||
MessageFromYou: "Pesan dari Anda",
|
||||
MessageFromChatGPT: "Pesan dari ChatGPT",
|
||||
Download: "Unduh File",
|
||||
Share: "Bagikan ke ShareGPT",
|
||||
MessageFromYou: "Pengguna",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "Format Ekspor",
|
||||
SubTitle: "Markdown atau Gambar PNG",
|
||||
SubTitle: "Dapat mengekspor teks Markdown atau gambar PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Sertakan Konteks",
|
||||
SubTitle: "Apakah akan menyertakan masks",
|
||||
Title: "Sertakan Konteks Masker",
|
||||
SubTitle: "Apakah akan menampilkan konteks masker dalam pesan",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Pilih",
|
||||
Preview: "Pratinjau",
|
||||
Preview: "Prabaca",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Sedang Membuat Screenshot",
|
||||
Modal: "Tekan lama atau klik kanan untuk menyimpan gambar",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Cari",
|
||||
Search: "Cari Pesan",
|
||||
All: "Pilih Semua",
|
||||
Latest: "Pilih Terbaru",
|
||||
Clear: "Bersihkan",
|
||||
Latest: "Beberapa Terbaru",
|
||||
Clear: "Hapus Pilihan",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Prompt Memori",
|
||||
EmptyContent: "Belum ada yang tersedia.",
|
||||
Send: "Kirim Memori",
|
||||
Copy: "Salin Memori",
|
||||
Reset: "Reset",
|
||||
ResetConfirm:
|
||||
"Jika Anda mereset, riwayat obrolan saat ini dan memori historis akan dihapus. Apakah Anda yakin ingin melakukan reset?",
|
||||
Title: "Ringkasan Sejarah",
|
||||
EmptyContent: "Isi percakapan terlalu pendek, tidak perlu dirangkum",
|
||||
Send: "Otomatis kompres riwayat obrolan dan kirim sebagai konteks",
|
||||
Copy: "Salin Ringkasan",
|
||||
Reset: "[unused]",
|
||||
ResetConfirm: "Konfirmasi untuk menghapus ringkasan sejarah?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Obrolan Baru",
|
||||
DeleteChat: "Anda yakin ingin menghapus percakapan yang dipilih?",
|
||||
DeleteChat: "Konfirmasi untuk menghapus percakapan yang dipilih?",
|
||||
DeleteToast: "Percakapan telah dihapus",
|
||||
Revert: "Kembali",
|
||||
Revert: "Batalkan",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Pengaturan",
|
||||
SubTitle: "Semua Pengaturan",
|
||||
SubTitle: "Semua opsi pengaturan",
|
||||
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Setel Ulang Semua Pengaturan",
|
||||
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
|
||||
Action: "Setel Ulang",
|
||||
Confirm:
|
||||
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
||||
Title: "Atur Ulang Semua Pengaturan",
|
||||
SubTitle: "Atur ulang semua opsi pengaturan ke nilai default",
|
||||
Action: "Atur Ulang Sekarang",
|
||||
Confirm: "Konfirmasi untuk mengatur ulang semua pengaturan?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Hapus Semua Data",
|
||||
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
|
||||
Action: "Hapus",
|
||||
SubTitle: "Hapus semua data obrolan dan pengaturan",
|
||||
Action: "Hapus Sekarang",
|
||||
Confirm:
|
||||
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
|
||||
"Konfirmasi untuk menghapus semua data obrolan dan pengaturan?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Language", // PERHATIAN: jika Anda ingin menambahkan terjemahan baru, harap jangan terjemahkan nilai ini, biarkan sebagai `Language`
|
||||
All: "Semua Bahasa",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Ukuran Font",
|
||||
SubTitle: "Ubah ukuran font konten chat",
|
||||
SubTitle: "Ukuran font untuk konten obrolan",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "Font Obrolan",
|
||||
SubTitle:
|
||||
"Font dari konten obrolan, biarkan kosong untuk menerapkan font default global",
|
||||
Placeholder: "Nama Font",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Suntikkan Petunjuk Sistem",
|
||||
Title: "Suntikkan Pesan Sistem",
|
||||
SubTitle:
|
||||
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
||||
"Memaksa menambahkan pesan sistem simulasi ChatGPT di awal daftar pesan setiap permintaan",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Template Input",
|
||||
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
||||
Title: "Pra-pemrosesan Input Pengguna",
|
||||
SubTitle: "Pesan terbaru pengguna akan diisi ke template ini",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
IsLatest: "Versi terbaru",
|
||||
Version: (x: string) => `Versi Saat Ini: ${x}`,
|
||||
IsLatest: "Sudah versi terbaru",
|
||||
CheckUpdate: "Periksa Pembaruan",
|
||||
IsChecking: "Memeriksa pembaruan...",
|
||||
FoundUpdate: (x: string) => `Versi terbaru ditemukan: ${x}`,
|
||||
GoToUpdate: "Perbarui Sekarang",
|
||||
IsChecking: "Sedang memeriksa pembaruan...",
|
||||
FoundUpdate: (x: string) => `Versi Baru Ditemukan: ${x}`,
|
||||
GoToUpdate: "Pergi ke Pembaruan",
|
||||
},
|
||||
SendKey: "Kunci Kirim",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Mode Tanpa Border",
|
||||
SendPreviewBubble: {
|
||||
Title: "Preview Bubble",
|
||||
SubTitle: "Pratinjau konten Markdown di bubble pratinjau",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Hasilkan Judul Otomatis",
|
||||
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
|
||||
Title: "Otomatis Membuat Judul",
|
||||
SubTitle: "Membuat judul yang sesuai berdasarkan konten obrolan",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Pembaruan Terakhir",
|
||||
CloudState: "Data Cloud",
|
||||
NotSyncYet: "Belum disinkronkan",
|
||||
Success: "Sinkronisasi Berhasil",
|
||||
Fail: "Sinkronisasi Gagal",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Konfigurasi Sinkronisasi",
|
||||
Title: "Konfigurasi Sinkronisasi Cloud",
|
||||
Check: "Periksa Ketersediaan",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Tipe Sinkronisasi",
|
||||
SubTitle: "Pilih layanan sinkronisasi favorit Anda",
|
||||
Title: "Jenis Sinkronisasi",
|
||||
SubTitle: "Pilih server sinkronisasi favorit",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Aktifkan Proxy CORS",
|
||||
Title: "Aktifkan Proxy",
|
||||
SubTitle:
|
||||
"Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber",
|
||||
"Saat menyinkronkan di browser, proxy harus diaktifkan untuk menghindari pembatasan lintas domain",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Lokasi Titik Akhir Proxy CORS",
|
||||
SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini",
|
||||
Title: "Alamat Proxy",
|
||||
SubTitle: "Hanya berlaku untuk proxy lintas domain bawaan proyek ini",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Lokasi Titik Akhir WebDAV",
|
||||
UserName: "User Pengguna",
|
||||
Endpoint: "Alamat WebDAV",
|
||||
UserName: "Nama Pengguna",
|
||||
Password: "Kata Sandi",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "Url REST Redis UpStash",
|
||||
UserName: "Nama Cadangan",
|
||||
Password: "Token REST Redis UpStash",
|
||||
},
|
||||
},
|
||||
},
|
||||
SendKey: "Kirim",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Batas Ketat",
|
||||
SendPreviewBubble: {
|
||||
Title: "Pratinjau Obrolan",
|
||||
SubTitle: "Pratinjau Obrolan dengan markdown",
|
||||
|
||||
LocalState: "Data Lokal",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} percakapan, ${overview.message} pesan, ${overview.prompt} prompt, ${overview.mask} masker`;
|
||||
},
|
||||
ImportFailed: "Impor Gagal",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Layar Pembuka Masks",
|
||||
SubTitle:
|
||||
"Tampilkan layar pembuka masks sebelum memulai percakapan baru",
|
||||
Title: "Halaman Awal Masker",
|
||||
SubTitle: "Tampilkan halaman awal masker saat memulai obrolan baru",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Sembunyikan Masks Bawaan",
|
||||
SubTitle: "Sembunyikan Masks bawaan dari daftar masks",
|
||||
Title: "Sembunyikan Masker Bawaan",
|
||||
SubTitle: "Sembunyikan masker bawaan dari semua daftar masker",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Nonaktifkan Otomatisasi",
|
||||
SubTitle: "Aktifkan/Matikan otomatisasi",
|
||||
Title: "Nonaktifkan Pelengkapan Prompt Otomatis",
|
||||
SubTitle:
|
||||
"Ketik / di awal kotak input untuk memicu pelengkapan otomatis",
|
||||
},
|
||||
List: "Daftar Prompt",
|
||||
List: "Daftar Prompt Kustom",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} bawaan, ${custom} penggunaan khusus`,
|
||||
`Bawaan ${builtin} item, pengguna ${custom} item`,
|
||||
Edit: "Edit",
|
||||
Modal: {
|
||||
Title: "Daftar Prompt",
|
||||
Add: "Tambahkan",
|
||||
Add: "Baru",
|
||||
Search: "Cari Prompt",
|
||||
},
|
||||
EditModal: {
|
||||
@@ -230,155 +263,308 @@ const id: PartialLocaleType = {
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Jumlah Pesan Riwayat",
|
||||
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
||||
Title: "Jumlah Pesan Sejarah",
|
||||
SubTitle: "Jumlah pesan sejarah yang dibawa setiap permintaan",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Batas Kompresi Riwayat",
|
||||
Title: "Ambang Batas Kompresi Pesan Sejarah",
|
||||
SubTitle:
|
||||
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
|
||||
"Ketika pesan sejarah yang tidak terkompresi melebihi nilai ini, akan dikompresi",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo Akun",
|
||||
Title: "Cek Saldo",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
|
||||
return `Digunakan bulan ini $${used}, total langganan $${total}`;
|
||||
},
|
||||
IsChecking: "Sedang memeriksa…",
|
||||
Check: "Periksa Lagi",
|
||||
NoAccess: "Masukkan API Key atau kata sandi akses untuk melihat saldo",
|
||||
},
|
||||
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Kata Sandi Akses",
|
||||
SubTitle: "Administrator telah mengaktifkan akses terenkripsi",
|
||||
Placeholder: "Masukkan kata sandi akses",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Antarmuka Kustom",
|
||||
SubTitle: "Apakah akan menggunakan layanan Azure atau OpenAI kustom",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Penyedia Layanan Model",
|
||||
SubTitle: "Ganti penyedia layanan yang berbeda",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle:
|
||||
"Gunakan OpenAI Key kustom untuk menghindari batasan akses kata sandi",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Selain alamat default, harus menyertakan http(s)://",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Kunci Antarmuka",
|
||||
SubTitle:
|
||||
"Gunakan Azure Key kustom untuk menghindari batasan akses kata sandi",
|
||||
Placeholder: "Azure API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Contoh:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versi Antarmuka (azure api version)",
|
||||
SubTitle: "Pilih versi parsial tertentu",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Kunci Antarmuka",
|
||||
SubTitle:
|
||||
"Gunakan Anthropic Key kustom untuk menghindari batasan akses kata sandi",
|
||||
Placeholder: "Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Contoh:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versi Antarmuka (claude api version)",
|
||||
SubTitle: "Pilih versi API tertentu",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "Kunci API",
|
||||
SubTitle: "Dapatkan kunci API Anda dari Google AI",
|
||||
Placeholder: "Masukkan kunci API Studio Google AI Anda",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Alamat Akhir",
|
||||
SubTitle: "Contoh:",
|
||||
},
|
||||
|
||||
ApiVersion: {
|
||||
Title: "Versi API (hanya untuk gemini-pro)",
|
||||
SubTitle: "Pilih versi API tertentu",
|
||||
},
|
||||
GoogleSafetySettings: {
|
||||
Title: "Tingkat Filter Keamanan Google",
|
||||
SubTitle: "Atur tingkat filter konten",
|
||||
},
|
||||
},
|
||||
Baidu: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Gunakan Baidu API Key kustom",
|
||||
Placeholder: "Baidu API Key",
|
||||
},
|
||||
SecretKey: {
|
||||
Title: "Secret Key",
|
||||
SubTitle: "Gunakan Baidu Secret Key kustom",
|
||||
Placeholder: "Baidu Secret Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Tidak mendukung kustom, pergi ke .env untuk konfigurasi",
|
||||
},
|
||||
},
|
||||
ByteDance: {
|
||||
ApiKey: {
|
||||
Title: "Kunci Antarmuka",
|
||||
SubTitle: "Gunakan ByteDance API Key kustom",
|
||||
Placeholder: "ByteDance API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Contoh:",
|
||||
},
|
||||
},
|
||||
Alibaba: {
|
||||
ApiKey: {
|
||||
Title: "Kunci Antarmuka",
|
||||
SubTitle: "Gunakan Alibaba Cloud API Key kustom",
|
||||
Placeholder: "Alibaba Cloud API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Alamat Antarmuka",
|
||||
SubTitle: "Contoh:",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Nama Model Kustom",
|
||||
SubTitle: "Tambahkan opsi model kustom, pisahkan dengan koma",
|
||||
},
|
||||
IsChecking: "Memeriksa...",
|
||||
Check: "Periksa",
|
||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Suhu",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
||||
Title: "Randomness (temperature)",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak responsnya",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Tidak mengubah nilai dengan suhu",
|
||||
Title: "Sampling Inti (top_p)",
|
||||
SubTitle:
|
||||
"Mirip dengan randomness, tetapi jangan ubah bersama randomness",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Token Maksimum",
|
||||
SubTitle: "Panjang maksimum token input dan output",
|
||||
Title: "Batas Token Per Respons",
|
||||
SubTitle: "Jumlah token maksimum yang digunakan per interaksi",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalti Kehadiran",
|
||||
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
|
||||
Title: "Kedekatan Topik (presence_penalty)",
|
||||
SubTitle:
|
||||
"Semakin tinggi nilainya, semakin besar kemungkinan memperluas ke topik baru",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalti Frekuensi",
|
||||
Title: "Hukuman Frekuensi (frequency_penalty)",
|
||||
SubTitle:
|
||||
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
||||
"Semakin tinggi nilainya, semakin besar kemungkinan mengurangi kata-kata yang berulang",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Percakapan Baru",
|
||||
BotHello: "Halo! Bagaimana saya bisa membantu Anda hari ini?",
|
||||
Error: "Terjadi kesalahan, silakan coba lagi nanti.",
|
||||
DefaultTopic: "Obrolan Baru",
|
||||
BotHello: "Ada yang bisa saya bantu?",
|
||||
Error: "Terjadi kesalahan, coba lagi nanti",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Ini adalah ringkasan singkat dari riwayat percakapan: " + content,
|
||||
"Ini adalah ringkasan obrolan sebelumnya sebagai latar belakang: " +
|
||||
content,
|
||||
Topic:
|
||||
"Buat judul berisi empat hingga lima kata untuk percakapan kita yang tidak akan disertakan dalam ringkasan percakapan, seperti instruksi, format, kutipan, tanda baca awal, tanda kutip pendahuluan, atau karakter tambahan. Silakan coba dengan kutipan berakhir.",
|
||||
"Gunakan empat hingga lima kata untuk langsung memberikan ringkasan topik kalimat ini, tanpa penjelasan, tanpa tanda baca, tanpa kata pengisi, tanpa teks tambahan, tanpa menebalkan. Jika tidak ada topik, langsung jawab 'Obrolan Santai'",
|
||||
Summarize:
|
||||
"Buat ringkasan percakapan dalam 200 kata yang akan digunakan sebagai promp di masa depan.",
|
||||
"Berikan ringkasan singkat tentang konten obrolan, untuk digunakan sebagai prompt konteks selanjutnya, dalam 200 kata atau kurang",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Tersalin ke clipboard",
|
||||
Failed:
|
||||
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
|
||||
Success: "Telah disalin ke clipboard",
|
||||
Failed: "Gagal menyalin, mohon berikan izin clipboard",
|
||||
},
|
||||
Download: {
|
||||
Success: "Konten berhasil diunduh ke direktori Anda.",
|
||||
Success: "Konten telah diunduh ke direktori Anda.",
|
||||
Failed: "Unduhan gagal.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
|
||||
Toast: (x: any) => `Berisi ${x} prompt preset`,
|
||||
Edit: "Pengaturan Obrolan Saat Ini",
|
||||
Add: "Tambahkan Promp",
|
||||
Clear: "Bersihkan Konteks",
|
||||
Revert: "Kembali ke Posisi Sebelumnya",
|
||||
Add: "Tambah Obrolan",
|
||||
Clear: "Konteks telah dihapus",
|
||||
Revert: "Kembalikan Konteks",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Anda adalah asisten yang",
|
||||
Sysmessage: "Anda adalah seorang asisten",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masks",
|
||||
SearchChat: {
|
||||
Name: "Cari",
|
||||
Page: {
|
||||
Title: "Template Promp",
|
||||
SubTitle: (count: number) => `${count} template prompt`,
|
||||
Search: "Cari template",
|
||||
Create: "Buat",
|
||||
Title: "Cari riwayat obrolan",
|
||||
Search: "Masukkan kata kunci pencarian",
|
||||
NoResult: "Tidak ada hasil ditemukan",
|
||||
NoData: "Tidak ada data",
|
||||
Loading: "Memuat",
|
||||
|
||||
SubTitle: (count: number) => `Ditemukan ${count} hasil`,
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompt`,
|
||||
View: "Lihat",
|
||||
},
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masker",
|
||||
Page: {
|
||||
Title: "Preset Karakter Masker",
|
||||
SubTitle: (count: number) => `${count} definisi karakter preset`,
|
||||
Search: "Cari Masker Karakter",
|
||||
Create: "Buat Baru",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `Berisi ${count} obrolan preset`,
|
||||
Chat: "Obrolan",
|
||||
View: "Lihat",
|
||||
Edit: "Edit",
|
||||
Delete: "Hapus",
|
||||
DeleteConfirm: "Anda yakin ingin menghapus?",
|
||||
DeleteConfirm: "Konfirmasi penghapusan?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Edit Template Prompt ${readonly ? "(hanya baca)" : ""}`,
|
||||
Download: "Unduh",
|
||||
Clone: "Duplikat",
|
||||
`Edit Masker Preset ${readonly ? "(Hanya Baca)" : ""}`,
|
||||
Download: "Unduh Preset",
|
||||
Clone: "Klon Preset",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar Bot",
|
||||
Name: "Nama Bot",
|
||||
Avatar: "Avatar Karakter",
|
||||
Name: "Nama Karakter",
|
||||
Sync: {
|
||||
Title: "Gunakan Konfigurasi Global",
|
||||
SubTitle: "Gunakan konfigurasi global dalam percakapan ini",
|
||||
Title: "Gunakan Pengaturan Global",
|
||||
SubTitle:
|
||||
"Apakah obrolan saat ini akan menggunakan pengaturan model global?",
|
||||
Confirm:
|
||||
"Pastikan untuk mengganti konfigurasi kustom dengan konfigurasi global?",
|
||||
"Pengaturan kustom obrolan saat ini akan ditimpa secara otomatis, konfirmasi untuk mengaktifkan pengaturan global?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Sembunyikan Prompt Konteks",
|
||||
SubTitle: "Tidak menampilkan prompt konteks dalam obrolan",
|
||||
Title: "Sembunyikan Obrolan Preset",
|
||||
SubTitle:
|
||||
"Setelah disembunyikan, obrolan preset tidak akan muncul di antarmuka obrolan",
|
||||
},
|
||||
Share: {
|
||||
Title: "Bagikan Masks Ini",
|
||||
SubTitle: "Buat tautan untuk masks ini",
|
||||
Title: "Bagikan Masker Ini",
|
||||
SubTitle: "Hasilkan tautan langsung ke masker ini",
|
||||
Action: "Salin Tautan",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Kembali",
|
||||
Skip: "Lewati",
|
||||
Title: "Pilih Masks",
|
||||
SubTitle: "Berkonversasilah dengan diri Anda di balik masks",
|
||||
More: "Lebih Lanjut",
|
||||
NotShow: "Jangan Tampilkan Sekarang",
|
||||
Skip: "Mulai Sekarang",
|
||||
NotShow: "Jangan Tampilkan Lagi",
|
||||
ConfirmNoShow:
|
||||
"Pastikan untuk menonaktifkannya? Anda dapat mengaktifkannya nanti melalui pengaturan.",
|
||||
"Konfirmasi untuk menonaktifkan? Setelah dinonaktifkan, Anda dapat mengaktifkannya kembali kapan saja di pengaturan.",
|
||||
Title: "Pilih Masker",
|
||||
SubTitle: "Mulai sekarang, berinteraksi dengan pemikiran di balik masker",
|
||||
More: "Lihat Semua",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Terdeteksi bahwa tautan sudah mengandung kode akses, apakah akan diisi secara otomatis?",
|
||||
Settings:
|
||||
"Terdeteksi bahwa tautan mengandung pengaturan preset, apakah akan diisi secara otomatis?",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Konfirmasi",
|
||||
Cancel: "Batal",
|
||||
Close: "Tutup",
|
||||
Create: "Buat",
|
||||
Create: "Buat Baru",
|
||||
Edit: "Edit",
|
||||
Export: "Ekspor",
|
||||
Import: "Impor",
|
||||
Sync: "Sinkronkan",
|
||||
Config: "Konfigurasi",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
|
||||
},
|
||||
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan",
|
||||
},
|
||||
Model: "Model",
|
||||
Messages: "Pesan",
|
||||
Topic: "Topik",
|
||||
Time: "Tanggal & Waktu",
|
||||
},
|
||||
URLCommand: {
|
||||
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",
|
||||
Settings: "Pengaturan terdeteksi dari url, konfirmasi untuk diterapkan ?",
|
||||
Time: "Waktu",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ import ar from "./ar";
|
||||
import bn from "./bn";
|
||||
import sk from "./sk";
|
||||
import { merge } from "../utils/merge";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
|
||||
import type { LocaleType } from "./cn";
|
||||
export type { LocaleType, PartialLocaleType } from "./cn";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
const ALL_LANGS = {
|
||||
cn,
|
||||
en,
|
||||
@@ -70,28 +73,6 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
sk: "Slovensky",
|
||||
};
|
||||
|
||||
const LANG_CODE_MAPPING = {
|
||||
cn: "zh-CN",
|
||||
en: "en-US",
|
||||
tw: "zh-TW",
|
||||
pt: "pt-BR",
|
||||
jp: "ja-JP",
|
||||
ko: "ko-KR",
|
||||
id: "id-ID",
|
||||
fr: "fr-FR",
|
||||
es: "es-ES",
|
||||
it: "it-IT",
|
||||
tr: "tr-TR",
|
||||
de: "de-DE",
|
||||
vi: "vi-VN",
|
||||
ru: "ru-RU",
|
||||
cs: "cs-CZ",
|
||||
no: "nb-NO",
|
||||
ar: "ar-SA",
|
||||
bn: "bn-BD",
|
||||
sk: "sk-SK",
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
const DEFAULT_LANG = "en";
|
||||
|
||||
@@ -103,30 +84,27 @@ merge(fallbackLang, targetLang);
|
||||
|
||||
export default fallbackLang as LocaleType;
|
||||
|
||||
export const formatLang = (languageCode: string) => {
|
||||
return (
|
||||
LANG_CODE_MAPPING[languageCode as keyof typeof LANG_CODE_MAPPING] ||
|
||||
languageCode
|
||||
);
|
||||
};
|
||||
|
||||
function getItem(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function setItem(key: string, value: string) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {}
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
try {
|
||||
return navigator.language.toLowerCase();
|
||||
const locale = new Intl.Locale(navigator.language).maximize();
|
||||
const region = locale?.region?.toLowerCase();
|
||||
// 1. check region code in ALL_LANGS
|
||||
if (AllLangs.includes(region as Lang)) {
|
||||
return region as Lang;
|
||||
}
|
||||
// 2. check language code in ALL_LANGS
|
||||
if (AllLangs.includes(locale.language as Lang)) {
|
||||
return locale.language as Lang;
|
||||
}
|
||||
return DEFAULT_LANG;
|
||||
} catch {
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
@@ -139,15 +117,7 @@ export function getLang(): Lang {
|
||||
return savedLang as Lang;
|
||||
}
|
||||
|
||||
const lang = getLanguage();
|
||||
|
||||
for (const option of AllLangs) {
|
||||
if (lang.includes(option)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LANG;
|
||||
return getLanguage();
|
||||
}
|
||||
|
||||
export function changeLang(lang: Lang) {
|
||||
|
||||