Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
748cb7d446 | ||
|
|
348186c405 | ||
|
|
08f7c2fd63 | ||
|
|
ed326f02c0 | ||
|
|
1ec668f697 | ||
|
|
b591891464 | ||
|
|
e318f92177 | ||
|
|
589de36af7 | ||
|
|
5245d037ac | ||
|
|
58bead0398 | ||
|
|
9e9306fd2b | ||
|
|
0ea9e19d79 | ||
|
|
86bc0c8c73 | ||
|
|
858f5ae1fe | ||
|
|
cc3312157b | ||
|
|
4809d363b3 | ||
|
|
b1d204fdd4 | ||
|
|
25d09363df | ||
|
|
aa489be53b | ||
|
|
e3d253b040 | ||
|
|
2d7d6cfffc | ||
|
|
ef5af1e273 | ||
|
|
0546ad58be | ||
|
|
f3e5700d49 | ||
|
|
ed13da8aba | ||
|
|
48e37973a3 | ||
|
|
e2a8ad5fca | ||
|
|
190b934ab5 | ||
|
|
1e76dbc9a0 | ||
|
|
b3da09f52c | ||
|
|
4ab5050379 | ||
|
|
40c1b49e6d | ||
|
|
8e94618c51 | ||
|
|
83efebe06b | ||
|
|
e6fad5ccb0 | ||
|
|
424141d1da | ||
|
|
4651db8c09 | ||
|
|
5997774ab8 | ||
|
|
760c62739a | ||
|
|
e627b8bf21 | ||
|
|
31124d9deb | ||
|
|
56c93bc2ac | ||
|
|
f83eb7326f | ||
|
|
8422d3ea79 | ||
|
|
77426266d2 | ||
|
|
7d4f9134bc | ||
|
|
4d8ba5c7f0 | ||
|
|
4148d70ec0 | ||
|
|
6f6be2c03f | ||
|
|
bfdefbf6e7 | ||
|
|
063dabbf4a | ||
|
|
302c5074e9 | ||
|
|
f202a95661 | ||
|
|
e82d9c873b | ||
|
|
489c45ffdf | ||
|
|
81b7cdfed7 | ||
|
|
67df928c7a | ||
|
|
743bbae5d1 | ||
|
|
2717fe7c20 | ||
|
|
51191168bc | ||
|
|
a59fb6b9eb | ||
|
|
3c382d4c6c | ||
|
|
8651bec915 | ||
|
|
a41b195f46 | ||
|
|
5e458d490a | ||
|
|
948f2e913e | ||
|
|
aa4b2cc36f | ||
|
|
df7517f9c4 | ||
|
|
98369fba22 | ||
|
|
7a1f1d36a1 | ||
|
|
040ea70585 | ||
|
|
1cf4fa96c1 | ||
|
|
d19143dd2b | ||
|
|
fe3291acb5 | ||
|
|
9873cad3d6 | ||
|
|
1e919abda3 | ||
|
|
c533ed91f5 | ||
|
|
38321355d3 | ||
|
|
22c50f62cb | ||
|
|
eefe01454f | ||
|
|
74dd9b561a | ||
|
|
2fd2f792d6 | ||
|
|
93bd20b854 | ||
|
|
291c7595c4 | ||
|
|
68de49e533 | ||
|
|
d6a0805966 | ||
|
|
a5f8e87f3f | ||
|
|
45ecaaf392 | ||
|
|
4c221eabef | ||
|
|
b12edb4a7a | ||
|
|
50e8979c00 | ||
|
|
459a41774e | ||
|
|
fbd520bf07 | ||
|
|
00b01c973e | ||
|
|
f04164378a | ||
|
|
c468df2f71 | ||
|
|
fa9593b4e8 | ||
|
|
cbd18ec63c | ||
|
|
7e5bda6016 | ||
|
|
713934edb6 | ||
|
|
710850e442 | ||
|
|
2a10438b4d | ||
|
|
efa258c695 | ||
|
|
7eb14437ff | ||
|
|
9451726ee6 | ||
|
|
97d68a6a05 | ||
|
|
cd79afb425 | ||
|
|
b79596332a | ||
|
|
21cc6ebcb7 | ||
|
|
80ecea980e | ||
|
|
4c8932d71c | ||
|
|
7a5a3c45e0 | ||
|
|
3df03fa3fe | ||
|
|
77ec9dd1f2 | ||
|
|
22b694e0fd | ||
|
|
5e9ace1c6e | ||
|
|
76bd77bc56 | ||
|
|
a4083f43cb | ||
|
|
b67f80f7a4 | ||
|
|
a53c1cfc77 | ||
|
|
b788514a10 | ||
|
|
5f16ec077a | ||
|
|
941dc41c3d | ||
|
|
44f9e930d2 | ||
|
|
c235a3d539 | ||
|
|
e720afacfa | ||
|
|
b352732b43 | ||
|
|
0caf04617e | ||
|
|
e0ebd7aeaf | ||
|
|
2ffe55f128 | ||
|
|
9d73f22aff | ||
|
|
9dad7e7c9a | ||
|
|
b5372bf715 | ||
|
|
20a3db975d | ||
|
|
851754700a | ||
|
|
375056f8dc | ||
|
|
236e2c040c | ||
|
|
cf811edefb | ||
|
|
f6e839611b | ||
|
|
2a79c30657 | ||
|
|
d882cb41ac | ||
|
|
323b9adc63 | ||
|
|
3488b7f006 | ||
|
|
daed66f7c6 | ||
|
|
b219906fa3 | ||
|
|
a556e39651 | ||
|
|
b35316177b | ||
|
|
3ed509c5d3 | ||
|
|
6699f0c339 | ||
|
|
9dbd223b4e | ||
|
|
11e0aaa32f | ||
|
|
9aea0fb03d | ||
|
|
b51922809b | ||
|
|
314b5b5f6c | ||
|
|
7c0c11b776 | ||
|
|
44ad7c4132 | ||
|
|
748a930a5f | ||
|
|
dba45acbda | ||
|
|
ac7bb03cb3 | ||
|
|
1be60c9729 | ||
|
|
54a4b7db14 | ||
|
|
741ed5dc4c | ||
|
|
b1b72441bb | ||
|
|
b48e73fa43 | ||
|
|
2e0def73eb | ||
|
|
36ce157907 | ||
|
|
9cdb5bf9fe | ||
|
|
2952e61167 | ||
|
|
8b10b058e5 | ||
|
|
0c441b588c | ||
|
|
a938ffb586 | ||
|
|
b5d882606a | ||
|
|
d9ce1d3ea3 | ||
|
|
9091513c39 | ||
|
|
0f5ecafc57 | ||
|
|
98c16776f8 | ||
|
|
91dda2401d | ||
|
|
6238495d61 | ||
|
|
0708a3d75e | ||
|
|
b25e3ed364 | ||
|
|
1fa3bf6793 | ||
|
|
b93337e62f | ||
|
|
d2d255228c | ||
|
|
23b674ddda | ||
|
|
0d3ce18a61 | ||
|
|
067c94810a | ||
|
|
916ce9bc4f | ||
|
|
0981fae161 | ||
|
|
2649d29a3e | ||
|
|
838778aa3e | ||
|
|
8c8388ea9f | ||
|
|
b40054c54d | ||
|
|
fffd42e4d7 | ||
|
|
db817fcf29 | ||
|
|
faa5884150 | ||
|
|
922628c1ee | ||
|
|
8e30948de9 | ||
|
|
e08c144f0b | ||
|
|
51afb1e378 | ||
|
|
c49cc3fa86 | ||
|
|
e17d66eb61 | ||
|
|
d6260fd20c | ||
|
|
79a4abc3ec | ||
|
|
6623ecb302 | ||
|
|
18a3f06a62 | ||
|
|
1d8496eabb | ||
|
|
8a9cf44dbc | ||
|
|
872ea83c50 | ||
|
|
ff01398812 | ||
|
|
3b8ea55bc8 | ||
|
|
81c8717d75 | ||
|
|
429242b4d3 | ||
|
|
0399a69b73 | ||
|
|
9e726a32e6 | ||
|
|
78284e49d7 | ||
|
|
64a6db4b55 | ||
|
|
16c9042318 | ||
|
|
7a47ba14f7 | ||
|
|
988123a2c8 | ||
|
|
3a661fda1a | ||
|
|
a85e93f1f9 | ||
|
|
7050d53718 | ||
|
|
cd91d8a987 | ||
|
|
eddaa7fa89 | ||
|
|
550fc63ebd | ||
|
|
57a81d70c1 | ||
|
|
295472fca1 | ||
|
|
a3e41db8d7 | ||
|
|
546efe0d7b | ||
|
|
27f01b0bc8 | ||
|
|
62ec2651ba | ||
|
|
b9cadff16b | ||
|
|
c0d273f162 | ||
|
|
098ba6ea4e | ||
|
|
582d11f191 | ||
|
|
cb2158a794 | ||
|
|
abfcceecef | ||
|
|
f4b87ecb23 | ||
|
|
48aad65514 | ||
|
|
cbb21a148d | ||
|
|
2f6e683163 | ||
|
|
1448c32fdf | ||
|
|
0a3b99b94a | ||
|
|
0bae789d39 | ||
|
|
304bf9d9b1 | ||
|
|
839efa4443 | ||
|
|
0a488c783f | ||
|
|
d6275ee941 | ||
|
|
2da7dd67ea | ||
|
|
506a061387 | ||
|
|
1a2971ae7b | ||
|
|
da9a54288e | ||
|
|
28c8b5841c | ||
|
|
e48cdf63d9 | ||
|
|
a649dc80c0 | ||
|
|
733d69425e | ||
|
|
e844e7f708 | ||
|
|
3b3d0cce1e | ||
|
|
b07641e58a | ||
|
|
72110c293a | ||
|
|
f64ac3269f | ||
|
|
eb5ed905eb | ||
|
|
4207f80ce9 | ||
|
|
073aecd427 | ||
|
|
3e1c679f2d | ||
|
|
47aeab81d8 | ||
|
|
f4e165d028 | ||
|
|
02d3fb427b | ||
|
|
117e07b0d4 | ||
|
|
efa6aa4b4c | ||
|
|
c73e8916ed | ||
|
|
df2cb16086 | ||
|
|
f267f3f7b0 | ||
|
|
8d8075e81f | ||
|
|
0a403eb129 | ||
|
|
f74642df1b | ||
|
|
782cce7c44 | ||
|
|
395ca82175 | ||
|
|
663b5adaf2 | ||
|
|
924ebf035b | ||
|
|
fafb8cd263 | ||
|
|
8110a872d5 | ||
|
|
331fe04df7 | ||
|
|
aed4763bfe | ||
|
|
8d0f3fbde9 | ||
|
|
318d4d1ce1 | ||
|
|
ec530ac9f8 | ||
|
|
11741ea7f0 | ||
|
|
bee0338763 | ||
|
|
073f06d449 | ||
|
|
5a787ab2c5 | ||
|
|
81dbc65853 | ||
|
|
8a7075c3bf | ||
|
|
78565e554b | ||
|
|
7081755e19 | ||
|
|
cf54adf5c4 | ||
|
|
ae9922a2cd | ||
|
|
bad7dca51e | ||
|
|
69716a5cec | ||
|
|
1af62a70f0 | ||
|
|
5694f16624 | ||
|
|
a68b95c95f | ||
|
|
f34b9733e3 | ||
|
|
8db03f3ab2 | ||
|
|
d001d7afb1 | ||
|
|
183b84beae | ||
|
|
075dddf3d3 | ||
|
|
7d64ada43e | ||
|
|
802eb3beec | ||
|
|
ac9308dbed | ||
|
|
b7ced4e4f1 | ||
|
|
536fd347b9 | ||
|
|
c8f7bb990c | ||
|
|
b8790200e9 | ||
|
|
4200ad111c | ||
|
|
0f90332e61 | ||
|
|
f87805b46f | ||
|
|
7a7d1931d9 | ||
|
|
e61e1b079f | ||
|
|
7813c3316f | ||
|
|
ac470e64e0 | ||
|
|
c52cc46d2c | ||
|
|
a2bd9b8639 | ||
|
|
0c43897f3d | ||
|
|
4195af4942 | ||
|
|
9f82f5abba | ||
|
|
075fbedb02 | ||
|
|
bcf79c8366 | ||
|
|
3b66aa55c0 | ||
|
|
3b06096c52 | ||
|
|
a602089b64 | ||
|
|
8d1db9a1c0 | ||
|
|
46774aa5cd | ||
|
|
62392aa88a | ||
|
|
c32d41b47f | ||
|
|
9404b6135a | ||
|
|
8caad59cb4 | ||
|
|
21132420d8 | ||
|
|
c89070797c | ||
|
|
1f85354856 | ||
|
|
bfd066cc2f | ||
|
|
000bea84ae | ||
|
|
944efd2cd8 | ||
|
|
4a09342a71 | ||
|
|
aad13607c0 | ||
|
|
1509c2c2ee | ||
|
|
d94bc21ac8 | ||
|
|
947c392f72 | ||
|
|
15d24a6a11 | ||
|
|
a3d6343dbe | ||
|
|
ca8fd8af1d | ||
|
|
d0d01c95f9 | ||
|
|
f669c0e78e | ||
|
|
33ad2381aa | ||
|
|
14fcf4d267 | ||
|
|
83ad488e31 | ||
|
|
fa61e738c3 | ||
|
|
0aa4d305c7 | ||
|
|
1066096b33 | ||
|
|
398b2076cd | ||
|
|
fb516ea694 | ||
|
|
b35cc36e19 | ||
|
|
6d1ce99537 | ||
|
|
7c5f6d71b3 | ||
|
|
e49e04c56a | ||
|
|
da8646cae9 | ||
|
|
ba0523cd69 | ||
|
|
38c34b4444 | ||
|
|
b11af9ea0e | ||
|
|
56369fea3a | ||
|
|
24fb77759e | ||
|
|
f9106cf394 | ||
|
|
807a60cccf | ||
|
|
ac4675900e | ||
|
|
f7a1fc207a | ||
|
|
b9b1d64bd5 | ||
|
|
7e6a00b009 | ||
|
|
5aad328040 | ||
|
|
03a7f88dca | ||
|
|
f61869d90d | ||
|
|
95505bc4c1 | ||
|
|
eca59f759d | ||
|
|
9975cb17a9 | ||
|
|
0b823f90e6 | ||
|
|
c0fff4c69f | ||
|
|
180dd3966b | ||
|
|
e4d101e550 | ||
|
|
88c5b7f495 | ||
|
|
4e9ca31146 | ||
|
|
48e7376336 | ||
|
|
488301f150 | ||
|
|
287c95e72d | ||
|
|
af5a7a35c0 | ||
|
|
5faa05a117 | ||
|
|
e273a99153 | ||
|
|
151055590d | ||
|
|
7978adbf45 | ||
|
|
94976e5ed3 | ||
|
|
3a1d747677 | ||
|
|
7121465eda | ||
|
|
52e83891b4 | ||
|
|
456a78ada9 | ||
|
|
cd60dbe08c | ||
|
|
a0d932b986 | ||
|
|
10cad60566 | ||
|
|
c6296e5108 | ||
|
|
dd9cbd0ab9 | ||
|
|
865c268312 | ||
|
|
dcac8a773b | ||
|
|
96d75dfc1b | ||
|
|
49428eb7ef | ||
|
|
6c0e3972dc | ||
|
|
458c317736 | ||
|
|
98f68b8b08 | ||
|
|
b76e934140 | ||
|
|
0cdcc2deb6 | ||
|
|
58992d592c | ||
|
|
04e172deac | ||
|
|
9bcb37ea10 | ||
|
|
a363c1f2f1 | ||
|
|
6425e794f6 | ||
|
|
f340178bb7 | ||
|
|
aa6c8b1bfd | ||
|
|
d1ca6b50d9 | ||
|
|
099b1d066b | ||
|
|
9763d885be | ||
|
|
57530d87f6 | ||
|
|
2c030218f8 | ||
|
|
6bb299ae25 | ||
|
|
a0a064f4c8 | ||
|
|
40e1e212d4 | ||
|
|
5558514ff1 | ||
|
|
83c7633acb | ||
|
|
6c3f2f8d38 | ||
|
|
d72653cdea | ||
|
|
6c96361402 | ||
|
|
554e56687c | ||
|
|
77b1edcd0f | ||
|
|
d667faea33 | ||
|
|
4cb2cc62d2 | ||
|
|
698bfcf87f | ||
|
|
edeff20e1d | ||
|
|
eda157e303 | ||
|
|
653a0ff02f | ||
|
|
8175a323e9 | ||
|
|
11850b75c8 | ||
|
|
562e40a7bd | ||
|
|
0e3b7a11e3 | ||
|
|
196f91d68c | ||
|
|
bfbfdae1c5 | ||
|
|
a6c154d839 | ||
|
|
7563619e26 | ||
|
|
1b5c92d66c | ||
|
|
2f23325c90 | ||
|
|
cef676429f | ||
|
|
a0935dec60 | ||
|
|
587a8c5992 | ||
|
|
4ddb7e124e | ||
|
|
68cfdd89c4 | ||
|
|
144c9059a3 | ||
|
|
ee38b3c84d | ||
|
|
86aa2ca6cb | ||
|
|
1d7ca44017 | ||
|
|
174c082c69 | ||
|
|
bbb63104ea | ||
|
|
43832d165f | ||
|
|
d630485051 | ||
|
|
784572590f | ||
|
|
90a03c331d | ||
|
|
b391ad12ae | ||
|
|
6c2eb814ed | ||
|
|
3b0cb7945f | ||
|
|
46a181042b | ||
|
|
5af8d0612a | ||
|
|
d4fabeee3c | ||
|
|
e47d6ce1f6 | ||
|
|
15f4153fda | ||
|
|
1371e66ed9 | ||
|
|
2fbb92aceb | ||
|
|
f96beeee8f | ||
|
|
0c367412c3 | ||
|
|
150152ddbd | ||
|
|
12287f8680 | ||
|
|
047c9fe82c | ||
|
|
ce20f05ef1 | ||
|
|
242311aee1 | ||
|
|
bbc34d9be5 | ||
|
|
09da5e71e2 | ||
|
|
74638bbd6e | ||
|
|
f34fe49bdd | ||
|
|
50089710f3 | ||
|
|
c9f17ecb3a | ||
|
|
4bf38e5287 | ||
|
|
50f6addd6f | ||
|
|
c42cb438f9 | ||
|
|
b93bdfb222 | ||
|
|
c85a2e8cd4 | ||
|
|
46e3208cde | ||
|
|
c6bb0ded5e | ||
|
|
d187b1615a | ||
|
|
5e24e03fe5 | ||
|
|
2fcc4aec4b | ||
|
|
67865fc5ad | ||
|
|
9ad097d265 | ||
|
|
572eefe181 | ||
|
|
cf44209730 | ||
|
|
dfeadf9595 | ||
|
|
25c71d8ac2 | ||
|
|
19d2121252 | ||
|
|
c24d9d3bea | ||
|
|
976c714063 | ||
|
|
f87d5d3d9b | ||
|
|
29f13f34d3 | ||
|
|
7d45d2762f | ||
|
|
e5408995d8 | ||
|
|
d69dfb7c8b | ||
|
|
addee2f248 | ||
|
|
0c021e42f5 | ||
|
|
64e6003627 | ||
|
|
26121c5d88 | ||
|
|
d245b756a3 | ||
|
|
b4b56f9c85 | ||
|
|
81e928030f | ||
|
|
0b62bbb52e | ||
|
|
cb364f0ac7 | ||
|
|
d12a7247bc | ||
|
|
941bb7c603 | ||
|
|
3edc547389 | ||
|
|
427ca4e3f5 | ||
|
|
5eac5c54f8 | ||
|
|
a1fc2f4df0 | ||
|
|
3688955c77 | ||
|
|
6307adfba1 | ||
|
|
4f78acaa5c | ||
|
|
7f405c74cd | ||
|
|
5d6cee0c58 | ||
|
|
af366cf732 | ||
|
|
32cb5f3ceb | ||
|
|
eecc7f3529 | ||
|
|
8962d948cd | ||
|
|
c91a5d8b1f | ||
|
|
77f4ffddc1 | ||
|
|
784a6ec85e | ||
|
|
eb004ccfc2 | ||
|
|
c6f9e74477 | ||
|
|
302bd11b33 | ||
|
|
c19ea89a1d | ||
|
|
9560f9196d | ||
|
|
42bb1ec089 | ||
|
|
c91c95431d | ||
|
|
1d64d31275 | ||
|
|
fdef2abdfb | ||
|
|
fc4e762b05 | ||
|
|
c08631d6ff | ||
|
|
3f973fe77f | ||
|
|
953d05428e | ||
|
|
6dc4b748fb | ||
|
|
fde0139bf7 | ||
|
|
afa591afb2 | ||
|
|
2588da0e27 | ||
|
|
8ce48dc7d1 | ||
|
|
9bea40bd40 | ||
|
|
9a8a48b879 | ||
|
|
1f6739337b | ||
|
|
132d741c55 | ||
|
|
5abe0089cb | ||
|
|
c5948d3e2c | ||
|
|
c004ecdccc | ||
|
|
e3b1cbbb86 | ||
|
|
f6cef312f2 | ||
|
|
75a40dead6 | ||
|
|
29e48b1c1f | ||
|
|
62ab163316 | ||
|
|
384b7e8462 | ||
|
|
50bec32153 |
No files matched your search
+5
-1
@@ -9,4 +9,8 @@ OPENAI_API_KEY=''
|
|||||||
|
|
||||||
# DO NOT TRACK
|
# DO NOT TRACK
|
||||||
SCARF_NO_ANALYTICS=true
|
SCARF_NO_ANALYTICS=true
|
||||||
DO_NOT_TRACK=true
|
DO_NOT_TRACK=true
|
||||||
|
|
||||||
|
# Use locally bundled version of the LiteLLM cost map json
|
||||||
|
# to avoid repetitive startup connections
|
||||||
|
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||||
@@ -24,6 +24,9 @@ assignees: ''
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
|
- **Open WebUI Version:** [e.g., 0.1.120]
|
||||||
|
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
|
||||||
|
|
||||||
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
||||||
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
||||||
|
|
||||||
|
|||||||
@@ -57,3 +57,14 @@ jobs:
|
|||||||
path: .
|
path: .
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Trigger Docker build workflow
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
workflow_id: 'docker-build.yaml',
|
||||||
|
ref: 'v${{ steps.get_version.outputs.version }}',
|
||||||
|
})
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
#
|
name: Create and publish Docker images with specific build args
|
||||||
name: Create and publish a Docker image
|
|
||||||
|
|
||||||
# Configures this workflow to run every time a change is pushed to the branch called `release`.
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -10,30 +9,39 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-main-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
#
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
# Required for multi architecture build
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
# Required for multi architecture build
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -41,12 +49,11 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata for Docker images
|
- name: Extract metadata for Docker images (default latest tag)
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
# This configuration dynamically generates tags based on the branch, tag, commit, and custom suffix for lite version.
|
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
@@ -56,11 +63,322 @@ jobs:
|
|||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ github.ref == 'refs/heads/main' }}
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build Docker image (latest)
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
id: build
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build-cuda-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker images (default latest tag)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=git-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
suffix=-cuda,onlatest=true
|
||||||
|
|
||||||
|
- name: Build Docker image (cuda)
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: USE_CUDA=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build-ollama-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker images (ollama tag)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=git-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
suffix=-ollama,onlatest=true
|
||||||
|
|
||||||
|
- name: Build Docker image (ollama)
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: USE_OLLAMA=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge-main-images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ build-main-image ]
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: digests-main-*
|
||||||
|
path: /tmp/digests
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker images (default latest tag)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=git-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
|
|
||||||
|
merge-cuda-images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ build-cuda-image ]
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: digests-cuda-*
|
||||||
|
path: /tmp/digests
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker images (default latest tag)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=git-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
suffix=-cuda,onlatest=true
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
|
merge-ollama-images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ build-ollama-image ]
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: digests-ollama-*
|
||||||
|
path: /tmp/digests
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker images (default ollama tag)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.FULL_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=git-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||||
|
flavor: |
|
||||||
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
suffix=-ollama,onlatest=true
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
@@ -1,27 +1,39 @@
|
|||||||
name: Python CI
|
name: Python CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: 'Format Backend'
|
name: 'Format Backend'
|
||||||
env:
|
|
||||||
PUBLIC_API_BASE_URL: ''
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
python-version: [3.11]
|
||||||
- latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use Python
|
|
||||||
uses: actions/setup-python@v4
|
- name: Set up Python
|
||||||
- name: Use Bun
|
uses: actions/setup-python@v2
|
||||||
uses: oven-sh/setup-bun@v1
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install yapf
|
pip install black
|
||||||
|
|
||||||
- name: Format backend
|
- name: Format backend
|
||||||
run: bun run format:backend
|
run: npm run format:backend
|
||||||
|
|
||||||
|
- name: Check for changes after format
|
||||||
|
run: git diff --exit-code
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
name: Bun CI
|
name: Frontend Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: 'Format & Build Frontend'
|
name: 'Format & Build Frontend'
|
||||||
env:
|
|
||||||
PUBLIC_API_BASE_URL: ''
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout Repository
|
||||||
- name: Use Bun
|
uses: actions/checkout@v4
|
||||||
uses: oven-sh/setup-bun@v1
|
|
||||||
- run: bun --version
|
- name: Setup Node.js
|
||||||
- name: Install frontend dependencies
|
uses: actions/setup-node@v3
|
||||||
run: bun install
|
with:
|
||||||
- name: Format frontend
|
node-version: '20' # Or specify any other version you want to use
|
||||||
run: bun run format
|
|
||||||
- name: Build frontend
|
- name: Install Dependencies
|
||||||
run: bun run build
|
run: npm install
|
||||||
|
|
||||||
|
- name: Format Frontend
|
||||||
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Check for Changes After Format
|
||||||
|
run: git diff --exit-code
|
||||||
|
|
||||||
|
- name: Build Frontend
|
||||||
|
run: npm run build
|
||||||
+104
@@ -5,6 +5,110 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.121] - 2024-04-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Translation Issues**: Addressed various translation discrepancies.
|
||||||
|
- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
|
||||||
|
- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
|
||||||
|
- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
|
||||||
|
- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
|
||||||
|
- **🏷️ Tag Display**: Corrected tag display inconsistencies.
|
||||||
|
- **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
|
||||||
|
- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
|
||||||
|
|
||||||
|
## [0.1.120] - 2024-04-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
|
||||||
|
- **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
||||||
|
- **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
|
||||||
|
- **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
|
||||||
|
- **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Model Selector**: Resolved issue where default model selection was not saving.
|
||||||
|
- **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
|
||||||
|
- **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
|
||||||
|
|
||||||
|
## [0.1.119] - 2024-04-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
|
||||||
|
- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
|
||||||
|
- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
|
||||||
|
- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
|
||||||
|
- **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
|
||||||
|
- **🌍 Additional Language Support**: Added Polish language support.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
|
||||||
|
- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
|
||||||
|
|
||||||
|
## [0.1.118] - 2024-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
|
||||||
|
- **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
|
||||||
|
- **👤 User Initials Profile Photo**: User initials are now the default profile photo.
|
||||||
|
- **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
|
||||||
|
- **🌍 Additional Language Support**: Added Turkish language support.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
|
||||||
|
- **🛠 Modal Close**: Modals can now be closed using the Esc key.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
|
||||||
|
- **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
|
||||||
|
|
||||||
|
## [0.1.117] - 2024-04-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
|
||||||
|
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
|
||||||
|
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
|
||||||
|
- 📝 **Improved Logging**: Enhancements to logging functionality.
|
||||||
|
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
|
||||||
|
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
|
||||||
|
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
|
||||||
|
- 🔒 **Security Patches**: Applied necessary security patches.
|
||||||
|
|
||||||
|
## [0.1.116] - 2024-03-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
|
||||||
|
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
|
||||||
|
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
|
||||||
|
- **🌐 Localization**: Korean translation (I18n) now available.
|
||||||
|
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
|
||||||
|
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
|
||||||
|
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
|
||||||
|
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
|
||||||
|
- **✅ Spelling**: Corrected various spelling issues for improved readability.
|
||||||
|
|
||||||
## [0.1.115] - 2024-03-24
|
## [0.1.115] - 2024-03-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+87
-45
@@ -1,77 +1,117 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
# Initialize device type args
|
||||||
|
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
|
||||||
|
ARG USE_CUDA=false
|
||||||
|
ARG USE_OLLAMA=false
|
||||||
|
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
||||||
|
ARG USE_CUDA_VER=cu121
|
||||||
|
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
||||||
|
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
||||||
|
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||||
|
# IMPORTANT: If you change the default model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||||
|
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||||
|
|
||||||
FROM node:alpine as build
|
######## WebUI frontend ########
|
||||||
|
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# wget embedding model weight from alpine (does not exist from slim-buster)
|
|
||||||
RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" -O - | \
|
|
||||||
tar -xzf - -C /app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
######## WebUI backend ########
|
||||||
FROM python:3.11-slim-bookworm as base
|
FROM python:3.11-slim-bookworm as base
|
||||||
|
|
||||||
ENV ENV=prod
|
# Use args
|
||||||
ENV PORT ""
|
ARG USE_CUDA
|
||||||
|
ARG USE_OLLAMA
|
||||||
|
ARG USE_CUDA_VER
|
||||||
|
ARG USE_EMBEDDING_MODEL
|
||||||
|
|
||||||
ENV OLLAMA_BASE_URL "/ollama"
|
## Basis ##
|
||||||
|
ENV ENV=prod \
|
||||||
|
PORT=8080 \
|
||||||
|
# pass build args to the build
|
||||||
|
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
||||||
|
USE_CUDA_DOCKER=${USE_CUDA} \
|
||||||
|
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
||||||
|
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
|
||||||
|
|
||||||
ENV OPENAI_API_BASE_URL ""
|
## Basis URL Config ##
|
||||||
ENV OPENAI_API_KEY ""
|
ENV OLLAMA_BASE_URL="/ollama" \
|
||||||
|
OPENAI_API_BASE_URL=""
|
||||||
|
|
||||||
ENV WEBUI_SECRET_KEY ""
|
## API Key and Security Config ##
|
||||||
|
ENV OPENAI_API_KEY="" \
|
||||||
|
WEBUI_SECRET_KEY="" \
|
||||||
|
SCARF_NO_ANALYTICS=true \
|
||||||
|
DO_NOT_TRACK=true
|
||||||
|
|
||||||
ENV SCARF_NO_ANALYTICS true
|
# Use locally bundled version of the LiteLLM cost map json
|
||||||
ENV DO_NOT_TRACK true
|
# to avoid repetitive startup connections
|
||||||
|
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
|
||||||
|
|
||||||
######## Preloaded models ########
|
|
||||||
# whisper TTS Settings
|
|
||||||
ENV WHISPER_MODEL="base"
|
|
||||||
ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
|
||||||
|
|
||||||
# RAG Embedding Model Settings
|
#### Other models #########################################################
|
||||||
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
## whisper TTS model settings ##
|
||||||
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
ENV WHISPER_MODEL="base" \
|
||||||
# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
||||||
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
|
||||||
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
|
|
||||||
# device type for whisper tts and embbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
|
||||||
ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu"
|
|
||||||
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
|
|
||||||
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
|
|
||||||
|
|
||||||
######## Preloaded models ########
|
## RAG Embedding model settings ##
|
||||||
|
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||||
|
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
|
||||||
|
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
||||||
|
#### Other models ##########################################################
|
||||||
|
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
|
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||||
|
apt-get update && \
|
||||||
|
# Install pandoc and netcat
|
||||||
|
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
|
||||||
|
# for RAG OCR
|
||||||
|
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||||
|
# install helper tools
|
||||||
|
apt-get install -y --no-install-recommends curl && \
|
||||||
|
# install ollama
|
||||||
|
curl -fsSL https://ollama.com/install.sh | sh && \
|
||||||
|
# cleanup
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
else \
|
||||||
|
apt-get update && \
|
||||||
|
# Install pandoc and netcat
|
||||||
|
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
|
||||||
|
# for RAG OCR
|
||||||
|
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||||
|
# cleanup
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
# install python dependencies
|
# install python dependencies
|
||||||
COPY ./backend/requirements.txt ./requirements.txt
|
COPY ./backend/requirements.txt ./requirements.txt
|
||||||
|
|
||||||
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
|
RUN pip3 install uv && \
|
||||||
|
if [ "$USE_CUDA" = "true" ]; then \
|
||||||
|
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||||
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||||
|
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||||
|
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||||
|
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||||
|
else \
|
||||||
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||||
|
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||||
|
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||||
|
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
|
|
||||||
RUN pip3 install -r requirements.txt --no-cache-dir
|
|
||||||
|
|
||||||
# Install pandoc and netcat
|
|
||||||
# RUN python -c "import pypandoc; pypandoc.download_pandoc()"
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y pandoc netcat-openbsd \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# preload embedding model
|
|
||||||
RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])"
|
|
||||||
# preload tts model
|
|
||||||
RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"
|
|
||||||
|
|
||||||
# copy embedding weight from build
|
# copy embedding weight from build
|
||||||
RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
||||||
COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
||||||
|
|
||||||
# copy built frontend files
|
# copy built frontend files
|
||||||
COPY --from=build /app/build /app/build
|
COPY --from=build /app/build /app/build
|
||||||
@@ -81,4 +121,6 @@ COPY --from=build /app/package.json /app/package.json
|
|||||||
# copy backend files
|
# copy backend files
|
||||||
COPY ./backend .
|
COPY ./backend .
|
||||||
|
|
||||||
CMD [ "bash", "start.sh"]
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD [ "bash", "start.sh"]
|
||||||
@@ -8,6 +8,8 @@ remove:
|
|||||||
|
|
||||||
start:
|
start:
|
||||||
@docker-compose start
|
@docker-compose start
|
||||||
|
startAndBuild:
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@docker-compose stop
|
@docker-compose stop
|
||||||
|
|||||||
@@ -25,22 +25,28 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
||||||
|
|
||||||
|
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
|
||||||
|
|
||||||
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
||||||
|
|
||||||
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
||||||
|
|
||||||
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
||||||
|
|
||||||
|
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
|
||||||
|
|
||||||
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
||||||
|
|
||||||
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
||||||
|
|
||||||
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
||||||
|
|
||||||
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
||||||
|
|
||||||
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
||||||
|
|
||||||
|
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
|
||||||
|
|
||||||
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
||||||
|
|
||||||
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
||||||
@@ -53,28 +59,42 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
|||||||
|
|
||||||
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
||||||
|
|
||||||
|
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
|
||||||
|
|
||||||
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
||||||
|
|
||||||
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
||||||
|
|
||||||
|
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
|
||||||
|
|
||||||
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
||||||
|
|
||||||
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
||||||
|
|
||||||
|
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
||||||
|
|
||||||
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
||||||
|
|
||||||
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using AUTOMATIC1111 API (local) and DALL-E, enriching your chat experience with dynamic visual content.
|
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
|
||||||
|
|
||||||
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
||||||
|
|
||||||
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
||||||
|
|
||||||
|
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
|
||||||
|
|
||||||
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
||||||
|
|
||||||
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
||||||
|
|
||||||
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
||||||
|
|
||||||
|
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
|
||||||
|
|
||||||
|
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
|
||||||
|
|
||||||
|
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
|
||||||
|
|
||||||
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
||||||
|
|
||||||
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
||||||
@@ -94,24 +114,27 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
|||||||
|
|
||||||
### Quick Start with Docker 🐳
|
### Quick Start with Docker 🐳
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!WARNING]
|
||||||
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
||||||
|
|
||||||
- **If Ollama is on your computer**, use this command:
|
> [!TIP]
|
||||||
|
> If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system.
|
||||||
|
|
||||||
```bash
|
**If Ollama is on your computer**, use this command:
|
||||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
|
||||||
```
|
|
||||||
|
|
||||||
- **If Ollama is on a Different Server**, use this command:
|
```bash
|
||||||
|
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||||
|
```
|
||||||
|
|
||||||
- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
**If Ollama is on a Different Server**, use this command:
|
||||||
|
|
||||||
```bash
|
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
||||||
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
|
||||||
```
|
|
||||||
|
|
||||||
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
```bash
|
||||||
|
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
||||||
|
|
||||||
#### Open WebUI: Server Connection Error
|
#### Open WebUI: Server Connection Error
|
||||||
|
|
||||||
@@ -182,4 +205,4 @@ If you have any questions, suggestions, or need assistance, please open an issue
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open Web UI even more amazing together! 💪
|
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
||||||
+137
-8
@@ -10,8 +10,19 @@ from fastapi import (
|
|||||||
File,
|
File,
|
||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
@@ -22,7 +33,17 @@ from utils.utils import (
|
|||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
CACHE_DIR,
|
||||||
|
UPLOAD_DIR,
|
||||||
|
WHISPER_MODEL,
|
||||||
|
WHISPER_MODEL_DIR,
|
||||||
|
WHISPER_MODEL_AUTO_UPDATE,
|
||||||
|
DEVICE_TYPE,
|
||||||
|
AUDIO_OPENAI_API_BASE_URL,
|
||||||
|
AUDIO_OPENAI_API_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||||
@@ -37,7 +58,103 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/transcribe")
|
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
||||||
|
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
||||||
|
|
||||||
|
# setting device type for whisper model
|
||||||
|
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||||
|
log.info(f"whisper_device_type: {whisper_device_type}")
|
||||||
|
|
||||||
|
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||||
|
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIConfigUpdateForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config")
|
||||||
|
async def get_openai_config(user=Depends(get_admin_user)):
|
||||||
|
return {
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/config/update")
|
||||||
|
async def update_openai_config(
|
||||||
|
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
if form_data.key == "":
|
||||||
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URL = form_data.url
|
||||||
|
app.state.OPENAI_API_KEY = form_data.key
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/speech")
|
||||||
|
async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
|
body = await request.body()
|
||||||
|
name = hashlib.sha256(body).hexdigest()
|
||||||
|
|
||||||
|
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
||||||
|
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
||||||
|
|
||||||
|
# Check if the file already exists in the cache
|
||||||
|
if file_path.is_file():
|
||||||
|
return FileResponse(file_path)
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
|
||||||
|
data=body,
|
||||||
|
headers=headers,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Save the streaming content to a file
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump(json.loads(body.decode("utf-8")), f)
|
||||||
|
|
||||||
|
# Return the saved file
|
||||||
|
return FileResponse(file_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']['message']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=r.status_code if r != None else 500,
|
||||||
|
detail=error_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/transcriptions")
|
||||||
def transcribe(
|
def transcribe(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
@@ -58,12 +175,24 @@ def transcribe(
|
|||||||
f.write(contents)
|
f.write(contents)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
model = WhisperModel(
|
whisper_kwargs = {
|
||||||
WHISPER_MODEL,
|
"model_size_or_path": WHISPER_MODEL,
|
||||||
device="auto",
|
"device": whisper_device_type,
|
||||||
compute_type="int8",
|
"compute_type": "int8",
|
||||||
download_root=WHISPER_MODEL_DIR,
|
"download_root": WHISPER_MODEL_DIR,
|
||||||
)
|
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = WhisperModel(**whisper_kwargs)
|
||||||
|
except:
|
||||||
|
log.warning(
|
||||||
|
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||||
|
)
|
||||||
|
whisper_kwargs["local_files_only"] = False
|
||||||
|
model = WhisperModel(**whisper_kwargs)
|
||||||
|
|
||||||
segments, info = model.transcribe(file_path, beam_size=5)
|
segments, info = model.transcribe(file_path, beam_size=5)
|
||||||
log.info(
|
log.info(
|
||||||
|
|||||||
+32
-16
@@ -29,7 +29,15 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
CACHE_DIR,
|
||||||
|
ENABLE_IMAGE_GENERATION,
|
||||||
|
AUTOMATIC1111_BASE_URL,
|
||||||
|
COMFYUI_BASE_URL,
|
||||||
|
IMAGES_OPENAI_API_BASE_URL,
|
||||||
|
IMAGES_OPENAI_API_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -48,9 +56,11 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.state.ENGINE = ""
|
app.state.ENGINE = ""
|
||||||
app.state.ENABLED = False
|
app.state.ENABLED = ENABLE_IMAGE_GENERATION
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||||
|
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||||
|
|
||||||
app.state.OPENAI_API_KEY = ""
|
|
||||||
app.state.MODEL = ""
|
app.state.MODEL = ""
|
||||||
|
|
||||||
|
|
||||||
@@ -125,27 +135,33 @@ async def update_engine_url(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIKeyUpdateForm(BaseModel):
|
class OpenAIConfigUpdateForm(BaseModel):
|
||||||
|
url: str
|
||||||
key: str
|
key: str
|
||||||
|
|
||||||
|
|
||||||
@app.get("/key")
|
@app.get("/openai/config")
|
||||||
async def get_openai_key(user=Depends(get_admin_user)):
|
async def get_openai_config(user=Depends(get_admin_user)):
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
return {
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/key/update")
|
@app.post("/openai/config/update")
|
||||||
async def update_openai_key(
|
async def update_openai_config(
|
||||||
form_data: OpenAIKeyUpdateForm, user=Depends(get_admin_user)
|
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
if form_data.key == "":
|
if form_data.key == "":
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URL = form_data.url
|
||||||
app.state.OPENAI_API_KEY = form_data.key
|
app.state.OPENAI_API_KEY = form_data.key
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
|
||||||
"status": True,
|
"status": True,
|
||||||
|
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -325,7 +341,7 @@ def save_url_image(url):
|
|||||||
|
|
||||||
return image_id
|
return image_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving image: {e}")
|
log.exception(f"Error saving image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -354,7 +370,7 @@ def generate_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"https://api.openai.com/v1/images/generations",
|
url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
|
||||||
json=data,
|
json=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
@@ -397,7 +413,7 @@ def generate_image(
|
|||||||
user.id,
|
user.id,
|
||||||
app.state.COMFYUI_BASE_URL,
|
app.state.COMFYUI_BASE_URL,
|
||||||
)
|
)
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
|
|
||||||
@@ -409,7 +425,7 @@ def generate_image(
|
|||||||
with open(file_body_path, "w") as f:
|
with open(file_body_path, "w") as f:
|
||||||
json.dump(data.model_dump(exclude_none=True), f)
|
json.dump(data.model_dump(exclude_none=True), f)
|
||||||
|
|
||||||
print(images)
|
log.debug(f"images: {images}")
|
||||||
return images
|
return images
|
||||||
else:
|
else:
|
||||||
if form_data.model:
|
if form_data.model:
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import json
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import random
|
import random
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -121,7 +127,7 @@ COMFYUI_DEFAULT_PROMPT = """
|
|||||||
|
|
||||||
|
|
||||||
def queue_prompt(prompt, client_id, base_url):
|
def queue_prompt(prompt, client_id, base_url):
|
||||||
print("queue_prompt")
|
log.info("queue_prompt")
|
||||||
p = {"prompt": prompt, "client_id": client_id}
|
p = {"prompt": prompt, "client_id": client_id}
|
||||||
data = json.dumps(p).encode("utf-8")
|
data = json.dumps(p).encode("utf-8")
|
||||||
req = urllib.request.Request(f"{base_url}/prompt", data=data)
|
req = urllib.request.Request(f"{base_url}/prompt", data=data)
|
||||||
@@ -129,7 +135,7 @@ def queue_prompt(prompt, client_id, base_url):
|
|||||||
|
|
||||||
|
|
||||||
def get_image(filename, subfolder, folder_type, base_url):
|
def get_image(filename, subfolder, folder_type, base_url):
|
||||||
print("get_image")
|
log.info("get_image")
|
||||||
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
url_values = urllib.parse.urlencode(data)
|
url_values = urllib.parse.urlencode(data)
|
||||||
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
|
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
|
||||||
@@ -137,14 +143,14 @@ def get_image(filename, subfolder, folder_type, base_url):
|
|||||||
|
|
||||||
|
|
||||||
def get_image_url(filename, subfolder, folder_type, base_url):
|
def get_image_url(filename, subfolder, folder_type, base_url):
|
||||||
print("get_image")
|
log.info("get_image")
|
||||||
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
url_values = urllib.parse.urlencode(data)
|
url_values = urllib.parse.urlencode(data)
|
||||||
return f"{base_url}/view?{url_values}"
|
return f"{base_url}/view?{url_values}"
|
||||||
|
|
||||||
|
|
||||||
def get_history(prompt_id, base_url):
|
def get_history(prompt_id, base_url):
|
||||||
print("get_history")
|
log.info("get_history")
|
||||||
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
|
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
|
||||||
return json.loads(response.read())
|
return json.loads(response.read())
|
||||||
|
|
||||||
@@ -189,7 +195,7 @@ class ImageGenerationPayload(BaseModel):
|
|||||||
def comfyui_generate_image(
|
def comfyui_generate_image(
|
||||||
model: str, payload: ImageGenerationPayload, client_id, base_url
|
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||||
):
|
):
|
||||||
host = base_url.replace("http://", "").replace("https://", "")
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
|
||||||
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||||
|
|
||||||
@@ -211,16 +217,16 @@ def comfyui_generate_image(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ws = websocket.WebSocket()
|
ws = websocket.WebSocket()
|
||||||
ws.connect(f"ws://{host}/ws?clientId={client_id}")
|
ws.connect(f"{ws_url}/ws?clientId={client_id}")
|
||||||
print("WebSocket connection established.")
|
log.info("WebSocket connection established.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to connect to WebSocket server: {e}")
|
log.exception(f"Failed to connect to WebSocket server: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error while receiving images: {e}")
|
log.exception(f"Error while receiving images: {e}")
|
||||||
images = None
|
images = None
|
||||||
|
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|||||||
+313
-62
@@ -1,17 +1,25 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import ProxyConfig, initialize
|
|
||||||
from litellm.proxy.proxy_server import app
|
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status, Response
|
from fastapi import FastAPI, Request, Depends, status, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
from starlette.responses import StreamingResponse
|
from starlette.responses import StreamingResponse
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
from utils.utils import get_http_authorization_cred, get_current_user
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from utils.utils import get_verified_user, get_current_user, get_admin_user
|
||||||
from config import SRC_LOG_LEVELS, ENV
|
from config import SRC_LOG_LEVELS, ENV
|
||||||
|
from constants import MESSAGES
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
||||||
@@ -20,81 +28,324 @@ log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
|||||||
from config import (
|
from config import (
|
||||||
MODEL_FILTER_ENABLED,
|
MODEL_FILTER_ENABLED,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
|
DATA_DIR,
|
||||||
|
LITELLM_PROXY_PORT,
|
||||||
|
LITELLM_PROXY_HOST,
|
||||||
|
)
|
||||||
|
|
||||||
|
from litellm.utils import get_llm_provider
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
origins = ["*"]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "r") as file:
|
||||||
|
litellm_config = yaml.safe_load(file)
|
||||||
|
|
||||||
|
app.state.CONFIG = litellm_config
|
||||||
|
|
||||||
|
# Global variable to store the subprocess reference
|
||||||
|
background_process = None
|
||||||
|
|
||||||
|
|
||||||
async def config():
|
async def run_background_process(command):
|
||||||
router, model_list, general_settings = await proxy_config.load_config(
|
global background_process
|
||||||
router=None, config_file_path="./data/litellm/config.yaml"
|
log.info("run_background_process")
|
||||||
)
|
|
||||||
|
|
||||||
await initialize(config="./data/litellm/config.yaml", telemetry=False)
|
try:
|
||||||
|
# Log the command to be executed
|
||||||
|
log.info(f"Executing command: {command}")
|
||||||
|
# Execute the command and create a subprocess
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
background_process = process
|
||||||
|
log.info("Subprocess started successfully.")
|
||||||
|
|
||||||
|
# Capture STDERR for debugging purposes
|
||||||
|
stderr_output = await process.stderr.read()
|
||||||
|
stderr_text = stderr_output.decode().strip()
|
||||||
|
if stderr_text:
|
||||||
|
log.info(f"Subprocess STDERR: {stderr_text}")
|
||||||
|
|
||||||
|
# log.info output line by line
|
||||||
|
async for line in process.stdout:
|
||||||
|
log.info(line.decode().strip())
|
||||||
|
|
||||||
|
# Wait for the process to finish
|
||||||
|
returncode = await process.wait()
|
||||||
|
log.info(f"Subprocess exited with return code {returncode}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to start subprocess: {e}")
|
||||||
|
raise # Optionally re-raise the exception if you want it to propagate
|
||||||
|
|
||||||
|
|
||||||
async def startup():
|
async def start_litellm_background():
|
||||||
await config()
|
log.info("start_litellm_background")
|
||||||
|
# Command to run in the background
|
||||||
|
command = [
|
||||||
|
"litellm",
|
||||||
|
"--port",
|
||||||
|
str(LITELLM_PROXY_PORT),
|
||||||
|
"--host",
|
||||||
|
LITELLM_PROXY_HOST,
|
||||||
|
"--telemetry",
|
||||||
|
"False",
|
||||||
|
"--config",
|
||||||
|
LITELLM_CONFIG_DIR,
|
||||||
|
]
|
||||||
|
|
||||||
|
await run_background_process(command)
|
||||||
|
|
||||||
|
|
||||||
|
async def shutdown_litellm_background():
|
||||||
|
log.info("shutdown_litellm_background")
|
||||||
|
global background_process
|
||||||
|
if background_process:
|
||||||
|
background_process.terminate()
|
||||||
|
await background_process.wait() # Ensure the process has terminated
|
||||||
|
log.info("Subprocess terminated")
|
||||||
|
background_process = None
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def on_startup():
|
async def startup_event():
|
||||||
await startup()
|
log.info("startup_event")
|
||||||
|
# TODO: Check config.yaml file and create one
|
||||||
|
asyncio.create_task(start_litellm_background())
|
||||||
|
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.get("/")
|
||||||
async def auth_middleware(request: Request, call_next):
|
async def get_status():
|
||||||
auth_header = request.headers.get("Authorization", "")
|
return {"status": True}
|
||||||
request.state.user = None
|
|
||||||
|
|
||||||
|
async def restart_litellm():
|
||||||
|
"""
|
||||||
|
Endpoint to restart the litellm background service.
|
||||||
|
"""
|
||||||
|
log.info("Requested restart of litellm service.")
|
||||||
|
try:
|
||||||
|
# Shut down the existing process if it is running
|
||||||
|
await shutdown_litellm_background()
|
||||||
|
log.info("litellm service shutdown complete.")
|
||||||
|
|
||||||
|
# Restart the background service
|
||||||
|
|
||||||
|
asyncio.create_task(start_litellm_background())
|
||||||
|
log.info("litellm service restart complete.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "litellm service restarted successfully.",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.info(f"Error restarting litellm service: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/restart")
|
||||||
|
async def restart_litellm_handler(user=Depends(get_admin_user)):
|
||||||
|
return await restart_litellm()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config")
|
||||||
|
async def get_config(user=Depends(get_admin_user)):
|
||||||
|
return app.state.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class LiteLLMConfigForm(BaseModel):
|
||||||
|
general_settings: Optional[dict] = None
|
||||||
|
litellm_settings: Optional[dict] = None
|
||||||
|
model_list: Optional[List[dict]] = None
|
||||||
|
router_settings: Optional[dict] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/config/update")
|
||||||
|
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.CONFIG = form_data.model_dump(exclude_none=True)
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
return app.state.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/models")
|
||||||
|
@app.get("/v1/models")
|
||||||
|
async def get_models(user=Depends(get_current_user)):
|
||||||
|
while not background_process:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
|
||||||
|
r = None
|
||||||
|
try:
|
||||||
|
r = requests.request(method="GET", url=f"{url}/models")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if app.state.MODEL_FILTER_ENABLED:
|
||||||
|
if user and user.role == "user":
|
||||||
|
data["data"] = list(
|
||||||
|
filter(
|
||||||
|
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
||||||
|
data["data"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": model["model_name"],
|
||||||
|
"object": "model",
|
||||||
|
"created": int(time.time()),
|
||||||
|
"owned_by": "openai",
|
||||||
|
}
|
||||||
|
for model in app.state.CONFIG["model_list"]
|
||||||
|
],
|
||||||
|
"object": "list",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/model/info")
|
||||||
|
async def get_model_list(user=Depends(get_admin_user)):
|
||||||
|
return {"data": app.state.CONFIG["model_list"]}
|
||||||
|
|
||||||
|
|
||||||
|
class AddLiteLLMModelForm(BaseModel):
|
||||||
|
model_name: str
|
||||||
|
litellm_params: dict
|
||||||
|
|
||||||
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/model/new")
|
||||||
|
async def add_model_to_config(
|
||||||
|
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
get_llm_provider(model=form_data.model_name)
|
||||||
|
app.state.CONFIG["model_list"].append(form_data.model_dump())
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
|
||||||
|
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLiteLLMModelForm(BaseModel):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/model/delete")
|
||||||
|
async def delete_model_from_config(
|
||||||
|
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
app.state.CONFIG["model_list"] = [
|
||||||
|
model
|
||||||
|
for model in app.state.CONFIG["model_list"]
|
||||||
|
if model["model_name"] != form_data.id
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(LITELLM_CONFIG_DIR, "w") as file:
|
||||||
|
yaml.dump(app.state.CONFIG, file)
|
||||||
|
|
||||||
|
await restart_litellm()
|
||||||
|
|
||||||
|
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
|
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
|
body = await request.body()
|
||||||
|
|
||||||
|
url = f"http://localhost:{LITELLM_PROXY_PORT}"
|
||||||
|
|
||||||
|
target_url = f"{url}/{path}"
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
# headers["Authorization"] = f"Bearer {key}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
r = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = get_current_user(get_http_authorization_cred(auth_header))
|
r = requests.request(
|
||||||
log.debug(f"user: {user}")
|
method=request.method,
|
||||||
request.state.user = user
|
url=target_url,
|
||||||
|
data=body,
|
||||||
|
headers=headers,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Check if response is SSE
|
||||||
|
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||||
|
return StreamingResponse(
|
||||||
|
r.iter_content(chunk_size=8192),
|
||||||
|
status_code=r.status_code,
|
||||||
|
headers=dict(r.headers),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response_data = r.json()
|
||||||
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(status_code=400, content={"detail": str(e)})
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
response = await call_next(request)
|
raise HTTPException(
|
||||||
return response
|
status_code=r.status_code if r else 500, detail=error_detail
|
||||||
|
)
|
||||||
|
|
||||||
class ModifyModelsResponseMiddleware(BaseHTTPMiddleware):
|
|
||||||
async def dispatch(
|
|
||||||
self, request: Request, call_next: RequestResponseEndpoint
|
|
||||||
) -> Response:
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
user = request.state.user
|
|
||||||
|
|
||||||
if "/models" in request.url.path:
|
|
||||||
if isinstance(response, StreamingResponse):
|
|
||||||
# Read the content of the streaming response
|
|
||||||
body = b""
|
|
||||||
async for chunk in response.body_iterator:
|
|
||||||
body += chunk
|
|
||||||
|
|
||||||
data = json.loads(body.decode("utf-8"))
|
|
||||||
|
|
||||||
if app.state.MODEL_FILTER_ENABLED:
|
|
||||||
if user and user.role == "user":
|
|
||||||
data["data"] = list(
|
|
||||||
filter(
|
|
||||||
lambda model: model["id"]
|
|
||||||
in app.state.MODEL_FILTER_LIST,
|
|
||||||
data["data"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Modified Flag
|
|
||||||
data["modified"] = True
|
|
||||||
return JSONResponse(content=data)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(ModifyModelsResponseMiddleware)
|
|
||||||
+113
-13
@@ -33,7 +33,13 @@ from constants import ERROR_MESSAGES
|
|||||||
from utils.utils import decode_token, get_current_user, get_admin_user
|
from utils.utils import decode_token, get_current_user, get_admin_user
|
||||||
|
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
OLLAMA_BASE_URLS,
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
UPLOAD_DIR,
|
||||||
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -75,6 +81,12 @@ async def check_url(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.head("/")
|
||||||
|
@app.get("/")
|
||||||
|
async def get_status():
|
||||||
|
return {"status": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/urls")
|
@app.get("/urls")
|
||||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||||
@@ -203,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
|||||||
|
|
||||||
if len(responses) > 0:
|
if len(responses) > 0:
|
||||||
lowest_version = min(
|
lowest_version = min(
|
||||||
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
|
responses,
|
||||||
|
key=lambda x: tuple(map(int, x["version"].split("-")[0].split("."))),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"version": lowest_version["version"]}
|
return {"version": lowest_version["version"]}
|
||||||
@@ -266,7 +279,7 @@ async def pull_model(
|
|||||||
if request_id in REQUEST_POOL:
|
if request_id in REQUEST_POOL:
|
||||||
yield chunk
|
yield chunk
|
||||||
else:
|
else:
|
||||||
print("User: canceled request")
|
log.warning("User: canceled request")
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
if hasattr(r, "close"):
|
if hasattr(r, "close"):
|
||||||
@@ -599,8 +612,13 @@ async def generate_embeddings(
|
|||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
if form_data.model in app.state.MODELS:
|
model = form_data.model
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
|
||||||
|
if ":" not in model:
|
||||||
|
model = f"{model}:latest"
|
||||||
|
|
||||||
|
if model in app.state.MODELS:
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -636,6 +654,60 @@ async def generate_embeddings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ollama_embeddings(
|
||||||
|
form_data: GenerateEmbeddingsForm,
|
||||||
|
url_idx: Optional[int] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
log.info(f"generate_ollama_embeddings {form_data}")
|
||||||
|
|
||||||
|
if url_idx == None:
|
||||||
|
model = form_data.model
|
||||||
|
|
||||||
|
if ":" not in model:
|
||||||
|
model = f"{model}:latest"
|
||||||
|
|
||||||
|
if model in app.state.MODELS:
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||||
|
)
|
||||||
|
|
||||||
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
|
log.info(f"url: {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.request(
|
||||||
|
method="POST",
|
||||||
|
url=f"{url}/api/embeddings",
|
||||||
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
log.info(f"generate_ollama_embeddings {data}")
|
||||||
|
|
||||||
|
if "embedding" in data:
|
||||||
|
return data["embedding"]
|
||||||
|
else:
|
||||||
|
raise "Something went wrong :/"
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
|
if r is not None:
|
||||||
|
try:
|
||||||
|
res = r.json()
|
||||||
|
if "error" in res:
|
||||||
|
error_detail = f"Ollama: {res['error']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"Ollama: {e}"
|
||||||
|
|
||||||
|
raise error_detail
|
||||||
|
|
||||||
|
|
||||||
class GenerateCompletionForm(BaseModel):
|
class GenerateCompletionForm(BaseModel):
|
||||||
model: str
|
model: str
|
||||||
prompt: str
|
prompt: str
|
||||||
@@ -659,12 +731,17 @@ async def generate_completion(
|
|||||||
):
|
):
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
if form_data.model in app.state.MODELS:
|
model = form_data.model
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
|
||||||
|
if ":" not in model:
|
||||||
|
model = f"{model}:latest"
|
||||||
|
|
||||||
|
if model in app.state.MODELS:
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="error_detail",
|
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||||
)
|
)
|
||||||
|
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
@@ -757,8 +834,13 @@ async def generate_chat_completion(
|
|||||||
):
|
):
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
if form_data.model in app.state.MODELS:
|
model = form_data.model
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
|
||||||
|
if ":" not in model:
|
||||||
|
model = f"{model}:latest"
|
||||||
|
|
||||||
|
if model in app.state.MODELS:
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -770,7 +852,11 @@ async def generate_chat_completion(
|
|||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
log.debug("form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(form_data.model_dump_json(exclude_none=True).encode()))
|
log.debug(
|
||||||
|
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
||||||
|
form_data.model_dump_json(exclude_none=True).encode()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_request():
|
def get_request():
|
||||||
nonlocal form_data
|
nonlocal form_data
|
||||||
@@ -857,8 +943,13 @@ async def generate_openai_chat_completion(
|
|||||||
):
|
):
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
if form_data.model in app.state.MODELS:
|
model = form_data.model
|
||||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
|
||||||
|
if ":" not in model:
|
||||||
|
model = f"{model}:latest"
|
||||||
|
|
||||||
|
if model in app.state.MODELS:
|
||||||
|
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -1019,6 +1110,14 @@ async def download_model(
|
|||||||
url_idx: Optional[int] = None,
|
url_idx: Optional[int] = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
||||||
|
|
||||||
|
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
||||||
|
)
|
||||||
|
|
||||||
if url_idx == None:
|
if url_idx == None:
|
||||||
url_idx = 0
|
url_idx = 0
|
||||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||||
@@ -1027,6 +1126,7 @@ async def download_model(
|
|||||||
|
|
||||||
if file_name:
|
if file_name:
|
||||||
file_path = f"{UPLOAD_DIR}/{file_name}"
|
file_path = f"{UPLOAD_DIR}/{file_name}"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
download_file_stream(url, form_data.url, file_path, file_name),
|
download_file_stream(url, form_data.url, file_path, file_name),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ async def get_openai_urls(user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@app.post("/urls/update")
|
@app.post("/urls/update")
|
||||||
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
await get_all_models()
|
||||||
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
||||||
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
|
|
||||||
@@ -341,7 +342,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
try:
|
try:
|
||||||
res = r.json()
|
res = r.json()
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
error_detail = f"External: {res['error']}"
|
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
except:
|
except:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
|||||||
+243
-97
@@ -8,19 +8,19 @@ from fastapi import (
|
|||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil, logging
|
import os, shutil, logging, re
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sentence_transformers import SentenceTransformer
|
from chromadb.utils.batch_utils import create_batches
|
||||||
from chromadb.utils import embedding_functions
|
|
||||||
|
|
||||||
from langchain_community.document_loaders import (
|
from langchain_community.document_loaders import (
|
||||||
WebBaseLoader,
|
WebBaseLoader,
|
||||||
TextLoader,
|
TextLoader,
|
||||||
PyPDFLoader,
|
PyPDFLoader,
|
||||||
CSVLoader,
|
CSVLoader,
|
||||||
|
BSHTMLLoader,
|
||||||
Docx2txtLoader,
|
Docx2txtLoader,
|
||||||
UnstructuredEPubLoader,
|
UnstructuredEPubLoader,
|
||||||
UnstructuredWordDocumentLoader,
|
UnstructuredWordDocumentLoader,
|
||||||
@@ -37,6 +37,9 @@ import mimetypes
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import sentence_transformers
|
||||||
|
|
||||||
|
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
|
||||||
|
|
||||||
from apps.web.models.documents import (
|
from apps.web.models.documents import (
|
||||||
Documents,
|
Documents,
|
||||||
@@ -44,7 +47,11 @@ from apps.web.models.documents import (
|
|||||||
DocumentResponse,
|
DocumentResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
from apps.rag.utils import query_doc, query_collection
|
from apps.rag.utils import (
|
||||||
|
query_embeddings_doc,
|
||||||
|
query_embeddings_collection,
|
||||||
|
generate_openai_embeddings,
|
||||||
|
)
|
||||||
|
|
||||||
from utils.misc import (
|
from utils.misc import (
|
||||||
calculate_sha256,
|
calculate_sha256,
|
||||||
@@ -57,8 +64,12 @@ from config import (
|
|||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
|
RAG_EMBEDDING_ENGINE,
|
||||||
RAG_EMBEDDING_MODEL,
|
RAG_EMBEDDING_MODEL,
|
||||||
RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
|
RAG_OPENAI_API_BASE_URL,
|
||||||
|
RAG_OPENAI_API_KEY,
|
||||||
|
DEVICE_TYPE,
|
||||||
CHROMA_CLIENT,
|
CHROMA_CLIENT,
|
||||||
CHUNK_SIZE,
|
CHUNK_SIZE,
|
||||||
CHUNK_OVERLAP,
|
CHUNK_OVERLAP,
|
||||||
@@ -70,30 +81,29 @@ from constants import ERROR_MESSAGES
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
#
|
|
||||||
# if RAG_EMBEDDING_MODEL:
|
|
||||||
# sentence_transformer_ef = SentenceTransformer(
|
|
||||||
# model_name_or_path=RAG_EMBEDDING_MODEL,
|
|
||||||
# cache_folder=RAG_EMBEDDING_MODEL_DIR,
|
|
||||||
# device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
app.state.PDF_EXTRACT_IMAGES = False
|
|
||||||
|
app.state.TOP_K = 4
|
||||||
app.state.CHUNK_SIZE = CHUNK_SIZE
|
app.state.CHUNK_SIZE = CHUNK_SIZE
|
||||||
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||||
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
|
||||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
|
||||||
app.state.TOP_K = 4
|
|
||||||
|
|
||||||
app.state.sentence_transformer_ef = (
|
|
||||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||||
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||||
|
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||||
|
|
||||||
|
app.state.PDF_EXTRACT_IMAGES = False
|
||||||
|
|
||||||
|
if app.state.RAG_EMBEDDING_ENGINE == "":
|
||||||
|
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||||
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
device=DEVICE_TYPE,
|
||||||
|
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
@@ -114,6 +124,7 @@ class CollectionNameForm(BaseModel):
|
|||||||
class StoreWebForm(CollectionNameForm):
|
class StoreWebForm(CollectionNameForm):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
return {
|
return {
|
||||||
@@ -121,38 +132,77 @@ async def get_status():
|
|||||||
"chunk_size": app.state.CHUNK_SIZE,
|
"chunk_size": app.state.CHUNK_SIZE,
|
||||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||||
"template": app.state.RAG_TEMPLATE,
|
"template": app.state.RAG_TEMPLATE,
|
||||||
|
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/embedding/model")
|
@app.get("/embedding")
|
||||||
async def get_embedding_model(user=Depends(get_admin_user)):
|
async def get_embedding_config(user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
|
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
"openai_config": {
|
||||||
|
"url": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"key": app.state.OPENAI_API_KEY,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIConfigForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModelUpdateForm(BaseModel):
|
class EmbeddingModelUpdateForm(BaseModel):
|
||||||
|
openai_config: Optional[OpenAIConfigForm] = None
|
||||||
|
embedding_engine: str
|
||||||
embedding_model: str
|
embedding_model: str
|
||||||
|
|
||||||
|
|
||||||
@app.post("/embedding/model/update")
|
@app.post("/embedding/update")
|
||||||
async def update_embedding_model(
|
async def update_embedding_config(
|
||||||
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
log.info(
|
||||||
app.state.sentence_transformer_ef = (
|
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
|
||||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
|
||||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
|
||||||
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
||||||
|
|
||||||
return {
|
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||||
"status": True,
|
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
app.state.sentence_transformer_ef = None
|
||||||
}
|
|
||||||
|
if form_data.openai_config != None:
|
||||||
|
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||||
|
app.state.OPENAI_API_KEY = form_data.openai_config.key
|
||||||
|
else:
|
||||||
|
sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||||
|
app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
device=DEVICE_TYPE,
|
||||||
|
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
|
)
|
||||||
|
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||||
|
app.state.sentence_transformer_ef = sentence_transformer_ef
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
|
||||||
|
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
"openai_config": {
|
||||||
|
"url": app.state.OPENAI_API_BASE_URL,
|
||||||
|
"key": app.state.OPENAI_API_KEY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Problem updating embedding model: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
@@ -235,14 +285,35 @@ def query_doc_handler(
|
|||||||
form_data: QueryDocForm,
|
form_data: QueryDocForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return query_doc(
|
if app.state.RAG_EMBEDDING_ENGINE == "":
|
||||||
|
query_embeddings = app.state.sentence_transformer_ef.encode(
|
||||||
|
form_data.query
|
||||||
|
).tolist()
|
||||||
|
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
||||||
|
query_embeddings = generate_ollama_embeddings(
|
||||||
|
GenerateEmbeddingsForm(
|
||||||
|
**{
|
||||||
|
"model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
"prompt": form_data.query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
||||||
|
query_embeddings = generate_openai_embeddings(
|
||||||
|
model=app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
text=form_data.query,
|
||||||
|
key=app.state.OPENAI_API_KEY,
|
||||||
|
url=app.state.OPENAI_API_BASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return query_embeddings_doc(
|
||||||
collection_name=form_data.collection_name,
|
collection_name=form_data.collection_name,
|
||||||
query=form_data.query,
|
query=form_data.query,
|
||||||
|
query_embeddings=query_embeddings,
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -262,12 +333,40 @@ def query_collection_handler(
|
|||||||
form_data: QueryCollectionsForm,
|
form_data: QueryCollectionsForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
return query_collection(
|
try:
|
||||||
collection_names=form_data.collection_names,
|
if app.state.RAG_EMBEDDING_ENGINE == "":
|
||||||
query=form_data.query,
|
query_embeddings = app.state.sentence_transformer_ef.encode(
|
||||||
k=form_data.k if form_data.k else app.state.TOP_K,
|
form_data.query
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
).tolist()
|
||||||
)
|
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
||||||
|
query_embeddings = generate_ollama_embeddings(
|
||||||
|
GenerateEmbeddingsForm(
|
||||||
|
**{
|
||||||
|
"model": app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
"prompt": form_data.query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
||||||
|
query_embeddings = generate_openai_embeddings(
|
||||||
|
model=app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
text=form_data.query,
|
||||||
|
key=app.state.OPENAI_API_KEY,
|
||||||
|
url=app.state.OPENAI_API_BASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return query_embeddings_collection(
|
||||||
|
collection_names=form_data.collection_names,
|
||||||
|
query_embeddings=query_embeddings,
|
||||||
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/web")
|
@app.post("/web")
|
||||||
@@ -296,13 +395,20 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
chunk_size=app.state.CHUNK_SIZE,
|
chunk_size=app.state.CHUNK_SIZE,
|
||||||
chunk_overlap=app.state.CHUNK_OVERLAP,
|
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||||
add_start_index=True,
|
add_start_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
docs = text_splitter.split_documents(data)
|
docs = text_splitter.split_documents(data)
|
||||||
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
|
||||||
|
if len(docs) > 0:
|
||||||
|
log.info(f"store_data_in_vector_db {docs}")
|
||||||
|
return store_docs_in_vector_db(docs, collection_name, overwrite), None
|
||||||
|
else:
|
||||||
|
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
def store_text_in_vector_db(
|
def store_text_in_vector_db(
|
||||||
@@ -318,27 +424,56 @@ def store_text_in_vector_db(
|
|||||||
|
|
||||||
|
|
||||||
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
||||||
|
|
||||||
texts = [doc.page_content for doc in docs]
|
texts = [doc.page_content for doc in docs]
|
||||||
|
texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||||
|
|
||||||
metadatas = [doc.metadata for doc in docs]
|
metadatas = [doc.metadata for doc in docs]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if overwrite:
|
if overwrite:
|
||||||
for collection in CHROMA_CLIENT.list_collections():
|
for collection in CHROMA_CLIENT.list_collections():
|
||||||
if collection_name == collection.name:
|
if collection_name == collection.name:
|
||||||
print(f"deleting existing collection {collection_name}")
|
log.info(f"deleting existing collection {collection_name}")
|
||||||
CHROMA_CLIENT.delete_collection(name=collection_name)
|
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||||
|
|
||||||
collection = CHROMA_CLIENT.create_collection(
|
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
||||||
name=collection_name,
|
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
if app.state.RAG_EMBEDDING_ENGINE == "":
|
||||||
)
|
embeddings = app.state.sentence_transformer_ef.encode(texts).tolist()
|
||||||
|
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
|
||||||
|
embeddings = [
|
||||||
|
generate_ollama_embeddings(
|
||||||
|
GenerateEmbeddingsForm(
|
||||||
|
**{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for text in texts
|
||||||
|
]
|
||||||
|
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
|
||||||
|
embeddings = [
|
||||||
|
generate_openai_embeddings(
|
||||||
|
model=app.state.RAG_EMBEDDING_MODEL,
|
||||||
|
text=text,
|
||||||
|
key=app.state.OPENAI_API_KEY,
|
||||||
|
url=app.state.OPENAI_API_BASE_URL,
|
||||||
|
)
|
||||||
|
for text in texts
|
||||||
|
]
|
||||||
|
|
||||||
|
for batch in create_batches(
|
||||||
|
api=CHROMA_CLIENT,
|
||||||
|
ids=[str(uuid.uuid1()) for _ in texts],
|
||||||
|
metadatas=metadatas,
|
||||||
|
embeddings=embeddings,
|
||||||
|
documents=texts,
|
||||||
|
):
|
||||||
|
collection.add(*batch)
|
||||||
|
|
||||||
collection.add(
|
|
||||||
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
if e.__class__.__name__ == "UniqueConstraintError":
|
if e.__class__.__name__ == "UniqueConstraintError":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -402,6 +537,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
||||||
elif file_ext == "xml":
|
elif file_ext == "xml":
|
||||||
loader = UnstructuredXMLLoader(file_path)
|
loader = UnstructuredXMLLoader(file_path)
|
||||||
|
elif file_ext in ["htm", "html"]:
|
||||||
|
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
||||||
elif file_ext == "md":
|
elif file_ext == "md":
|
||||||
loader = UnstructuredMarkdownLoader(file_path)
|
loader = UnstructuredMarkdownLoader(file_path)
|
||||||
elif file_content_type == "application/epub+zip":
|
elif file_content_type == "application/epub+zip":
|
||||||
@@ -438,8 +575,11 @@ def store_doc(
|
|||||||
|
|
||||||
log.info(f"file.content_type: {file.content_type}")
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
try:
|
try:
|
||||||
filename = file.filename
|
unsanitized_filename = file.filename
|
||||||
|
filename = os.path.basename(unsanitized_filename)
|
||||||
|
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
|
|
||||||
contents = file.file.read()
|
contents = file.file.read()
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
@@ -450,21 +590,23 @@ def store_doc(
|
|||||||
collection_name = calculate_sha256(f)[:63]
|
collection_name = calculate_sha256(f)[:63]
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
loader, known_type = get_loader(file.filename, file.content_type, file_path)
|
loader, known_type = get_loader(filename, file.content_type, file_path)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
|
||||||
|
|
||||||
if result:
|
try:
|
||||||
return {
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
"status": True,
|
|
||||||
"collection_name": collection_name,
|
if result:
|
||||||
"filename": filename,
|
return {
|
||||||
"known_type": known_type,
|
"status": True,
|
||||||
}
|
"collection_name": collection_name,
|
||||||
else:
|
"filename": filename,
|
||||||
|
"known_type": known_type,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(),
|
detail=e,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@@ -529,38 +671,42 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
|||||||
)
|
)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
|
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
try:
|
||||||
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
sanitized_filename = sanitize_filename(filename)
|
sanitized_filename = sanitize_filename(filename)
|
||||||
doc = Documents.get_doc_by_name(sanitized_filename)
|
doc = Documents.get_doc_by_name(sanitized_filename)
|
||||||
|
|
||||||
if doc == None:
|
if doc == None:
|
||||||
doc = Documents.insert_new_doc(
|
doc = Documents.insert_new_doc(
|
||||||
user.id,
|
user.id,
|
||||||
DocumentForm(
|
DocumentForm(
|
||||||
**{
|
**{
|
||||||
"name": sanitized_filename,
|
"name": sanitized_filename,
|
||||||
"title": filename,
|
"title": filename,
|
||||||
"collection_name": collection_name,
|
"collection_name": collection_name,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"content": (
|
"content": (
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"tags": list(
|
"tags": list(
|
||||||
map(
|
map(
|
||||||
lambda name: {"name": name},
|
lambda name: {"name": name},
|
||||||
tags,
|
tags,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
)
|
if len(tags)
|
||||||
if len(tags)
|
else "{}"
|
||||||
else "{}"
|
),
|
||||||
),
|
}
|
||||||
}
|
),
|
||||||
),
|
)
|
||||||
)
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
pass
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|||||||
+98
-34
@@ -1,24 +1,32 @@
|
|||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from apps.ollama.main import (
|
||||||
|
generate_ollama_embeddings,
|
||||||
|
GenerateEmbeddingsForm,
|
||||||
|
)
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
def query_doc(collection_name: str, query: str, k: int, embedding_function):
|
def query_embeddings_doc(collection_name: str, query: str, query_embeddings, k: int):
|
||||||
try:
|
try:
|
||||||
# if you use docker use the model from the environment variable
|
# if you use docker use the model from the environment variable
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
log.info(f"query_embeddings_doc {query_embeddings}")
|
||||||
name=collection_name,
|
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||||
embedding_function=embedding_function,
|
|
||||||
)
|
|
||||||
result = collection.query(
|
result = collection.query(
|
||||||
query_texts=[query],
|
query_embeddings=[query_embeddings],
|
||||||
n_results=k,
|
n_results=k,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.info(f"query_embeddings_doc:result {result}")
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
@@ -69,23 +77,20 @@ def merge_and_sort_query_results(query_results, k):
|
|||||||
return merged_query_results
|
return merged_query_results
|
||||||
|
|
||||||
|
|
||||||
def query_collection(
|
def query_embeddings_collection(
|
||||||
collection_names: List[str], query: str, k: int, embedding_function
|
collection_names: List[str], query: str, query_embeddings, k: int
|
||||||
):
|
):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
log.info(f"query_embeddings_collection {query_embeddings}")
|
||||||
|
|
||||||
for collection_name in collection_names:
|
for collection_name in collection_names:
|
||||||
try:
|
try:
|
||||||
# if you use docker use the model from the environment variable
|
result = query_embeddings_doc(
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
collection_name=collection_name,
|
||||||
name=collection_name,
|
query=query,
|
||||||
embedding_function=embedding_function,
|
query_embeddings=query_embeddings,
|
||||||
)
|
k=k,
|
||||||
|
|
||||||
result = collection.query(
|
|
||||||
query_texts=[query],
|
|
||||||
n_results=k,
|
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except:
|
except:
|
||||||
@@ -100,8 +105,20 @@ def rag_template(template: str, context: str, query: str):
|
|||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def rag_messages(docs, messages, template, k, embedding_function):
|
def rag_messages(
|
||||||
log.debug(f"docs: {docs}")
|
docs,
|
||||||
|
messages,
|
||||||
|
template,
|
||||||
|
k,
|
||||||
|
embedding_engine,
|
||||||
|
embedding_model,
|
||||||
|
embedding_function,
|
||||||
|
openai_key,
|
||||||
|
openai_url,
|
||||||
|
):
|
||||||
|
log.debug(
|
||||||
|
f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
|
||||||
|
)
|
||||||
|
|
||||||
last_user_message_idx = None
|
last_user_message_idx = None
|
||||||
for i in range(len(messages) - 1, -1, -1):
|
for i in range(len(messages) - 1, -1, -1):
|
||||||
@@ -134,28 +151,52 @@ def rag_messages(docs, messages, template, k, embedding_function):
|
|||||||
context = None
|
context = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if doc["type"] == "collection":
|
|
||||||
context = query_collection(
|
if doc["type"] == "text":
|
||||||
collection_names=doc["collection_names"],
|
|
||||||
query=query,
|
|
||||||
k=k,
|
|
||||||
embedding_function=embedding_function,
|
|
||||||
)
|
|
||||||
elif doc["type"] == "text":
|
|
||||||
context = doc["content"]
|
context = doc["content"]
|
||||||
else:
|
else:
|
||||||
context = query_doc(
|
if embedding_engine == "":
|
||||||
collection_name=doc["collection_name"],
|
query_embeddings = embedding_function.encode(query).tolist()
|
||||||
query=query,
|
elif embedding_engine == "ollama":
|
||||||
k=k,
|
query_embeddings = generate_ollama_embeddings(
|
||||||
embedding_function=embedding_function,
|
GenerateEmbeddingsForm(
|
||||||
)
|
**{
|
||||||
|
"model": embedding_model,
|
||||||
|
"prompt": query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif embedding_engine == "openai":
|
||||||
|
query_embeddings = generate_openai_embeddings(
|
||||||
|
model=embedding_model,
|
||||||
|
text=query,
|
||||||
|
key=openai_key,
|
||||||
|
url=openai_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
if doc["type"] == "collection":
|
||||||
|
context = query_embeddings_collection(
|
||||||
|
collection_names=doc["collection_names"],
|
||||||
|
query=query,
|
||||||
|
query_embeddings=query_embeddings,
|
||||||
|
k=k,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context = query_embeddings_doc(
|
||||||
|
collection_name=doc["collection_name"],
|
||||||
|
query=query,
|
||||||
|
query_embeddings=query_embeddings,
|
||||||
|
k=k,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
relevant_contexts.append(context)
|
relevant_contexts.append(context)
|
||||||
|
|
||||||
|
log.debug(f"relevant_contexts: {relevant_contexts}")
|
||||||
|
|
||||||
context_string = ""
|
context_string = ""
|
||||||
for context in relevant_contexts:
|
for context in relevant_contexts:
|
||||||
if context:
|
if context:
|
||||||
@@ -186,3 +227,26 @@ def rag_messages(docs, messages, template, k, embedding_function):
|
|||||||
messages[last_user_message_idx] = new_user_message
|
messages[last_user_message_idx] = new_user_message
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def generate_openai_embeddings(
|
||||||
|
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
f"{url}/embeddings",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {key}",
|
||||||
|
},
|
||||||
|
json={"input": text, "model": model},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if "data" in data:
|
||||||
|
return data["data"][0]["embedding"]
|
||||||
|
else:
|
||||||
|
raise "Something went wrong :/"
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from peewee import *
|
from peewee import *
|
||||||
|
from peewee_migrate import Router
|
||||||
from config import SRC_LOG_LEVELS, DATA_DIR
|
from config import SRC_LOG_LEVELS, DATA_DIR
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -16,4 +17,6 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
||||||
DB.connect()
|
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
|
||||||
|
router.run()
|
||||||
|
DB.connect(reuse_if_open=True)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Peewee migrations -- 001_initial_schema.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Auth(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
password = pw.CharField(max_length=255)
|
||||||
|
active = pw.BooleanField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "auth"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Chat(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.CharField()
|
||||||
|
chat = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chat"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class ChatIdTag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
tag_name = pw.CharField(max_length=255)
|
||||||
|
chat_id = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "chatidtag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Document(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
collection_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255, unique=True)
|
||||||
|
title = pw.CharField()
|
||||||
|
filename = pw.CharField()
|
||||||
|
content = pw.TextField(null=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "document"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Modelfile(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
tag_name = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
modelfile = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "modelfile"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Prompt(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
command = pw.CharField(max_length=255, unique=True)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
title = pw.CharField()
|
||||||
|
content = pw.TextField()
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "prompt"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class Tag(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
user_id = pw.CharField(max_length=255)
|
||||||
|
data = pw.TextField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "tag"
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class User(pw.Model):
|
||||||
|
id = pw.CharField(max_length=255, unique=True)
|
||||||
|
name = pw.CharField(max_length=255)
|
||||||
|
email = pw.CharField(max_length=255)
|
||||||
|
role = pw.CharField(max_length=255)
|
||||||
|
profile_image_url = pw.CharField(max_length=255)
|
||||||
|
timestamp = pw.DateField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "user"
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_model("user")
|
||||||
|
|
||||||
|
migrator.remove_model("tag")
|
||||||
|
|
||||||
|
migrator.remove_model("prompt")
|
||||||
|
|
||||||
|
migrator.remove_model("modelfile")
|
||||||
|
|
||||||
|
migrator.remove_model("document")
|
||||||
|
|
||||||
|
migrator.remove_model("chatidtag")
|
||||||
|
|
||||||
|
migrator.remove_model("chat")
|
||||||
|
|
||||||
|
migrator.remove_model("auth")
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields("chat", "share_id")
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields("user", "api_key")
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields("chat", archived=pw.BooleanField(default=False))
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields("chat", "archived")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Peewee migrations -- 002_add_local_sharing.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
# Adding fields created_at and updated_at to the 'chat' table
|
||||||
|
migrator.add_fields(
|
||||||
|
"chat",
|
||||||
|
created_at=pw.DateTimeField(null=True), # Allow null for transition
|
||||||
|
updated_at=pw.DateTimeField(null=True), # Allow null for transition
|
||||||
|
)
|
||||||
|
|
||||||
|
# Populate the new fields from an existing 'timestamp' field
|
||||||
|
migrator.sql(
|
||||||
|
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now that the data has been copied, remove the original 'timestamp' field
|
||||||
|
migrator.remove_fields("chat", "timestamp")
|
||||||
|
|
||||||
|
# Update the fields to be not null now that they are populated
|
||||||
|
migrator.change_fields(
|
||||||
|
"chat",
|
||||||
|
created_at=pw.DateTimeField(null=False),
|
||||||
|
updated_at=pw.DateTimeField(null=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
# Recreate the timestamp field initially allowing null values for safe transition
|
||||||
|
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
|
||||||
|
|
||||||
|
# Copy the earliest created_at date back into the new timestamp field
|
||||||
|
# This assumes created_at was originally a copy of timestamp
|
||||||
|
migrator.sql("UPDATE chat SET timestamp = created_at")
|
||||||
|
|
||||||
|
# Remove the created_at and updated_at fields
|
||||||
|
migrator.remove_fields("chat", "created_at", "updated_at")
|
||||||
|
|
||||||
|
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
||||||
|
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Database Migrations
|
||||||
|
|
||||||
|
This directory contains all the database migrations for the web app.
|
||||||
|
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
|
||||||
|
|
||||||
|
Migrations are automatically ran at app startup.
|
||||||
|
|
||||||
|
## Creating a migration
|
||||||
|
|
||||||
|
Have you made a change to the schema of an existing model?
|
||||||
|
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
|
||||||
|
|
||||||
|
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
|
||||||
|
2. Make your changes to the models.
|
||||||
|
3. From the `backend` directory, run the following command:
|
||||||
|
```bash
|
||||||
|
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
|
||||||
|
```
|
||||||
|
- `$SQLITE_DB` should be the path to the database file.
|
||||||
|
- `$MIGRATION_NAME` should be a descriptive name for the migration.
|
||||||
|
4. The migration file will be created in the `apps/web/internal/migrations` directory.
|
||||||
@@ -20,6 +20,7 @@ from config import (
|
|||||||
ENABLE_SIGNUP,
|
ENABLE_SIGNUP,
|
||||||
USER_PERMISSIONS,
|
USER_PERMISSIONS,
|
||||||
WEBHOOK_URL,
|
WEBHOOK_URL,
|
||||||
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
|||||||
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||||
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
app.state.USER_PERMISSIONS = USER_PERMISSIONS
|
||||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from utils.utils import verify_password
|
|||||||
from apps.web.internal.db import DB
|
from apps.web.internal.db import DB
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
@@ -46,6 +47,10 @@ class Token(BaseModel):
|
|||||||
token_type: str
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKey(BaseModel):
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
email: str
|
email: str
|
||||||
@@ -81,6 +86,7 @@ class SignupForm(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
password: str
|
password: str
|
||||||
|
profile_image_url: Optional[str] = "/user.png"
|
||||||
|
|
||||||
|
|
||||||
class AuthsTable:
|
class AuthsTable:
|
||||||
@@ -89,7 +95,12 @@ class AuthsTable:
|
|||||||
self.db.create_tables([Auth])
|
self.db.create_tables([Auth])
|
||||||
|
|
||||||
def insert_new_auth(
|
def insert_new_auth(
|
||||||
self, email: str, password: str, name: str, role: str = "pending"
|
self,
|
||||||
|
email: str,
|
||||||
|
password: str,
|
||||||
|
name: str,
|
||||||
|
profile_image_url: str = "/user.png",
|
||||||
|
role: str = "pending",
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
log.info("insert_new_auth")
|
log.info("insert_new_auth")
|
||||||
|
|
||||||
@@ -100,7 +111,7 @@ class AuthsTable:
|
|||||||
)
|
)
|
||||||
result = Auth.create(**auth.model_dump())
|
result = Auth.create(**auth.model_dump())
|
||||||
|
|
||||||
user = Users.insert_new_user(id, name, email, role)
|
user = Users.insert_new_user(id, name, email, profile_image_url, role)
|
||||||
|
|
||||||
if result and user:
|
if result and user:
|
||||||
return user
|
return user
|
||||||
@@ -122,6 +133,28 @@ class AuthsTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
|
log.info(f"authenticate_user_by_api_key: {api_key}")
|
||||||
|
# if no api_key, return None
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = Users.get_user_by_api_key(api_key)
|
||||||
|
return user if user else None
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
|
||||||
|
log.info(f"authenticate_user_by_trusted_header: {email}")
|
||||||
|
try:
|
||||||
|
auth = Auth.get(Auth.email == email, Auth.active == True)
|
||||||
|
if auth:
|
||||||
|
user = Users.get_user_by_id(auth.id)
|
||||||
|
return user
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
||||||
try:
|
try:
|
||||||
query = Auth.update(password=new_password).where(Auth.id == id)
|
query = Auth.update(password=new_password).where(Auth.id == id)
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ class Chat(Model):
|
|||||||
user_id = CharField()
|
user_id = CharField()
|
||||||
title = CharField()
|
title = CharField()
|
||||||
chat = TextField() # Save Chat JSON as Text
|
chat = TextField() # Save Chat JSON as Text
|
||||||
timestamp = DateField()
|
|
||||||
|
created_at = DateTimeField()
|
||||||
|
updated_at = DateTimeField()
|
||||||
|
|
||||||
|
share_id = CharField(null=True, unique=True)
|
||||||
|
archived = BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
@@ -30,7 +35,12 @@ class ChatModel(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
title: str
|
title: str
|
||||||
chat: str
|
chat: str
|
||||||
timestamp: int # timestamp in epoch
|
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
share_id: Optional[str] = None
|
||||||
|
archived: bool = False
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
@@ -51,12 +61,17 @@ class ChatResponse(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
title: str
|
title: str
|
||||||
chat: dict
|
chat: dict
|
||||||
timestamp: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
share_id: Optional[str] = None # id of the chat to be shared
|
||||||
|
archived: bool
|
||||||
|
|
||||||
|
|
||||||
class ChatTitleIdResponse(BaseModel):
|
class ChatTitleIdResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
updated_at: int
|
||||||
|
created_at: int
|
||||||
|
|
||||||
|
|
||||||
class ChatTable:
|
class ChatTable:
|
||||||
@@ -74,7 +89,8 @@ class ChatTable:
|
|||||||
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
|
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
|
||||||
),
|
),
|
||||||
"chat": json.dumps(form_data.chat),
|
"chat": json.dumps(form_data.chat),
|
||||||
"timestamp": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
|
"updated_at": int(time.time()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +102,7 @@ class ChatTable:
|
|||||||
query = Chat.update(
|
query = Chat.update(
|
||||||
chat=json.dumps(chat),
|
chat=json.dumps(chat),
|
||||||
title=chat["title"] if "title" in chat else "New Chat",
|
title=chat["title"] if "title" in chat else "New Chat",
|
||||||
timestamp=int(time.time()),
|
updated_at=int(time.time()),
|
||||||
).where(Chat.id == id)
|
).where(Chat.id == id)
|
||||||
query.execute()
|
query.execute()
|
||||||
|
|
||||||
@@ -95,14 +111,108 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
|
# Get the existing chat to share
|
||||||
|
chat = Chat.get(Chat.id == chat_id)
|
||||||
|
# Check if the chat is already shared
|
||||||
|
if chat.share_id:
|
||||||
|
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
|
||||||
|
# Create a new chat with the same data, but with a new ID
|
||||||
|
shared_chat = ChatModel(
|
||||||
|
**{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"user_id": f"shared-{chat_id}",
|
||||||
|
"title": chat.title,
|
||||||
|
"chat": chat.chat,
|
||||||
|
"created_at": chat.created_at,
|
||||||
|
"updated_at": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
shared_result = Chat.create(**shared_chat.model_dump())
|
||||||
|
# Update the original chat with the share_id
|
||||||
|
result = (
|
||||||
|
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return shared_chat if (shared_result and result) else None
|
||||||
|
|
||||||
|
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
print("update_shared_chat_by_id")
|
||||||
|
chat = Chat.get(Chat.id == chat_id)
|
||||||
|
print(chat)
|
||||||
|
|
||||||
|
query = Chat.update(
|
||||||
|
title=chat.title,
|
||||||
|
chat=chat.chat,
|
||||||
|
).where(Chat.id == chat.share_id)
|
||||||
|
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
chat = Chat.get(Chat.id == chat.share_id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_chat_share_id_by_id(
|
||||||
|
self, id: str, share_id: Optional[str]
|
||||||
|
) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
query = Chat.update(
|
||||||
|
share_id=share_id,
|
||||||
|
).where(Chat.id == id)
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
chat = self.get_chat_by_id(id)
|
||||||
|
query = Chat.update(
|
||||||
|
archived=(not chat.archived),
|
||||||
|
).where(Chat.id == id)
|
||||||
|
|
||||||
|
query.execute()
|
||||||
|
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_archived_chat_lists_by_user_id(
|
||||||
|
self, user_id: str, skip: int = 0, limit: int = 50
|
||||||
|
) -> List[ChatModel]:
|
||||||
|
return [
|
||||||
|
ChatModel(**model_to_dict(chat))
|
||||||
|
for chat in Chat.select()
|
||||||
|
.where(Chat.archived == True)
|
||||||
|
.where(Chat.user_id == user_id)
|
||||||
|
.order_by(Chat.updated_at.desc())
|
||||||
|
# .limit(limit)
|
||||||
|
# .offset(skip)
|
||||||
|
]
|
||||||
|
|
||||||
def get_chat_lists_by_user_id(
|
def get_chat_lists_by_user_id(
|
||||||
self, user_id: str, skip: int = 0, limit: int = 50
|
self, user_id: str, skip: int = 0, limit: int = 50
|
||||||
) -> List[ChatModel]:
|
) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
ChatModel(**model_to_dict(chat))
|
ChatModel(**model_to_dict(chat))
|
||||||
for chat in Chat.select()
|
for chat in Chat.select()
|
||||||
|
.where(Chat.archived == False)
|
||||||
.where(Chat.user_id == user_id)
|
.where(Chat.user_id == user_id)
|
||||||
.order_by(Chat.timestamp.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
# .limit(limit)
|
# .limit(limit)
|
||||||
# .offset(skip)
|
# .offset(skip)
|
||||||
]
|
]
|
||||||
@@ -113,14 +223,15 @@ class ChatTable:
|
|||||||
return [
|
return [
|
||||||
ChatModel(**model_to_dict(chat))
|
ChatModel(**model_to_dict(chat))
|
||||||
for chat in Chat.select()
|
for chat in Chat.select()
|
||||||
|
.where(Chat.archived == False)
|
||||||
.where(Chat.id.in_(chat_ids))
|
.where(Chat.id.in_(chat_ids))
|
||||||
.order_by(Chat.timestamp.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_all_chats(self) -> List[ChatModel]:
|
def get_all_chats(self) -> List[ChatModel]:
|
||||||
return [
|
return [
|
||||||
ChatModel(**model_to_dict(chat))
|
ChatModel(**model_to_dict(chat))
|
||||||
for chat in Chat.select().order_by(Chat.timestamp.desc())
|
for chat in Chat.select().order_by(Chat.updated_at.desc())
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||||
@@ -128,9 +239,28 @@ class ChatTable:
|
|||||||
ChatModel(**model_to_dict(chat))
|
ChatModel(**model_to_dict(chat))
|
||||||
for chat in Chat.select()
|
for chat in Chat.select()
|
||||||
.where(Chat.user_id == user_id)
|
.where(Chat.user_id == user_id)
|
||||||
.order_by(Chat.timestamp.desc())
|
.order_by(Chat.updated_at.desc())
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
|
||||||
|
try:
|
||||||
|
chat = Chat.get(Chat.share_id == id)
|
||||||
|
|
||||||
|
if chat:
|
||||||
|
chat = Chat.get(Chat.id == id)
|
||||||
|
return ChatModel(**model_to_dict(chat))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
||||||
try:
|
try:
|
||||||
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
||||||
@@ -149,12 +279,15 @@ class ChatTable:
|
|||||||
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
return True
|
return True and self.delete_shared_chat_by_chat_id(id)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
self.delete_shared_chats_by_user_id(user_id)
|
||||||
|
|
||||||
query = Chat.delete().where(Chat.user_id == user_id)
|
query = Chat.delete().where(Chat.user_id == user_id)
|
||||||
query.execute() # Remove the rows, return number of rows removed.
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
@@ -162,5 +295,19 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
shared_chat_ids = [
|
||||||
|
f"shared-{chat.id}"
|
||||||
|
for chat in Chat.select().where(Chat.user_id == user_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
|
||||||
|
query.execute() # Remove the rows, return number of rows removed.
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
Chats = ChatTable(DB)
|
Chats = ChatTable(DB)
|
||||||
@@ -13,6 +13,7 @@ from apps.web.internal.db import DB
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class ModelfilesTable:
|
|||||||
self.db.create_tables([Modelfile])
|
self.db.create_tables([Modelfile])
|
||||||
|
|
||||||
def insert_new_modelfile(
|
def insert_new_modelfile(
|
||||||
self, user_id: str,
|
self, user_id: str, form_data: ModelfileForm
|
||||||
form_data: ModelfileForm) -> Optional[ModelfileModel]:
|
) -> Optional[ModelfileModel]:
|
||||||
if "tagName" in form_data.modelfile:
|
if "tagName" in form_data.modelfile:
|
||||||
modelfile = ModelfileModel(
|
modelfile = ModelfileModel(
|
||||||
**{
|
**{
|
||||||
@@ -73,7 +73,8 @@ class ModelfilesTable:
|
|||||||
"tag_name": form_data.modelfile["tagName"],
|
"tag_name": form_data.modelfile["tagName"],
|
||||||
"modelfile": json.dumps(form_data.modelfile),
|
"modelfile": json.dumps(form_data.modelfile),
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Modelfile.create(**modelfile.model_dump())
|
result = Modelfile.create(**modelfile.model_dump())
|
||||||
@@ -87,29 +88,28 @@ class ModelfilesTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_modelfile_by_tag_name(self,
|
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
|
||||||
tag_name: str) -> Optional[ModelfileModel]:
|
|
||||||
try:
|
try:
|
||||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||||
return ModelfileModel(**model_to_dict(modelfile))
|
return ModelfileModel(**model_to_dict(modelfile))
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_modelfiles(self,
|
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50) -> List[ModelfileResponse]:
|
|
||||||
return [
|
return [
|
||||||
ModelfileResponse(
|
ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**model_to_dict(modelfile),
|
**model_to_dict(modelfile),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
}) for modelfile in Modelfile.select()
|
)
|
||||||
|
for modelfile in Modelfile.select()
|
||||||
# .limit(limit).offset(skip)
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_modelfile_by_tag_name(
|
def update_modelfile_by_tag_name(
|
||||||
self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]:
|
self, tag_name: str, modelfile: dict
|
||||||
|
) -> Optional[ModelfileModel]:
|
||||||
try:
|
try:
|
||||||
query = Modelfile.update(
|
query = Modelfile.update(
|
||||||
modelfile=json.dumps(modelfile),
|
modelfile=json.dumps(modelfile),
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ class PromptsTable:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.db.create_tables([Prompt])
|
self.db.create_tables([Prompt])
|
||||||
|
|
||||||
def insert_new_prompt(self, user_id: str,
|
def insert_new_prompt(
|
||||||
form_data: PromptForm) -> Optional[PromptModel]:
|
self, user_id: str, form_data: PromptForm
|
||||||
|
) -> Optional[PromptModel]:
|
||||||
prompt = PromptModel(
|
prompt = PromptModel(
|
||||||
**{
|
**{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
@@ -61,7 +62,8 @@ class PromptsTable:
|
|||||||
"title": form_data.title,
|
"title": form_data.title,
|
||||||
"content": form_data.content,
|
"content": form_data.content,
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = Prompt.create(**prompt.model_dump())
|
result = Prompt.create(**prompt.model_dump())
|
||||||
@@ -81,13 +83,14 @@ class PromptsTable:
|
|||||||
|
|
||||||
def get_prompts(self) -> List[PromptModel]:
|
def get_prompts(self) -> List[PromptModel]:
|
||||||
return [
|
return [
|
||||||
PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select()
|
PromptModel(**model_to_dict(prompt))
|
||||||
|
for prompt in Prompt.select()
|
||||||
# .limit(limit).offset(skip)
|
# .limit(limit).offset(skip)
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_prompt_by_command(
|
def update_prompt_by_command(
|
||||||
self, command: str,
|
self, command: str, form_data: PromptForm
|
||||||
form_data: PromptForm) -> Optional[PromptModel]:
|
) -> Optional[PromptModel]:
|
||||||
try:
|
try:
|
||||||
query = Prompt.update(
|
query = Prompt.update(
|
||||||
title=form_data.title,
|
title=form_data.title,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import logging
|
|||||||
from apps.web.internal.db import DB
|
from apps.web.internal.db import DB
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
@@ -135,7 +136,9 @@ class TagTable:
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
TagModel(**model_to_dict(tag))
|
TagModel(**model_to_dict(tag))
|
||||||
for tag in Tag.select().where(Tag.name.in_(tag_names))
|
for tag in Tag.select()
|
||||||
|
.where(Tag.user_id == user_id)
|
||||||
|
.where(Tag.name.in_(tag_names))
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_tags_by_chat_id_and_user_id(
|
def get_tags_by_chat_id_and_user_id(
|
||||||
@@ -150,7 +153,9 @@ class TagTable:
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
TagModel(**model_to_dict(tag))
|
TagModel(**model_to_dict(tag))
|
||||||
for tag in Tag.select().where(Tag.name.in_(tag_names))
|
for tag in Tag.select()
|
||||||
|
.where(Tag.user_id == user_id)
|
||||||
|
.where(Tag.name.in_(tag_names))
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_chat_ids_by_tag_name_and_user_id(
|
def get_chat_ids_by_tag_name_and_user_id(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class User(Model):
|
|||||||
role = CharField()
|
role = CharField()
|
||||||
profile_image_url = CharField()
|
profile_image_url = CharField()
|
||||||
timestamp = DateField()
|
timestamp = DateField()
|
||||||
|
api_key = CharField(null=True, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = DB
|
database = DB
|
||||||
@@ -30,8 +31,9 @@ class UserModel(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
role: str = "pending"
|
role: str = "pending"
|
||||||
profile_image_url: str = "/user.png"
|
profile_image_url: str
|
||||||
timestamp: int # timestamp in epoch
|
timestamp: int # timestamp in epoch
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
@@ -57,7 +59,12 @@ class UsersTable:
|
|||||||
self.db.create_tables([User])
|
self.db.create_tables([User])
|
||||||
|
|
||||||
def insert_new_user(
|
def insert_new_user(
|
||||||
self, id: str, name: str, email: str, role: str = "pending"
|
self,
|
||||||
|
id: str,
|
||||||
|
name: str,
|
||||||
|
email: str,
|
||||||
|
profile_image_url: str = "/user.png",
|
||||||
|
role: str = "pending",
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
user = UserModel(
|
user = UserModel(
|
||||||
**{
|
**{
|
||||||
@@ -65,7 +72,7 @@ class UsersTable:
|
|||||||
"name": name,
|
"name": name,
|
||||||
"email": email,
|
"email": email,
|
||||||
"role": role,
|
"role": role,
|
||||||
"profile_image_url": "/user.png",
|
"profile_image_url": profile_image_url,
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -82,6 +89,13 @@ class UsersTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||||
|
try:
|
||||||
|
user = User.get(User.api_key == api_key)
|
||||||
|
return UserModel(**model_to_dict(user))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||||
try:
|
try:
|
||||||
user = User.get(User.email == email)
|
user = User.get(User.email == email)
|
||||||
@@ -149,5 +163,21 @@ class UsersTable:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
|
||||||
|
try:
|
||||||
|
query = User.update(api_key=api_key).where(User.id == id)
|
||||||
|
result = query.execute()
|
||||||
|
|
||||||
|
return True if result == 1 else False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
user = User.get(User.id == id)
|
||||||
|
return user.api_key
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
Users = UsersTable(DB)
|
Users = UsersTable(DB)
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
from fastapi import Response, Request
|
from fastapi import Request
|
||||||
from fastapi import Depends, FastAPI, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import List, Union
|
|
||||||
|
|
||||||
from fastapi import APIRouter, status
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
from apps.web.models.auths import (
|
from apps.web.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
@@ -17,6 +14,7 @@ from apps.web.models.auths import (
|
|||||||
UserResponse,
|
UserResponse,
|
||||||
SigninResponse,
|
SigninResponse,
|
||||||
Auths,
|
Auths,
|
||||||
|
ApiKey,
|
||||||
)
|
)
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
@@ -25,10 +23,12 @@ from utils.utils import (
|
|||||||
get_current_user,
|
get_current_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
create_token,
|
create_token,
|
||||||
|
create_api_key,
|
||||||
)
|
)
|
||||||
from utils.misc import parse_duration, validate_email_format
|
from utils.misc import parse_duration, validate_email_format
|
||||||
from utils.webhook import post_webhook
|
from utils.webhook import post_webhook
|
||||||
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
|
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -79,6 +79,8 @@ async def update_profile(
|
|||||||
async def update_password(
|
async def update_password(
|
||||||
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
|
||||||
):
|
):
|
||||||
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||||
if session_user:
|
if session_user:
|
||||||
user = Auths.authenticate_user(session_user.email, form_data.password)
|
user = Auths.authenticate_user(session_user.email, form_data.password)
|
||||||
|
|
||||||
@@ -98,7 +100,22 @@ async def update_password(
|
|||||||
|
|
||||||
@router.post("/signin", response_model=SigninResponse)
|
@router.post("/signin", response_model=SigninResponse)
|
||||||
async def signin(request: Request, form_data: SigninForm):
|
async def signin(request: Request, form_data: SigninForm):
|
||||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
|
||||||
|
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
|
||||||
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
|
||||||
|
|
||||||
|
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
|
||||||
|
if not Users.get_user_by_email(trusted_email.lower()):
|
||||||
|
await signup(
|
||||||
|
request,
|
||||||
|
SignupForm(
|
||||||
|
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
|
||||||
|
),
|
||||||
|
)
|
||||||
|
user = Auths.authenticate_user_by_trusted_header(trusted_email)
|
||||||
|
else:
|
||||||
|
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
token = create_token(
|
token = create_token(
|
||||||
data={"id": user.id},
|
data={"id": user.id},
|
||||||
@@ -146,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm):
|
|||||||
)
|
)
|
||||||
hashed = get_password_hash(form_data.password)
|
hashed = get_password_hash(form_data.password)
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(
|
||||||
form_data.email.lower(), hashed, form_data.name, role
|
form_data.email.lower(),
|
||||||
|
hashed,
|
||||||
|
form_data.name,
|
||||||
|
form_data.profile_image_url,
|
||||||
|
role,
|
||||||
)
|
)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
@@ -249,3 +270,40 @@ async def update_token_expires_duration(
|
|||||||
return request.app.state.JWT_EXPIRES_IN
|
return request.app.state.JWT_EXPIRES_IN
|
||||||
else:
|
else:
|
||||||
return request.app.state.JWT_EXPIRES_IN
|
return request.app.state.JWT_EXPIRES_IN
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# API Key
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
# create api key
|
||||||
|
@router.post("/api_key", response_model=ApiKey)
|
||||||
|
async def create_api_key_(user=Depends(get_current_user)):
|
||||||
|
api_key = create_api_key()
|
||||||
|
success = Users.update_user_api_key_by_id(user.id, api_key)
|
||||||
|
if success:
|
||||||
|
return {
|
||||||
|
"api_key": api_key,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
# delete api key
|
||||||
|
@router.delete("/api_key", response_model=bool)
|
||||||
|
async def delete_api_key(user=Depends(get_current_user)):
|
||||||
|
success = Users.update_user_api_key_by_id(user.id, None)
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
# get api key
|
||||||
|
@router.get("/api_key", response_model=ApiKey)
|
||||||
|
async def get_api_key(user=Depends(get_current_user)):
|
||||||
|
api_key = Users.get_user_api_key_by_id(user.id)
|
||||||
|
if api_key:
|
||||||
|
return {
|
||||||
|
"api_key": api_key,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
@@ -28,7 +28,8 @@ from apps.web.models.tags import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
@@ -46,6 +47,18 @@ async def get_user_chats(
|
|||||||
return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
|
return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetArchivedChats
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/archived", response_model=List[ChatTitleIdResponse])
|
||||||
|
async def get_archived_user_chats(
|
||||||
|
user=Depends(get_current_user), skip: int = 0, limit: int = 50
|
||||||
|
):
|
||||||
|
return Chats.get_archived_chat_lists_by_user_id(user.id, skip, limit)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetAllChats
|
# GetAllChats
|
||||||
############################
|
############################
|
||||||
@@ -66,6 +79,11 @@ async def get_all_user_chats(user=Depends(get_current_user)):
|
|||||||
|
|
||||||
@router.get("/all/db", response_model=List[ChatResponse])
|
@router.get("/all/db", response_model=List[ChatResponse])
|
||||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||||
|
if not ENABLE_ADMIN_EXPORT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
for chat in Chats.get_all_chats()
|
for chat in Chats.get_all_chats()
|
||||||
@@ -188,6 +206,103 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# ArchiveChat
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/archive", response_model=Optional[ChatResponse])
|
||||||
|
async def archive_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||||
|
if chat:
|
||||||
|
chat = Chats.toggle_chat_archive_by_id(id)
|
||||||
|
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# ShareChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/share", response_model=Optional[ChatResponse])
|
||||||
|
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||||
|
if chat:
|
||||||
|
if chat.share_id:
|
||||||
|
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
|
||||||
|
return ChatResponse(
|
||||||
|
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
|
||||||
|
if not shared_chat:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# DeletedSharedChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}/share", response_model=Optional[bool])
|
||||||
|
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
|
||||||
|
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||||
|
if chat:
|
||||||
|
if not chat.share_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = Chats.delete_shared_chat_by_chat_id(id)
|
||||||
|
update_result = Chats.update_chat_share_id_by_id(id, None)
|
||||||
|
|
||||||
|
return result and update_result != None
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# GetSharedChatById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
|
||||||
|
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
|
||||||
|
if user.role == "pending":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.role == "user":
|
||||||
|
chat = Chats.get_chat_by_share_id(share_id)
|
||||||
|
elif user.role == "admin":
|
||||||
|
chat = Chats.get_chat_by_id(share_id)
|
||||||
|
|
||||||
|
if chat:
|
||||||
|
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetChatTagsById
|
# GetChatTagsById
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import uuid
|
|||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token
|
from utils.utils import (
|
||||||
|
get_password_hash,
|
||||||
|
get_current_user,
|
||||||
|
get_admin_user,
|
||||||
|
create_token,
|
||||||
|
)
|
||||||
from utils.misc import get_gravatar_url, validate_email_format
|
from utils.misc import get_gravatar_url, validate_email_format
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -43,7 +48,6 @@ async def set_global_default_models(
|
|||||||
return request.app.state.DEFAULT_MODELS
|
return request.app.state.DEFAULT_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
||||||
async def set_global_default_suggestions(
|
async def set_global_default_suggestions(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[ModelfileResponse])
|
@router.get("/", response_model=List[ModelfileResponse])
|
||||||
async def get_modelfiles(skip: int = 0,
|
async def get_modelfiles(
|
||||||
limit: int = 50,
|
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
|
||||||
user=Depends(get_current_user)):
|
):
|
||||||
return Modelfiles.get_modelfiles(skip, limit)
|
return Modelfiles.get_modelfiles(skip, limit)
|
||||||
|
|
||||||
|
|
||||||
@@ -36,17 +36,16 @@ async def get_modelfiles(skip: int = 0,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[ModelfileResponse])
|
@router.post("/create", response_model=Optional[ModelfileResponse])
|
||||||
async def create_new_modelfile(form_data: ModelfileForm,
|
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
|
||||||
user=Depends(get_admin_user)):
|
|
||||||
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
||||||
|
|
||||||
if modelfile:
|
if modelfile:
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -60,17 +59,18 @@ async def create_new_modelfile(form_data: ModelfileForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Optional[ModelfileResponse])
|
@router.post("/", response_model=Optional[ModelfileResponse])
|
||||||
async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
async def get_modelfile_by_tag_name(
|
||||||
user=Depends(get_current_user)):
|
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
|
||||||
|
):
|
||||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||||
|
|
||||||
if modelfile:
|
if modelfile:
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -84,8 +84,9 @@ async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/update", response_model=Optional[ModelfileResponse])
|
@router.post("/update", response_model=Optional[ModelfileResponse])
|
||||||
async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
async def update_modelfile_by_tag_name(
|
||||||
user=Depends(get_admin_user)):
|
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||||
if modelfile:
|
if modelfile:
|
||||||
updated_modelfile = {
|
updated_modelfile = {
|
||||||
@@ -94,14 +95,15 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
||||||
form_data.tag_name, updated_modelfile)
|
form_data.tag_name, updated_modelfile
|
||||||
|
)
|
||||||
|
|
||||||
return ModelfileResponse(
|
return ModelfileResponse(
|
||||||
**{
|
**{
|
||||||
**modelfile.model_dump(),
|
**modelfile.model_dump(),
|
||||||
"modelfile":
|
"modelfile": json.loads(modelfile.modelfile),
|
||||||
json.loads(modelfile.modelfile),
|
}
|
||||||
})
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -115,7 +117,8 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/delete", response_model=bool)
|
@router.delete("/delete", response_model=bool)
|
||||||
async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
|
async def delete_modelfile_by_tag_name(
|
||||||
user=Depends(get_admin_user)):
|
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
||||||
return result
|
return result
|
||||||
@@ -16,6 +16,7 @@ from utils.utils import get_current_user, get_password_hash, get_admin_user
|
|||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from config import SRC_LOG_LEVELS
|
from config import SRC_LOG_LEVELS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
|
from fastapi import APIRouter, UploadFile, File, Response
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from starlette.responses import StreamingResponse, FileResponse
|
from starlette.responses import StreamingResponse, FileResponse
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
import requests
|
|
||||||
import os
|
from fpdf import FPDF
|
||||||
import aiohttp
|
import markdown
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
from utils.utils import get_admin_user
|
from utils.utils import get_admin_user
|
||||||
@@ -16,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
|
|||||||
|
|
||||||
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
|
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
from typing import List
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -28,9 +25,77 @@ async def get_gravatar(
|
|||||||
return get_gravatar_url(email)
|
return get_gravatar_url(email)
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownForm(BaseModel):
|
||||||
|
md: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/markdown")
|
||||||
|
async def get_html_from_markdown(
|
||||||
|
form_data: MarkdownForm,
|
||||||
|
):
|
||||||
|
return {"html": markdown.markdown(form_data.md)}
|
||||||
|
|
||||||
|
|
||||||
|
class ChatForm(BaseModel):
|
||||||
|
title: str
|
||||||
|
messages: List[dict]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/pdf")
|
||||||
|
async def download_chat_as_pdf(
|
||||||
|
form_data: ChatForm,
|
||||||
|
):
|
||||||
|
pdf = FPDF()
|
||||||
|
pdf.add_page()
|
||||||
|
|
||||||
|
STATIC_DIR = "./static"
|
||||||
|
FONTS_DIR = f"{STATIC_DIR}/fonts"
|
||||||
|
|
||||||
|
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
||||||
|
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
||||||
|
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
|
||||||
|
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
|
||||||
|
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
|
||||||
|
|
||||||
|
pdf.set_font("NotoSans", size=12)
|
||||||
|
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
|
||||||
|
|
||||||
|
pdf.set_auto_page_break(auto=True, margin=15)
|
||||||
|
|
||||||
|
# Adjust the effective page width for multi_cell
|
||||||
|
effective_page_width = (
|
||||||
|
pdf.w - 2 * pdf.l_margin - 10
|
||||||
|
) # Subtracted an additional 10 for extra padding
|
||||||
|
|
||||||
|
# Add chat messages
|
||||||
|
for message in form_data.messages:
|
||||||
|
role = message["role"]
|
||||||
|
content = message["content"]
|
||||||
|
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
|
||||||
|
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
|
||||||
|
pdf.ln(1) # Extra space between messages
|
||||||
|
|
||||||
|
pdf.set_font("NotoSans", size=10) # Regular for content
|
||||||
|
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
|
||||||
|
pdf.ln(1.5) # Extra space between messages
|
||||||
|
|
||||||
|
# Save the pdf with name .pdf
|
||||||
|
pdf_bytes = pdf.output()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=bytes(pdf_bytes),
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={"Content-Disposition": f"attachment;filename=chat.pdf"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/db/download")
|
@router.get("/db/download")
|
||||||
async def download_db(user=Depends(get_admin_user)):
|
async def download_db(user=Depends(get_admin_user)):
|
||||||
|
if not ENABLE_ADMIN_EXPORT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
f"{DATA_DIR}/webui.db",
|
f"{DATA_DIR}/webui.db",
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
|
|||||||
+169
-68
@@ -18,6 +18,51 @@ from secrets import token_bytes
|
|||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LOGGING
|
||||||
|
####################################
|
||||||
|
|
||||||
|
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
||||||
|
|
||||||
|
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
||||||
|
if GLOBAL_LOG_LEVEL in log_levels:
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
||||||
|
else:
|
||||||
|
GLOBAL_LOG_LEVEL = "INFO"
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||||
|
|
||||||
|
log_sources = [
|
||||||
|
"AUDIO",
|
||||||
|
"COMFYUI",
|
||||||
|
"CONFIG",
|
||||||
|
"DB",
|
||||||
|
"IMAGES",
|
||||||
|
"LITELLM",
|
||||||
|
"MAIN",
|
||||||
|
"MODELS",
|
||||||
|
"OLLAMA",
|
||||||
|
"OPENAI",
|
||||||
|
"RAG",
|
||||||
|
"WEBHOOK",
|
||||||
|
]
|
||||||
|
|
||||||
|
SRC_LOG_LEVELS = {}
|
||||||
|
|
||||||
|
for source in log_sources:
|
||||||
|
log_env_var = source + "_LOG_LEVEL"
|
||||||
|
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
||||||
|
if SRC_LOG_LEVELS[source] not in log_levels:
|
||||||
|
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
||||||
|
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
||||||
|
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Load .env file
|
||||||
|
####################################
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
|
||||||
@@ -25,8 +70,8 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
log.warning("dotenv not installed, skipping...")
|
log.warning("dotenv not installed, skipping...")
|
||||||
|
|
||||||
WEBUI_NAME = "Open WebUI"
|
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||||
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# ENV (dev,test,prod)
|
# ENV (dev,test,prod)
|
||||||
@@ -101,64 +146,6 @@ for version in soup.find_all("h2"):
|
|||||||
|
|
||||||
CHANGELOG = changelog_json
|
CHANGELOG = changelog_json
|
||||||
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# LOGGING
|
|
||||||
####################################
|
|
||||||
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
|
||||||
|
|
||||||
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
|
||||||
if GLOBAL_LOG_LEVEL in log_levels:
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
|
||||||
else:
|
|
||||||
GLOBAL_LOG_LEVEL = "INFO"
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
|
||||||
|
|
||||||
log_sources = ["AUDIO", "CONFIG", "DB", "IMAGES", "LITELLM", "MAIN", "MODELS", "OLLAMA", "OPENAI", "RAG"]
|
|
||||||
|
|
||||||
SRC_LOG_LEVELS = {}
|
|
||||||
|
|
||||||
for source in log_sources:
|
|
||||||
log_env_var = source + "_LOG_LEVEL"
|
|
||||||
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
|
||||||
if SRC_LOG_LEVELS[source] not in log_levels:
|
|
||||||
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
|
||||||
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
|
||||||
|
|
||||||
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
|
||||||
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# CUSTOM_NAME
|
|
||||||
####################################
|
|
||||||
|
|
||||||
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
|
||||||
if CUSTOM_NAME:
|
|
||||||
try:
|
|
||||||
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
|
||||||
data = r.json()
|
|
||||||
if r.ok:
|
|
||||||
if "logo" in data:
|
|
||||||
url = (
|
|
||||||
f"https://api.openwebui.com{data['logo']}"
|
|
||||||
if data["logo"][0] == "/"
|
|
||||||
else data["logo"]
|
|
||||||
)
|
|
||||||
|
|
||||||
r = requests.get(url, stream=True)
|
|
||||||
if r.status_code == 200:
|
|
||||||
with open("./static/favicon.png", "wb") as f:
|
|
||||||
r.raw.decode_content = True
|
|
||||||
shutil.copyfileobj(r.raw, f)
|
|
||||||
|
|
||||||
WEBUI_NAME = data["name"]
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(e)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# DATA/FRONTEND BUILD DIR
|
# DATA/FRONTEND BUILD DIR
|
||||||
####################################
|
####################################
|
||||||
@@ -172,6 +159,47 @@ try:
|
|||||||
except:
|
except:
|
||||||
CONFIG_DATA = {}
|
CONFIG_DATA = {}
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Static DIR
|
||||||
|
####################################
|
||||||
|
|
||||||
|
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
|
||||||
|
|
||||||
|
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# CUSTOM_NAME
|
||||||
|
####################################
|
||||||
|
|
||||||
|
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
||||||
|
|
||||||
|
if CUSTOM_NAME:
|
||||||
|
try:
|
||||||
|
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
||||||
|
data = r.json()
|
||||||
|
if r.ok:
|
||||||
|
if "logo" in data:
|
||||||
|
WEBUI_FAVICON_URL = url = (
|
||||||
|
f"https://api.openwebui.com{data['logo']}"
|
||||||
|
if data["logo"][0] == "/"
|
||||||
|
else data["logo"]
|
||||||
|
)
|
||||||
|
|
||||||
|
r = requests.get(url, stream=True)
|
||||||
|
if r.status_code == 200:
|
||||||
|
with open(f"{STATIC_DIR}/favicon.png", "wb") as f:
|
||||||
|
r.raw.decode_content = True
|
||||||
|
shutil.copyfileobj(r.raw, f)
|
||||||
|
|
||||||
|
WEBUI_NAME = data["name"]
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if WEBUI_NAME != "Open WebUI":
|
||||||
|
WEBUI_NAME += " (Open WebUI)"
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# File Upload DIR
|
# File Upload DIR
|
||||||
####################################
|
####################################
|
||||||
@@ -238,7 +266,8 @@ OLLAMA_API_BASE_URL = os.environ.get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||||
|
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
||||||
|
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
||||||
|
|
||||||
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
||||||
OLLAMA_BASE_URL = (
|
OLLAMA_BASE_URL = (
|
||||||
@@ -248,8 +277,15 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ENV == "prod":
|
if ENV == "prod":
|
||||||
if OLLAMA_BASE_URL == "/ollama":
|
if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
|
||||||
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
if USE_OLLAMA_DOCKER.lower() == "true":
|
||||||
|
# if you use all-in-one docker container (Open WebUI + Ollama)
|
||||||
|
# with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
|
||||||
|
OLLAMA_BASE_URL = "http://localhost:11434"
|
||||||
|
else:
|
||||||
|
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
||||||
|
elif K8S_FLAG:
|
||||||
|
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
|
||||||
|
|
||||||
|
|
||||||
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
||||||
@@ -285,6 +321,18 @@ OPENAI_API_BASE_URLS = [
|
|||||||
for url in OPENAI_API_BASE_URLS.split(";")
|
for url in OPENAI_API_BASE_URLS.split(";")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
OPENAI_API_KEY = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
OPENAI_API_KEY = OPENAI_API_KEYS[
|
||||||
|
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||||
|
]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI
|
# WEBUI
|
||||||
####################################
|
####################################
|
||||||
@@ -334,6 +382,8 @@ MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
|||||||
|
|
||||||
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
||||||
|
|
||||||
|
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_VERSION
|
# WEBUI_VERSION
|
||||||
####################################
|
####################################
|
||||||
@@ -345,6 +395,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
WEBUI_AUTH = True
|
WEBUI_AUTH = True
|
||||||
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||||
|
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_SECRET_KEY
|
# WEBUI_SECRET_KEY
|
||||||
@@ -365,12 +418,28 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2)
|
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||||
RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
|
||||||
# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
|
||||||
RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get(
|
|
||||||
"RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu"
|
RAG_EMBEDDING_MODEL = os.environ.get(
|
||||||
|
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
|
||||||
)
|
)
|
||||||
|
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
|
||||||
|
|
||||||
|
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||||
|
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||||
|
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||||
|
|
||||||
|
if USE_CUDA.lower() == "true":
|
||||||
|
DEVICE_TYPE = "cuda"
|
||||||
|
else:
|
||||||
|
DEVICE_TYPE = "cpu"
|
||||||
|
|
||||||
|
|
||||||
CHROMA_CLIENT = chromadb.PersistentClient(
|
CHROMA_CLIENT = chromadb.PersistentClient(
|
||||||
path=CHROMA_DATA_PATH,
|
path=CHROMA_DATA_PATH,
|
||||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||||
@@ -393,17 +462,49 @@ And answer according to the language of the user's question.
|
|||||||
Given the context information, answer the query.
|
Given the context information, answer the query.
|
||||||
Query: [query]"""
|
Query: [query]"""
|
||||||
|
|
||||||
|
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||||
|
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Transcribe
|
# Transcribe
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
|
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
|
||||||
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
|
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
|
||||||
|
WHISPER_MODEL_AUTO_UPDATE = (
|
||||||
|
os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Images
|
# Images
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
ENABLE_IMAGE_GENERATION = (
|
||||||
|
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
|
||||||
|
)
|
||||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||||
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
||||||
|
|
||||||
|
|
||||||
|
IMAGES_OPENAI_API_BASE_URL = os.getenv(
|
||||||
|
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
|
||||||
|
)
|
||||||
|
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Audio
|
||||||
|
####################################
|
||||||
|
|
||||||
|
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
|
||||||
|
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# LiteLLM
|
||||||
|
####################################
|
||||||
|
|
||||||
|
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
|
||||||
|
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
|
||||||
|
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
|
||||||
|
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
|
||||||
+10
-1
@@ -3,6 +3,10 @@ from enum import Enum
|
|||||||
|
|
||||||
class MESSAGES(str, Enum):
|
class MESSAGES(str, Enum):
|
||||||
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
|
||||||
|
MODEL_DELETED = (
|
||||||
|
lambda model="": f"The model '{model}' has been deleted successfully."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WEBHOOK_MESSAGES(str, Enum):
|
class WEBHOOK_MESSAGES(str, Enum):
|
||||||
@@ -20,6 +24,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
||||||
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
||||||
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
||||||
|
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
|
||||||
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
||||||
USERNAME_TAKEN = (
|
USERNAME_TAKEN = (
|
||||||
"Uh-oh! This username is already registered. Please choose another username."
|
"Uh-oh! This username is already registered. Please choose another username."
|
||||||
@@ -36,6 +41,7 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
INVALID_PASSWORD = (
|
INVALID_PASSWORD = (
|
||||||
"The password provided is incorrect. Please check for typos and try again."
|
"The password provided is incorrect. Please check for typos and try again."
|
||||||
)
|
)
|
||||||
|
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
|
||||||
UNAUTHORIZED = "401 Unauthorized"
|
UNAUTHORIZED = "401 Unauthorized"
|
||||||
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
||||||
ACTION_PROHIBITED = (
|
ACTION_PROHIBITED = (
|
||||||
@@ -58,5 +64,8 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
||||||
|
|
||||||
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
||||||
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
|
OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
|
||||||
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
||||||
|
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
|
||||||
|
|
||||||
|
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
||||||
+23
-34
@@ -1,35 +1,24 @@
|
|||||||
{
|
{
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"ui": {
|
"ui": {
|
||||||
"prompt_suggestions": [
|
"default_locale": "en-US",
|
||||||
{
|
"prompt_suggestions": [
|
||||||
"title": [
|
{
|
||||||
"Help me study",
|
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||||
"vocabulary for a college entrance exam"
|
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
|
||||||
],
|
},
|
||||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
|
{
|
||||||
},
|
"title": ["Give me ideas", "for what to do with my kids' art"],
|
||||||
{
|
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
|
||||||
"title": [
|
},
|
||||||
"Give me ideas",
|
{
|
||||||
"for what to do with my kids' art"
|
"title": ["Tell me a fun fact", "about the Roman Empire"],
|
||||||
],
|
"content": "Tell me a random fun fact about the Roman Empire"
|
||||||
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
|
},
|
||||||
},
|
{
|
||||||
{
|
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||||
"title": [
|
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||||
"Tell me a fun fact",
|
}
|
||||||
"about the Roman Empire"
|
]
|
||||||
],
|
}
|
||||||
"content": "Tell me a random fun fact about the Roman Empire"
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": [
|
|
||||||
"Show me a code snippet",
|
|
||||||
"of a website's sticky header"
|
|
||||||
],
|
|
||||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+75
-16
@@ -5,6 +5,7 @@ import time
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
import aiohttp
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status
|
from fastapi import FastAPI, Request, Depends, status
|
||||||
@@ -18,12 +19,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
|
|
||||||
from apps.ollama.main import app as ollama_app
|
from apps.ollama.main import app as ollama_app
|
||||||
from apps.openai.main import app as openai_app
|
from apps.openai.main import app as openai_app
|
||||||
from apps.litellm.main import app as litellm_app, startup as litellm_app_startup
|
|
||||||
|
from apps.litellm.main import (
|
||||||
|
app as litellm_app,
|
||||||
|
start_litellm_background,
|
||||||
|
shutdown_litellm_background,
|
||||||
|
)
|
||||||
from apps.audio.main import app as audio_app
|
from apps.audio.main import app as audio_app
|
||||||
from apps.images.main import app as images_app
|
from apps.images.main import app as images_app
|
||||||
from apps.rag.main import app as rag_app
|
from apps.rag.main import app as rag_app
|
||||||
from apps.web.main import app as webui_app
|
from apps.web.main import app as webui_app
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -32,16 +39,20 @@ from utils.utils import get_admin_user
|
|||||||
from apps.rag.utils import rag_messages
|
from apps.rag.utils import rag_messages
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
|
CONFIG_DATA,
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
ENV,
|
ENV,
|
||||||
VERSION,
|
VERSION,
|
||||||
CHANGELOG,
|
CHANGELOG,
|
||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
|
CACHE_DIR,
|
||||||
|
STATIC_DIR,
|
||||||
MODEL_FILTER_ENABLED,
|
MODEL_FILTER_ENABLED,
|
||||||
MODEL_FILTER_LIST,
|
MODEL_FILTER_LIST,
|
||||||
GLOBAL_LOG_LEVEL,
|
GLOBAL_LOG_LEVEL,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
WEBHOOK_URL,
|
WEBHOOK_URL,
|
||||||
|
ENABLE_ADMIN_EXPORT,
|
||||||
)
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -49,6 +60,7 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
class SPAStaticFiles(StaticFiles):
|
class SPAStaticFiles(StaticFiles):
|
||||||
async def get_response(self, path: str, scope):
|
async def get_response(self, path: str, scope):
|
||||||
try:
|
try:
|
||||||
@@ -60,6 +72,21 @@ class SPAStaticFiles(StaticFiles):
|
|||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"""
|
||||||
|
___ __ __ _ _ _ ___
|
||||||
|
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
|
||||||
|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
|
||||||
|
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
|
||||||
|
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
v{VERSION} - building the best open-source AI user interface.
|
||||||
|
https://github.com/open-webui/open-webui
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
@@ -67,7 +94,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
|||||||
|
|
||||||
app.state.WEBHOOK_URL = WEBHOOK_URL
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
|
|
||||||
@@ -88,14 +114,17 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
|||||||
# Example: Add a new key-value pair or modify existing ones
|
# Example: Add a new key-value pair or modify existing ones
|
||||||
# data["modified"] = True # Example modification
|
# data["modified"] = True # Example modification
|
||||||
if "docs" in data:
|
if "docs" in data:
|
||||||
|
|
||||||
data = {**data}
|
data = {**data}
|
||||||
data["messages"] = rag_messages(
|
data["messages"] = rag_messages(
|
||||||
data["docs"],
|
data["docs"],
|
||||||
data["messages"],
|
data["messages"],
|
||||||
rag_app.state.RAG_TEMPLATE,
|
rag_app.state.RAG_TEMPLATE,
|
||||||
rag_app.state.TOP_K,
|
rag_app.state.TOP_K,
|
||||||
|
rag_app.state.RAG_EMBEDDING_ENGINE,
|
||||||
|
rag_app.state.RAG_EMBEDDING_MODEL,
|
||||||
rag_app.state.sentence_transformer_ef,
|
rag_app.state.sentence_transformer_ef,
|
||||||
|
rag_app.state.OPENAI_API_KEY,
|
||||||
|
rag_app.state.OPENAI_API_BASE_URL,
|
||||||
)
|
)
|
||||||
del data["docs"]
|
del data["docs"]
|
||||||
|
|
||||||
@@ -147,7 +176,7 @@ async def check_url(request: Request, call_next):
|
|||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def on_startup():
|
async def on_startup():
|
||||||
await litellm_app_startup()
|
asyncio.create_task(start_litellm_background())
|
||||||
|
|
||||||
|
|
||||||
app.mount("/api/v1", webui_app)
|
app.mount("/api/v1", webui_app)
|
||||||
@@ -163,14 +192,23 @@ app.mount("/rag/api/v1", rag_app)
|
|||||||
|
|
||||||
@app.get("/api/config")
|
@app.get("/api/config")
|
||||||
async def get_app_config():
|
async def get_app_config():
|
||||||
|
# Checking and Handling the Absence of 'ui' in CONFIG_DATA
|
||||||
|
|
||||||
|
default_locale = "en-US"
|
||||||
|
if "ui" in CONFIG_DATA:
|
||||||
|
default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US")
|
||||||
|
|
||||||
|
# The Rest of the Function Now Uses the Variables Defined Above
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"name": WEBUI_NAME,
|
"name": WEBUI_NAME,
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
|
"default_locale": default_locale,
|
||||||
"images": images_app.state.ENABLED,
|
"images": images_app.state.ENABLED,
|
||||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
|
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||||
|
"admin_export_enabled": ENABLE_ADMIN_EXPORT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +229,6 @@ class ModelFilterConfigForm(BaseModel):
|
|||||||
async def update_model_filter_config(
|
async def update_model_filter_config(
|
||||||
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
||||||
app.state.MODEL_FILTER_LIST = form_data.models
|
app.state.MODEL_FILTER_LIST = form_data.models
|
||||||
|
|
||||||
@@ -201,6 +238,9 @@ async def update_model_filter_config(
|
|||||||
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
|
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"enabled": app.state.MODEL_FILTER_ENABLED,
|
"enabled": app.state.MODEL_FILTER_ENABLED,
|
||||||
"models": app.state.MODEL_FILTER_LIST,
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
@@ -231,7 +271,6 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@app.get("/api/version")
|
@app.get("/api/version")
|
||||||
async def get_app_config():
|
async def get_app_config():
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
}
|
}
|
||||||
@@ -239,32 +278,52 @@ async def get_app_config():
|
|||||||
|
|
||||||
@app.get("/api/changelog")
|
@app.get("/api/changelog")
|
||||||
async def get_app_changelog():
|
async def get_app_changelog():
|
||||||
return CHANGELOG
|
return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version/updates")
|
@app.get("/api/version/updates")
|
||||||
async def get_app_latest_release_version():
|
async def get_app_latest_release_version():
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
async with aiohttp.ClientSession() as session:
|
||||||
f"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
async with session.get(
|
||||||
)
|
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
||||||
response.raise_for_status()
|
) as response:
|
||||||
latest_version = response.json()["tag_name"]
|
response.raise_for_status()
|
||||||
|
data = await response.json()
|
||||||
|
latest_version = data["tag_name"]
|
||||||
|
|
||||||
return {"current": VERSION, "latest": latest_version[1:]}
|
return {"current": VERSION, "latest": latest_version[1:]}
|
||||||
except Exception as e:
|
except aiohttp.ClientError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
|
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
@app.get("/manifest.json")
|
||||||
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
|
async def get_manifest_json():
|
||||||
|
return {
|
||||||
|
"name": WEBUI_NAME,
|
||||||
|
"short_name": WEBUI_NAME,
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#343541",
|
||||||
|
"theme_color": "#343541",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
|
||||||
|
|
||||||
app.mount(
|
app.mount(
|
||||||
"/",
|
"/",
|
||||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||||
name="spa-static-files",
|
name="spa-static-files",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
await shutdown_litellm_background()
|
||||||
@@ -14,14 +14,20 @@ uuid
|
|||||||
requests
|
requests
|
||||||
aiohttp
|
aiohttp
|
||||||
peewee
|
peewee
|
||||||
|
peewee-migrate
|
||||||
bcrypt
|
bcrypt
|
||||||
|
|
||||||
litellm==1.30.7
|
litellm==1.35.17
|
||||||
|
litellm[proxy]==1.35.17
|
||||||
|
|
||||||
|
boto3
|
||||||
|
|
||||||
argon2-cffi
|
argon2-cffi
|
||||||
apscheduler
|
apscheduler
|
||||||
google-generativeai
|
google-generativeai
|
||||||
|
|
||||||
langchain
|
langchain
|
||||||
|
langchain-chroma
|
||||||
langchain-community
|
langchain-community
|
||||||
fake_useragent
|
fake_useragent
|
||||||
chromadb
|
chromadb
|
||||||
@@ -39,6 +45,9 @@ xlrd
|
|||||||
opencv-python-headless
|
opencv-python-headless
|
||||||
rapidocr-onnxruntime
|
rapidocr-onnxruntime
|
||||||
|
|
||||||
|
fpdf2
|
||||||
|
rank_bm25
|
||||||
|
|
||||||
faster-whisper
|
faster-whisper
|
||||||
|
|
||||||
PyJWT
|
PyJWT
|
||||||
|
|||||||
+16
-6
@@ -7,16 +7,26 @@ KEY_FILE=.webui_secret_key
|
|||||||
|
|
||||||
PORT="${PORT:-8080}"
|
PORT="${PORT:-8080}"
|
||||||
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
||||||
echo No WEBUI_SECRET_KEY provided
|
echo "No WEBUI_SECRET_KEY provided"
|
||||||
|
|
||||||
if ! [ -e "$KEY_FILE" ]; then
|
if ! [ -e "$KEY_FILE" ]; then
|
||||||
echo Generating WEBUI_SECRET_KEY
|
echo "Generating WEBUI_SECRET_KEY"
|
||||||
# Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one.
|
# Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one.
|
||||||
echo $(head -c 12 /dev/random | base64) > $KEY_FILE
|
echo $(head -c 12 /dev/random | base64) > "$KEY_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo Loading WEBUI_SECRET_KEY from $KEY_FILE
|
echo "Loading WEBUI_SECRET_KEY from $KEY_FILE"
|
||||||
WEBUI_SECRET_KEY=`cat $KEY_FILE`
|
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
|
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
|
||||||
|
echo "USE_OLLAMA is set to true, starting ollama serve."
|
||||||
|
ollama serve &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$USE_CUDA_DOCKER" = "true" ]; then
|
||||||
|
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
||||||
|
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||||
|
fi
|
||||||
|
|
||||||
|
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,8 @@
|
|||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi import HTTPException, status, Depends
|
from fastapi import HTTPException, status, Depends
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
@@ -8,6 +10,7 @@ from passlib.context import CryptContext
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import requests
|
import requests
|
||||||
import jwt
|
import jwt
|
||||||
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
import config
|
import config
|
||||||
|
|
||||||
@@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
|
|||||||
return auth_header[len("Bearer ") :]
|
return auth_header[len("Bearer ") :]
|
||||||
|
|
||||||
|
|
||||||
|
def create_api_key():
|
||||||
|
key = str(uuid.uuid4()).replace("-", "")
|
||||||
|
return f"sk-{key}"
|
||||||
|
|
||||||
|
|
||||||
def get_http_authorization_cred(auth_header: str):
|
def get_http_authorization_cred(auth_header: str):
|
||||||
try:
|
try:
|
||||||
scheme, credentials = auth_header.split(" ")
|
scheme, credentials = auth_header.split(" ")
|
||||||
@@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
|
|||||||
def get_current_user(
|
def get_current_user(
|
||||||
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
||||||
):
|
):
|
||||||
|
# auth by api key
|
||||||
|
if auth_token.credentials.startswith("sk-"):
|
||||||
|
return get_current_user_by_api_key(auth_token.credentials)
|
||||||
|
# auth by jwt token
|
||||||
data = decode_token(auth_token.credentials)
|
data = decode_token(auth_token.credentials)
|
||||||
if data != None and "id" in data:
|
if data != None and "id" in data:
|
||||||
user = Users.get_user_by_id(data["id"])
|
user = Users.get_user_by_id(data["id"])
|
||||||
@@ -85,6 +97,16 @@ def get_current_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_by_api_key(api_key: str):
|
||||||
|
user = Users.get_user_by_api_key(api_key)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_verified_user(user=Depends(get_current_user)):
|
def get_verified_user(user=Depends(get_current_user)):
|
||||||
if user.role not in {"user", "admin"}:
|
if user.role not in {"user", "admin"}:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -1,20 +1,54 @@
|
|||||||
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
|
||||||
|
|
||||||
|
|
||||||
def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
||||||
try:
|
try:
|
||||||
payload = {}
|
payload = {}
|
||||||
|
|
||||||
if "https://hooks.slack.com" in url:
|
# Slack and Google Chat Webhooks
|
||||||
|
if "https://hooks.slack.com" in url or "https://chat.googleapis.com" in url:
|
||||||
payload["text"] = message
|
payload["text"] = message
|
||||||
|
# Discord Webhooks
|
||||||
elif "https://discord.com/api/webhooks" in url:
|
elif "https://discord.com/api/webhooks" in url:
|
||||||
payload["content"] = message
|
payload["content"] = message
|
||||||
|
# Microsoft Teams Webhooks
|
||||||
|
elif "webhook.office.com" in url:
|
||||||
|
action = event_data.get("action", "undefined")
|
||||||
|
facts = [
|
||||||
|
{"name": name, "value": value}
|
||||||
|
for name, value in json.loads(event_data.get("user", {})).items()
|
||||||
|
]
|
||||||
|
payload = {
|
||||||
|
"@type": "MessageCard",
|
||||||
|
"@context": "http://schema.org/extensions",
|
||||||
|
"themeColor": "0076D7",
|
||||||
|
"summary": message,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"activityTitle": message,
|
||||||
|
"activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
|
||||||
|
"activityImage": WEBUI_FAVICON_URL,
|
||||||
|
"facts": facts,
|
||||||
|
"markdown": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
# Default Payload
|
||||||
else:
|
else:
|
||||||
payload = {**event_data}
|
payload = {**event_data}
|
||||||
|
|
||||||
|
log.debug(f"payload: {payload}")
|
||||||
r = requests.post(url, json=payload)
|
r = requests.post(url, json=payload)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
log.debug(f"r.text: {r.text}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
return False
|
return False
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
ollama:
|
||||||
|
devices:
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
image: ollama/ollama:${OLLAMA_DOCKER_TAG-rocm}
|
||||||
|
environment:
|
||||||
|
- 'HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION-11.0.0}'
|
||||||
+2
-2
@@ -8,7 +8,7 @@ services:
|
|||||||
pull_policy: always
|
pull_policy: always
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
|
||||||
|
|
||||||
open-webui:
|
open-webui:
|
||||||
build:
|
build:
|
||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
OLLAMA_BASE_URL: '/ollama'
|
OLLAMA_BASE_URL: '/ollama'
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: ghcr.io/open-webui/open-webui:main
|
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
|
||||||
container_name: open-webui
|
container_name: open-webui
|
||||||
volumes:
|
volumes:
|
||||||
- open-webui:/app/backend/data
|
- open-webui:/app/backend/data
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ ollama
|
|||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "ollama.url" -}}
|
{{- define "ollama.url" -}}
|
||||||
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
|
{{- if .Values.ollama.externalHost }}
|
||||||
|
{{- printf .Values.ollama.externalHost }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "http://%s.%s.svc.cluster.local:%d" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- define "chart.name" -}}
|
{{- define "chart.name" -}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if not .Values.ollama.externalHost }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
@@ -19,3 +20,4 @@ spec:
|
|||||||
port: {{ .port }}
|
port: {{ .port }}
|
||||||
targetPort: http
|
targetPort: http
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if not .Values.ollama.externalHost }}
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
@@ -88,9 +89,10 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.ollama.persistence.size | quote }}
|
storage: {{ .Values.ollama.persistence.size | quote }}
|
||||||
storageClass: {{ .Values.ollama.persistence.storageClass }}
|
storageClassName: {{ .Values.ollama.persistence.storageClass }}
|
||||||
{{- with .Values.ollama.persistence.selector }}
|
{{- with .Values.ollama.persistence.selector }}
|
||||||
selector:
|
selector:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -17,7 +17,9 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.webui.persistence.size }}
|
storage: {{ .Values.webui.persistence.size }}
|
||||||
storageClass: {{ .Values.webui.persistence.storageClass }}
|
{{- if .Values.webui.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.webui.persistence.storageClass }}
|
||||||
|
{{- end }}
|
||||||
{{- with .Values.webui.persistence.selector }}
|
{{- with .Values.webui.persistence.selector }}
|
||||||
selector:
|
selector:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
ollama:
|
ollama:
|
||||||
|
externalHost: ""
|
||||||
annotations: {}
|
annotations: {}
|
||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: webui-volume
|
- name: webui-volume
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: ollama-webui-pvc
|
claimName: open-webui-pvc
|
||||||
@@ -2,8 +2,8 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: ollama-webui
|
app: open-webui
|
||||||
name: ollama-webui-pvc
|
name: open-webui-pvc
|
||||||
namespace: open-webui
|
namespace: open-webui
|
||||||
spec:
|
spec:
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
|||||||
Generated
+195
-11
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.114",
|
"version": "0.1.121",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.114",
|
"version": "0.1.121",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
|
"jspdf": "^2.5.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
@@ -1067,6 +1068,12 @@
|
|||||||
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -1402,6 +1409,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/atob": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||||
|
"bin": {
|
||||||
|
"atob": "bin/atob.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.19",
|
"version": "10.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||||
@@ -1459,6 +1477,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -1666,6 +1693,17 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/btoa": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||||
|
"bin": {
|
||||||
|
"btoa": "bin/btoa.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -1758,6 +1796,31 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -1944,6 +2007,17 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.36.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
|
||||||
|
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-util-is": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
@@ -1964,6 +2038,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||||
@@ -2156,6 +2239,12 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "2.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz",
|
||||||
|
"integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
@@ -2584,6 +2673,11 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||||
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -3003,6 +3097,19 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
@@ -3403,10 +3510,27 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.14.0",
|
||||||
|
"atob": "^2.1.2",
|
||||||
|
"btoa": "^1.2.1",
|
||||||
|
"fflate": "^0.4.8"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.6",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^2.2.0",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/katex": {
|
"node_modules/katex": {
|
||||||
"version": "0.16.9",
|
"version": "0.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
|
||||||
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
|
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://opencollective.com/katex",
|
"https://opencollective.com/katex",
|
||||||
"https://github.com/sponsors/katex"
|
"https://github.com/sponsors/katex"
|
||||||
@@ -3971,6 +4095,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/periscopic": {
|
"node_modules/periscopic": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||||
@@ -4391,6 +4521,15 @@
|
|||||||
"rimraf": "bin.js"
|
"rimraf": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -4494,6 +4633,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
@@ -4814,6 +4962,15 @@
|
|||||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stream-composer": {
|
"node_modules/stream-composer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz",
|
||||||
@@ -5215,6 +5372,15 @@
|
|||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symlink-or-copy": {
|
"node_modules/symlink-or-copy": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
|
||||||
@@ -5353,6 +5519,15 @@
|
|||||||
"streamx": "^2.12.5"
|
"streamx": "^2.12.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -5513,9 +5688,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "5.28.3",
|
"version": "5.28.4",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
|
||||||
"integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==",
|
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^2.0.0"
|
"@fastify/busboy": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5583,6 +5758,15 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
@@ -5676,9 +5860,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.2",
|
"version": "4.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||||
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
|
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.115",
|
"version": "0.1.121",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"lint:types": "npm run check",
|
"lint:types": "npm run check",
|
||||||
"lint:backend": "pylint backend/",
|
"lint:backend": "pylint backend/",
|
||||||
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
|
||||||
"format:backend": "yapf --recursive backend -p -i",
|
"format:backend": "black . --exclude \"/venv/\"",
|
||||||
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -53,10 +53,11 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
|
"jspdf": "^2.5.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,3 +78,7 @@ select {
|
|||||||
/* for Chrome */
|
/* for Chrome */
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.katex-mathml {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
+6
-2
@@ -3,13 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<script>
|
<script>
|
||||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||||
(() => {
|
(() => {
|
||||||
if (
|
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else if (
|
||||||
localStorage.theme === 'light' ||
|
localStorage.theme === 'light' ||
|
||||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+100
-1
@@ -1,11 +1,73 @@
|
|||||||
import { AUDIO_API_BASE_URL } from '$lib/constants';
|
import { AUDIO_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export const getAudioConfig = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${AUDIO_API_BASE_URL}/config`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenAIConfigForm = {
|
||||||
|
url: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${AUDIO_API_BASE_URL}/config/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const transcribeAudio = async (token: string, file: File) => {
|
export const transcribeAudio = async (token: string, file: File) => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
const res = await fetch(`${AUDIO_API_BASE_URL}/transcribe`, {
|
const res = await fetch(`${AUDIO_API_BASE_URL}/transcriptions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -29,3 +91,40 @@ export const transcribeAudio = async (token: string, file: File) => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const synthesizeOpenAISpeech = async (
|
||||||
|
token: string = '',
|
||||||
|
speaker: string = 'alloy',
|
||||||
|
text: string = ''
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${AUDIO_API_BASE_URL}/speech`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'tts-1',
|
||||||
|
input: text,
|
||||||
|
voice: speaker
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
@@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userSignUp = async (name: string, email: string, password: string) => {
|
export const userSignUp = async (
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
profile_image_url: string
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
|
||||||
@@ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string)
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: name,
|
name: name,
|
||||||
email: email,
|
email: email,
|
||||||
password: password
|
password: password,
|
||||||
|
profile_image_url: profile_image_url
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -318,3 +324,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res.api_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res.api_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAPIKey = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
@@ -62,6 +62,37 @@ export const getChatList = async (token: string = '') => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getArchivedChatList = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archived`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllChats = async (token: string) => {
|
export const getAllChats = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -218,6 +249,134 @@ export const getChatById = async (token: string, id: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getChatByShareId = async (token: string, share_id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shareChatById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const archiveChatById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/archive`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSharedChatById = async (token: string, id: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateChatById = async (token: string, id: string, chat: object) => {
|
export const updateChatById = async (token: string, id: string, chat: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ export const updateImageGenerationConfig = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenAIKey = async (token: string = '') => {
|
export const getOpenAIConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/key`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -101,13 +101,13 @@ export const getOpenAIKey = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOpenAIKey = async (token: string = '', key: string) => {
|
export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/key/update`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -115,6 +115,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
key: key
|
key: key
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -136,7 +137,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => {
|
|||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name ?? model.id,
|
name: model.name ?? model.id,
|
||||||
external: true,
|
external: true,
|
||||||
source: 'litellm'
|
source: 'LiteLLM'
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||||
|
import { promptTemplate } from '$lib/utils';
|
||||||
|
|
||||||
export const getOllamaUrls = async (token: string = '') => {
|
export const getOllamaUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -144,7 +145,7 @@ export const generateTitle = async (
|
|||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
template = template.replace(/{{prompt}}/g, prompt);
|
template = promptTemplate(template, prompt);
|
||||||
|
|
||||||
console.log(template);
|
console.log(template);
|
||||||
|
|
||||||
@@ -219,6 +220,32 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateEmbeddings = async (token: string = '', model: string, text: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
prompt: text
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
|
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||||
|
import { promptTemplate } from '$lib/utils';
|
||||||
|
|
||||||
export const getOpenAIUrls = async (token: string = '') => {
|
export const getOpenAIUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
@@ -263,3 +264,53 @@ export const synthesizeOpenAISpeech = async (
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateTitle = async (
|
||||||
|
token: string = '',
|
||||||
|
template: string,
|
||||||
|
model: string,
|
||||||
|
prompt: string,
|
||||||
|
url: string = OPENAI_API_BASE_URL
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
template = promptTemplate(template, prompt);
|
||||||
|
|
||||||
|
console.log(template);
|
||||||
|
|
||||||
|
const res = await fetch(`${url}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: template
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res?.choices[0]?.message?.content ?? 'New Chat';
|
||||||
|
};
|
||||||
@@ -345,3 +345,71 @@ export const resetVectorDB = async (token: string) => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEmbeddingConfig = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/embedding`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenAIConfigForm = {
|
||||||
|
key: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddingModelUpdateForm = {
|
||||||
|
openai_config?: OpenAIConfigForm;
|
||||||
|
embedding_engine: string;
|
||||||
|
embedding_model: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/embedding/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
type TextStreamUpdate = {
|
||||||
|
done: boolean;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// createOpenAITextStream takes a ReadableStreamDefaultReader from an SSE response,
|
||||||
|
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
|
||||||
|
export async function createOpenAITextStream(
|
||||||
|
messageStream: ReadableStreamDefaultReader,
|
||||||
|
splitLargeDeltas: boolean
|
||||||
|
): Promise<AsyncGenerator<TextStreamUpdate>> {
|
||||||
|
let iterator = openAIStreamToIterator(messageStream);
|
||||||
|
if (splitLargeDeltas) {
|
||||||
|
iterator = streamLargeDeltasAsRandomChunks(iterator);
|
||||||
|
}
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* openAIStreamToIterator(
|
||||||
|
reader: ReadableStreamDefaultReader
|
||||||
|
): AsyncGenerator<TextStreamUpdate> {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
yield { done: true, value: '' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const lines = value.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line !== '') {
|
||||||
|
console.log(line);
|
||||||
|
if (line === 'data: [DONE]') {
|
||||||
|
yield { done: true, value: '' };
|
||||||
|
} else {
|
||||||
|
const data = JSON.parse(line.replace(/^data: /, ''));
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
yield { done: false, value: data.choices[0].delta.content ?? '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
|
||||||
|
// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once
|
||||||
|
async function* streamLargeDeltasAsRandomChunks(
|
||||||
|
iterator: AsyncGenerator<TextStreamUpdate>
|
||||||
|
): AsyncGenerator<TextStreamUpdate> {
|
||||||
|
for await (const textStreamUpdate of iterator) {
|
||||||
|
if (textStreamUpdate.done) {
|
||||||
|
yield textStreamUpdate;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let content = textStreamUpdate.value;
|
||||||
|
if (content.length < 5) {
|
||||||
|
yield { done: false, value: content };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (content != '') {
|
||||||
|
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
|
||||||
|
const chunk = content.slice(0, chunkSize);
|
||||||
|
yield { done: false, value: chunk };
|
||||||
|
await sleep(5);
|
||||||
|
content = content.slice(chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -22,6 +22,57 @@ export const getGravatarUrl = async (email: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const downloadChatAsPDF = async (chat: object) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: chat.title,
|
||||||
|
messages: chat.messages
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.blob();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHTMLFromMarkdown = async (md: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
md: md
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.html;
|
||||||
|
};
|
||||||
|
|
||||||
export const downloadDatabase = async (token: string) => {
|
export const downloadDatabase = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
localStorage.version = $config.version;
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { downloadDatabase } from '$lib/apis/utils';
|
import { downloadDatabase } from '$lib/apis/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { config } from '$lib/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -24,32 +25,34 @@
|
|||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
||||||
|
|
||||||
<button
|
{#if $config?.admin_export_enabled ?? true}
|
||||||
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
<button
|
||||||
type="button"
|
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
type="button"
|
||||||
// exportAllUserChats();
|
on:click={() => {
|
||||||
|
// exportAllUserChats();
|
||||||
|
|
||||||
downloadDatabase(localStorage.token);
|
downloadDatabase(localStorage.token);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -295,6 +295,13 @@
|
|||||||
|
|
||||||
const dropZone = document.querySelector('body');
|
const dropZone = document.querySelector('body');
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
console.log('Escape');
|
||||||
|
dragged = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDragOver = (e) => {
|
const onDragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragged = true;
|
dragged = true;
|
||||||
@@ -309,39 +316,38 @@
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
if (e.dataTransfer?.files) {
|
if (e.dataTransfer?.files) {
|
||||||
let reader = new FileReader();
|
const inputFiles = Array.from(e.dataTransfer?.files);
|
||||||
|
|
||||||
reader.onload = (event) => {
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
url: `${event.target.result}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputFiles = e.dataTransfer?.files;
|
|
||||||
|
|
||||||
if (inputFiles && inputFiles.length > 0) {
|
if (inputFiles && inputFiles.length > 0) {
|
||||||
const file = inputFiles[0];
|
inputFiles.forEach((file) => {
|
||||||
console.log(file, file.name.split('.').at(-1));
|
console.log(file, file.name.split('.').at(-1));
|
||||||
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||||
reader.readAsDataURL(file);
|
let reader = new FileReader();
|
||||||
} else if (
|
reader.onload = (event) => {
|
||||||
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
files = [
|
||||||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
...files,
|
||||||
) {
|
{
|
||||||
uploadDoc(file);
|
type: 'image',
|
||||||
} else {
|
url: `${event.target.result}`
|
||||||
toast.error(
|
}
|
||||||
$i18n.t(
|
];
|
||||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
};
|
||||||
{ file_type: file['type'] }
|
reader.readAsDataURL(file);
|
||||||
)
|
} else if (
|
||||||
);
|
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||||
uploadDoc(file);
|
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||||
}
|
) {
|
||||||
|
uploadDoc(file);
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(
|
||||||
|
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||||
|
{ file_type: file['type'] }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
uploadDoc(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`File not found.`));
|
toast.error($i18n.t(`File not found.`));
|
||||||
}
|
}
|
||||||
@@ -350,11 +356,15 @@
|
|||||||
dragged = false;
|
dragged = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
dropZone?.addEventListener('dragover', onDragOver);
|
dropZone?.addEventListener('dragover', onDragOver);
|
||||||
dropZone?.addEventListener('drop', onDrop);
|
dropZone?.addEventListener('drop', onDrop);
|
||||||
dropZone?.addEventListener('dragleave', onDragLeave);
|
dropZone?.addEventListener('dragleave', onDragLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
dropZone?.removeEventListener('dragover', onDragOver);
|
dropZone?.removeEventListener('dragover', onDragOver);
|
||||||
dropZone?.removeEventListener('drop', onDrop);
|
dropZone?.removeEventListener('drop', onDrop);
|
||||||
dropZone?.removeEventListener('dragleave', onDragLeave);
|
dropZone?.removeEventListener('dragleave', onDragLeave);
|
||||||
@@ -456,40 +466,42 @@
|
|||||||
bind:files={inputFiles}
|
bind:files={inputFiles}
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
|
multiple
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
url: `${event.target.result}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
inputFiles = null;
|
|
||||||
filesInputElement.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
if (inputFiles && inputFiles.length > 0) {
|
if (inputFiles && inputFiles.length > 0) {
|
||||||
const file = inputFiles[0];
|
const _inputFiles = Array.from(inputFiles);
|
||||||
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
_inputFiles.forEach((file) => {
|
||||||
reader.readAsDataURL(file);
|
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
|
||||||
} else if (
|
let reader = new FileReader();
|
||||||
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
reader.onload = (event) => {
|
||||||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
files = [
|
||||||
) {
|
...files,
|
||||||
uploadDoc(file);
|
{
|
||||||
filesInputElement.value = '';
|
type: 'image',
|
||||||
} else {
|
url: `${event.target.result}`
|
||||||
toast.error(
|
}
|
||||||
$i18n.t(
|
];
|
||||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
inputFiles = null;
|
||||||
{ file_type: file['type'] }
|
filesInputElement.value = '';
|
||||||
)
|
};
|
||||||
);
|
reader.readAsDataURL(file);
|
||||||
uploadDoc(file);
|
} else if (
|
||||||
filesInputElement.value = '';
|
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||||
}
|
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||||
|
) {
|
||||||
|
uploadDoc(file);
|
||||||
|
filesInputElement.value = '';
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(
|
||||||
|
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||||
|
{ file_type: file['type'] }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
uploadDoc(file);
|
||||||
|
filesInputElement.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error($i18n.t(`File not found.`));
|
toast.error($i18n.t(`File not found.`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
import Placeholder from './Messages/Placeholder.svelte';
|
import Placeholder from './Messages/Placeholder.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
|
export let readOnly = false;
|
||||||
export let sendPrompt: Function;
|
export let sendPrompt: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
export let regenerateResponse: Function;
|
export let regenerateResponse: Function;
|
||||||
@@ -41,40 +43,11 @@
|
|||||||
element.scrollTop = element.scrollHeight;
|
element.scrollTop = element.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = (text) => {
|
const copyToClipboardWithToast = async (text) => {
|
||||||
if (!navigator.clipboard) {
|
const res = await copyToClipboard(text);
|
||||||
var textArea = document.createElement('textarea');
|
if (res) {
|
||||||
textArea.value = text;
|
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||||
|
|
||||||
// Avoid scrolling to bottom
|
|
||||||
textArea.style.top = '0';
|
|
||||||
textArea.style.left = '0';
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var successful = document.execCommand('copy');
|
|
||||||
var msg = successful ? 'successful' : 'unsuccessful';
|
|
||||||
console.log('Fallback: Copying text command was ' + msg);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
navigator.clipboard.writeText(text).then(
|
|
||||||
function () {
|
|
||||||
console.log('Async: Copying to clipboard was successful!');
|
|
||||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
console.error('Async: Could not copy text: ', err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmEditMessage = async (messageId, content) => {
|
const confirmEditMessage = async (messageId, content) => {
|
||||||
@@ -106,12 +79,8 @@
|
|||||||
await sendPrompt(userPrompt, userMessageId, chatId);
|
await sendPrompt(userPrompt, userMessageId, chatId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmEditResponseMessage = async (messageId, content) => {
|
const updateChatMessages = async () => {
|
||||||
history.messages[messageId].originalContent = history.messages[messageId].content;
|
|
||||||
history.messages[messageId].content = content;
|
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
await updateChatById(localStorage.token, chatId, {
|
await updateChatById(localStorage.token, chatId, {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
history: history
|
history: history
|
||||||
@@ -120,15 +89,20 @@
|
|||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rateMessage = async (messageId, rating) => {
|
const confirmEditResponseMessage = async (messageId, content) => {
|
||||||
history.messages[messageId].rating = rating;
|
history.messages[messageId].originalContent = history.messages[messageId].content;
|
||||||
await tick();
|
history.messages[messageId].content = content;
|
||||||
await updateChatById(localStorage.token, chatId, {
|
|
||||||
messages: messages,
|
|
||||||
history: history
|
|
||||||
});
|
|
||||||
|
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await updateChatMessages();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rateMessage = async (messageId, rating) => {
|
||||||
|
history.messages[messageId].annotation = {
|
||||||
|
...history.messages[messageId].annotation,
|
||||||
|
rating: rating
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateChatMessages();
|
||||||
};
|
};
|
||||||
|
|
||||||
const showPreviousMessage = async (message) => {
|
const showPreviousMessage = async (message) => {
|
||||||
@@ -317,6 +291,7 @@
|
|||||||
<UserMessage
|
<UserMessage
|
||||||
on:delete={() => messageDeleteHandler(message.id)}
|
on:delete={() => messageDeleteHandler(message.id)}
|
||||||
user={$user}
|
user={$user}
|
||||||
|
{readOnly}
|
||||||
{message}
|
{message}
|
||||||
isFirstMessage={messageIdx === 0}
|
isFirstMessage={messageIdx === 0}
|
||||||
siblings={message.parentId !== null
|
siblings={message.parentId !== null
|
||||||
@@ -327,7 +302,7 @@
|
|||||||
{confirmEditMessage}
|
{confirmEditMessage}
|
||||||
{showPreviousMessage}
|
{showPreviousMessage}
|
||||||
{showNextMessage}
|
{showNextMessage}
|
||||||
{copyToClipboard}
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<ResponseMessage
|
<ResponseMessage
|
||||||
@@ -335,11 +310,13 @@
|
|||||||
modelfiles={selectedModelfiles}
|
modelfiles={selectedModelfiles}
|
||||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||||
isLastMessage={messageIdx + 1 === messages.length}
|
isLastMessage={messageIdx + 1 === messages.length}
|
||||||
|
{readOnly}
|
||||||
|
{updateChatMessages}
|
||||||
{confirmEditResponseMessage}
|
{confirmEditResponseMessage}
|
||||||
{showPreviousMessage}
|
{showPreviousMessage}
|
||||||
{showNextMessage}
|
{showNextMessage}
|
||||||
{rateMessage}
|
{rateMessage}
|
||||||
{copyToClipboard}
|
copyToClipboard={copyToClipboardWithToast}
|
||||||
{continueGeneration}
|
{continueGeneration}
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
on:save={async (e) => {
|
on:save={async (e) => {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let messageId = null;
|
||||||
|
export let show = false;
|
||||||
|
export let message;
|
||||||
|
|
||||||
|
let LIKE_REASONS = [];
|
||||||
|
let DISLIKE_REASONS = [];
|
||||||
|
|
||||||
|
function loadReasons() {
|
||||||
|
LIKE_REASONS = [
|
||||||
|
$i18n.t('Accurate information'),
|
||||||
|
$i18n.t('Followed instructions perfectly'),
|
||||||
|
$i18n.t('Showcased creativity'),
|
||||||
|
$i18n.t('Positive attitude'),
|
||||||
|
$i18n.t('Attention to detail'),
|
||||||
|
$i18n.t('Thorough explanation'),
|
||||||
|
$i18n.t('Other')
|
||||||
|
];
|
||||||
|
|
||||||
|
DISLIKE_REASONS = [
|
||||||
|
$i18n.t("Don't like the style"),
|
||||||
|
$i18n.t('Not factually correct'),
|
||||||
|
$i18n.t("Didn't fully follow instructions"),
|
||||||
|
$i18n.t("Refused when it shouldn't have"),
|
||||||
|
$i18n.t('Being lazy'),
|
||||||
|
$i18n.t('Other')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let reasons = [];
|
||||||
|
let selectedReason = null;
|
||||||
|
let comment = '';
|
||||||
|
|
||||||
|
$: if (message.annotation.rating === 1) {
|
||||||
|
reasons = LIKE_REASONS;
|
||||||
|
} else if (message.annotation.rating === -1) {
|
||||||
|
reasons = DISLIKE_REASONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
selectedReason = message.annotation.reason;
|
||||||
|
comment = message.annotation.comment;
|
||||||
|
loadReasons();
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitHandler = () => {
|
||||||
|
console.log('submitHandler');
|
||||||
|
|
||||||
|
message.annotation.reason = selectedReason;
|
||||||
|
message.annotation.comment = comment;
|
||||||
|
|
||||||
|
dispatch('submit');
|
||||||
|
|
||||||
|
toast.success($i18n.t('Thanks for your feedback!'));
|
||||||
|
show = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
|
||||||
|
id="message-feedback-{messageId}"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class=" text-sm">{$i18n.t('Tell us more:')}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if reasons.length > 0}
|
||||||
|
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
|
||||||
|
{#each reasons as reason}
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1 border dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||||
|
reason
|
||||||
|
? 'bg-gray-200 dark:bg-gray-800'
|
||||||
|
: ''} transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
selectedReason = reason;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{reason}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<textarea
|
||||||
|
bind:value={comment}
|
||||||
|
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
|
||||||
|
placeholder={$i18n.t('Feel free to add specific details')}
|
||||||
|
rows="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
|
||||||
|
on:click={() => {
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -15,9 +15,14 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { config, settings } from '$lib/stores';
|
import { config, settings } from '$lib/stores';
|
||||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
import { extractSentences } from '$lib/utils';
|
import {
|
||||||
|
approximateToHumanReadable,
|
||||||
|
extractSentences,
|
||||||
|
revertSanitizedResponseContent,
|
||||||
|
sanitizeResponseContent
|
||||||
|
} from '$lib/utils';
|
||||||
|
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
@@ -26,6 +31,7 @@
|
|||||||
import Image from '$lib/components/common/Image.svelte';
|
import Image from '$lib/components/common/Image.svelte';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import RateComment from './RateComment.svelte';
|
||||||
|
|
||||||
export let modelfiles = [];
|
export let modelfiles = [];
|
||||||
export let message;
|
export let message;
|
||||||
@@ -33,6 +39,9 @@
|
|||||||
|
|
||||||
export let isLastMessage = true;
|
export let isLastMessage = true;
|
||||||
|
|
||||||
|
export let readOnly = false;
|
||||||
|
|
||||||
|
export let updateChatMessages: Function;
|
||||||
export let confirmEditResponseMessage: Function;
|
export let confirmEditResponseMessage: Function;
|
||||||
export let showPreviousMessage: Function;
|
export let showPreviousMessage: Function;
|
||||||
export let showNextMessage: Function;
|
export let showNextMessage: Function;
|
||||||
@@ -54,7 +63,9 @@
|
|||||||
let loadingSpeech = false;
|
let loadingSpeech = false;
|
||||||
let generatingImage = false;
|
let generatingImage = false;
|
||||||
|
|
||||||
$: tokens = marked.lexer(message.content);
|
let showRateComment = false;
|
||||||
|
|
||||||
|
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
@@ -112,7 +123,10 @@
|
|||||||
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
|
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
|
||||||
eval_duration: ${
|
eval_duration: ${
|
||||||
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
|
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
|
||||||
}ms</span>`,
|
}ms<br/>
|
||||||
|
approximate_total: ${approximateToHumanReadable(
|
||||||
|
message.info.total_duration
|
||||||
|
)}</span>`,
|
||||||
allowHTML: true
|
allowHTML: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,7 +142,7 @@
|
|||||||
// • auto-render specific keys, e.g.:
|
// • auto-render specific keys, e.g.:
|
||||||
delimiters: [
|
delimiters: [
|
||||||
{ left: '$$', right: '$$', display: false },
|
{ left: '$$', right: '$$', display: false },
|
||||||
{ left: '$', right: '$', display: false },
|
{ left: '$ ', right: ' $', display: false },
|
||||||
{ left: '\\(', right: '\\)', display: false },
|
{ left: '\\(', right: '\\)', display: false },
|
||||||
{ left: '\\[', right: '\\]', display: false },
|
{ left: '\\[', right: '\\]', display: false },
|
||||||
{ left: '[ ', right: ' ]', display: false }
|
{ left: '[ ', right: ' ]', display: false }
|
||||||
@@ -162,10 +176,12 @@
|
|||||||
|
|
||||||
const toggleSpeakMessage = async () => {
|
const toggleSpeakMessage = async () => {
|
||||||
if (speaking) {
|
if (speaking) {
|
||||||
speechSynthesis.cancel();
|
try {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
|
||||||
sentencesAudio[speakingIdx].pause();
|
sentencesAudio[speakingIdx].pause();
|
||||||
sentencesAudio[speakingIdx].currentTime = 0;
|
sentencesAudio[speakingIdx].currentTime = 0;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
speaking = null;
|
speaking = null;
|
||||||
speakingIdx = null;
|
speakingIdx = null;
|
||||||
@@ -207,6 +223,10 @@
|
|||||||
sentence
|
sentence
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
|
|
||||||
|
speaking = null;
|
||||||
|
loadingSpeech = false;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,7 +236,6 @@
|
|||||||
const audio = new Audio(blobUrl);
|
const audio = new Audio(blobUrl);
|
||||||
sentencesAudio[idx] = audio;
|
sentencesAudio[idx] = audio;
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
|
|
||||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,8 +422,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each tokens as token}
|
{#each tokens as token}
|
||||||
{#if token.type === 'code'}
|
{#if token.type === 'code'}
|
||||||
<!-- {token.text} -->
|
<CodeBlock
|
||||||
<CodeBlock lang={token.lang} code={token.text} />
|
lang={token.lang}
|
||||||
|
code={revertSanitizedResponseContent(token.text)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{@html marked.parse(token.raw, {
|
{@html marked.parse(token.raw, {
|
||||||
...defaults,
|
...defaults,
|
||||||
@@ -422,7 +443,7 @@
|
|||||||
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center min-w-fit -mt-1">
|
<div class="flex self-center min-w-fit">
|
||||||
<button
|
<button
|
||||||
class="self-center dark:hover:text-white hover:text-black transition"
|
class="self-center dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -469,31 +490,33 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Edit" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Edit" placement="bottom">
|
||||||
class="{isLastMessage
|
<button
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
? 'visible'
|
||||||
on:click={() => {
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
editMessageHandler();
|
on:click={() => {
|
||||||
}}
|
editMessageHandler();
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
</Tooltip>
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content="Copy" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -521,59 +544,76 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="Good Response" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Good Response" placement="bottom">
|
||||||
class="{isLastMessage
|
<button
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
? 'visible'
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
|
||||||
: ''} dark:hover:text-white hover:text-black transition"
|
?.rating === 1
|
||||||
on:click={() => {
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
rateMessage(message.id, 1);
|
: ''} dark:hover:text-white hover:text-black transition"
|
||||||
}}
|
on:click={() => {
|
||||||
>
|
rateMessage(message.id, 1);
|
||||||
<svg
|
showRateComment = true;
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-4 h-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="Bad Response" placement="bottom">
|
window.setTimeout(() => {
|
||||||
<button
|
document
|
||||||
class="{isLastMessage
|
.getElementById(`message-feedback-${message.id}`)
|
||||||
? 'visible'
|
?.scrollIntoView();
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
}, 0);
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
}}
|
||||||
: ''} dark:hover:text-white hover:text-black transition"
|
|
||||||
on:click={() => {
|
|
||||||
rateMessage(message.id, -1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="w-4 h-4"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><path
|
|
||||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
|
||||||
/></svg
|
|
||||||
>
|
>
|
||||||
</button>
|
<svg
|
||||||
</Tooltip>
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Bad Response" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
|
||||||
|
?.rating === -1
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
|
: ''} dark:hover:text-white hover:text-black transition"
|
||||||
|
on:click={() => {
|
||||||
|
rateMessage(message.id, -1);
|
||||||
|
showRateComment = true;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document
|
||||||
|
.getElementById(`message-feedback-${message.id}`)
|
||||||
|
?.scrollIntoView();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Read Aloud" placement="bottom">
|
<Tooltip content="Read Aloud" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -656,7 +696,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if $config.images}
|
{#if $config.images && !readOnly}
|
||||||
<Tooltip content="Generate Image" placement="bottom">
|
<Tooltip content="Generate Image" placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
@@ -752,7 +792,7 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage}
|
{#if isLastMessage && !readOnly}
|
||||||
<Tooltip content="Continue Response" placement="bottom">
|
<Tooltip content="Continue Response" placement="bottom">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -812,6 +852,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showRateComment}
|
||||||
|
<RateComment
|
||||||
|
messageId={message.id}
|
||||||
|
bind:show={showRateComment}
|
||||||
|
bind:message
|
||||||
|
on:submit={() => {
|
||||||
|
updateChatMessages();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
export let message;
|
export let message;
|
||||||
export let siblings;
|
export let siblings;
|
||||||
export let isFirstMessage: boolean;
|
export let isFirstMessage: boolean;
|
||||||
|
export let readOnly: boolean;
|
||||||
|
|
||||||
export let confirmEditMessage: Function;
|
export let confirmEditMessage: Function;
|
||||||
export let showPreviousMessage: Function;
|
export let showPreviousMessage: Function;
|
||||||
@@ -175,10 +176,23 @@
|
|||||||
e.target.style.height = '';
|
e.target.style.height = '';
|
||||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
document.getElementById('close-edit-message-button')?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
|
||||||
|
const isEnterPressed = e.key === 'Enter';
|
||||||
|
|
||||||
|
if (isCmdOrCtrlPressed && isEnterPressed) {
|
||||||
|
document.getElementById('save-edit-message-button')?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
|
id="save-edit-message-button"
|
||||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
|
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
@@ -188,6 +202,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
id="close-edit-message-button"
|
||||||
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
|
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
cancelEditMessage();
|
cancelEditMessage();
|
||||||
@@ -203,7 +218,7 @@
|
|||||||
|
|
||||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center -mt-1">
|
<div class="flex self-center">
|
||||||
<button
|
<button
|
||||||
class="self-center dark:hover:text-white hover:text-black transition"
|
class="self-center dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -250,29 +265,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Edit" placement="bottom">
|
{#if !readOnly}
|
||||||
<button
|
<Tooltip content="Edit" placement="bottom">
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
<button
|
||||||
on:click={() => {
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||||
editMessageHandler();
|
on:click={() => {
|
||||||
}}
|
editMessageHandler();
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
</Tooltip>
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content="Copy" placement="bottom">
|
<Tooltip content="Copy" placement="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -298,7 +315,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if !isFirstMessage}
|
{#if !isFirstMessage && !readOnly}
|
||||||
<Tooltip content="Delete" placement="bottom">
|
<Tooltip content="Delete" placement="bottom">
|
||||||
<button
|
<button
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Collapsible } from 'bits-ui';
|
||||||
|
|
||||||
import { setDefaultModels } from '$lib/apis/configs';
|
import { setDefaultModels } from '$lib/apis/configs';
|
||||||
import { models, showSettings, settings, user } from '$lib/stores';
|
import { models, showSettings, settings, user } from '$lib/stores';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import Select from '../common/Select.svelte';
|
import Selector from './ModelSelector/Selector.svelte';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -33,19 +36,19 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col my-2 w-full">
|
<div class="flex flex-col mt-0.5 w-full">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="overflow-hidden w-full">
|
<div class="overflow-hidden w-full">
|
||||||
<div class="mr-2 max-w-full">
|
<div class="mr-0.5 max-w-full">
|
||||||
<Select
|
<Selector
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
items={$models
|
items={$models
|
||||||
.filter((model) => model.name !== 'hr')
|
.filter((model) => model.name !== 'hr')
|
||||||
.map((model) => ({
|
.map((model) => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label:
|
label: model.name,
|
||||||
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`
|
info: model
|
||||||
}))}
|
}))}
|
||||||
bind:value={selectedModel}
|
bind:value={selectedModel}
|
||||||
/>
|
/>
|
||||||
@@ -53,84 +56,56 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
{#if selectedModelIdx === 0}
|
||||||
<button
|
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
|
||||||
class=" self-center {selectedModelIdx === 0
|
<Tooltip content="Add Model">
|
||||||
? 'mr-3'
|
<button
|
||||||
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
|
class=" "
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedModels = [...selectedModels, ''];
|
selectedModels = [...selectedModels, ''];
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
|
||||||
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
|
<Tooltip content="Remove Model">
|
||||||
0
|
<button
|
||||||
? 'mr-3'
|
{disabled}
|
||||||
: 'mr-7'}"
|
on:click={() => {
|
||||||
{disabled}
|
selectedModels.splice(selectedModelIdx, 1);
|
||||||
on:click={() => {
|
selectedModels = selectedModels;
|
||||||
selectedModels.splice(selectedModelIdx, 1);
|
}}
|
||||||
selectedModels = selectedModels;
|
>
|
||||||
}}
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
stroke-width="1.5"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
class="w-4 h-4"
|
||||||
stroke="currentColor"
|
>
|
||||||
class="w-4 h-4"
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
||||||
>
|
</svg>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
</button>
|
||||||
</svg>
|
</Tooltip>
|
||||||
</button>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
|
||||||
<button
|
|
||||||
class=" self-center dark:hover:text-gray-300"
|
|
||||||
id="open-settings-button"
|
|
||||||
on:click={async () => {
|
|
||||||
await showSettings.set(!$showSettings);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-left mt-1.5 ml-1 text-xs text-gray-500">
|
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
|
||||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select } from 'bits-ui';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||||
|
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
import Check from '$lib/components/icons/Check.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
|
||||||
|
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||||
|
|
||||||
|
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let placeholder = 'Select a model';
|
||||||
|
export let searchEnabled = true;
|
||||||
|
export let searchPlaceholder = 'Search a model';
|
||||||
|
|
||||||
|
export let items = [{ value: 'mango', label: 'Mango' }];
|
||||||
|
|
||||||
|
let searchValue = '';
|
||||||
|
let ollamaVersion = null;
|
||||||
|
|
||||||
|
$: filteredItems = searchValue
|
||||||
|
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
|
||||||
|
: items;
|
||||||
|
|
||||||
|
const pullModelHandler = async () => {
|
||||||
|
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||||
|
|
||||||
|
console.log($MODEL_DOWNLOAD_POOL);
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
||||||
|
modelTag: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) {
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const reader = res.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(splitStream('\n'))
|
||||||
|
.getReader();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
let lines = value.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line !== '') {
|
||||||
|
let data = JSON.parse(line);
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
requestId: data.id,
|
||||||
|
reader,
|
||||||
|
done: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status) {
|
||||||
|
if (data.digest) {
|
||||||
|
let downloadProgress = 0;
|
||||||
|
if (data.completed) {
|
||||||
|
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
||||||
|
} else {
|
||||||
|
downloadProgress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
pullProgress: downloadProgress,
|
||||||
|
digest: data.digest
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success(data.status);
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
done: data.status === 'success'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error);
|
||||||
|
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
|
||||||
|
toast.success(
|
||||||
|
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
|
||||||
|
modelName: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
} else {
|
||||||
|
toast.error('Download canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
|
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
|
||||||
|
if (reader) {
|
||||||
|
await reader.cancel();
|
||||||
|
|
||||||
|
await cancelOllamaRequest(localStorage.token, requestId);
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[model];
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
await deleteModel(localStorage.token, model);
|
||||||
|
toast.success(`${model} download has been canceled`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root
|
||||||
|
{items}
|
||||||
|
onOpenChange={async () => {
|
||||||
|
searchValue = '';
|
||||||
|
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||||
|
}}
|
||||||
|
selected={items.find((item) => item.value === value) ?? ''}
|
||||||
|
onSelectedChange={(selectedItem) => {
|
||||||
|
value = selectedItem.value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
||||||
|
<Select.Value
|
||||||
|
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content
|
||||||
|
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
||||||
|
transition={flyAndScale}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
{#if searchEnabled}
|
||||||
|
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
|
||||||
|
<Search className="size-4" strokeWidth="2.5" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="model-search-input"
|
||||||
|
bind:value={searchValue}
|
||||||
|
class="w-full text-sm bg-transparent outline-none"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="px-3 my-2 max-h-72 overflow-y-auto">
|
||||||
|
{#each filteredItems as item}
|
||||||
|
<Select.Item
|
||||||
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
value={item.value}
|
||||||
|
label={item.label}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
{item.label}
|
||||||
|
|
||||||
|
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||||
|
>{item.info?.details?.parameter_size ?? ''}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- {JSON.stringify(item.info)} -->
|
||||||
|
|
||||||
|
{#if item.info.external}
|
||||||
|
<Tooltip content={item.info?.source ?? 'External'}>
|
||||||
|
<div class=" mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<Tooltip
|
||||||
|
content={`${
|
||||||
|
item.info?.details?.quantization_level
|
||||||
|
? item.info?.details?.quantization_level + ' '
|
||||||
|
: ''
|
||||||
|
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
|
||||||
|
>
|
||||||
|
<div class=" mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if value === item.value}
|
||||||
|
<div class="ml-auto">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Select.Item>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
|
||||||
|
<button
|
||||||
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
on:click={() => {
|
||||||
|
pullModelHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pull "{searchValue}" from Ollama.com
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||||
|
<div
|
||||||
|
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||||
|
<svg
|
||||||
|
class="size-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><style>
|
||||||
|
.spinner_ajPY {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner_AtaB 0.75s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes spinner_AtaB {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><path
|
||||||
|
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||||
|
opacity=".25"
|
||||||
|
/><path
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
class="spinner_ajPY"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col self-start">
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
|
||||||
|
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
|
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
|
||||||
|
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mr-2 translate-y-0.5">
|
||||||
|
<Tooltip content="Cancel">
|
||||||
|
<button
|
||||||
|
class="text-gray-800 dark:text-gray-100"
|
||||||
|
on:click={() => {
|
||||||
|
cancelModelPullHandler(model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-800 dark:text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18 17.94 6M18 18 6.06 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
import { compareVersion } from '$lib/utils';
|
import { compareVersion } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
@@ -51,8 +53,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||||
<div>
|
<div class="flex gap-1">
|
||||||
v{WEBUI_VERSION}
|
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}>
|
||||||
|
v{WEBUI_VERSION}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||||
@@ -126,7 +130,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{$i18n.t('Created by')}
|
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||||
|
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||||
|
{/if}{$i18n.t('Created by')}
|
||||||
<a
|
<a
|
||||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||||
href="https://github.com/tjbck"
|
href="https://github.com/tjbck"
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { updateUserProfile } from '$lib/apis/auths';
|
import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
|
||||||
|
|
||||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||||
import { getGravatarUrl } from '$lib/apis/utils';
|
import { getGravatarUrl } from '$lib/apis/utils';
|
||||||
|
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -15,11 +18,25 @@
|
|||||||
|
|
||||||
let profileImageUrl = '';
|
let profileImageUrl = '';
|
||||||
let name = '';
|
let name = '';
|
||||||
|
|
||||||
|
let showAPIKeys = false;
|
||||||
|
|
||||||
let showJWTToken = false;
|
let showJWTToken = false;
|
||||||
let JWTTokenCopied = false;
|
let JWTTokenCopied = false;
|
||||||
|
|
||||||
|
let APIKey = '';
|
||||||
|
let showAPIKey = false;
|
||||||
|
let APIKeyCopied = false;
|
||||||
|
|
||||||
let profileImageInputElement: HTMLInputElement;
|
let profileImageInputElement: HTMLInputElement;
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
|
if (name !== $user.name) {
|
||||||
|
if (profileImageUrl === generateInitialsImage($user.name) || profileImageUrl === '') {
|
||||||
|
profileImageUrl = generateInitialsImage(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
@@ -33,9 +50,23 @@
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
const createAPIKeyHandler = async () => {
|
||||||
|
APIKey = await createAPIKey(localStorage.token);
|
||||||
|
if (APIKey) {
|
||||||
|
toast.success($i18n.t('API Key created.'));
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to create API Key.'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
name = $user.name;
|
name = $user.name;
|
||||||
profileImageUrl = $user.profile_image_url;
|
profileImageUrl = $user.profile_image_url;
|
||||||
|
|
||||||
|
APIKey = await getAPIKey(localStorage.token).catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -103,59 +134,93 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
|
<div class="space-y-1">
|
||||||
|
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
|
||||||
|
|
||||||
<div class="flex space-x-5">
|
<div class="flex space-x-5">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="self-center">
|
<div class="self-center mt-2">
|
||||||
<button
|
<button
|
||||||
class="relative rounded-full dark:bg-gray-700"
|
class="relative rounded-full dark:bg-gray-700"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
profileImageInputElement.click();
|
profileImageInputElement.click();
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
|
|
||||||
alt="profile"
|
|
||||||
class=" rounded-full w-16 h-16 object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50"
|
|
||||||
>
|
>
|
||||||
<div class="my-auto text-gray-100">
|
<img
|
||||||
<svg
|
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
alt="profile"
|
||||||
viewBox="0 0 20 20"
|
class=" rounded-full size-16 object-cover"
|
||||||
fill="currentColor"
|
/>
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class=" text-xs text-gray-600"
|
|
||||||
on:click={async () => {
|
|
||||||
const url = await getGravatarUrl($user.email);
|
|
||||||
|
|
||||||
profileImageUrl = url;
|
<div
|
||||||
}}>{$i18n.t('Use Gravatar')}</button
|
class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50"
|
||||||
>
|
>
|
||||||
|
<div class="my-auto text-gray-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col self-center gap-0.5">
|
||||||
|
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('Profile Image')}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
|
||||||
|
on:click={async () => {
|
||||||
|
if (canvasPixelTest()) {
|
||||||
|
profileImageUrl = generateInitialsImage(name);
|
||||||
|
} else {
|
||||||
|
toast.info(
|
||||||
|
$i18n.t(
|
||||||
|
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: 1000 * 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>{$i18n.t('Use Initials')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
|
||||||
|
on:click={async () => {
|
||||||
|
const url = await getGravatarUrl($user.email);
|
||||||
|
|
||||||
|
profileImageUrl = url;
|
||||||
|
}}>{$i18n.t('Use Gravatar')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg px-2 py-1"
|
||||||
|
on:click={async () => {
|
||||||
|
profileImageUrl = '/user.png';
|
||||||
|
}}>{$i18n.t('Remove')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="pt-0.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
<div class=" mb-1 text-xs font-medium">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
required
|
required
|
||||||
@@ -165,110 +230,262 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-4" />
|
<div class="py-0.5">
|
||||||
<UpdatePassword />
|
<UpdatePassword />
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700 my-4" />
|
|
||||||
|
|
||||||
<div class=" w-full justify-between">
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-2">
|
|
||||||
<div class="flex w-full">
|
|
||||||
<input
|
|
||||||
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
|
||||||
type={showJWTToken ? 'text' : 'password'}
|
|
||||||
value={localStorage.token}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
|
|
||||||
on:click={() => {
|
|
||||||
showJWTToken = !showJWTToken;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if showJWTToken}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
|
|
||||||
on:click={() => {
|
|
||||||
copyToClipboard(localStorage.token);
|
|
||||||
JWTTokenCopied = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
JWTTokenCopied = false;
|
|
||||||
}, 2000);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if JWTTokenCopied}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-4" />
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
||||||
|
<button
|
||||||
|
class=" text-xs font-medium text-gray-500"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showAPIKeys = !showAPIKeys;
|
||||||
|
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showAPIKeys}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="justify-between w-full">
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2">
|
||||||
|
<div class="flex w-full">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type={showJWTToken ? 'text' : 'password'}
|
||||||
|
value={localStorage.token}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
|
||||||
|
on:click={() => {
|
||||||
|
showJWTToken = !showJWTToken;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if showJWTToken}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
copyToClipboard(localStorage.token);
|
||||||
|
JWTTokenCopied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
JWTTokenCopied = false;
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if JWTTokenCopied}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justify-between w-full">
|
||||||
|
<div class="flex justify-between w-full">
|
||||||
|
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2">
|
||||||
|
{#if APIKey}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
type={showAPIKey ? 'text' : 'password'}
|
||||||
|
value={APIKey}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
|
||||||
|
on:click={() => {
|
||||||
|
showAPIKey = !showAPIKey;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if showAPIKey}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
copyToClipboard(APIKey);
|
||||||
|
APIKeyCopied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
APIKeyCopied = false;
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if APIKeyCopied}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Tooltip content="Create new key">
|
||||||
|
<button
|
||||||
|
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
|
||||||
|
on:click={() => {
|
||||||
|
createAPIKeyHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => {
|
||||||
|
createAPIKeyHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus strokeWidth="2" className=" size-3.5" />
|
||||||
|
|
||||||
|
Create new secret key</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
||||||
|
import { user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -9,6 +11,9 @@
|
|||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
|
|
||||||
|
let OpenAIUrl = '';
|
||||||
|
let OpenAIKey = '';
|
||||||
|
|
||||||
let STTEngines = ['', 'openai'];
|
let STTEngines = ['', 'openai'];
|
||||||
let STTEngine = '';
|
let STTEngine = '';
|
||||||
|
|
||||||
@@ -69,6 +74,20 @@
|
|||||||
saveSettings({ speechAutoSend: speechAutoSend });
|
saveSettings({ speechAutoSend: speechAutoSend });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateConfigHandler = async () => {
|
||||||
|
if (TTSEngine === 'openai') {
|
||||||
|
const res = await updateAudioConfig(localStorage.token, {
|
||||||
|
url: OpenAIUrl,
|
||||||
|
key: OpenAIKey
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||||
|
OpenAIKey = res.OPENAI_API_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
|
|
||||||
@@ -85,12 +104,24 @@
|
|||||||
} else {
|
} else {
|
||||||
getWebAPIVoices();
|
getWebAPIVoices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($user.role === 'admin') {
|
||||||
|
const res = await getAudioConfig(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||||
|
OpenAIKey = res.OPENAI_API_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={async () => {
|
||||||
|
if ($user.role === 'admin') {
|
||||||
|
await updateConfigHandler();
|
||||||
|
}
|
||||||
saveSettings({
|
saveSettings({
|
||||||
audio: {
|
audio: {
|
||||||
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
||||||
@@ -101,7 +132,7 @@
|
|||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||||
|
|
||||||
@@ -196,6 +227,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $user.role === 'admin'}
|
||||||
|
{#if TTSEngine === 'openai'}
|
||||||
|
<div class="mt-1 flex gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={OpenAIUrl}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={OpenAIKey}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
||||||
|
|
||||||
@@ -223,7 +274,7 @@
|
|||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={speaker}
|
bind:value={speaker}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
>
|
>
|
||||||
@@ -241,16 +292,18 @@
|
|||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
list="voice-list"
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={speaker}
|
bind:value={speaker}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
>
|
/>
|
||||||
|
|
||||||
|
<datalist id="voice-list">
|
||||||
{#each voices as voice}
|
{#each voices as voice}
|
||||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
<option value={voice.name} />
|
||||||
>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin' && ($config?.admin_export_enabled ?? true)}
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
// General
|
// General
|
||||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
|
||||||
let selectedTheme = 'system';
|
let selectedTheme = 'system';
|
||||||
|
|
||||||
let languages = [];
|
let languages = [];
|
||||||
@@ -91,12 +91,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const applyTheme = (_theme: string) => {
|
const applyTheme = (_theme: string) => {
|
||||||
let themeToApply = _theme;
|
let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme;
|
||||||
|
|
||||||
if (_theme === 'system') {
|
if (_theme === 'system') {
|
||||||
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (themeToApply === 'dark' && !_theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#171717');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
|
||||||
|
}
|
||||||
|
|
||||||
themes
|
themes
|
||||||
.filter((e) => e !== themeToApply)
|
.filter((e) => e !== themeToApply)
|
||||||
.forEach((e) => {
|
.forEach((e) => {
|
||||||
@@ -115,7 +120,11 @@
|
|||||||
const themeChangeHandler = (_theme: string) => {
|
const themeChangeHandler = (_theme: string) => {
|
||||||
theme.set(_theme);
|
theme.set(_theme);
|
||||||
localStorage.setItem('theme', _theme);
|
localStorage.setItem('theme', _theme);
|
||||||
|
if (_theme.includes('oled')) {
|
||||||
|
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||||
|
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
applyTheme(_theme);
|
applyTheme(_theme);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -136,6 +145,7 @@
|
|||||||
>
|
>
|
||||||
<option value="system">⚙️ {$i18n.t('System')}</option>
|
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||||
|
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
||||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||||
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
||||||
@@ -175,7 +185,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
updateImageSize,
|
updateImageSize,
|
||||||
getImageSteps,
|
getImageSteps,
|
||||||
updateImageSteps,
|
updateImageSteps,
|
||||||
getOpenAIKey,
|
getOpenAIConfig,
|
||||||
updateOpenAIKey
|
updateOpenAIConfig
|
||||||
} from '$lib/apis/images';
|
} from '$lib/apis/images';
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
let AUTOMATIC1111_BASE_URL = '';
|
let AUTOMATIC1111_BASE_URL = '';
|
||||||
let COMFYUI_BASE_URL = '';
|
let COMFYUI_BASE_URL = '';
|
||||||
|
|
||||||
|
let OPENAI_API_BASE_URL = '';
|
||||||
let OPENAI_API_KEY = '';
|
let OPENAI_API_KEY = '';
|
||||||
|
|
||||||
let selectedModel = '';
|
let selectedModel = '';
|
||||||
@@ -131,7 +132,10 @@
|
|||||||
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
||||||
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
const config = await getOpenAIConfig(localStorage.token);
|
||||||
|
|
||||||
|
OPENAI_API_KEY = config.OPENAI_API_KEY;
|
||||||
|
OPENAI_API_BASE_URL = config.OPENAI_API_BASE_URL;
|
||||||
|
|
||||||
imageSize = await getImageSize(localStorage.token);
|
imageSize = await getImageSize(localStorage.token);
|
||||||
steps = await getImageSteps(localStorage.token);
|
steps = await getImageSteps(localStorage.token);
|
||||||
@@ -149,7 +153,7 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
if (imageGenerationEngine === 'openai') {
|
if (imageGenerationEngine === 'openai') {
|
||||||
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||||
@@ -300,13 +304,22 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if imageGenerationEngine === 'openai'}
|
{:else if imageGenerationEngine === 'openai'}
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
|
<div>
|
||||||
<div class="flex w-full">
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
||||||
<div class="flex-1 mr-2">
|
|
||||||
|
<div class="flex gap-2 mb-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder={$i18n.t('Enter API Key')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
|
bind:value={OPENAI_API_BASE_URL}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={OPENAI_API_KEY}
|
bind:value={OPENAI_API_KEY}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,19 +332,39 @@
|
|||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
{#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')}
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
<div class="flex w-full">
|
||||||
bind:value={selectedModel}
|
<div class="flex-1">
|
||||||
placeholder={$i18n.t('Select a model')}
|
<input
|
||||||
required
|
list="model-list"
|
||||||
>
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
{#if !selectedModel}
|
bind:value={selectedModel}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
placeholder="Select a model"
|
||||||
{/if}
|
/>
|
||||||
{#each models ?? [] as model}
|
|
||||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
<datalist id="model-list">
|
||||||
{/each}
|
{#each models ?? [] as model}
|
||||||
</select>
|
<option value={model.id}>{model.name}</option>
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={selectedModel}
|
||||||
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{#if !selectedModel}
|
||||||
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
|
{/if}
|
||||||
|
{#each models ?? [] as model}
|
||||||
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||||
import { config, models, user } from '$lib/stores';
|
import { config, models, settings, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -14,13 +14,20 @@
|
|||||||
let titleAutoGenerate = true;
|
let titleAutoGenerate = true;
|
||||||
let responseAutoCopy = false;
|
let responseAutoCopy = false;
|
||||||
let titleAutoGenerateModel = '';
|
let titleAutoGenerateModel = '';
|
||||||
|
let titleAutoGenerateModelExternal = '';
|
||||||
let fullScreenMode = false;
|
let fullScreenMode = false;
|
||||||
let titleGenerationPrompt = '';
|
let titleGenerationPrompt = '';
|
||||||
|
let splitLargeChunks = false;
|
||||||
|
|
||||||
// Interface
|
// Interface
|
||||||
let promptSuggestions = [];
|
let promptSuggestions = [];
|
||||||
let showUsername = false;
|
let showUsername = false;
|
||||||
|
|
||||||
|
const toggleSplitLargeChunks = async () => {
|
||||||
|
splitLargeChunks = !splitLargeChunks;
|
||||||
|
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||||
|
};
|
||||||
|
|
||||||
const toggleFullScreenMode = async () => {
|
const toggleFullScreenMode = async () => {
|
||||||
fullScreenMode = !fullScreenMode;
|
fullScreenMode = !fullScreenMode;
|
||||||
saveSettings({ fullScreenMode: fullScreenMode });
|
saveSettings({ fullScreenMode: fullScreenMode });
|
||||||
@@ -33,7 +40,12 @@
|
|||||||
|
|
||||||
const toggleTitleAutoGenerate = async () => {
|
const toggleTitleAutoGenerate = async () => {
|
||||||
titleAutoGenerate = !titleAutoGenerate;
|
titleAutoGenerate = !titleAutoGenerate;
|
||||||
saveSettings({ titleAutoGenerate: titleAutoGenerate });
|
saveSettings({
|
||||||
|
title: {
|
||||||
|
...$settings.title,
|
||||||
|
auto: titleAutoGenerate
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleResponseAutoCopy = async () => {
|
const toggleResponseAutoCopy = async () => {
|
||||||
@@ -65,8 +77,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
title: {
|
||||||
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
...$settings.title,
|
||||||
|
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||||
|
modelExternal:
|
||||||
|
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
|
||||||
|
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,16 +94,18 @@
|
|||||||
|
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
|
|
||||||
titleAutoGenerate = settings.titleAutoGenerate ?? true;
|
titleAutoGenerate = settings?.title?.auto ?? true;
|
||||||
responseAutoCopy = settings.responseAutoCopy ?? false;
|
titleAutoGenerateModel = settings?.title?.model ?? '';
|
||||||
showUsername = settings.showUsername ?? false;
|
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
|
||||||
fullScreenMode = settings.fullScreenMode ?? false;
|
|
||||||
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
|
||||||
titleGenerationPrompt =
|
titleGenerationPrompt =
|
||||||
settings.titleGenerationPrompt ??
|
settings?.title?.prompt ??
|
||||||
$i18n.t(
|
$i18n.t(
|
||||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
||||||
) + ' {{prompt}}';
|
) + ' {{prompt}}';
|
||||||
|
|
||||||
|
responseAutoCopy = settings.responseAutoCopy ?? false;
|
||||||
|
showUsername = settings.showUsername ?? false;
|
||||||
|
fullScreenMode = settings.fullScreenMode ?? false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -184,14 +203,37 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Fluidly stream large external response chunks')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleSplitLargeChunks();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if splitLargeChunks === true}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-2 pr-2">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">Local Models</div>
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={titleAutoGenerateModel}
|
bind:value={titleAutoGenerateModel}
|
||||||
@@ -207,6 +249,24 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class=" text-xs mb-1">External Models</div>
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={titleAutoGenerateModelExternal}
|
||||||
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
>
|
||||||
|
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||||
|
{#each $models as model}
|
||||||
|
{#if model.name !== 'hr'}
|
||||||
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 mr-2">
|
<div class="mt-3 mr-2">
|
||||||
@@ -256,20 +316,20 @@
|
|||||||
<div class="flex border-b dark:border-gray-600 w-full">
|
<div class="flex border-b dark:border-gray-600 w-full">
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||||
placeholder="Title (e.g. Tell me a fun fact)"
|
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||||
bind:value={prompt.title[0]}
|
bind:value={prompt.title[0]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||||
placeholder="Subtitle (e.g. about the Roman Empire)"
|
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||||
bind:value={prompt.title[1]}
|
bind:value={prompt.title[1]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||||
placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)"
|
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||||
bind:value={prompt.content}
|
bind:value={prompt.content}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
uploadModel
|
uploadModel
|
||||||
} from '$lib/apis/ollama';
|
} from '$lib/apis/ollama';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, models, user } from '$lib/stores';
|
import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user } from '$lib/stores';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
let liteLLMRPM = '';
|
let liteLLMRPM = '';
|
||||||
let liteLLMMaxTokens = '';
|
let liteLLMMaxTokens = '';
|
||||||
|
|
||||||
let deleteLiteLLMModelId = '';
|
let deleteLiteLLMModelName = '';
|
||||||
|
|
||||||
$: liteLLMModelName = liteLLMModel;
|
$: liteLLMModelName = liteLLMModel;
|
||||||
|
|
||||||
@@ -50,12 +50,6 @@
|
|||||||
let showExperimentalOllama = false;
|
let showExperimentalOllama = false;
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
const MAX_PARALLEL_DOWNLOADS = 3;
|
const MAX_PARALLEL_DOWNLOADS = 3;
|
||||||
const modelDownloadQueue = queue(
|
|
||||||
(task: { modelName: string }, cb) =>
|
|
||||||
pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }),
|
|
||||||
MAX_PARALLEL_DOWNLOADS
|
|
||||||
);
|
|
||||||
let modelDownloadStatus: Record<string, any> = {};
|
|
||||||
|
|
||||||
let modelTransferring = false;
|
let modelTransferring = false;
|
||||||
let modelTag = '';
|
let modelTag = '';
|
||||||
@@ -139,8 +133,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pullModelHandler = async () => {
|
const pullModelHandler = async () => {
|
||||||
const sanitizedModelTag = modelTag.trim();
|
const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
|
||||||
if (modelDownloadStatus[sanitizedModelTag]) {
|
console.log($MODEL_DOWNLOAD_POOL);
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
|
||||||
modelTag: sanitizedModelTag
|
modelTag: sanitizedModelTag
|
||||||
@@ -148,40 +143,117 @@
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Object.keys(modelDownloadStatus).length === 3) {
|
if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransferring = true;
|
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
modelDownloadQueue.push(
|
if (res) {
|
||||||
{ modelName: sanitizedModelTag },
|
const reader = res.body
|
||||||
async (data: { modelName: string; success: boolean; error?: Error }) => {
|
.pipeThrough(new TextDecoderStream())
|
||||||
const { modelName } = data;
|
.pipeThrough(splitStream('\n'))
|
||||||
// Remove the downloaded model
|
.getReader();
|
||||||
delete modelDownloadStatus[modelName];
|
|
||||||
|
|
||||||
modelDownloadStatus = { ...modelDownloadStatus };
|
while (true) {
|
||||||
|
try {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
if (!data.success) {
|
let lines = value.split('\n');
|
||||||
toast.error(data.error);
|
|
||||||
} else {
|
|
||||||
toast.success(
|
|
||||||
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName })
|
|
||||||
);
|
|
||||||
|
|
||||||
const notification = new Notification($WEBUI_NAME, {
|
for (const line of lines) {
|
||||||
body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }),
|
if (line !== '') {
|
||||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
let data = JSON.parse(line);
|
||||||
});
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
if (data.detail) {
|
||||||
|
throw data.detail;
|
||||||
|
}
|
||||||
|
|
||||||
models.set(await getModels());
|
if (data.id) {
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
requestId: data.id,
|
||||||
|
reader,
|
||||||
|
done: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status) {
|
||||||
|
if (data.digest) {
|
||||||
|
let downloadProgress = 0;
|
||||||
|
if (data.completed) {
|
||||||
|
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
||||||
|
} else {
|
||||||
|
downloadProgress = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
pullProgress: downloadProgress,
|
||||||
|
digest: data.digest
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success(data.status);
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL,
|
||||||
|
[sanitizedModelTag]: {
|
||||||
|
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
|
||||||
|
done: data.status === 'success'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error);
|
||||||
|
// opts.callback({ success: false, error, modelName: opts.modelName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
|
||||||
|
|
||||||
|
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
|
||||||
|
toast.success(
|
||||||
|
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
|
||||||
|
modelName: sanitizedModelTag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
models.set(await getModels(localStorage.token));
|
||||||
|
} else {
|
||||||
|
toast.error('Download canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
|
||||||
|
|
||||||
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
|
...$MODEL_DOWNLOAD_POOL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
modelTag = '';
|
modelTag = '';
|
||||||
modelTransferring = false;
|
modelTransferring = false;
|
||||||
@@ -258,6 +330,9 @@
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const error = await fileResponse?.json();
|
||||||
|
toast.error(error?.detail ?? error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
@@ -349,88 +424,18 @@
|
|||||||
models.set(await getModels());
|
models.set(await getModels());
|
||||||
};
|
};
|
||||||
|
|
||||||
const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
|
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
|
||||||
(error) => {
|
if (reader) {
|
||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
await reader.cancel();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res) {
|
await cancelOllamaRequest(localStorage.token, requestId);
|
||||||
const reader = res.body
|
delete $MODEL_DOWNLOAD_POOL[model];
|
||||||
.pipeThrough(new TextDecoderStream())
|
MODEL_DOWNLOAD_POOL.set({
|
||||||
.pipeThrough(splitStream('\n'))
|
...$MODEL_DOWNLOAD_POOL
|
||||||
.getReader();
|
});
|
||||||
|
await deleteModel(localStorage.token, model);
|
||||||
while (true) {
|
toast.success(`${model} download has been canceled`);
|
||||||
try {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
let lines = value.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line !== '') {
|
|
||||||
let data = JSON.parse(line);
|
|
||||||
console.log(data);
|
|
||||||
if (data.error) {
|
|
||||||
throw data.error;
|
|
||||||
}
|
|
||||||
if (data.detail) {
|
|
||||||
throw data.detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.id) {
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
requestId: data.id,
|
|
||||||
reader,
|
|
||||||
done: false
|
|
||||||
};
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status) {
|
|
||||||
if (data.digest) {
|
|
||||||
let downloadProgress = 0;
|
|
||||||
if (data.completed) {
|
|
||||||
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
|
|
||||||
} else {
|
|
||||||
downloadProgress = 100;
|
|
||||||
}
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
pullProgress: downloadProgress,
|
|
||||||
digest: data.digest
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
toast.success(data.status);
|
|
||||||
|
|
||||||
modelDownloadStatus[opts.modelName] = {
|
|
||||||
...modelDownloadStatus[opts.modelName],
|
|
||||||
done: data.status === 'success'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
if (typeof error !== 'string') {
|
|
||||||
error = error.message;
|
|
||||||
}
|
|
||||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(modelDownloadStatus[opts.modelName]);
|
|
||||||
|
|
||||||
if (modelDownloadStatus[opts.modelName].done) {
|
|
||||||
opts.callback({ success: true, modelName: opts.modelName });
|
|
||||||
} else {
|
|
||||||
opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -469,7 +474,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteLiteLLMModelHandler = async () => {
|
const deleteLiteLLMModelHandler = async () => {
|
||||||
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelId).catch(
|
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelName).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
@@ -482,7 +487,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLiteLLMModelId = '';
|
deleteLiteLLMModelName = '';
|
||||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||||
models.set(await getModels());
|
models.set(await getModels());
|
||||||
};
|
};
|
||||||
@@ -500,18 +505,6 @@
|
|||||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelModelPullHandler = async (model: string) => {
|
|
||||||
const { reader, requestId } = modelDownloadStatus[model];
|
|
||||||
if (reader) {
|
|
||||||
await reader.cancel();
|
|
||||||
|
|
||||||
await cancelOllamaRequest(localStorage.token, requestId);
|
|
||||||
delete modelDownloadStatus[model];
|
|
||||||
await deleteModel(localStorage.token, model);
|
|
||||||
toast.success(`${model} download has been canceled`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
@@ -640,9 +633,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
{#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
|
||||||
{#each Object.keys(modelDownloadStatus) as model}
|
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||||
{#if 'pullProgress' in modelDownloadStatus[model]}
|
{#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-medium mb-1">{model}</div>
|
<div class="font-medium mb-1">{model}</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
@@ -652,10 +645,10 @@
|
|||||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||||
style="width: {Math.max(
|
style="width: {Math.max(
|
||||||
15,
|
15,
|
||||||
modelDownloadStatus[model].pullProgress ?? 0
|
$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
|
||||||
)}%"
|
)}%"
|
||||||
>
|
>
|
||||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
{$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -686,9 +679,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{#if 'digest' in modelDownloadStatus[model]}
|
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
|
||||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||||
{modelDownloadStatus[model].digest}
|
{$MODEL_DOWNLOAD_POOL[model].digest}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1096,14 +1089,14 @@
|
|||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<select
|
<select
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={deleteLiteLLMModelId}
|
bind:value={deleteLiteLLMModelName}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
{#if !deleteLiteLLMModelId}
|
{#if !deleteLiteLLMModelName}
|
||||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each liteLLMModelInfo as model}
|
{#each liteLLMModelInfo as model}
|
||||||
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.model_name} class="bg-gray-100 dark:bg-gray-700"
|
||||||
>{model.model_name}</option
|
>{model.model_name}</option
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { models, settings, user } from '$lib/stores';
|
import { models, settings, user } from '$lib/stores';
|
||||||
|
|
||||||
import { getOllamaModels } from '$lib/apis/ollama';
|
import { getModels as _getModels } from '$lib/utils';
|
||||||
import { getOpenAIModels } from '$lib/apis/openai';
|
|
||||||
import { getLiteLLMModels } from '$lib/apis/litellm';
|
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Account from './Settings/Account.svelte';
|
import Account from './Settings/Account.svelte';
|
||||||
@@ -29,33 +27,11 @@
|
|||||||
localStorage.setItem('settings', JSON.stringify($settings));
|
localStorage.setItem('settings', JSON.stringify($settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectedTab = 'general';
|
|
||||||
|
|
||||||
const getModels = async () => {
|
const getModels = async () => {
|
||||||
let models = await Promise.all([
|
return await _getModels(localStorage.token);
|
||||||
await getOllamaModels(localStorage.token).catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
await getOpenAIModels(localStorage.token).catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
await getLiteLLMModels(localStorage.token).catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
models = models
|
|
||||||
.filter((models) => models)
|
|
||||||
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
|
|
||||||
|
|
||||||
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
|
|
||||||
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
|
||||||
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
|
|
||||||
return models;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let selectedTab = 'general';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
|
|||||||
@@ -1,41 +1,185 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
|
||||||
|
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
|
||||||
|
import { modelfiles } from '$lib/stores';
|
||||||
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import Link from '../icons/Link.svelte';
|
||||||
|
|
||||||
|
export let chatId;
|
||||||
|
|
||||||
|
let chat = null;
|
||||||
|
let shareUrl = null;
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let downloadChat: Function;
|
const shareLocalChat = async () => {
|
||||||
export let shareChat: Function;
|
const _chat = chat;
|
||||||
|
|
||||||
|
const sharedChat = await shareChatById(localStorage.token, chatId);
|
||||||
|
shareUrl = `${window.location.origin}/s/${sharedChat.id}`;
|
||||||
|
console.log(shareUrl);
|
||||||
|
chat = await getChatById(localStorage.token, chatId);
|
||||||
|
|
||||||
|
return shareUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareChat = async () => {
|
||||||
|
const _chat = chat.chat;
|
||||||
|
console.log('share', _chat);
|
||||||
|
|
||||||
|
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||||
|
const url = 'https://openwebui.com';
|
||||||
|
// const url = 'http://localhost:5173';
|
||||||
|
|
||||||
|
const tab = await window.open(`${url}/chats/upload`, '_blank');
|
||||||
|
window.addEventListener(
|
||||||
|
'message',
|
||||||
|
(event) => {
|
||||||
|
if (event.origin !== url) return;
|
||||||
|
if (event.data === 'loaded') {
|
||||||
|
tab.postMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
chat: _chat,
|
||||||
|
modelfiles: $modelfiles.filter((modelfile) =>
|
||||||
|
_chat.models.includes(modelfile.tagName)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
(async () => {
|
||||||
|
if (chatId) {
|
||||||
|
chat = await getChatById(localStorage.token, chatId);
|
||||||
|
} else {
|
||||||
|
chat = null;
|
||||||
|
console.log(chat);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show size="xs">
|
<Modal bind:show size="sm">
|
||||||
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
<div>
|
||||||
<button
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white"
|
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
shareChat();
|
|
||||||
show = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$i18n.t('Share to OpenWebUI Community')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex justify-center space-x-1 mt-1.5">
|
|
||||||
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
class="self-center"
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
downloadChat();
|
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$i18n.t('Download as a File')}
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
|
{#if chat}
|
||||||
|
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||||
|
<div class=" text-sm dark:text-gray-300 mb-1">
|
||||||
|
{#if chat.share_id}
|
||||||
|
<a href="/s/{chat.share_id}" target="_blank"
|
||||||
|
>You have shared this chat <span class=" underline">before</span>.</a
|
||||||
|
>
|
||||||
|
Click here to
|
||||||
|
<button
|
||||||
|
class="underline"
|
||||||
|
on:click={async () => {
|
||||||
|
const res = await deleteSharedChatById(localStorage.token, chatId);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
chat = await getChatById(localStorage.token, chatId);
|
||||||
|
}
|
||||||
|
}}>delete this link</button
|
||||||
|
> and create a new shared link.
|
||||||
|
{:else}
|
||||||
|
Messages you send after creating your link won't be shared. Users with the URL will be
|
||||||
|
able to view the shared chat.
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="flex flex-col items-end space-x-1 mt-1.5">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
shareChat();
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Share to OpenWebUI Community')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||||
|
type="button"
|
||||||
|
on:click={async () => {
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isSafari) {
|
||||||
|
// Oh, Safari, you're so special, let's give you some extra love and attention
|
||||||
|
console.log('isSafari');
|
||||||
|
|
||||||
|
const getUrlPromise = async () => {
|
||||||
|
const url = await shareLocalChat();
|
||||||
|
return new Blob([url], { type: 'text/plain' });
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'text/plain': getUrlPromise()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Async: Could not copy text: ', error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
copyToClipboard(await shareLocalChat());
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link />
|
||||||
|
|
||||||
|
{#if chat.share_id}
|
||||||
|
{$i18n.t('Update and Copy Link')}
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Copy Link')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
|
import Tags from '../common/Tags.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let tags;
|
||||||
|
export let deleteTag: Function;
|
||||||
|
export let addTag: Function;
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:show size="xs">
|
||||||
|
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
|
||||||
|
<Tags {tags} {deleteTag} {addTag} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script>
|
||||||
|
import {
|
||||||
|
addTagById,
|
||||||
|
deleteTagById,
|
||||||
|
getAllChatTags,
|
||||||
|
getTagsById,
|
||||||
|
updateChatById
|
||||||
|
} from '$lib/apis/chats';
|
||||||
|
import { tags as _tags } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import Tags from '../common/Tags.svelte';
|
||||||
|
|
||||||
|
export let chatId = '';
|
||||||
|
let tags = [];
|
||||||
|
|
||||||
|
const getTags = async () => {
|
||||||
|
return await getTagsById(localStorage.token, chatId).catch(async (error) => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTag = async (tagName) => {
|
||||||
|
const res = await addTagById(localStorage.token, chatId, tagName);
|
||||||
|
tags = await getTags();
|
||||||
|
|
||||||
|
await updateChatById(localStorage.token, chatId, {
|
||||||
|
tags: tags
|
||||||
|
});
|
||||||
|
|
||||||
|
_tags.set(await getAllChatTags(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = async (tagName) => {
|
||||||
|
const res = await deleteTagById(localStorage.token, chatId, tagName);
|
||||||
|
tags = await getTags();
|
||||||
|
|
||||||
|
await updateChatById(localStorage.token, chatId, {
|
||||||
|
tags: tags
|
||||||
|
});
|
||||||
|
|
||||||
|
_tags.set(await getAllChatTags(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (chatId) {
|
||||||
|
tags = await getTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tags {tags} {deleteTag} {addTag} />
|
||||||
@@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
|
bind:open={show}
|
||||||
onOpenChange={(state) => {
|
onOpenChange={(state) => {
|
||||||
dispatch('change', state);
|
dispatch('change', state);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
export let show = true;
|
export let show = true;
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
|
|
||||||
|
let modalElement = null;
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
const sizeToWidth = (size) => {
|
const sizeToWidth = (size) => {
|
||||||
@@ -14,8 +15,17 @@
|
|||||||
return 'w-[16rem]';
|
return 'w-[16rem]';
|
||||||
} else if (size === 'sm') {
|
} else if (size === 'sm') {
|
||||||
return 'w-[30rem]';
|
return 'w-[30rem]';
|
||||||
} else {
|
} else if (size === 'md') {
|
||||||
return 'w-[44rem]';
|
return 'w-[44rem]';
|
||||||
|
} else {
|
||||||
|
return 'w-[48rem]';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
console.log('Escape');
|
||||||
|
show = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,8 +35,10 @@
|
|||||||
|
|
||||||
$: if (mounted) {
|
$: if (mounted) {
|
||||||
if (show) {
|
if (show) {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
document.body.style.overflow = 'unset';
|
document.body.style.overflow = 'unset';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +48,8 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
bind:this={modalElement}
|
||||||
|
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
||||||
in:fade={{ duration: 10 }}
|
in:fade={{ duration: 10 }}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
show = false;
|
show = false;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Pagination } from 'bits-ui';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import ChevronLeft from '../icons/ChevronLeft.svelte';
|
||||||
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
|
|
||||||
|
export let page = 0;
|
||||||
|
export let count = 0;
|
||||||
|
export let perPage = 20;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Pagination.Root bind:page {count} {perPage} let:pages>
|
||||||
|
<div class="my-2 flex items-center">
|
||||||
|
<Pagination.PrevButton
|
||||||
|
class="mr-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-4" strokeWidth="2" />
|
||||||
|
</Pagination.PrevButton>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
{#each pages as page (page.key)}
|
||||||
|
{#if page.type === 'ellipsis'}
|
||||||
|
<div class="text-sm font-medium text-foreground-alt">...</div>
|
||||||
|
{:else}
|
||||||
|
<Pagination.Page
|
||||||
|
{page}
|
||||||
|
class="inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-sm font-medium hover:bg-dark-10 active:scale-98 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent data-[selected]:bg-black data-[selected]:text-gray-100 data-[selected]:hover:bg-black dark:data-[selected]:bg-white dark:data-[selected]:text-gray-900 dark:data-[selected]:hover:bg-white"
|
||||||
|
>
|
||||||
|
{page.value}
|
||||||
|
</Pagination.Page>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Pagination.NextButton
|
||||||
|
class="ml-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-4" strokeWidth="2" />
|
||||||
|
</Pagination.NextButton>
|
||||||
|
</div>
|
||||||
|
</Pagination.Root>
|
||||||
|
</div>
|
||||||
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user