Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac294a74e7 | ||
|
|
4c9598918a | ||
|
|
ffa168211e | ||
|
|
371dfc1143 | ||
|
|
a1faa307b1 | ||
|
|
a4c4a0370e | ||
|
|
3e0aa29683 | ||
|
|
df21a92542 | ||
|
|
b218b02d93 | ||
|
|
ff8a55a861 | ||
|
|
7e0ea8f77d | ||
|
|
c2d6d3230b | ||
|
|
8d108060d5 | ||
|
|
5cb6e9b0b4 | ||
|
|
cea74eaf52 | ||
|
|
adf9ccb5eb | ||
|
|
98624a406f | ||
|
|
862c96fcef | ||
|
|
cbaaacfb75 | ||
|
|
3e0d9ad74f | ||
|
|
e008738f30 | ||
|
|
244f34c24e | ||
|
|
642ca5a94a | ||
|
|
85187deb4b | ||
|
|
3305ac5928 | ||
|
|
51a4e49ae1 | ||
|
|
6c9f552733 | ||
|
|
7e6d499486 | ||
|
|
d291821bf3 | ||
|
|
45311bfa15 | ||
|
|
b03f4371d6 | ||
|
|
f662aab9e8 | ||
|
|
129253d90d | ||
|
|
2d1b67561b | ||
|
|
dca3dbbce6 | ||
|
|
e9f1a714e8 | ||
|
|
d5b5155c20 | ||
|
|
6e79530e0e | ||
|
|
b0a2eb31ee | ||
|
|
c0549498bf | ||
|
|
f8a922e966 | ||
|
|
5c1e3a460e | ||
|
|
bc04553a7c | ||
|
|
c296e9061e | ||
|
|
e14ad508c9 | ||
|
|
e9a198d8ed | ||
|
|
cb0e858c04 | ||
|
|
848d2382bc | ||
|
|
106727109e | ||
|
|
c97051e6eb | ||
|
|
4140e1b84a | ||
|
|
fe8a3ac2d7 | ||
|
|
b5111c4b34 | ||
|
|
92cc87d908 | ||
|
|
9e37d6edcc | ||
|
|
77ec296ac3 | ||
|
|
bc80f1438c | ||
|
|
af4caec4f5 | ||
|
|
c5e9ceeb06 | ||
|
|
47a04a7e3c | ||
|
|
c54fb92263 | ||
|
|
f3c113cb04 | ||
|
|
5f26b2a9e3 | ||
|
|
8935dca279 | ||
|
|
f675a18b05 | ||
|
|
f74f2ea765 | ||
|
|
5a90e90c0b | ||
|
|
2fa94956f4 | ||
|
|
8e52ba8b91 | ||
|
|
ed0d4b5a2a | ||
|
|
a1412d0b55 | ||
|
|
1a240e103e | ||
|
|
93c90dc186 | ||
|
|
8cb7127fbc | ||
|
|
4b91ac352a | ||
|
|
09dc3b7b67 | ||
|
|
bdc51b45ec | ||
|
|
6313a98287 | ||
|
|
ae347bb5e7 | ||
|
|
fa65be2ad2 | ||
|
|
2481e48a3a | ||
|
|
2c6e2d5e8a | ||
|
|
e6dd0bfbe0 | ||
|
|
b22ed64770 | ||
|
|
16fe0ee167 | ||
|
|
87344adf29 | ||
|
|
349fd0c87d | ||
|
|
d4a11ca33f | ||
|
|
bd84ee0c6a | ||
|
|
a9e411fdb9 | ||
|
|
de8de7e008 | ||
|
|
63edaae8d8 | ||
|
|
d3cf9a7535 | ||
|
|
7c8727151e | ||
|
|
c3f805b809 | ||
|
|
8508319c23 | ||
|
|
b9382f7051 | ||
|
|
53fb3d9c3e | ||
|
|
b21091bc01 | ||
|
|
4de591b930 | ||
|
|
492d4f3403 | ||
|
|
0b163c2c2d | ||
|
|
7bb8e91edb | ||
|
|
49faff497e | ||
|
|
33888ffaf8 | ||
|
|
b10eb0dacc | ||
|
|
7f197a70f9 | ||
|
|
61b4ab1e63 | ||
|
|
51342179ad | ||
|
|
f1925841e2 | ||
|
|
e86db74333 | ||
|
|
c569280806 | ||
|
|
bc5d322cbb | ||
|
|
d865b9fe59 | ||
|
|
b188143f01 | ||
|
|
621719c6ac | ||
|
|
ec9895a14a | ||
|
|
212e051683 | ||
|
|
47081011d6 | ||
|
|
c94d72bca8 | ||
|
|
c3dd11de16 | ||
|
|
1f0dcb4fce | ||
|
|
1bfcd801b7 | ||
|
|
19f69c821f | ||
|
|
8f8a68e002 | ||
|
|
94c845d71f | ||
|
|
adbcb06892 | ||
|
|
3c01932a78 | ||
|
|
a57586a2b7 | ||
|
|
41cb6d7a54 | ||
|
|
549408d808 | ||
|
|
f25a967a0b | ||
|
|
f55dae3027 | ||
|
|
db0712aefd | ||
|
|
f1716f45b9 | ||
|
|
6f3acb347d | ||
|
|
e414b9ea6d | ||
|
|
1e03871748 | ||
|
|
f90236f521 | ||
|
|
cf82aa9268 | ||
|
|
ddb654f293 | ||
|
|
ed964260ba | ||
|
|
a636e784bc | ||
|
|
ffeae78b88 | ||
|
|
79a02b85a8 | ||
|
|
36a5588957 | ||
|
|
e664c38377 | ||
|
|
264eb28455 | ||
|
|
06f7cba401 | ||
|
|
df55ced0c0 | ||
|
|
71def5c4cb | ||
|
|
d6905d6297 | ||
|
|
91efd6cb63 | ||
|
|
5ce421e7fa | ||
|
|
51ef5cfa45 | ||
|
|
072b499a50 | ||
|
|
d6e89d074d | ||
|
|
cd92cf0da5 | ||
|
|
f9bc35a11c | ||
|
|
7cef008b44 | ||
|
|
b85385385a | ||
|
|
aeb265c789 | ||
|
|
1e590b79cf | ||
|
|
c0de658dac | ||
|
|
a65d042c73 | ||
|
|
e99ab95987 | ||
|
|
dd22f1aebc | ||
|
|
81fba4774b | ||
|
|
8a23704138 | ||
|
|
5d6182eb7f | ||
|
|
fafe42e487 | ||
|
|
ccc1c81256 | ||
|
|
dbdc602791 | ||
|
|
9ee0feae32 | ||
|
|
aa3985e879 | ||
|
|
2a5bc2b2ad | ||
|
|
c6e14ce327 | ||
|
|
04e3b168a6 | ||
|
|
75607541c6 | ||
|
|
3790790a18 | ||
|
|
5a567ce4d0 | ||
|
|
7f78e58488 | ||
|
|
cc4b82a3f3 | ||
|
|
97842d037e | ||
|
|
26a187f5ac | ||
|
|
be3ab88c88 | ||
|
|
1091ee5a9b | ||
|
|
7ae4669f35 | ||
|
|
e3fc97241d | ||
|
|
b4c770d74b | ||
|
|
11ca2703b0 | ||
|
|
8df6b137cb | ||
|
|
89634046e7 | ||
|
|
88d324b52d | ||
|
|
78f79ef670 | ||
|
|
64afde6c0e | ||
|
|
1e9d40af42 | ||
|
|
98948814fd | ||
|
|
7545a717ff | ||
|
|
b8902072fd | ||
|
|
96ada23272 | ||
|
|
a02ac67b5c | ||
|
|
350489e600 | ||
|
|
8a08ba0791 | ||
|
|
c6667510c4 | ||
|
|
bd84753c6b | ||
|
|
a4ca1fc5c4 | ||
|
|
bcabd3df84 | ||
|
|
81daf4ceb9 | ||
|
|
b550e23bf6 | ||
|
|
83a900bd4a | ||
|
|
1a93021cc4 | ||
|
|
27153ddb03 | ||
|
|
b94dc767ff | ||
|
|
6e57fda8f4 | ||
|
|
6d5ff8d469 | ||
|
|
219466374d | ||
|
|
d40edc09e1 | ||
|
|
d99114518c | ||
|
|
784ee6f521 | ||
|
|
d936353da0 | ||
|
|
9f58ed5afa | ||
|
|
dfcc314283 | ||
|
|
6c58bb59be | ||
|
|
c49491e516 | ||
|
|
6ba62cf25d | ||
|
|
19700e9074 | ||
|
|
7e5e2c42c9 | ||
|
|
9235864f56 | ||
|
|
f221e39c24 | ||
|
|
30503b5958 | ||
|
|
fe7610d380 | ||
|
|
0221acd163 | ||
|
|
c7ca7d03ee | ||
|
|
dd3a4b3889 | ||
|
|
90513941bc | ||
|
|
e8ffb2c023 | ||
|
|
48e8b45dc9 | ||
|
|
f1c8e7f5d1 | ||
|
|
f4bd442495 | ||
|
|
f2c7db7637 | ||
|
|
404aa62a8a | ||
|
|
9881022b11 | ||
|
|
9a83bde7e5 | ||
|
|
2b84af878a | ||
|
|
3c4cc60277 | ||
|
|
f605bd96be | ||
|
|
171084ea0b | ||
|
|
712f0e3501 | ||
|
|
04ddbf4306 | ||
|
|
f73cae2a73 | ||
|
|
3fa5144cf6 | ||
|
|
275a8256bb | ||
|
|
d4c8a7dab2 | ||
|
|
1f2d704f76 | ||
|
|
12c74b2cd1 | ||
|
|
fcc6189091 | ||
|
|
93ed5851ca | ||
|
|
d40fbcd06b | ||
|
|
8c596ea2fc | ||
|
|
0d127ffef1 | ||
|
|
f5afb0ca52 | ||
|
|
203cd84922 | ||
|
|
f7e10e1fb6 | ||
|
|
03690de773 | ||
|
|
80e2125266 | ||
|
|
7031aa14e8 | ||
|
|
08aa60b33e | ||
|
|
bbf9deabf0 | ||
|
|
f55525c680 | ||
|
|
a901031896 | ||
|
|
c724e19e62 | ||
|
|
b76eb46d86 | ||
|
|
f4a1885298 | ||
|
|
41378748b8 | ||
|
|
ef9a799e77 | ||
|
|
8ed5759d0e | ||
|
|
6c4e368ac7 | ||
|
|
df49e4e1cc | ||
|
|
14c38d31a7 | ||
|
|
3823908f37 | ||
|
|
35918cefcc | ||
|
|
34c7b6783c | ||
|
|
0476c5f30b | ||
|
|
53adc6a0ca | ||
|
|
b88c64f80e | ||
|
|
dc6e91a23d | ||
|
|
bb98c10abb | ||
|
|
e17e2e7c17 | ||
|
|
c255cba198 | ||
|
|
51d509bafb | ||
|
|
df298e1001 | ||
|
|
0142c2de43 | ||
|
|
92e21acb4c | ||
|
|
ad237b61f3 | ||
|
|
6a271a1691 | ||
|
|
c9173b71b6 | ||
|
|
f741adc6c9 | ||
|
|
5ae121b087 | ||
|
|
434595b3e6 | ||
|
|
534ba9e4f0 | ||
|
|
34b6a99601 | ||
|
|
0aa4e3aafb | ||
|
|
7258a794f1 | ||
|
|
213e08a8a5 | ||
|
|
e4eb32709d | ||
|
|
20284dc66d | ||
|
|
2b016adc6e | ||
|
|
a9d74b669e | ||
|
|
8fb5f54751 | ||
|
|
2111398d13 | ||
|
|
25e0f0de42 | ||
|
|
089a63e0c6 | ||
|
|
3455f8993b | ||
|
|
132e05ad57 | ||
|
|
95b20f3cb8 | ||
|
|
df8aeb39c6 | ||
|
|
2b3c8194eb | ||
|
|
71dba592bf | ||
|
|
31fc9a04b6 | ||
|
|
53f69ace6c | ||
|
|
66342140a3 | ||
|
|
b5b860f575 | ||
|
|
7f9049c3a1 | ||
|
|
1fad6e10cd | ||
|
|
124020f824 | ||
|
|
fbdac832bb | ||
|
|
8181d98e99 | ||
|
|
8626ae60dd | ||
|
|
8d34324d12 | ||
|
|
b2dd2f191d | ||
|
|
8fd7bc342d | ||
|
|
f04d60b6d9 | ||
|
|
b9f53abf37 | ||
|
|
fcb991db63 | ||
|
|
03907f9a8a | ||
|
|
475105473c | ||
|
|
2d59847fb9 | ||
|
|
32495256fa | ||
|
|
5f5b6ec141 | ||
|
|
8c01197483 | ||
|
|
6782e95c75 | ||
|
|
4d67dd977d | ||
|
|
ec8f229f5a | ||
|
|
d4fd6c5a57 | ||
|
|
7a79aab5d4 | ||
|
|
3fe5f00114 | ||
|
|
7a30dbc839 | ||
|
|
b986c2aefd | ||
|
|
20263f742a | ||
|
|
5d4ff85228 | ||
|
|
50acdb09de | ||
|
|
0def6f8724 | ||
|
|
15e0f9de86 | ||
|
|
fe040ce82b | ||
|
|
f4f6724d96 | ||
|
|
13f0052122 | ||
|
|
a5c1ac9680 | ||
|
|
0fc76d46db | ||
|
|
cc471597cd | ||
|
|
fe19f125d4 | ||
|
|
05221ea135 | ||
|
|
304006aa8c | ||
|
|
893b738cbf | ||
|
|
670b65c066 | ||
|
|
6d2f462090 | ||
|
|
2250387382 | ||
|
|
f9f3fe5ec0 | ||
|
|
eb51ad14e4 | ||
|
|
65804e657a | ||
|
|
25c0316fc6 | ||
|
|
4a1e506a35 | ||
|
|
47a05a47b4 | ||
|
|
9694c6569f | ||
|
|
9201769ba5 | ||
|
|
79d7e135ec | ||
|
|
bf3f3c580e | ||
|
|
901e7a33fa | ||
|
|
656f8dab05 | ||
|
|
da0202851f | ||
|
|
45f6908154 | ||
|
|
fbed07f651 | ||
|
|
511b52f642 | ||
|
|
3c471ee2ca | ||
|
|
06ce5aed4a | ||
|
|
7a77f3c2c0 | ||
|
|
fab89a76b1 | ||
|
|
2379e94cbf | ||
|
|
9b86e0bb41 | ||
|
|
b9444d00b3 | ||
|
|
0bc3e0a302 | ||
|
|
f9482275f2 | ||
|
|
63c0927022 | ||
|
|
edb63c2280 | ||
|
|
6d854125cc | ||
|
|
74809e7330 | ||
|
|
a4c6a8d5a4 | ||
|
|
9caf6c26ff | ||
|
|
a12b5ffab3 | ||
|
|
b85c6ef667 | ||
|
|
6c70d0f770 | ||
|
|
96e2ee48f7 | ||
|
|
63e7246c97 | ||
|
|
6f4a863562 | ||
|
|
a7ae141301 | ||
|
|
2564b154fc | ||
|
|
8310392c9d | ||
|
|
78d9c469a2 | ||
|
|
e5c812f1d2 | ||
|
|
8461b92f08 | ||
|
|
3396723057 | ||
|
|
4fe47f43be | ||
|
|
705f5aecd7 | ||
|
|
19d49df76f | ||
|
|
30c94ff10f | ||
|
|
941ab5d6cf | ||
|
|
5a81ca7e39 | ||
|
|
f4122fd0ce | ||
|
|
5745b9c5db | ||
|
|
3fd55ebc32 | ||
|
|
776b463ff3 | ||
|
|
5640b38694 | ||
|
|
a181b2b63b | ||
|
|
32361dc3da | ||
|
|
59ae5b6d34 | ||
|
|
a36627cbce | ||
|
|
8a63d22000 | ||
|
|
781be2779b | ||
|
|
f347d664ac | ||
|
|
260a8b8b83 | ||
|
|
1d4d1ddbea | ||
|
|
8f2d580991 | ||
|
|
6bbc3721b4 | ||
|
|
7fe412374c | ||
|
|
03210c9204 | ||
|
|
d24b6fa9af | ||
|
|
7c7e16c382 | ||
|
|
70f807e8c9 | ||
|
|
210ae331cc | ||
|
|
bbeaa92904 | ||
|
|
a7a817d12b | ||
|
|
6df2505bf0 | ||
|
|
3a7ded3f80 | ||
|
|
6d8b1385bb | ||
|
|
92c922b061 | ||
|
|
63628a70a6 | ||
|
|
95dd618a62 | ||
|
|
f8cf43c0f7 | ||
|
|
334d5246ba | ||
|
|
42251c232a | ||
|
|
6b0eae9fad | ||
|
|
1ff0c9a95d | ||
|
|
0a2ee691db | ||
|
|
6245146661 | ||
|
|
588daace91 | ||
|
|
8f4a12b479 | ||
|
|
81a50c5104 | ||
|
|
d1656d5da8 | ||
|
|
0f6bdaa602 | ||
|
|
a016171573 | ||
|
|
7d0d504390 | ||
|
|
6bc627bbfb | ||
|
|
5adff7767f | ||
|
|
ae97a96379 |
@@ -7,7 +7,6 @@ node_modules
|
|||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# Ollama URL for the backend to connect
|
# Ollama URL for the backend to connect
|
||||||
# The path '/ollama/api' will be redirected to the specified backend URL
|
# The path '/ollama' will be redirected to the specified backend URL
|
||||||
OLLAMA_API_BASE_URL='http://localhost:11434/api'
|
OLLAMA_BASE_URL='http://localhost:11434'
|
||||||
|
|
||||||
OPENAI_API_BASE_URL=''
|
OPENAI_API_BASE_URL=''
|
||||||
OPENAI_API_KEY=''
|
OPENAI_API_KEY=''
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ assignees: ''
|
|||||||
**Confirmation:**
|
**Confirmation:**
|
||||||
|
|
||||||
- [ ] I have read and followed all the instructions provided in the README.md.
|
- [ ] I have read and followed all the instructions provided in the README.md.
|
||||||
- [ ] I have reviewed the troubleshooting.md document.
|
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
||||||
- [ ] I have included the browser console logs.
|
- [ ] I have included the browser console logs.
|
||||||
- [ ] I have included the Docker container logs.
|
- [ ] I have included the Docker container logs.
|
||||||
|
|
||||||
|
|||||||
@@ -19,24 +19,34 @@ jobs:
|
|||||||
echo "No changes to package.json"
|
echo "No changes to package.json"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Get version number from package.json
|
- name: Get version number from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(jq -r '.version' package.json)
|
VERSION=$(jq -r '.version' package.json)
|
||||||
echo "::set-output name=version::$VERSION"
|
echo "::set-output name=version::$VERSION"
|
||||||
|
|
||||||
|
- name: Extract latest CHANGELOG entry
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
|
||||||
|
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
|
||||||
|
echo "Extracted latest release notes from CHANGELOG.md:"
|
||||||
|
echo -e "$CHANGELOG_CONTENT"
|
||||||
|
echo "::set-output name=content::$CHANGELOG_ESCAPED"
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: actions/github-script@v5
|
uses: actions/github-script@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
const changelog = `${{ steps.changelog.outputs.content }}`;
|
||||||
const release = await github.rest.repos.createRelease({
|
const release = await github.rest.repos.createRelease({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
||||||
name: `v${{ steps.get_version.outputs.version }}`,
|
name: `v${{ steps.get_version.outputs.version }}`,
|
||||||
body: 'Automatically created new release',
|
body: changelog,
|
||||||
})
|
})
|
||||||
console.log(`Created release ${release.data.html_url}`)
|
console.log(`Created release ${release.data.html_url}`)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ jobs:
|
|||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=sha,prefix=git-
|
type=sha,prefix=git-
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ github.ref == 'refs/heads/main' }}
|
latest=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -166,7 +166,7 @@ cython_debug/
|
|||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
+172
@@ -5,6 +5,178 @@ 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.115] - 2024-03-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
|
||||||
|
- **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
|
||||||
|
- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
|
||||||
|
- **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
|
||||||
|
- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
|
||||||
|
|
||||||
|
## [0.1.114] - 2024-03-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
|
||||||
|
- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
|
||||||
|
- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
|
||||||
|
- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
|
||||||
|
|
||||||
|
## [0.1.113] - 2024-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization).
|
||||||
|
- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
|
||||||
|
- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
|
||||||
|
- **RAG Encoding Issue**: Fixed encoding problem in RAG.
|
||||||
|
- **npm Audit Fix**: Addressed npm audit findings.
|
||||||
|
- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
|
||||||
|
|
||||||
|
## [0.1.112] - 2024-03-15
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🗨️ Resolved chat malfunction after image generation.
|
||||||
|
- 🎨 Fixed various RAG issues.
|
||||||
|
- 🧪 Rectified experimental broken GGUF upload logic.
|
||||||
|
|
||||||
|
## [0.1.111] - 2024-03-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
|
||||||
|
- 🔄 **Update All Models**: Added a convenient button to update all models at once.
|
||||||
|
- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
|
||||||
|
- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
|
||||||
|
- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
|
||||||
|
- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
|
||||||
|
- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
|
||||||
|
- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
|
||||||
|
- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
|
||||||
|
|
||||||
|
## [0.1.110] - 2024-03-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
|
||||||
|
- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
|
||||||
|
- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
|
||||||
|
|
||||||
|
## [0.1.109] - 2024-03-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278).
|
||||||
|
- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064).
|
||||||
|
- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
|
||||||
|
- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024).
|
||||||
|
- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
|
||||||
|
- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
|
||||||
|
- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details.
|
||||||
|
|
||||||
|
## [0.1.108] - 2024-03-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter.
|
||||||
|
- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
|
||||||
|
- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
|
||||||
|
- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
|
||||||
|
- Corrected numbered list display issue in Safari (#963).
|
||||||
|
- Restricted user ability to delete chats without proper permissions (#993).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added.
|
||||||
|
- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
|
||||||
|
|
||||||
|
## [0.1.107] - 2024-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Corrected issue where links in the settings modal didn't appear clickable (#960).
|
||||||
|
- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
|
||||||
|
- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
|
||||||
|
- Enhanced accessibility with the addition of dark mode on the authentication page.
|
||||||
|
|
||||||
|
## [0.1.106] - 2024-02-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
|
||||||
|
- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
|
||||||
|
- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
|
||||||
|
- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930)
|
||||||
|
|
||||||
|
## [0.1.105] - 2024-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization.
|
||||||
|
|
||||||
## [0.1.104] - 2024-02-25
|
## [0.1.104] - 2024-02-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+5
-3
@@ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base
|
|||||||
ENV ENV=prod
|
ENV ENV=prod
|
||||||
ENV PORT ""
|
ENV PORT ""
|
||||||
|
|
||||||
ENV OLLAMA_API_BASE_URL "/ollama/api"
|
ENV OLLAMA_BASE_URL "/ollama"
|
||||||
|
|
||||||
ENV OPENAI_API_BASE_URL ""
|
ENV OPENAI_API_BASE_URL ""
|
||||||
ENV OPENAI_API_KEY ""
|
ENV OPENAI_API_KEY ""
|
||||||
@@ -41,7 +41,7 @@ ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
|||||||
# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||||
# 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.
|
# 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"
|
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
|
||||||
# device type for whisper tts and ebbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
# 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_DEVICE_TYPE="cpu"
|
||||||
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
|
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
|
||||||
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
|
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
|
||||||
@@ -53,6 +53,8 @@ WORKDIR /app/backend
|
|||||||
# 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 torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
|
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
|
RUN pip3 install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
@@ -79,4 +81,4 @@ COPY --from=build /app/package.json /app/package.json
|
|||||||
# copy backend files
|
# copy backend files
|
||||||
COPY ./backend .
|
COPY ./backend .
|
||||||
|
|
||||||
CMD [ "bash", "start.sh"]
|
CMD [ "bash", "start.sh"]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
install:
|
||||||
|
@docker-compose up -d
|
||||||
|
|
||||||
|
remove:
|
||||||
|
@chmod +x confirm_remove.sh
|
||||||
|
@./confirm_remove.sh
|
||||||
|
|
||||||
|
|
||||||
|
start:
|
||||||
|
@docker-compose start
|
||||||
|
|
||||||
|
stop:
|
||||||
|
@docker-compose stop
|
||||||
|
|
||||||
|
update:
|
||||||
|
# Calls the LLM update script
|
||||||
|
chmod +x update_ollama_models.sh
|
||||||
|
@./update_ollama_models.sh
|
||||||
|
@git pull
|
||||||
|
@docker-compose down
|
||||||
|
# Make sure the ollama-webui container is stopped before rebuilding
|
||||||
|
@docker stop open-webui || true
|
||||||
|
@docker-compose up --build -d
|
||||||
|
@docker-compose start
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
[](https://discord.gg/5rJgQTnV4s)
|
[](https://discord.gg/5rJgQTnV4s)
|
||||||
[](https://github.com/sponsors/tjbck)
|
[](https://github.com/sponsors/tjbck)
|
||||||
|
|
||||||
User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -53,8 +53,6 @@ User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-co
|
|||||||
|
|
||||||
- 💬 **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.
|
||||||
|
|
||||||
- 🤝 **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**.
|
|
||||||
|
|
||||||
- 🔄 **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.
|
||||||
@@ -65,12 +63,24 @@ User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-co
|
|||||||
|
|
||||||
- ⚙️ **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.
|
||||||
|
|
||||||
|
- 🤝 **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.
|
||||||
|
|
||||||
- 🔗 **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.
|
||||||
|
|
||||||
|
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
||||||
|
|
||||||
- 🔐 **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.
|
||||||
|
|
||||||
|
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
|
||||||
|
|
||||||
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
||||||
|
|
||||||
## 🔗 Also Check Out Open WebUI Community!
|
## 🔗 Also Check Out Open WebUI Community!
|
||||||
@@ -95,10 +105,10 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
|||||||
|
|
||||||
- **If Ollama is on a Different Server**, use this command:
|
- **If Ollama is on a Different Server**, use this command:
|
||||||
|
|
||||||
- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL:
|
- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
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! 😄
|
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
||||||
@@ -110,7 +120,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
|||||||
**Example Docker Command**:
|
**Example Docker Command**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other Installation Methods
|
### Other Installation Methods
|
||||||
@@ -160,6 +170,16 @@ This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LI
|
|||||||
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
||||||
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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 Web UI even more amazing together! 💪
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
|
The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
|
||||||
|
|
||||||
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend.
|
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
|
||||||
|
|
||||||
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
|
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
|||||||
**Example Docker Command**:
|
**Example Docker Command**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||||
```
|
```
|
||||||
|
|
||||||
### General Connection Errors
|
### General Connection Errors
|
||||||
@@ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_
|
|||||||
**Troubleshooting Steps**:
|
**Troubleshooting Steps**:
|
||||||
|
|
||||||
1. **Verify Ollama URL Format**:
|
1. **Verify Ollama URL Format**:
|
||||||
- When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups).
|
- When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
|
||||||
- In the Open WebUI, navigate to "Settings" > "General".
|
- In the Open WebUI, navigate to "Settings" > "General".
|
||||||
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix.
|
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
|
||||||
|
|
||||||
By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
|
By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
FastAPI,
|
FastAPI,
|
||||||
Request,
|
Request,
|
||||||
@@ -21,7 +22,10 @@ from utils.utils import (
|
|||||||
)
|
)
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
|
|
||||||
from config import CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR
|
from config import SRC_LOG_LEVELS, CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -38,7 +42,7 @@ def transcribe(
|
|||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
print(file.content_type)
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
|
|
||||||
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -62,7 +66,7 @@ def transcribe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
segments, info = model.transcribe(file_path, beam_size=5)
|
segments, info = model.transcribe(file_path, beam_size=5)
|
||||||
print(
|
log.info(
|
||||||
"Detected language '%s' with probability %f"
|
"Detected language '%s' with probability %f"
|
||||||
% (info.language, info.language_probability)
|
% (info.language, info.language_probability)
|
||||||
)
|
)
|
||||||
@@ -72,7 +76,7 @@ def transcribe(
|
|||||||
return {"text": transcript.strip()}
|
return {"text": transcript.strip()}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|||||||
+292
-58
@@ -18,10 +18,25 @@ from utils.utils import (
|
|||||||
get_current_user,
|
get_current_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
|
||||||
from utils.misc import calculate_sha256
|
from utils.misc import calculate_sha256
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from config import AUTOMATIC1111_BASE_URL
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
||||||
|
|
||||||
|
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
||||||
|
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -32,46 +47,104 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.state.ENGINE = ""
|
||||||
|
app.state.ENABLED = False
|
||||||
|
|
||||||
|
app.state.OPENAI_API_KEY = ""
|
||||||
|
app.state.MODEL = ""
|
||||||
|
|
||||||
|
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
|
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
app.state.IMAGE_SIZE = "512x512"
|
app.state.IMAGE_SIZE = "512x512"
|
||||||
app.state.IMAGE_STEPS = 50
|
app.state.IMAGE_STEPS = 50
|
||||||
|
|
||||||
|
|
||||||
@app.get("/enabled", response_model=bool)
|
@app.get("/config")
|
||||||
async def get_enable_status(request: Request, user=Depends(get_admin_user)):
|
async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return app.state.ENABLED
|
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/enabled/toggle", response_model=bool)
|
class ConfigUpdateForm(BaseModel):
|
||||||
async def toggle_enabled(request: Request, user=Depends(get_admin_user)):
|
engine: str
|
||||||
try:
|
enabled: bool
|
||||||
r = requests.head(app.state.AUTOMATIC1111_BASE_URL)
|
|
||||||
app.state.ENABLED = not app.state.ENABLED
|
|
||||||
return app.state.ENABLED
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
|
||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
@app.post("/config/update")
|
||||||
url: str
|
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.ENGINE = form_data.engine
|
||||||
|
app.state.ENABLED = form_data.enabled
|
||||||
|
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
|
||||||
|
|
||||||
|
|
||||||
|
class EngineUrlUpdateForm(BaseModel):
|
||||||
|
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
||||||
|
COMFYUI_BASE_URL: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
@app.get("/url")
|
||||||
async def get_openai_url(user=Depends(get_admin_user)):
|
async def get_engine_url(user=Depends(get_admin_user)):
|
||||||
return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL}
|
return {
|
||||||
|
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||||
|
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
@app.post("/url/update")
|
||||||
async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
async def update_engine_url(
|
||||||
|
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
|
||||||
if form_data.url == "":
|
if form_data.AUTOMATIC1111_BASE_URL == None:
|
||||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||||
else:
|
else:
|
||||||
app.state.AUTOMATIC1111_BASE_URL = form_data.url.strip("/")
|
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
||||||
|
try:
|
||||||
|
r = requests.head(url)
|
||||||
|
app.state.AUTOMATIC1111_BASE_URL = url
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
|
|
||||||
|
if form_data.COMFYUI_BASE_URL == None:
|
||||||
|
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||||
|
else:
|
||||||
|
url = form_data.COMFYUI_BASE_URL.strip("/")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.head(url)
|
||||||
|
app.state.COMFYUI_BASE_URL = url
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||||
|
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
|
||||||
|
"status": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIKeyUpdateForm(BaseModel):
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/key")
|
||||||
|
async def get_openai_key(user=Depends(get_admin_user)):
|
||||||
|
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/key/update")
|
||||||
|
async def update_openai_key(
|
||||||
|
form_data: OpenAIKeyUpdateForm, 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_KEY = form_data.key
|
||||||
|
return {
|
||||||
|
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
|
||||||
"status": True,
|
"status": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,9 +205,34 @@ async def update_image_size(
|
|||||||
@app.get("/models")
|
@app.get("/models")
|
||||||
def get_models(user=Depends(get_current_user)):
|
def get_models(user=Depends(get_current_user)):
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models")
|
if app.state.ENGINE == "openai":
|
||||||
models = r.json()
|
return [
|
||||||
return models
|
{"id": "dall-e-2", "name": "DALL·E 2"},
|
||||||
|
{"id": "dall-e-3", "name": "DALL·E 3"},
|
||||||
|
]
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
|
|
||||||
|
r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
|
||||||
|
info = r.json()
|
||||||
|
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda model: {"id": model, "name": model},
|
||||||
|
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
r = requests.get(
|
||||||
|
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
||||||
|
)
|
||||||
|
models = r.json()
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda model: {"id": model["title"], "name": model["model_name"]},
|
||||||
|
models,
|
||||||
|
)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.state.ENABLED = False
|
app.state.ENABLED = False
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
@@ -143,10 +241,14 @@ def get_models(user=Depends(get_current_user)):
|
|||||||
@app.get("/models/default")
|
@app.get("/models/default")
|
||||||
async def get_default_model(user=Depends(get_admin_user)):
|
async def get_default_model(user=Depends(get_admin_user)):
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
if app.state.ENGINE == "openai":
|
||||||
options = r.json()
|
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
return {"model": options["sd_model_checkpoint"]}
|
return {"model": app.state.MODEL if app.state.MODEL else ""}
|
||||||
|
else:
|
||||||
|
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||||
|
options = r.json()
|
||||||
|
return {"model": options["sd_model_checkpoint"]}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.state.ENABLED = False
|
app.state.ENABLED = False
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
@@ -157,16 +259,23 @@ class UpdateModelForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def set_model_handler(model: str):
|
def set_model_handler(model: str):
|
||||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
if app.state.ENGINE == "openai":
|
||||||
options = r.json()
|
app.state.MODEL = model
|
||||||
|
return app.state.MODEL
|
||||||
|
if app.state.ENGINE == "comfyui":
|
||||||
|
app.state.MODEL = model
|
||||||
|
return app.state.MODEL
|
||||||
|
else:
|
||||||
|
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||||
|
options = r.json()
|
||||||
|
|
||||||
if model != options["sd_model_checkpoint"]:
|
if model != options["sd_model_checkpoint"]:
|
||||||
options["sd_model_checkpoint"] = model
|
options["sd_model_checkpoint"] = model
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
|
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
|
||||||
)
|
)
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
@app.post("/models/default/update")
|
@app.post("/models/default/update")
|
||||||
@@ -181,45 +290,170 @@ class GenerateImageForm(BaseModel):
|
|||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
prompt: str
|
prompt: str
|
||||||
n: int = 1
|
n: int = 1
|
||||||
size: str = "512x512"
|
size: Optional[str] = None
|
||||||
negative_prompt: Optional[str] = None
|
negative_prompt: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def save_b64_image(b64_str):
|
||||||
|
image_id = str(uuid.uuid4())
|
||||||
|
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Split the base64 string to get the actual image data
|
||||||
|
img_data = base64.b64decode(b64_str)
|
||||||
|
|
||||||
|
# Write the image data to a file
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(img_data)
|
||||||
|
|
||||||
|
return image_id
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error saving image: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_url_image(url):
|
||||||
|
image_id = str(uuid.uuid4())
|
||||||
|
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
with open(file_path, "wb") as image_file:
|
||||||
|
image_file.write(r.content)
|
||||||
|
|
||||||
|
return image_id
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving image: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/generations")
|
@app.post("/generations")
|
||||||
def generate_image(
|
def generate_image(
|
||||||
form_data: GenerateImageForm,
|
form_data: GenerateImageForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
print(form_data)
|
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
||||||
|
|
||||||
|
r = None
|
||||||
try:
|
try:
|
||||||
if form_data.model:
|
if app.state.ENGINE == "openai":
|
||||||
set_model_handler(form_data.model)
|
|
||||||
|
|
||||||
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
headers = {}
|
||||||
|
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"prompt": form_data.prompt,
|
"model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
|
||||||
"batch_size": form_data.n,
|
"prompt": form_data.prompt,
|
||||||
"width": width,
|
"n": form_data.n,
|
||||||
"height": height,
|
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
|
||||||
}
|
"response_format": "b64_json",
|
||||||
|
}
|
||||||
|
|
||||||
if app.state.IMAGE_STEPS != None:
|
r = requests.post(
|
||||||
data["steps"] = app.state.IMAGE_STEPS
|
url=f"https://api.openai.com/v1/images/generations",
|
||||||
|
json=data,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
if form_data.negative_prompt != None:
|
r.raise_for_status()
|
||||||
data["negative_prompt"] = form_data.negative_prompt
|
res = r.json()
|
||||||
|
|
||||||
print(data)
|
images = []
|
||||||
|
|
||||||
r = requests.post(
|
for image in res["data"]:
|
||||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
image_id = save_b64_image(image["b64_json"])
|
||||||
json=data,
|
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||||
)
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
elif app.state.ENGINE == "comfyui":
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"prompt": form_data.prompt,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"n": form_data.n,
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.state.IMAGE_STEPS != None:
|
||||||
|
data["steps"] = app.state.IMAGE_STEPS
|
||||||
|
|
||||||
|
if form_data.negative_prompt != None:
|
||||||
|
data["negative_prompt"] = form_data.negative_prompt
|
||||||
|
|
||||||
|
data = ImageGenerationPayload(**data)
|
||||||
|
|
||||||
|
res = comfyui_generate_image(
|
||||||
|
app.state.MODEL,
|
||||||
|
data,
|
||||||
|
user.id,
|
||||||
|
app.state.COMFYUI_BASE_URL,
|
||||||
|
)
|
||||||
|
print(res)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
|
||||||
|
for image in res["data"]:
|
||||||
|
image_id = save_url_image(image["url"])
|
||||||
|
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||||
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump(data.model_dump(exclude_none=True), f)
|
||||||
|
|
||||||
|
print(images)
|
||||||
|
return images
|
||||||
|
else:
|
||||||
|
if form_data.model:
|
||||||
|
set_model_handler(form_data.model)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"prompt": form_data.prompt,
|
||||||
|
"batch_size": form_data.n,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.state.IMAGE_STEPS != None:
|
||||||
|
data["steps"] = app.state.IMAGE_STEPS
|
||||||
|
|
||||||
|
if form_data.negative_prompt != None:
|
||||||
|
data["negative_prompt"] = form_data.negative_prompt
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
res = r.json()
|
||||||
|
|
||||||
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
|
images = []
|
||||||
|
|
||||||
|
for image in res["images"]:
|
||||||
|
image_id = save_b64_image(image)
|
||||||
|
images.append({"url": f"/cache/image/generations/{image_id}.png"})
|
||||||
|
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
|
||||||
|
|
||||||
|
with open(file_body_path, "w") as f:
|
||||||
|
json.dump({**data, "info": res["info"]}, f)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
return r.json()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
error = e
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
|
||||||
|
if r != None:
|
||||||
|
data = r.json()
|
||||||
|
if "error" in data:
|
||||||
|
error = data["error"]["message"]
|
||||||
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import random
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
COMFYUI_DEFAULT_PROMPT = """
|
||||||
|
{
|
||||||
|
"3": {
|
||||||
|
"inputs": {
|
||||||
|
"seed": 0,
|
||||||
|
"steps": 20,
|
||||||
|
"cfg": 8,
|
||||||
|
"sampler_name": "euler",
|
||||||
|
"scheduler": "normal",
|
||||||
|
"denoise": 1,
|
||||||
|
"model": [
|
||||||
|
"4",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"positive": [
|
||||||
|
"6",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"negative": [
|
||||||
|
"7",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"latent_image": [
|
||||||
|
"5",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "KSampler",
|
||||||
|
"_meta": {
|
||||||
|
"title": "KSampler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"inputs": {
|
||||||
|
"ckpt_name": "model.safetensors"
|
||||||
|
},
|
||||||
|
"class_type": "CheckpointLoaderSimple",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Load Checkpoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"inputs": {
|
||||||
|
"width": 512,
|
||||||
|
"height": 512,
|
||||||
|
"batch_size": 1
|
||||||
|
},
|
||||||
|
"class_type": "EmptyLatentImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Empty Latent Image"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "Prompt",
|
||||||
|
"clip": [
|
||||||
|
"4",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "CLIP Text Encode (Prompt)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"inputs": {
|
||||||
|
"text": "Negative Prompt",
|
||||||
|
"clip": [
|
||||||
|
"4",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "CLIPTextEncode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "CLIP Text Encode (Prompt)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"inputs": {
|
||||||
|
"samples": [
|
||||||
|
"3",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
"4",
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "VAEDecode",
|
||||||
|
"_meta": {
|
||||||
|
"title": "VAE Decode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"inputs": {
|
||||||
|
"filename_prefix": "ComfyUI",
|
||||||
|
"images": [
|
||||||
|
"8",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"_meta": {
|
||||||
|
"title": "Save Image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def queue_prompt(prompt, client_id, base_url):
|
||||||
|
print("queue_prompt")
|
||||||
|
p = {"prompt": prompt, "client_id": client_id}
|
||||||
|
data = json.dumps(p).encode("utf-8")
|
||||||
|
req = urllib.request.Request(f"{base_url}/prompt", data=data)
|
||||||
|
return json.loads(urllib.request.urlopen(req).read())
|
||||||
|
|
||||||
|
|
||||||
|
def get_image(filename, subfolder, folder_type, base_url):
|
||||||
|
print("get_image")
|
||||||
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
|
url_values = urllib.parse.urlencode(data)
|
||||||
|
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_url(filename, subfolder, folder_type, base_url):
|
||||||
|
print("get_image")
|
||||||
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
|
url_values = urllib.parse.urlencode(data)
|
||||||
|
return f"{base_url}/view?{url_values}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_history(prompt_id, base_url):
|
||||||
|
print("get_history")
|
||||||
|
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
|
||||||
|
return json.loads(response.read())
|
||||||
|
|
||||||
|
|
||||||
|
def get_images(ws, prompt, client_id, base_url):
|
||||||
|
prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"]
|
||||||
|
output_images = []
|
||||||
|
while True:
|
||||||
|
out = ws.recv()
|
||||||
|
if isinstance(out, str):
|
||||||
|
message = json.loads(out)
|
||||||
|
if message["type"] == "executing":
|
||||||
|
data = message["data"]
|
||||||
|
if data["node"] is None and data["prompt_id"] == prompt_id:
|
||||||
|
break # Execution is done
|
||||||
|
else:
|
||||||
|
continue # previews are binary data
|
||||||
|
|
||||||
|
history = get_history(prompt_id, base_url)[prompt_id]
|
||||||
|
for o in history["outputs"]:
|
||||||
|
for node_id in history["outputs"]:
|
||||||
|
node_output = history["outputs"][node_id]
|
||||||
|
if "images" in node_output:
|
||||||
|
for image in node_output["images"]:
|
||||||
|
url = get_image_url(
|
||||||
|
image["filename"], image["subfolder"], image["type"], base_url
|
||||||
|
)
|
||||||
|
output_images.append({"url": url})
|
||||||
|
return {"data": output_images}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageGenerationPayload(BaseModel):
|
||||||
|
prompt: str
|
||||||
|
negative_prompt: Optional[str] = ""
|
||||||
|
steps: Optional[int] = None
|
||||||
|
seed: Optional[int] = None
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
n: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_generate_image(
|
||||||
|
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||||
|
):
|
||||||
|
host = base_url.replace("http://", "").replace("https://", "")
|
||||||
|
|
||||||
|
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||||
|
|
||||||
|
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||||
|
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||||
|
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||||
|
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
||||||
|
|
||||||
|
# set the text prompt for our positive CLIPTextEncode
|
||||||
|
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
||||||
|
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||||
|
|
||||||
|
if payload.steps:
|
||||||
|
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||||
|
|
||||||
|
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||||
|
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
ws.connect(f"ws://{host}/ws?clientId={client_id}")
|
||||||
|
print("WebSocket connection established.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to WebSocket server: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while receiving images: {e}")
|
||||||
|
images = None
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return images
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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.responses import JSONResponse
|
||||||
|
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
import json
|
||||||
|
|
||||||
|
from utils.utils import get_http_authorization_cred, get_current_user
|
||||||
|
from config import SRC_LOG_LEVELS, ENV
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
||||||
|
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
proxy_config = ProxyConfig()
|
||||||
|
|
||||||
|
|
||||||
|
async def config():
|
||||||
|
router, model_list, general_settings = await proxy_config.load_config(
|
||||||
|
router=None, config_file_path="./data/litellm/config.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
await initialize(config="./data/litellm/config.yaml", telemetry=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def startup():
|
||||||
|
await config()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def on_startup():
|
||||||
|
await startup()
|
||||||
|
|
||||||
|
|
||||||
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def auth_middleware(request: Request, call_next):
|
||||||
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
request.state.user = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = get_current_user(get_http_authorization_cred(auth_header))
|
||||||
|
log.debug(f"user: {user}")
|
||||||
|
request.state.user = user
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(status_code=400, content={"detail": str(e)})
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
+1107
-17
File diff suppressed because it is too large
Load Diff
@@ -1,127 +0,0 @@
|
|||||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
|
||||||
from constants import ERROR_MESSAGES
|
|
||||||
from utils.utils import decode_token, get_current_user
|
|
||||||
from config import OLLAMA_API_BASE_URL, WEBUI_AUTH
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL
|
|
||||||
|
|
||||||
# TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
|
||||||
async def get_ollama_api_url(user=Depends(get_current_user)):
|
|
||||||
if user and user.role == "admin":
|
|
||||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
|
||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
|
||||||
url: str
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
|
||||||
async def update_ollama_api_url(
|
|
||||||
form_data: UrlUpdateForm, user=Depends(get_current_user)
|
|
||||||
):
|
|
||||||
if user and user.role == "admin":
|
|
||||||
app.state.OLLAMA_API_BASE_URL = form_data.url
|
|
||||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
|
||||||
|
|
||||||
|
|
||||||
# async def fetch_sse(method, target_url, body, headers):
|
|
||||||
# async with aiohttp.ClientSession() as session:
|
|
||||||
# try:
|
|
||||||
# async with session.request(
|
|
||||||
# method, target_url, data=body, headers=headers
|
|
||||||
# ) as response:
|
|
||||||
# print(response.status)
|
|
||||||
# async for line in response.content:
|
|
||||||
# yield line
|
|
||||||
# except Exception as e:
|
|
||||||
# print(e)
|
|
||||||
# error_detail = "Open WebUI: Server Connection Error"
|
|
||||||
# yield json.dumps({"error": error_detail, "message": str(e)}).encode()
|
|
||||||
|
|
||||||
|
|
||||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
||||||
async def proxy(path: str, request: Request, user=Depends(get_current_user)):
|
|
||||||
target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}"
|
|
||||||
print(target_url)
|
|
||||||
|
|
||||||
body = await request.body()
|
|
||||||
headers = dict(request.headers)
|
|
||||||
|
|
||||||
if user.role in ["user", "admin"]:
|
|
||||||
if path in ["pull", "delete", "push", "copy", "create"]:
|
|
||||||
if user.role != "admin":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
|
||||||
|
|
||||||
headers.pop("Host", None)
|
|
||||||
headers.pop("Authorization", None)
|
|
||||||
headers.pop("Origin", None)
|
|
||||||
headers.pop("Referer", None)
|
|
||||||
|
|
||||||
session = aiohttp.ClientSession()
|
|
||||||
response = None
|
|
||||||
try:
|
|
||||||
response = await session.request(
|
|
||||||
request.method, target_url, data=body, headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
print(response)
|
|
||||||
if not response.ok:
|
|
||||||
data = await response.json()
|
|
||||||
print(data)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
async def generate():
|
|
||||||
async for line in response.content:
|
|
||||||
print(line)
|
|
||||||
yield line
|
|
||||||
await session.close()
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), response.status)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
|
||||||
|
|
||||||
if response is not None:
|
|
||||||
try:
|
|
||||||
res = await response.json()
|
|
||||||
if "error" in res:
|
|
||||||
error_detail = f"Ollama: {res['error']}"
|
|
||||||
except:
|
|
||||||
error_detail = f"Ollama: {e}"
|
|
||||||
|
|
||||||
await session.close()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=response.status if response else 500,
|
|
||||||
detail=error_detail,
|
|
||||||
)
|
|
||||||
+240
-94
@@ -3,7 +3,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -15,11 +19,23 @@ from utils.utils import (
|
|||||||
get_verified_user,
|
get_verified_user,
|
||||||
get_admin_user,
|
get_admin_user,
|
||||||
)
|
)
|
||||||
from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
OPENAI_API_BASE_URLS,
|
||||||
|
OPENAI_API_KEYS,
|
||||||
|
CACHE_DIR,
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
)
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
@@ -29,132 +45,274 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL
|
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
|
||||||
app.state.OPENAI_API_KEY = OPENAI_API_KEY
|
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||||
|
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||||
|
|
||||||
|
app.state.MODELS = {}
|
||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
@app.middleware("http")
|
||||||
url: str
|
async def check_url(request: Request, call_next):
|
||||||
|
if len(app.state.MODELS) == 0:
|
||||||
|
await get_all_models()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class KeyUpdateForm(BaseModel):
|
class UrlsUpdateForm(BaseModel):
|
||||||
key: str
|
urls: List[str]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
class KeysUpdateForm(BaseModel):
|
||||||
async def get_openai_url(user=Depends(get_admin_user)):
|
keys: List[str]
|
||||||
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
@app.get("/urls")
|
||||||
async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
async def get_openai_urls(user=Depends(get_admin_user)):
|
||||||
app.state.OPENAI_API_BASE_URL = form_data.url
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/key")
|
@app.post("/urls/update")
|
||||||
async def get_openai_key(user=Depends(get_admin_user)):
|
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
||||||
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/key/update")
|
@app.get("/keys")
|
||||||
async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)):
|
async def get_openai_keys(user=Depends(get_admin_user)):
|
||||||
app.state.OPENAI_API_KEY = form_data.key
|
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
|
||||||
|
|
||||||
|
@app.post("/keys/update")
|
||||||
|
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.OPENAI_API_KEYS = form_data.keys
|
||||||
|
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/audio/speech")
|
@app.post("/audio/speech")
|
||||||
async def speech(request: Request, user=Depends(get_verified_user)):
|
async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech"
|
idx = None
|
||||||
|
|
||||||
if app.state.OPENAI_API_KEY == "":
|
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
|
||||||
|
|
||||||
body = await request.body()
|
|
||||||
|
|
||||||
name = hashlib.sha256(body).hexdigest()
|
|
||||||
|
|
||||||
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
|
||||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
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"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("openai")
|
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||||
r = requests.post(
|
body = await request.body()
|
||||||
url=target_url,
|
name = hashlib.sha256(body).hexdigest()
|
||||||
data=body,
|
|
||||||
headers=headers,
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
r.raise_for_status()
|
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||||
|
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
||||||
|
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
||||||
|
|
||||||
# Save the streaming content to a file
|
# Check if the file already exists in the cache
|
||||||
with open(file_path, "wb") as f:
|
if file_path.is_file():
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
return FileResponse(file_path)
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
with open(file_body_path, "w") as f:
|
headers = {}
|
||||||
json.dump(json.loads(body.decode("utf-8")), f)
|
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
# Return the saved file
|
r = None
|
||||||
return FileResponse(file_path)
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/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']}"
|
||||||
|
except:
|
||||||
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=r.status_code if r else 500, detail=error_detail
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_url(url, key):
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"Bearer {key}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, headers=headers) as response:
|
||||||
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
# Handle connection error here
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
log.error(f"Connection error: {e}")
|
||||||
if r is not None:
|
return None
|
||||||
try:
|
|
||||||
res = r.json()
|
|
||||||
if "error" in res:
|
|
||||||
error_detail = f"External: {res['error']}"
|
|
||||||
except:
|
|
||||||
error_detail = f"External: {e}"
|
|
||||||
|
|
||||||
raise HTTPException(status_code=r.status_code, detail=error_detail)
|
|
||||||
|
def merge_models_lists(model_lists):
|
||||||
|
merged_list = []
|
||||||
|
|
||||||
|
for idx, models in enumerate(model_lists):
|
||||||
|
if models is not None and "error" not in models:
|
||||||
|
merged_list.extend(
|
||||||
|
[
|
||||||
|
{**model, "urlIdx": idx}
|
||||||
|
for model in models
|
||||||
|
if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
|
||||||
|
or "gpt" in model["id"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return merged_list
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_models():
|
||||||
|
log.info("get_all_models()")
|
||||||
|
|
||||||
|
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
|
||||||
|
models = {"data": []}
|
||||||
|
else:
|
||||||
|
tasks = [
|
||||||
|
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
|
||||||
|
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
|
||||||
|
]
|
||||||
|
|
||||||
|
responses = await asyncio.gather(*tasks)
|
||||||
|
models = {
|
||||||
|
"data": merge_models_lists(
|
||||||
|
list(
|
||||||
|
map(
|
||||||
|
lambda response: (
|
||||||
|
response["data"]
|
||||||
|
if response and "data" in response
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
responses,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(f"models: {models}")
|
||||||
|
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||||
|
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/models")
|
||||||
|
@app.get("/models/{url_idx}")
|
||||||
|
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
|
||||||
|
if url_idx == None:
|
||||||
|
models = await get_all_models()
|
||||||
|
if app.state.MODEL_FILTER_ENABLED:
|
||||||
|
if user.role == "user":
|
||||||
|
models["data"] = list(
|
||||||
|
filter(
|
||||||
|
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
||||||
|
models["data"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return models
|
||||||
|
return models
|
||||||
|
else:
|
||||||
|
url = app.state.OPENAI_API_BASE_URLS[url_idx]
|
||||||
|
|
||||||
|
r = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.request(method="GET", url=f"{url}/models")
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
response_data = r.json()
|
||||||
|
if "api.openai.com" in url:
|
||||||
|
response_data["data"] = list(
|
||||||
|
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
||||||
|
)
|
||||||
|
|
||||||
|
return response_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}"
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=r.status_code if r else 500,
|
||||||
|
detail=error_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}"
|
idx = 0
|
||||||
print(target_url, app.state.OPENAI_API_KEY)
|
|
||||||
|
|
||||||
if app.state.OPENAI_API_KEY == "":
|
|
||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
|
||||||
|
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
|
|
||||||
# TODO: Remove below after gpt-4-vision fix from Open AI
|
# TODO: Remove below after gpt-4-vision fix from Open AI
|
||||||
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
||||||
try:
|
try:
|
||||||
body = body.decode("utf-8")
|
body = body.decode("utf-8")
|
||||||
body = json.loads(body)
|
body = json.loads(body)
|
||||||
|
|
||||||
|
idx = app.state.MODELS[body.get("model")]["urlIdx"]
|
||||||
|
|
||||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||||
# This is a workaround until OpenAI fixes the issue with this model
|
# This is a workaround until OpenAI fixes the issue with this model
|
||||||
if body.get("model") == "gpt-4-vision-preview":
|
if body.get("model") == "gpt-4-vision-preview":
|
||||||
if "max_tokens" not in body:
|
if "max_tokens" not in body:
|
||||||
body["max_tokens"] = 4000
|
body["max_tokens"] = 4000
|
||||||
print("Modified body_dict:", body)
|
log.debug("Modified body_dict:", body)
|
||||||
|
|
||||||
|
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
||||||
|
if "num_ctx" in body:
|
||||||
|
# If 'num_ctx' is in the dictionary, delete it
|
||||||
|
# Leaving it there generates an error with the
|
||||||
|
# OpenAI API (Feb 2024)
|
||||||
|
del body["num_ctx"]
|
||||||
|
|
||||||
# Convert the modified body back to JSON
|
# Convert the modified body back to JSON
|
||||||
body = json.dumps(body)
|
body = json.dumps(body)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print("Error loading request body into a dictionary:", e)
|
log.error("Error loading request body into a dictionary:", e)
|
||||||
|
|
||||||
|
url = app.state.OPENAI_API_BASE_URLS[idx]
|
||||||
|
key = app.state.OPENAI_API_KEYS[idx]
|
||||||
|
|
||||||
|
target_url = f"{url}/{path}"
|
||||||
|
|
||||||
|
if key == "":
|
||||||
|
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
r = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method=request.method,
|
method=request.method,
|
||||||
@@ -174,24 +332,10 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
headers=dict(r.headers),
|
headers=dict(r.headers),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# For non-SSE, read the response and return it
|
|
||||||
# response_data = (
|
|
||||||
# r.json()
|
|
||||||
# if r.headers.get("Content-Type", "")
|
|
||||||
# == "application/json"
|
|
||||||
# else r.text
|
|
||||||
# )
|
|
||||||
|
|
||||||
response_data = r.json()
|
response_data = r.json()
|
||||||
|
|
||||||
if "openai" in app.state.OPENAI_API_BASE_URL and path == "models":
|
|
||||||
response_data["data"] = list(
|
|
||||||
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
|
||||||
)
|
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
error_detail = "Open WebUI: Server Connection Error"
|
error_detail = "Open WebUI: Server Connection Error"
|
||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
@@ -201,4 +345,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
except:
|
except:
|
||||||
error_detail = f"External: {e}"
|
error_detail = f"External: {e}"
|
||||||
|
|
||||||
raise HTTPException(status_code=r.status_code, detail=error_detail)
|
raise HTTPException(
|
||||||
|
status_code=r.status_code if r else 500, detail=error_detail
|
||||||
|
)
|
||||||
|
|||||||
+160
-133
@@ -8,7 +8,7 @@ from fastapi import (
|
|||||||
Form,
|
Form,
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil
|
import os, shutil, logging
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -44,6 +44,8 @@ from apps.web.models.documents import (
|
|||||||
DocumentResponse,
|
DocumentResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from apps.rag.utils import query_doc, query_collection
|
||||||
|
|
||||||
from utils.misc import (
|
from utils.misc import (
|
||||||
calculate_sha256,
|
calculate_sha256,
|
||||||
calculate_sha256_string,
|
calculate_sha256_string,
|
||||||
@@ -52,6 +54,7 @@ from utils.misc import (
|
|||||||
)
|
)
|
||||||
from utils.utils import get_current_user, get_admin_user
|
from utils.utils import get_current_user, get_admin_user
|
||||||
from config import (
|
from config import (
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
RAG_EMBEDDING_MODEL,
|
RAG_EMBEDDING_MODEL,
|
||||||
@@ -64,6 +67,9 @@ from config import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
#
|
#
|
||||||
# if RAG_EMBEDDING_MODEL:
|
# if RAG_EMBEDDING_MODEL:
|
||||||
# sentence_transformer_ef = SentenceTransformer(
|
# sentence_transformer_ef = SentenceTransformer(
|
||||||
@@ -75,10 +81,13 @@ from constants import ERROR_MESSAGES
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.state.PDF_EXTRACT_IMAGES = False
|
||||||
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_TEMPLATE = RAG_TEMPLATE
|
||||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||||
|
app.state.TOP_K = 4
|
||||||
|
|
||||||
app.state.sentence_transformer_ef = (
|
app.state.sentence_transformer_ef = (
|
||||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
model_name=app.state.RAG_EMBEDDING_MODEL,
|
||||||
@@ -105,34 +114,6 @@ class CollectionNameForm(BaseModel):
|
|||||||
class StoreWebForm(CollectionNameForm):
|
class StoreWebForm(CollectionNameForm):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
def store_data_in_vector_db(data, collection_name) -> bool:
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
|
||||||
chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP
|
|
||||||
)
|
|
||||||
docs = text_splitter.split_documents(data)
|
|
||||||
|
|
||||||
texts = [doc.page_content for doc in docs]
|
|
||||||
metadatas = [doc.metadata for doc in docs]
|
|
||||||
|
|
||||||
try:
|
|
||||||
collection = CHROMA_CLIENT.create_collection(
|
|
||||||
name=collection_name,
|
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
|
||||||
|
|
||||||
collection.add(
|
|
||||||
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
if e.__class__.__name__ == "UniqueConstraintError":
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
return {
|
return {
|
||||||
@@ -174,12 +155,15 @@ async def update_embedding_model(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/chunk")
|
@app.get("/config")
|
||||||
async def get_chunk_params(user=Depends(get_admin_user)):
|
async def get_rag_config(user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"chunk_size": app.state.CHUNK_SIZE,
|
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
|
||||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
"chunk": {
|
||||||
|
"chunk_size": app.state.CHUNK_SIZE,
|
||||||
|
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -188,17 +172,24 @@ class ChunkParamUpdateForm(BaseModel):
|
|||||||
chunk_overlap: int
|
chunk_overlap: int
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chunk/update")
|
class ConfigUpdateForm(BaseModel):
|
||||||
async def update_chunk_params(
|
pdf_extract_images: bool
|
||||||
form_data: ChunkParamUpdateForm, user=Depends(get_admin_user)
|
chunk: ChunkParamUpdateForm
|
||||||
):
|
|
||||||
app.state.CHUNK_SIZE = form_data.chunk_size
|
|
||||||
app.state.CHUNK_OVERLAP = form_data.chunk_overlap
|
@app.post("/config/update")
|
||||||
|
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images
|
||||||
|
app.state.CHUNK_SIZE = form_data.chunk.chunk_size
|
||||||
|
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"chunk_size": app.state.CHUNK_SIZE,
|
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
|
||||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
"chunk": {
|
||||||
|
"chunk_size": app.state.CHUNK_SIZE,
|
||||||
|
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -210,40 +201,50 @@ async def get_rag_template(user=Depends(get_current_user)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RAGTemplateForm(BaseModel):
|
@app.get("/query/settings")
|
||||||
template: str
|
async def get_query_settings(user=Depends(get_admin_user)):
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"template": app.state.RAG_TEMPLATE,
|
||||||
|
"k": app.state.TOP_K,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/template/update")
|
class QuerySettingsForm(BaseModel):
|
||||||
async def update_rag_template(form_data: RAGTemplateForm, user=Depends(get_admin_user)):
|
k: Optional[int] = None
|
||||||
# TODO: check template requirements
|
template: Optional[str] = None
|
||||||
app.state.RAG_TEMPLATE = (
|
|
||||||
form_data.template if form_data.template != "" else RAG_TEMPLATE
|
|
||||||
)
|
@app.post("/query/settings/update")
|
||||||
|
async def update_query_settings(
|
||||||
|
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
|
||||||
|
app.state.TOP_K = form_data.k if form_data.k else 4
|
||||||
return {"status": True, "template": app.state.RAG_TEMPLATE}
|
return {"status": True, "template": app.state.RAG_TEMPLATE}
|
||||||
|
|
||||||
|
|
||||||
class QueryDocForm(BaseModel):
|
class QueryDocForm(BaseModel):
|
||||||
collection_name: str
|
collection_name: str
|
||||||
query: str
|
query: str
|
||||||
k: Optional[int] = 4
|
k: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/query/doc")
|
@app.post("/query/doc")
|
||||||
def query_doc(
|
def query_doc_handler(
|
||||||
form_data: QueryDocForm,
|
form_data: QueryDocForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# if you use docker use the model from the environment variable
|
return query_doc(
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
collection_name=form_data.collection_name,
|
||||||
name=form_data.collection_name,
|
query=form_data.query,
|
||||||
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
embedding_function=app.state.sentence_transformer_ef,
|
||||||
)
|
)
|
||||||
result = collection.query(query_texts=[form_data.query], n_results=form_data.k)
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -253,77 +254,20 @@ def query_doc(
|
|||||||
class QueryCollectionsForm(BaseModel):
|
class QueryCollectionsForm(BaseModel):
|
||||||
collection_names: List[str]
|
collection_names: List[str]
|
||||||
query: str
|
query: str
|
||||||
k: Optional[int] = 4
|
k: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
def merge_and_sort_query_results(query_results, k):
|
|
||||||
# Initialize lists to store combined data
|
|
||||||
combined_ids = []
|
|
||||||
combined_distances = []
|
|
||||||
combined_metadatas = []
|
|
||||||
combined_documents = []
|
|
||||||
|
|
||||||
# Combine data from each dictionary
|
|
||||||
for data in query_results:
|
|
||||||
combined_ids.extend(data["ids"][0])
|
|
||||||
combined_distances.extend(data["distances"][0])
|
|
||||||
combined_metadatas.extend(data["metadatas"][0])
|
|
||||||
combined_documents.extend(data["documents"][0])
|
|
||||||
|
|
||||||
# Create a list of tuples (distance, id, metadata, document)
|
|
||||||
combined = list(
|
|
||||||
zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort the list based on distances
|
|
||||||
combined.sort(key=lambda x: x[0])
|
|
||||||
|
|
||||||
# Unzip the sorted list
|
|
||||||
sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
|
|
||||||
|
|
||||||
# Slicing the lists to include only k elements
|
|
||||||
sorted_distances = list(sorted_distances)[:k]
|
|
||||||
sorted_ids = list(sorted_ids)[:k]
|
|
||||||
sorted_metadatas = list(sorted_metadatas)[:k]
|
|
||||||
sorted_documents = list(sorted_documents)[:k]
|
|
||||||
|
|
||||||
# Create the output dictionary
|
|
||||||
merged_query_results = {
|
|
||||||
"ids": [sorted_ids],
|
|
||||||
"distances": [sorted_distances],
|
|
||||||
"metadatas": [sorted_metadatas],
|
|
||||||
"documents": [sorted_documents],
|
|
||||||
"embeddings": None,
|
|
||||||
"uris": None,
|
|
||||||
"data": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged_query_results
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/query/collection")
|
@app.post("/query/collection")
|
||||||
def query_collection(
|
def query_collection_handler(
|
||||||
form_data: QueryCollectionsForm,
|
form_data: QueryCollectionsForm,
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
results = []
|
return query_collection(
|
||||||
|
collection_names=form_data.collection_names,
|
||||||
for collection_name in form_data.collection_names:
|
query=form_data.query,
|
||||||
try:
|
k=form_data.k if form_data.k else app.state.TOP_K,
|
||||||
# if you use docker use the model from the environment variable
|
embedding_function=app.state.sentence_transformer_ef,
|
||||||
collection = CHROMA_CLIENT.get_collection(
|
)
|
||||||
name=collection_name,
|
|
||||||
embedding_function=app.state.sentence_transformer_ef,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = collection.query(
|
|
||||||
query_texts=[form_data.query], n_results=form_data.k
|
|
||||||
)
|
|
||||||
results.append(result)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return merge_and_sort_query_results(results, form_data.k)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/web")
|
@app.post("/web")
|
||||||
@@ -337,20 +281,70 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
|||||||
if collection_name == "":
|
if collection_name == "":
|
||||||
collection_name = calculate_sha256_string(form_data.url)[:63]
|
collection_name = calculate_sha256_string(form_data.url)[:63]
|
||||||
|
|
||||||
store_data_in_vector_db(data, collection_name)
|
store_data_in_vector_db(data, collection_name, overwrite=True)
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"collection_name": collection_name,
|
"collection_name": collection_name,
|
||||||
"filename": form_data.url,
|
"filename": form_data.url,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=app.state.CHUNK_SIZE,
|
||||||
|
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||||
|
add_start_index=True,
|
||||||
|
)
|
||||||
|
docs = text_splitter.split_documents(data)
|
||||||
|
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
||||||
|
|
||||||
|
|
||||||
|
def store_text_in_vector_db(
|
||||||
|
text, metadata, collection_name, overwrite: bool = False
|
||||||
|
) -> bool:
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=app.state.CHUNK_SIZE,
|
||||||
|
chunk_overlap=app.state.CHUNK_OVERLAP,
|
||||||
|
add_start_index=True,
|
||||||
|
)
|
||||||
|
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
||||||
|
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
||||||
|
|
||||||
|
|
||||||
|
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
||||||
|
texts = [doc.page_content for doc in docs]
|
||||||
|
metadatas = [doc.metadata for doc in docs]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if overwrite:
|
||||||
|
for collection in CHROMA_CLIENT.list_collections():
|
||||||
|
if collection_name == collection.name:
|
||||||
|
print(f"deleting existing collection {collection_name}")
|
||||||
|
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||||
|
|
||||||
|
collection = CHROMA_CLIENT.create_collection(
|
||||||
|
name=collection_name,
|
||||||
|
embedding_function=app.state.sentence_transformer_ef,
|
||||||
|
)
|
||||||
|
|
||||||
|
collection.add(
|
||||||
|
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
if e.__class__.__name__ == "UniqueConstraintError":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_loader(filename: str, file_content_type: str, file_path: str):
|
def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
file_ext = filename.split(".")[-1].lower()
|
file_ext = filename.split(".")[-1].lower()
|
||||||
known_type = True
|
known_type = True
|
||||||
@@ -401,7 +395,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if file_ext == "pdf":
|
if file_ext == "pdf":
|
||||||
loader = PyPDFLoader(file_path)
|
loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
|
||||||
elif file_ext == "csv":
|
elif file_ext == "csv":
|
||||||
loader = CSVLoader(file_path)
|
loader = CSVLoader(file_path)
|
||||||
elif file_ext == "rst":
|
elif file_ext == "rst":
|
||||||
@@ -423,10 +417,12 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
|||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
] or file_ext in ["xls", "xlsx"]:
|
] or file_ext in ["xls", "xlsx"]:
|
||||||
loader = UnstructuredExcelLoader(file_path)
|
loader = UnstructuredExcelLoader(file_path)
|
||||||
elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0):
|
elif file_ext in known_source_ext or (
|
||||||
loader = TextLoader(file_path)
|
file_content_type and file_content_type.find("text/") >= 0
|
||||||
|
):
|
||||||
|
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||||
else:
|
else:
|
||||||
loader = TextLoader(file_path)
|
loader = TextLoader(file_path, autodetect_encoding=True)
|
||||||
known_type = False
|
known_type = False
|
||||||
|
|
||||||
return loader, known_type
|
return loader, known_type
|
||||||
@@ -440,7 +436,7 @@ def store_doc(
|
|||||||
):
|
):
|
||||||
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
||||||
|
|
||||||
print(file.content_type)
|
log.info(f"file.content_type: {file.content_type}")
|
||||||
try:
|
try:
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||||
@@ -471,7 +467,7 @@ def store_doc(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT(),
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
if "No pandoc was found" in str(e):
|
if "No pandoc was found" in str(e):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -484,6 +480,37 @@ def store_doc(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TextRAGForm(BaseModel):
|
||||||
|
name: str
|
||||||
|
content: str
|
||||||
|
collection_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/text")
|
||||||
|
def store_text(
|
||||||
|
form_data: TextRAGForm,
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
|
||||||
|
collection_name = form_data.collection_name
|
||||||
|
if collection_name == None:
|
||||||
|
collection_name = calculate_sha256_string(form_data.content)
|
||||||
|
|
||||||
|
result = store_text_in_vector_db(
|
||||||
|
form_data.content,
|
||||||
|
metadata={"name": form_data.name, "created_by": user.id},
|
||||||
|
collection_name=collection_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {"status": True, "collection_name": collection_name}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=ERROR_MESSAGES.DEFAULT(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/scan")
|
@app.get("/scan")
|
||||||
def scan_docs_dir(user=Depends(get_admin_user)):
|
def scan_docs_dir(user=Depends(get_admin_user)):
|
||||||
for path in Path(DOCS_DIR).rglob("./**/*"):
|
for path in Path(DOCS_DIR).rglob("./**/*"):
|
||||||
@@ -536,7 +563,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -557,11 +584,11 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
|||||||
elif os.path.isdir(file_path):
|
elif os.path.isdir(file_path):
|
||||||
shutil.rmtree(file_path)
|
shutil.rmtree(file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Failed to delete %s. Reason: %s" % (file_path, e))
|
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
CHROMA_CLIENT.reset()
|
CHROMA_CLIENT.reset()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
|
def query_doc(collection_name: str, query: str, k: int, embedding_function):
|
||||||
|
try:
|
||||||
|
# if you use docker use the model from the environment variable
|
||||||
|
collection = CHROMA_CLIENT.get_collection(
|
||||||
|
name=collection_name,
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
)
|
||||||
|
result = collection.query(
|
||||||
|
query_texts=[query],
|
||||||
|
n_results=k,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def merge_and_sort_query_results(query_results, k):
|
||||||
|
# Initialize lists to store combined data
|
||||||
|
combined_ids = []
|
||||||
|
combined_distances = []
|
||||||
|
combined_metadatas = []
|
||||||
|
combined_documents = []
|
||||||
|
|
||||||
|
# Combine data from each dictionary
|
||||||
|
for data in query_results:
|
||||||
|
combined_ids.extend(data["ids"][0])
|
||||||
|
combined_distances.extend(data["distances"][0])
|
||||||
|
combined_metadatas.extend(data["metadatas"][0])
|
||||||
|
combined_documents.extend(data["documents"][0])
|
||||||
|
|
||||||
|
# Create a list of tuples (distance, id, metadata, document)
|
||||||
|
combined = list(
|
||||||
|
zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort the list based on distances
|
||||||
|
combined.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Unzip the sorted list
|
||||||
|
sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
|
||||||
|
|
||||||
|
# Slicing the lists to include only k elements
|
||||||
|
sorted_distances = list(sorted_distances)[:k]
|
||||||
|
sorted_ids = list(sorted_ids)[:k]
|
||||||
|
sorted_metadatas = list(sorted_metadatas)[:k]
|
||||||
|
sorted_documents = list(sorted_documents)[:k]
|
||||||
|
|
||||||
|
# Create the output dictionary
|
||||||
|
merged_query_results = {
|
||||||
|
"ids": [sorted_ids],
|
||||||
|
"distances": [sorted_distances],
|
||||||
|
"metadatas": [sorted_metadatas],
|
||||||
|
"documents": [sorted_documents],
|
||||||
|
"embeddings": None,
|
||||||
|
"uris": None,
|
||||||
|
"data": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged_query_results
|
||||||
|
|
||||||
|
|
||||||
|
def query_collection(
|
||||||
|
collection_names: List[str], query: str, k: int, embedding_function
|
||||||
|
):
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for collection_name in collection_names:
|
||||||
|
try:
|
||||||
|
# if you use docker use the model from the environment variable
|
||||||
|
collection = CHROMA_CLIENT.get_collection(
|
||||||
|
name=collection_name,
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = collection.query(
|
||||||
|
query_texts=[query],
|
||||||
|
n_results=k,
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return merge_and_sort_query_results(results, k)
|
||||||
|
|
||||||
|
|
||||||
|
def rag_template(template: str, context: str, query: str):
|
||||||
|
template = template.replace("[context]", context)
|
||||||
|
template = template.replace("[query]", query)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def rag_messages(docs, messages, template, k, embedding_function):
|
||||||
|
log.debug(f"docs: {docs}")
|
||||||
|
|
||||||
|
last_user_message_idx = None
|
||||||
|
for i in range(len(messages) - 1, -1, -1):
|
||||||
|
if messages[i]["role"] == "user":
|
||||||
|
last_user_message_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
user_message = messages[last_user_message_idx]
|
||||||
|
|
||||||
|
if isinstance(user_message["content"], list):
|
||||||
|
# Handle list content input
|
||||||
|
content_type = "list"
|
||||||
|
query = ""
|
||||||
|
for content_item in user_message["content"]:
|
||||||
|
if content_item["type"] == "text":
|
||||||
|
query = content_item["text"]
|
||||||
|
break
|
||||||
|
elif isinstance(user_message["content"], str):
|
||||||
|
# Handle text content input
|
||||||
|
content_type = "text"
|
||||||
|
query = user_message["content"]
|
||||||
|
else:
|
||||||
|
# Fallback in case the input does not match expected types
|
||||||
|
content_type = None
|
||||||
|
query = ""
|
||||||
|
|
||||||
|
relevant_contexts = []
|
||||||
|
|
||||||
|
for doc in docs:
|
||||||
|
context = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if doc["type"] == "collection":
|
||||||
|
context = query_collection(
|
||||||
|
collection_names=doc["collection_names"],
|
||||||
|
query=query,
|
||||||
|
k=k,
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
)
|
||||||
|
elif doc["type"] == "text":
|
||||||
|
context = doc["content"]
|
||||||
|
else:
|
||||||
|
context = query_doc(
|
||||||
|
collection_name=doc["collection_name"],
|
||||||
|
query=query,
|
||||||
|
k=k,
|
||||||
|
embedding_function=embedding_function,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
context = None
|
||||||
|
|
||||||
|
relevant_contexts.append(context)
|
||||||
|
|
||||||
|
context_string = ""
|
||||||
|
for context in relevant_contexts:
|
||||||
|
if context:
|
||||||
|
context_string += " ".join(context["documents"][0]) + "\n"
|
||||||
|
|
||||||
|
ra_content = rag_template(
|
||||||
|
template=template,
|
||||||
|
context=context_string,
|
||||||
|
query=query,
|
||||||
|
)
|
||||||
|
|
||||||
|
if content_type == "list":
|
||||||
|
new_content = []
|
||||||
|
for content_item in user_message["content"]:
|
||||||
|
if content_item["type"] == "text":
|
||||||
|
# Update the text item's content with ra_content
|
||||||
|
new_content.append({"type": "text", "text": ra_content})
|
||||||
|
else:
|
||||||
|
# Keep other types of content as they are
|
||||||
|
new_content.append(content_item)
|
||||||
|
new_user_message = {**user_message, "content": new_content}
|
||||||
|
else:
|
||||||
|
new_user_message = {
|
||||||
|
**user_message,
|
||||||
|
"content": ra_content,
|
||||||
|
}
|
||||||
|
|
||||||
|
messages[last_user_message_idx] = new_user_message
|
||||||
|
|
||||||
|
return messages
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
from peewee import *
|
from peewee import *
|
||||||
from config import DATA_DIR
|
from config import SRC_LOG_LEVELS, DATA_DIR
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["DB"])
|
||||||
|
|
||||||
|
# Check if the file exists
|
||||||
|
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
||||||
|
# Rename the file
|
||||||
|
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
||||||
|
log.info("File renamed successfully.")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
DB = SqliteDatabase(f"{DATA_DIR}/ollama.db")
|
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
||||||
DB.connect()
|
DB.connect()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from config import (
|
|||||||
DEFAULT_USER_ROLE,
|
DEFAULT_USER_ROLE,
|
||||||
ENABLE_SIGNUP,
|
ENABLE_SIGNUP,
|
||||||
USER_PERMISSIONS,
|
USER_PERMISSIONS,
|
||||||
|
WEBHOOK_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -32,6 +33,7 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS
|
|||||||
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
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.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import logging
|
||||||
from peewee import *
|
from peewee import *
|
||||||
|
|
||||||
from apps.web.models.users import UserModel, Users
|
from apps.web.models.users import UserModel, Users
|
||||||
@@ -9,6 +10,10 @@ 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
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# DB MODEL
|
# DB MODEL
|
||||||
####################
|
####################
|
||||||
@@ -86,7 +91,7 @@ class AuthsTable:
|
|||||||
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, role: str = "pending"
|
||||||
) -> Optional[UserModel]:
|
) -> Optional[UserModel]:
|
||||||
print("insert_new_auth")
|
log.info("insert_new_auth")
|
||||||
|
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -103,7 +108,7 @@ class AuthsTable:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
||||||
print("authenticate_user", email)
|
log.info(f"authenticate_user: {email}")
|
||||||
try:
|
try:
|
||||||
auth = Auth.get(Auth.email == email, Auth.active == True)
|
auth = Auth.get(Auth.email == email, Auth.active == True)
|
||||||
if auth:
|
if auth:
|
||||||
|
|||||||
@@ -95,20 +95,6 @@ class ChatTable:
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
|
||||||
try:
|
|
||||||
query = Chat.update(
|
|
||||||
chat=json.dumps(chat),
|
|
||||||
title=chat["title"] if "title" in chat else "New Chat",
|
|
||||||
timestamp=int(time.time()),
|
|
||||||
).where(Chat.id == id)
|
|
||||||
query.execute()
|
|
||||||
|
|
||||||
chat = Chat.get(Chat.id == id)
|
|
||||||
return ChatModel(**model_to_dict(chat))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
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]:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from peewee import *
|
|||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
from typing import List, Union, Optional
|
from typing import List, Union, Optional
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
from utils.utils import decode_token
|
from utils.utils import decode_token
|
||||||
from utils.misc import get_gravatar_url
|
from utils.misc import get_gravatar_url
|
||||||
@@ -11,6 +12,10 @@ from apps.web.internal.db import DB
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Documents DB Schema
|
# Documents DB Schema
|
||||||
####################
|
####################
|
||||||
@@ -118,7 +123,7 @@ class DocumentsTable:
|
|||||||
doc = Document.get(Document.name == form_data.name)
|
doc = Document.get(Document.name == form_data.name)
|
||||||
return DocumentModel(**model_to_dict(doc))
|
return DocumentModel(**model_to_dict(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_doc_content_by_name(
|
def update_doc_content_by_name(
|
||||||
@@ -138,7 +143,7 @@ class DocumentsTable:
|
|||||||
doc = Document.get(Document.name == name)
|
doc = Document.get(Document.name == name)
|
||||||
return DocumentModel(**model_to_dict(doc))
|
return DocumentModel(**model_to_dict(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_doc_by_name(self, name: str) -> bool:
|
def delete_doc_by_name(self, name: str) -> bool:
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ from playhouse.shortcuts import model_to_dict
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.internal.db import DB
|
from apps.web.internal.db import DB
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Tag DB Schema
|
# Tag DB Schema
|
||||||
####################
|
####################
|
||||||
@@ -173,7 +178,7 @@ class TagTable:
|
|||||||
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
|
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
|
||||||
)
|
)
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
res = query.execute() # Remove the rows, return number of rows removed.
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
||||||
if tag_count == 0:
|
if tag_count == 0:
|
||||||
@@ -185,7 +190,7 @@ class TagTable:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("delete_tag", e)
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_tag_by_tag_name_and_chat_id_and_user_id(
|
def delete_tag_by_tag_name_and_chat_id_and_user_id(
|
||||||
@@ -198,7 +203,7 @@ class TagTable:
|
|||||||
& (ChatIdTag.user_id == user_id)
|
& (ChatIdTag.user_id == user_id)
|
||||||
)
|
)
|
||||||
res = query.execute() # Remove the rows, return number of rows removed.
|
res = query.execute() # Remove the rows, return number of rows removed.
|
||||||
print(res)
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
|
||||||
if tag_count == 0:
|
if tag_count == 0:
|
||||||
@@ -210,7 +215,7 @@ class TagTable:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("delete_tag", e)
|
log.error(f"delete_tag: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
|
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ from utils.utils import (
|
|||||||
create_token,
|
create_token,
|
||||||
)
|
)
|
||||||
from utils.misc import parse_duration, validate_email_format
|
from utils.misc import parse_duration, validate_email_format
|
||||||
from constants import ERROR_MESSAGES
|
from utils.webhook import post_webhook
|
||||||
|
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -155,6 +156,17 @@ async def signup(request: Request, form_data: SignupForm):
|
|||||||
)
|
)
|
||||||
# response.set_cookie(key='token', value=token, httponly=True)
|
# response.set_cookie(key='token', value=token, httponly=True)
|
||||||
|
|
||||||
|
if request.app.state.WEBHOOK_URL:
|
||||||
|
post_webhook(
|
||||||
|
request.app.state.WEBHOOK_URL,
|
||||||
|
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
|
{
|
||||||
|
"action": "signup",
|
||||||
|
"message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||||
|
"user": user.model_dump_json(exclude_none=True),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"token": token,
|
"token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from utils.utils import get_current_user, get_admin_user
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.models.users import Users
|
from apps.web.models.users import Users
|
||||||
from apps.web.models.chats import (
|
from apps.web.models.chats import (
|
||||||
@@ -27,6 +28,10 @@ from apps.web.models.tags import (
|
|||||||
|
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
from config import SRC_LOG_LEVELS
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -78,7 +83,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
|
|||||||
chat = Chats.insert_new_chat(user.id, form_data)
|
chat = Chats.insert_new_chat(user.id, form_data)
|
||||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
@@ -95,7 +100,7 @@ async def get_all_tags(user=Depends(get_current_user)):
|
|||||||
tags = Tags.get_tags_by_user_id(user.id)
|
tags = Tags.get_tags_by_user_id(user.id)
|
||||||
return tags
|
return tags
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
@@ -271,6 +276,16 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/", response_model=bool)
|
@router.delete("/", response_model=bool)
|
||||||
async def delete_all_user_chats(user=Depends(get_current_user)):
|
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.role == "user"
|
||||||
|
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
result = Chats.delete_chats_by_user_id(user.id)
|
result = Chats.delete_chats_by_user_id(user.id)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
|
||||||
from apps.web.models.auths import Auths
|
from apps.web.models.auths import Auths
|
||||||
@@ -14,6 +15,10 @@ from apps.web.models.auths import Auths
|
|||||||
from utils.utils import get_current_user, get_password_hash, get_admin_user
|
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
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -83,7 +88,7 @@ async def update_user_by_id(
|
|||||||
|
|
||||||
if form_data.password:
|
if form_data.password:
|
||||||
hashed = get_password_hash(form_data.password)
|
hashed = get_password_hash(form_data.password)
|
||||||
print(hashed)
|
log.debug(f"hashed: {hashed}")
|
||||||
Auths.update_user_password_by_id(user_id, hashed)
|
Auths.update_user_password_by_id(user_id, hashed)
|
||||||
|
|
||||||
Auths.update_email_by_id(user_id, form_data.email.lower())
|
Auths.update_email_by_id(user_id, form_data.email.lower())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
|
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from starlette.responses import StreamingResponse
|
from starlette.responses import StreamingResponse, FileResponse
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -9,166 +10,29 @@ import os
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
from utils.utils import get_admin_user
|
||||||
from utils.misc import calculate_sha256, get_gravatar_url
|
from utils.misc import calculate_sha256, get_gravatar_url
|
||||||
|
|
||||||
from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR
|
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class UploadBlobForm(BaseModel):
|
|
||||||
filename: str
|
|
||||||
|
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
def parse_huggingface_url(hf_url):
|
|
||||||
try:
|
|
||||||
# Parse the URL
|
|
||||||
parsed_url = urlparse(hf_url)
|
|
||||||
|
|
||||||
# Get the path and split it into components
|
|
||||||
path_components = parsed_url.path.split("/")
|
|
||||||
|
|
||||||
# Extract the desired output
|
|
||||||
user_repo = "/".join(path_components[1:3])
|
|
||||||
model_file = path_components[-1]
|
|
||||||
|
|
||||||
return model_file
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024):
|
|
||||||
done = False
|
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
current_size = os.path.getsize(file_path)
|
|
||||||
else:
|
|
||||||
current_size = 0
|
|
||||||
|
|
||||||
headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
|
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
||||||
async with session.get(url, headers=headers) as response:
|
|
||||||
total_size = int(response.headers.get("content-length", 0)) + current_size
|
|
||||||
|
|
||||||
with open(file_path, "ab+") as file:
|
|
||||||
async for data in response.content.iter_chunked(chunk_size):
|
|
||||||
current_size += len(data)
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
done = current_size == total_size
|
|
||||||
progress = round((current_size / total_size) * 100, 2)
|
|
||||||
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
|
||||||
|
|
||||||
if done:
|
|
||||||
file.seek(0)
|
|
||||||
hashed = calculate_sha256(file)
|
|
||||||
file.seek(0)
|
|
||||||
|
|
||||||
url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
|
|
||||||
response = requests.post(url, data=file)
|
|
||||||
|
|
||||||
if response.ok:
|
|
||||||
res = {
|
|
||||||
"done": done,
|
|
||||||
"blob": f"sha256:{hashed}",
|
|
||||||
"name": file_name,
|
|
||||||
}
|
|
||||||
os.remove(file_path)
|
|
||||||
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
else:
|
|
||||||
raise "Ollama: Could not create blob, Please try again."
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/download")
|
|
||||||
async def download(
|
|
||||||
url: str,
|
|
||||||
):
|
|
||||||
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
|
|
||||||
file_name = parse_huggingface_url(url)
|
|
||||||
|
|
||||||
if file_name:
|
|
||||||
file_path = f"{UPLOAD_DIR}/{file_name}"
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
download_file_stream(url, file_path, file_name),
|
|
||||||
media_type="text/event-stream",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload")
|
|
||||||
def upload(file: UploadFile = File(...)):
|
|
||||||
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
|
||||||
|
|
||||||
# Save file in chunks
|
|
||||||
with open(file_path, "wb+") as f:
|
|
||||||
for chunk in file.file:
|
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
def file_process_stream():
|
|
||||||
total_size = os.path.getsize(file_path)
|
|
||||||
chunk_size = 1024 * 1024
|
|
||||||
try:
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
total = 0
|
|
||||||
done = False
|
|
||||||
|
|
||||||
while not done:
|
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if not chunk:
|
|
||||||
done = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
total += len(chunk)
|
|
||||||
progress = round((total / total_size) * 100, 2)
|
|
||||||
|
|
||||||
res = {
|
|
||||||
"progress": progress,
|
|
||||||
"total": total_size,
|
|
||||||
"completed": total,
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
|
|
||||||
if done:
|
|
||||||
f.seek(0)
|
|
||||||
hashed = calculate_sha256(f)
|
|
||||||
f.seek(0)
|
|
||||||
|
|
||||||
url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
|
|
||||||
response = requests.post(url, data=f)
|
|
||||||
|
|
||||||
if response.ok:
|
|
||||||
res = {
|
|
||||||
"done": done,
|
|
||||||
"blob": f"sha256:{hashed}",
|
|
||||||
"name": file.filename,
|
|
||||||
}
|
|
||||||
os.remove(file_path)
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
"Ollama: Could not create blob, Please try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
res = {"error": str(e)}
|
|
||||||
yield f"data: {json.dumps(res)}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/gravatar")
|
@router.get("/gravatar")
|
||||||
async def get_gravatar(
|
async def get_gravatar(
|
||||||
email: str,
|
email: str,
|
||||||
):
|
):
|
||||||
return get_gravatar_url(email)
|
return get_gravatar_url(email)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/db/download")
|
||||||
|
async def download_db(user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
f"{DATA_DIR}/webui.db",
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
filename="webui.db",
|
||||||
|
)
|
||||||
|
|||||||
+84
-9
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
import chromadb
|
import chromadb
|
||||||
from chromadb import Settings
|
from chromadb import Settings
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
@@ -21,7 +23,7 @@ try:
|
|||||||
|
|
||||||
load_dotenv(find_dotenv("../.env"))
|
load_dotenv(find_dotenv("../.env"))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("dotenv not installed, skipping...")
|
log.warning("dotenv not installed, skipping...")
|
||||||
|
|
||||||
WEBUI_NAME = "Open WebUI"
|
WEBUI_NAME = "Open WebUI"
|
||||||
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
|
||||||
@@ -100,6 +102,34 @@ 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
|
||||||
####################################
|
####################################
|
||||||
@@ -125,7 +155,7 @@ if CUSTOM_NAME:
|
|||||||
|
|
||||||
WEBUI_NAME = data["name"]
|
WEBUI_NAME = data["name"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(e)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -194,22 +224,39 @@ def create_config_file(file_path):
|
|||||||
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
||||||
|
|
||||||
if not os.path.exists(LITELLM_CONFIG_PATH):
|
if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||||
print("Config file doesn't exist. Creating...")
|
log.info("Config file doesn't exist. Creating...")
|
||||||
create_config_file(LITELLM_CONFIG_PATH)
|
create_config_file(LITELLM_CONFIG_PATH)
|
||||||
print("Config file created successfully.")
|
log.info("Config file created successfully.")
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# OLLAMA_API_BASE_URL
|
# OLLAMA_BASE_URL
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
OLLAMA_API_BASE_URL = os.environ.get(
|
OLLAMA_API_BASE_URL = os.environ.get(
|
||||||
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
|
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||||
|
|
||||||
|
|
||||||
|
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
||||||
|
OLLAMA_BASE_URL = (
|
||||||
|
OLLAMA_API_BASE_URL[:-4]
|
||||||
|
if OLLAMA_API_BASE_URL.endswith("/api")
|
||||||
|
else OLLAMA_API_BASE_URL
|
||||||
|
)
|
||||||
|
|
||||||
if ENV == "prod":
|
if ENV == "prod":
|
||||||
if OLLAMA_API_BASE_URL == "/ollama/api":
|
if OLLAMA_BASE_URL == "/ollama":
|
||||||
OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api"
|
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
||||||
|
|
||||||
|
|
||||||
|
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
||||||
|
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
||||||
|
|
||||||
|
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# OPENAI_API
|
# OPENAI_API
|
||||||
@@ -218,15 +265,31 @@ if ENV == "prod":
|
|||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||||
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
||||||
|
|
||||||
|
|
||||||
if OPENAI_API_BASE_URL == "":
|
if OPENAI_API_BASE_URL == "":
|
||||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
|
||||||
|
OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
|
||||||
|
|
||||||
|
OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
|
||||||
|
|
||||||
|
|
||||||
|
OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
|
||||||
|
OPENAI_API_BASE_URLS = (
|
||||||
|
OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
OPENAI_API_BASE_URLS = [
|
||||||
|
url.strip() if url != "" else "https://api.openai.com/v1"
|
||||||
|
for url in OPENAI_API_BASE_URLS.split(";")
|
||||||
|
]
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI
|
# WEBUI
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True)
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||||
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
||||||
|
|
||||||
|
|
||||||
@@ -257,8 +320,19 @@ DEFAULT_PROMPT_SUGGESTIONS = (
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
|
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
|
||||||
USER_PERMISSIONS = {"chat": {"deletion": True}}
|
|
||||||
|
|
||||||
|
USER_PERMISSIONS_CHAT_DELETION = (
|
||||||
|
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
|
||||||
|
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
|
||||||
|
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
|
||||||
|
|
||||||
|
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_VERSION
|
# WEBUI_VERSION
|
||||||
@@ -332,3 +406,4 @@ WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models"
|
|||||||
####################################
|
####################################
|
||||||
|
|
||||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||||
|
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
|
||||||
|
|||||||
+13
-1
@@ -5,6 +5,13 @@ class MESSAGES(str, Enum):
|
|||||||
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
|
||||||
|
|
||||||
|
class WEBHOOK_MESSAGES(str, Enum):
|
||||||
|
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
||||||
|
USER_SIGNUP = lambda username="": (
|
||||||
|
f"New user signed up: {username}" if username else "New user signed up"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ERROR_MESSAGES(str, Enum):
|
class ERROR_MESSAGES(str, Enum):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
@@ -41,10 +48,15 @@ class ERROR_MESSAGES(str, Enum):
|
|||||||
NOT_FOUND = "We could not find what you're looking for :/"
|
NOT_FOUND = "We could not find what you're looking for :/"
|
||||||
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
||||||
API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
|
API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
|
||||||
|
|
||||||
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
||||||
|
|
||||||
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
||||||
INCORRECT_FORMAT = (
|
INCORRECT_FORMAT = (
|
||||||
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
|
lambda err="": f"Invalid format. Please use the correct format{err}"
|
||||||
)
|
)
|
||||||
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"
|
||||||
|
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
|
||||||
|
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"version": 0,
|
||||||
"ui": {
|
"ui": {
|
||||||
"prompt_suggestions": [
|
"prompt_suggestions": [
|
||||||
{
|
{
|
||||||
|
|||||||
+154
-41
@@ -4,33 +4,50 @@ import markdown
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends, status
|
from fastapi import FastAPI, Request, Depends, status
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import ProxyConfig, initialize
|
|
||||||
from litellm.proxy.proxy_server import app as litellm_app
|
|
||||||
|
|
||||||
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.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
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from config import WEBUI_NAME, ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR
|
|
||||||
|
from utils.utils import get_admin_user
|
||||||
|
from apps.rag.utils import rag_messages
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
WEBUI_NAME,
|
||||||
|
ENV,
|
||||||
|
VERSION,
|
||||||
|
CHANGELOG,
|
||||||
|
FRONTEND_BUILD_DIR,
|
||||||
|
MODEL_FILTER_ENABLED,
|
||||||
|
MODEL_FILTER_LIST,
|
||||||
|
GLOBAL_LOG_LEVEL,
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
WEBHOOK_URL,
|
||||||
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
from utils.utils import get_http_authorization_cred, get_current_user
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
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):
|
||||||
@@ -43,25 +60,72 @@ class SPAStaticFiles(StaticFiles):
|
|||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
|
||||||
|
|
||||||
|
|
||||||
async def config():
|
|
||||||
router, model_list, general_settings = await proxy_config.load_config(
|
|
||||||
router=None, config_file_path="./data/litellm/config.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
await initialize(config="./data/litellm/config.yaml", telemetry=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def startup():
|
|
||||||
await config()
|
|
||||||
|
|
||||||
|
|
||||||
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_LIST = MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
app.state.WEBHOOK_URL = WEBHOOK_URL
|
||||||
|
|
||||||
|
|
||||||
origins = ["*"]
|
origins = ["*"]
|
||||||
|
|
||||||
|
|
||||||
|
class RAGMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
if request.method == "POST" and (
|
||||||
|
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
|
||||||
|
):
|
||||||
|
log.debug(f"request.url.path: {request.url.path}")
|
||||||
|
|
||||||
|
# Read the original request body
|
||||||
|
body = await request.body()
|
||||||
|
# Decode body to string
|
||||||
|
body_str = body.decode("utf-8")
|
||||||
|
# Parse string to JSON
|
||||||
|
data = json.loads(body_str) if body_str else {}
|
||||||
|
|
||||||
|
# Example: Add a new key-value pair or modify existing ones
|
||||||
|
# data["modified"] = True # Example modification
|
||||||
|
if "docs" in data:
|
||||||
|
|
||||||
|
data = {**data}
|
||||||
|
data["messages"] = rag_messages(
|
||||||
|
data["docs"],
|
||||||
|
data["messages"],
|
||||||
|
rag_app.state.RAG_TEMPLATE,
|
||||||
|
rag_app.state.TOP_K,
|
||||||
|
rag_app.state.sentence_transformer_ef,
|
||||||
|
)
|
||||||
|
del data["docs"]
|
||||||
|
|
||||||
|
log.debug(f"data['messages']: {data['messages']}")
|
||||||
|
|
||||||
|
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||||
|
|
||||||
|
# Replace the request body with the modified one
|
||||||
|
request._body = modified_body_bytes
|
||||||
|
|
||||||
|
# Set custom header to ensure content-length matches new body length
|
||||||
|
request.headers.__dict__["_list"] = [
|
||||||
|
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
|
||||||
|
*[
|
||||||
|
(k, v)
|
||||||
|
for k, v in request.headers.raw
|
||||||
|
if k.lower() != b"content-length"
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def _receive(self, body: bytes):
|
||||||
|
return {"type": "http.request", "body": body, "more_body": False}
|
||||||
|
|
||||||
|
|
||||||
|
app.add_middleware(RAGMiddleware)
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
@@ -71,11 +135,6 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def on_startup():
|
|
||||||
await startup()
|
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def check_url(request: Request, call_next):
|
async def check_url(request: Request, call_next):
|
||||||
start_time = int(time.time())
|
start_time = int(time.time())
|
||||||
@@ -86,25 +145,15 @@ async def check_url(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@litellm_app.middleware("http")
|
@app.on_event("startup")
|
||||||
async def auth_middleware(request: Request, call_next):
|
async def on_startup():
|
||||||
auth_header = request.headers.get("Authorization", "")
|
await litellm_app_startup()
|
||||||
|
|
||||||
if ENV != "dev":
|
|
||||||
try:
|
|
||||||
user = get_current_user(get_http_authorization_cred(auth_header))
|
|
||||||
print(user)
|
|
||||||
except Exception as e:
|
|
||||||
return JSONResponse(status_code=400, content={"detail": str(e)})
|
|
||||||
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
app.mount("/api/v1", webui_app)
|
app.mount("/api/v1", webui_app)
|
||||||
app.mount("/litellm/api", litellm_app)
|
app.mount("/litellm/api", litellm_app)
|
||||||
|
|
||||||
app.mount("/ollama/api", ollama_app)
|
app.mount("/ollama", ollama_app)
|
||||||
app.mount("/openai/api", openai_app)
|
app.mount("/openai/api", openai_app)
|
||||||
|
|
||||||
app.mount("/images/api/v1", images_app)
|
app.mount("/images/api/v1", images_app)
|
||||||
@@ -125,6 +174,69 @@ async def get_app_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/config/model/filter")
|
||||||
|
async def get_model_filter_config(user=Depends(get_admin_user)):
|
||||||
|
return {
|
||||||
|
"enabled": app.state.MODEL_FILTER_ENABLED,
|
||||||
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ModelFilterConfigForm(BaseModel):
|
||||||
|
enabled: bool
|
||||||
|
models: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/config/model/filter")
|
||||||
|
async def update_model_filter_config(
|
||||||
|
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
|
||||||
|
app.state.MODEL_FILTER_ENABLED = form_data.enabled
|
||||||
|
app.state.MODEL_FILTER_LIST = form_data.models
|
||||||
|
|
||||||
|
ollama_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
|
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
|
||||||
|
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
|
||||||
|
|
||||||
|
return {
|
||||||
|
"enabled": app.state.MODEL_FILTER_ENABLED,
|
||||||
|
"models": app.state.MODEL_FILTER_LIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/webhook")
|
||||||
|
async def get_webhook_url(user=Depends(get_admin_user)):
|
||||||
|
return {
|
||||||
|
"url": app.state.WEBHOOK_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UrlForm(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/webhook")
|
||||||
|
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||||
|
app.state.WEBHOOK_URL = form_data.url
|
||||||
|
|
||||||
|
webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
|
||||||
|
|
||||||
|
return {
|
||||||
|
"url": app.state.WEBHOOK_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/version")
|
||||||
|
async def get_app_config():
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/changelog")
|
@app.get("/api/changelog")
|
||||||
async def get_app_changelog():
|
async def get_app_changelog():
|
||||||
return CHANGELOG
|
return CHANGELOG
|
||||||
@@ -148,6 +260,7 @@ async def get_app_latest_release_version():
|
|||||||
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
|
||||||
|
|
||||||
|
|
||||||
app.mount(
|
app.mount(
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ aiohttp
|
|||||||
peewee
|
peewee
|
||||||
bcrypt
|
bcrypt
|
||||||
|
|
||||||
litellm
|
litellm==1.30.7
|
||||||
|
argon2-cffi
|
||||||
apscheduler
|
apscheduler
|
||||||
google-generativeai
|
google-generativeai
|
||||||
|
|
||||||
langchain
|
langchain
|
||||||
langchain-community
|
langchain-community
|
||||||
|
fake_useragent
|
||||||
chromadb
|
chromadb
|
||||||
sentence_transformers
|
sentence_transformers
|
||||||
pypdf
|
pypdf
|
||||||
@@ -34,9 +36,13 @@ openpyxl
|
|||||||
pyxlsb
|
pyxlsb
|
||||||
xlrd
|
xlrd
|
||||||
|
|
||||||
|
opencv-python-headless
|
||||||
|
rapidocr-onnxruntime
|
||||||
|
|
||||||
faster-whisper
|
faster-whisper
|
||||||
|
|
||||||
PyJWT
|
PyJWT
|
||||||
pyjwt[crypto]
|
pyjwt[crypto]
|
||||||
|
|
||||||
black
|
black
|
||||||
|
langfuse
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def post_webhook(url: str, message: str, event_data: dict) -> bool:
|
||||||
|
try:
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
if "https://hooks.slack.com" in url:
|
||||||
|
payload["text"] = message
|
||||||
|
elif "https://discord.com/api/webhooks" in url:
|
||||||
|
payload["content"] = message
|
||||||
|
else:
|
||||||
|
payload = {**event_data}
|
||||||
|
|
||||||
|
r = requests.post(url, json=payload)
|
||||||
|
r.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
|
||||||
|
read ans
|
||||||
|
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
|
||||||
|
docker-compose down -v
|
||||||
|
else
|
||||||
|
echo "Operation cancelled."
|
||||||
|
fi
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 MiB After Width: | Height: | Size: 5.0 MiB |
+2
-2
@@ -14,7 +14,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
OLLAMA_API_BASE_URL: '/ollama/api'
|
OLLAMA_BASE_URL: '/ollama'
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: ghcr.io/open-webui/open-webui:main
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
container_name: open-webui
|
container_name: open-webui
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- ${OPEN_WEBUI_PORT-3000}:8080
|
- ${OPEN_WEBUI_PORT-3000}:8080
|
||||||
environment:
|
environment:
|
||||||
- 'OLLAMA_API_BASE_URL=http://ollama:11434/api'
|
- 'OLLAMA_BASE_URL=http://ollama:11434'
|
||||||
- 'WEBUI_SECRET_KEY='
|
- 'WEBUI_SECRET_KEY='
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ We welcome pull requests. Before submitting one, please:
|
|||||||
|
|
||||||
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
|
Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI.
|
||||||
|
|
||||||
|
### 🌐 Translations and Internationalization
|
||||||
|
|
||||||
|
Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project.
|
||||||
|
|
||||||
|
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language.
|
||||||
|
|
||||||
|
To add a new language:
|
||||||
|
|
||||||
|
- Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`.
|
||||||
|
- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object.
|
||||||
|
- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`.
|
||||||
|
|
||||||
### 🤔 Questions & Feedback
|
### 🤔 Questions & Feedback
|
||||||
|
|
||||||
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
|
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// i18next-parser.config.ts
|
||||||
|
import { getLanguages } from './src/lib/i18n/index.ts';
|
||||||
|
|
||||||
|
const getLangCodes = async () => {
|
||||||
|
const languages = await getLanguages();
|
||||||
|
return languages.map((l) => l.code);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
contextSeparator: '_',
|
||||||
|
createOldCatalogs: false,
|
||||||
|
defaultNamespace: 'translation',
|
||||||
|
defaultValue: '',
|
||||||
|
indentation: 2,
|
||||||
|
keepRemoved: false,
|
||||||
|
keySeparator: false,
|
||||||
|
lexers: {
|
||||||
|
svelte: ['JavascriptLexer'],
|
||||||
|
js: ['JavascriptLexer'],
|
||||||
|
ts: ['JavascriptLexer'],
|
||||||
|
|
||||||
|
default: ['JavascriptLexer']
|
||||||
|
},
|
||||||
|
lineEnding: 'auto',
|
||||||
|
locales: await getLangCodes(),
|
||||||
|
namespaceSeparator: false,
|
||||||
|
output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json',
|
||||||
|
pluralSeparator: '_',
|
||||||
|
input: 'src/**/*.{js,svelte}',
|
||||||
|
sort: true,
|
||||||
|
verbose: true,
|
||||||
|
failOnWarnings: false,
|
||||||
|
failOnUpdate: false,
|
||||||
|
customValueTemplate: null,
|
||||||
|
resetDefaultValueLocale: null,
|
||||||
|
i18nextOptions: null,
|
||||||
|
yamlOptions: null
|
||||||
|
};
|
||||||
@@ -40,7 +40,7 @@ spec:
|
|||||||
- name: data
|
- name: data
|
||||||
mountPath: /app/backend/data
|
mountPath: /app/backend/data
|
||||||
env:
|
env:
|
||||||
- name: OLLAMA_API_BASE_URL
|
- name: OLLAMA_BASE_URL
|
||||||
value: {{ include "ollama.url" . | quote }}
|
value: {{ include "ollama.url" . | quote }}
|
||||||
tty: true
|
tty: true
|
||||||
{{- with .Values.webui.nodeSelector }}
|
{{- with .Values.webui.nodeSelector }}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ metadata:
|
|||||||
name: {{ include "open-webui.name" . }}
|
name: {{ include "open-webui.name" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "open-webui.labels" . | nindent 4 }}
|
{{- include "open-webui.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.webui.service.labels }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
{{- with .Values.webui.service.annotations }}
|
{{- with .Values.webui.service.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
@@ -11,14 +14,16 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
{{- include "open-webui.selectorLabels" . | nindent 4 }}
|
||||||
{{- with .Values.webui.service }}
|
type: {{ .Values.webui.service.type | default "ClusterIP" }}
|
||||||
type: {{ .type }}
|
|
||||||
ports:
|
ports:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
port: {{ .port }}
|
port: {{ .Values.webui.service.port }}
|
||||||
targetPort: http
|
targetPort: http
|
||||||
{{- if .nodePort }}
|
{{- if .Values.webui.service.nodePort }}
|
||||||
nodePort: {{ .nodePort | int }}
|
nodePort: {{ .Values.webui.service.nodePort | int }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- if .Values.webui.service.loadBalancerClass }}
|
||||||
|
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
|||||||
@@ -70,3 +70,5 @@ webui:
|
|||||||
port: 80
|
port: 80
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
nodePort: ""
|
nodePort: ""
|
||||||
|
labels: {}
|
||||||
|
loadBalancerClass: ""
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ spec:
|
|||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
memory: "1Gi"
|
memory: "1Gi"
|
||||||
env:
|
env:
|
||||||
- name: OLLAMA_API_BASE_URL
|
- name: OLLAMA_BASE_URL
|
||||||
value: "http://ollama-service.open-webui.svc.cluster.local:11434/api"
|
value: "http://ollama-service.open-webui.svc.cluster.local:11434"
|
||||||
tty: true
|
tty: true
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: webui-volume
|
- name: webui-volume
|
||||||
|
|||||||
Generated
+3121
-2972
File diff suppressed because it is too large
Load Diff
+9
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.104",
|
"version": "0.1.115",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"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": "yapf --recursive backend -p -i",
|
||||||
|
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"i18next-parser": "^8.13.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
@@ -42,14 +44,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
|
"bits-ui": "^0.19.7",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
"i18next": "^23.10.0",
|
||||||
|
"i18next-browser-languagedetector": "^7.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",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -182,7 +182,7 @@ else
|
|||||||
export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
|
export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
|
||||||
fi
|
fi
|
||||||
if [[ -n $webui_port ]]; then
|
if [[ -n $webui_port ]]; then
|
||||||
export OLLAMA_WEBUI_PORT=$webui_port # Set OLLAMA_WEBUI_PORT environment variable
|
export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
|
||||||
fi
|
fi
|
||||||
DEFAULT_COMPOSE_COMMAND+=" up -d"
|
DEFAULT_COMPOSE_COMMAND+=" up -d"
|
||||||
DEFAULT_COMPOSE_COMMAND+=" --remove-orphans"
|
DEFAULT_COMPOSE_COMMAND+=" --remove-orphans"
|
||||||
|
|||||||
+19
@@ -28,6 +28,25 @@ math {
|
|||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ol > li {
|
||||||
|
counter-increment: list-number;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol > li::before {
|
||||||
|
content: counters(list-number, '.') '.';
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
color: var(--tw-prose-counters);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
li p {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
background-color: rgba(217, 217, 227, 0.8);
|
background-color: rgba(217, 217, 227, 0.8);
|
||||||
|
|||||||
+28
-11
@@ -8,18 +8,35 @@
|
|||||||
<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 (
|
(() => {
|
||||||
localStorage.theme === 'light' ||
|
if (
|
||||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
localStorage.theme === 'light' ||
|
||||||
) {
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
||||||
document.documentElement.classList.add('light');
|
) {
|
||||||
} else if (localStorage.theme) {
|
document.documentElement.classList.add('light');
|
||||||
localStorage.theme.split(' ').forEach((e) => {
|
} else if (localStorage.theme && localStorage.theme !== 'system') {
|
||||||
document.documentElement.classList.add(e);
|
localStorage.theme.split(' ').forEach((e) => {
|
||||||
|
document.documentElement.classList.add(e);
|
||||||
|
});
|
||||||
|
} else if (localStorage.theme && localStorage.theme === 'system') {
|
||||||
|
systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
document.documentElement.classList.add(systemTheme ? 'dark' : 'light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
|
||||||
|
if (localStorage.theme === 'system') {
|
||||||
|
if (e.matches) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.classList.remove('light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
})();
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ export const deleteAllChats = async (token: string) => {
|
|||||||
return json;
|
return json;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
error = err;
|
error = err.detail;
|
||||||
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export const createNewDoc = async (
|
|||||||
collection_name: string,
|
collection_name: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
name: string,
|
name: string,
|
||||||
title: string
|
title: string,
|
||||||
|
content: object | null = null
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -20,7 +21,8 @@ export const createNewDoc = async (
|
|||||||
collection_name: collection_name,
|
collection_name: collection_name,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
name: name,
|
name: name,
|
||||||
title: title
|
title: title,
|
||||||
|
...(content ? { content: JSON.stringify(content) } : {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IMAGES_API_BASE_URL } from '$lib/constants';
|
import { IMAGES_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getImageGenerationEnabledStatus = async (token: string = '') => {
|
export const getImageGenerationConfig = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/config`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -32,10 +32,50 @@ export const getImageGenerationEnabledStatus = async (token: string = '') => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleImageGenerationEnabledStatus = async (token: string = '') => {
|
export const updateImageGenerationConfig = async (
|
||||||
|
token: string = '',
|
||||||
|
engine: string,
|
||||||
|
enabled: boolean
|
||||||
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled/toggle`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
engine,
|
||||||
|
enabled
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
} else {
|
||||||
|
error = 'Server connection failed';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOpenAIKey = async (token: string = '') => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${IMAGES_API_BASE_URL}/key`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -61,10 +101,45 @@ export const toggleImageGenerationEnabledStatus = async (token: string = '') =>
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res.OPENAI_API_KEY;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAUTOMATIC1111Url = async (token: string = '') => {
|
export const updateOpenAIKey = async (token: string = '', key: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${IMAGES_API_BASE_URL}/key/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: key
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
} else {
|
||||||
|
error = 'Server connection failed';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.OPENAI_API_KEY;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
||||||
@@ -93,10 +168,10 @@ export const getAUTOMATIC1111Url = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.AUTOMATIC1111_BASE_URL;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
|
export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
||||||
@@ -107,7 +182,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url
|
...urls
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -128,7 +203,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.AUTOMATIC1111_BASE_URL;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageSize = async (token: string = '') => {
|
export const getImageSize = async (token: string = '') => {
|
||||||
@@ -263,7 +338,7 @@ export const updateImageSteps = async (token: string = '', steps: number) => {
|
|||||||
return res.IMAGE_STEPS;
|
return res.IMAGE_STEPS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiffusionModels = async (token: string = '') => {
|
export const getImageGenerationModels = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
|
||||||
@@ -295,7 +370,7 @@ export const getDiffusionModels = async (token: string = '') => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultDiffusionModel = async (token: string = '') => {
|
export const getDefaultImageGenerationModel = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
|
||||||
@@ -327,7 +402,7 @@ export const getDefaultDiffusionModel = async (token: string = '') => {
|
|||||||
return res.model;
|
return res.model;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDefaultDiffusionModel = async (token: string = '', model: string) => {
|
export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
|
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
|
||||||
|
|||||||
@@ -77,3 +77,122 @@ export const getVersionUpdates = async () => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getModelFilterConfig = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
|
||||||
|
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;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateModelFilterConfig = async (
|
||||||
|
token: string,
|
||||||
|
enabled: boolean,
|
||||||
|
models: string[]
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: enabled,
|
||||||
|
models: models
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWebhookUrl = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, {
|
||||||
|
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;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWebhookUrl = async (token: string, url: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.url;
|
||||||
|
};
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type AddLiteLLMModelForm = {
|
|||||||
api_base: string;
|
api_base: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
rpm: string;
|
rpm: string;
|
||||||
|
max_tokens: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => {
|
export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => {
|
||||||
@@ -95,7 +96,8 @@ export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMMod
|
|||||||
model: payload.model,
|
model: payload.model,
|
||||||
...(payload.api_base === '' ? {} : { api_base: payload.api_base }),
|
...(payload.api_base === '' ? {} : { api_base: payload.api_base }),
|
||||||
...(payload.api_key === '' ? {} : { api_key: payload.api_key }),
|
...(payload.api_key === '' ? {} : { api_key: payload.api_key }),
|
||||||
...(isNaN(parseInt(payload.rpm)) ? {} : { rpm: parseInt(payload.rpm) })
|
...(isNaN(parseInt(payload.rpm)) ? {} : { rpm: parseInt(payload.rpm) }),
|
||||||
|
...(payload.max_tokens === '' ? {} : { max_tokens: payload.max_tokens })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+141
-33
@@ -1,9 +1,9 @@
|
|||||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getOllamaAPIUrl = async (token: string = '') => {
|
export const getOllamaUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OLLAMA_API_BASE_URL;
|
return res.OLLAMA_BASE_URLS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
export const updateOllamaUrls = async (token: string = '', urls: string[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url
|
urls: urls
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -64,13 +64,13 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OLLAMA_API_BASE_URL;
|
return res.OLLAMA_BASE_URLS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOllamaVersion = async (token: string = '') => {
|
export const getOllamaVersion = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -102,7 +102,7 @@ export const getOllamaVersion = async (token: string = '') => {
|
|||||||
export const getOllamaModels = async (token: string = '') => {
|
export const getOllamaModels = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -148,10 +148,11 @@ export const generateTitle = async (
|
|||||||
|
|
||||||
console.log(template);
|
console.log(template);
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -186,10 +187,11 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
|||||||
conversation = '[no existing conversation]';
|
conversation = '[no existing conversation]';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -217,15 +219,43 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
prompt: text,
|
||||||
|
stream: true
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateChatCompletion = async (token: string = '', body: object) => {
|
export const generateChatCompletion = async (token: string = '', body: object) => {
|
||||||
let controller = new AbortController();
|
let controller = new AbortController();
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
@@ -241,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
|||||||
return [res, controller];
|
return [res, controller];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cancelChatCompletion = async (token: string = '', requestId: string) => {
|
export const cancelOllamaRequest = async (token: string = '', requestId: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, {
|
||||||
@@ -265,10 +295,11 @@ export const cancelChatCompletion = async (token: string = '', requestId: string
|
|||||||
export const createModel = async (token: string, tagName: string, content: string) => {
|
export const createModel = async (token: string, tagName: string, content: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -287,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteModel = async (token: string, tagName: string) => {
|
export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, {
|
const res = await fetch(
|
||||||
method: 'DELETE',
|
`${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'text/event-stream',
|
method: 'DELETE',
|
||||||
Authorization: `Bearer ${token}`
|
headers: {
|
||||||
},
|
Accept: 'application/json',
|
||||||
body: JSON.stringify({
|
'Content-Type': 'application/json',
|
||||||
name: tagName
|
Authorization: `Bearer ${token}`
|
||||||
})
|
},
|
||||||
})
|
body: JSON.stringify({
|
||||||
|
name: tagName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) throw await res.json();
|
if (!res.ok) throw await res.json();
|
||||||
return res.json();
|
return res.json();
|
||||||
@@ -310,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
error = err.error;
|
error = err;
|
||||||
|
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pullModel = async (token: string, tagName: string) => {
|
export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -349,6 +390,73 @@ export const pullModel = async (token: string, tagName: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const downloadModel = async (
|
||||||
|
token: string,
|
||||||
|
download_url: string,
|
||||||
|
urlIdx: string | null = null
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${OLLAMA_API_BASE_URL}/models/download${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: download_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadModel = async (token: string, file: File, urlIdx: string | null = null) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${OLLAMA_API_BASE_URL}/models/upload${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
|
||||||
|
if ('detail' in err) {
|
||||||
|
error = err.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
// export const pullModel = async (token: string, tagName: string) => {
|
// export const pullModel = async (token: string, tagName: string) => {
|
||||||
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
||||||
// method: 'POST',
|
// method: 'POST',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getOpenAIUrl = async (token: string = '') => {
|
export const getOpenAIUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/url`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_BASE_URL;
|
return res.OPENAI_API_BASE_URLS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOpenAIUrl = async (token: string = '', url: string) => {
|
export const updateOpenAIUrls = async (token: string = '', urls: string[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url
|
urls: urls
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_BASE_URL;
|
return res.OPENAI_API_BASE_URLS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenAIKey = async (token: string = '') => {
|
export const getOpenAIKeys = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/key`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res.OPENAI_API_KEYS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateOpenAIKey = async (token: string = '', key: string) => {
|
export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key: key
|
keys: keys
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res.OPENAI_API_KEYS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenAIModels = async (token: string = '') => {
|
export const getOpenAIModels = async (token: string = '') => {
|
||||||
@@ -150,7 +150,6 @@ export const getOpenAIModels = async (token: string = '') => {
|
|||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
|
||||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
+52
-11
@@ -1,9 +1,9 @@
|
|||||||
import { RAG_API_BASE_URL } from '$lib/constants';
|
import { RAG_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getChunkParams = async (token: string) => {
|
export const getRAGConfig = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${RAG_API_BASE_URL}/chunk`, {
|
const res = await fetch(`${RAG_API_BASE_URL}/config`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateChunkParams = async (token: string, size: number, overlap: number) => {
|
type ChunkConfigForm = {
|
||||||
|
chunk_size: number;
|
||||||
|
chunk_overlap: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RAGConfigForm = {
|
||||||
|
pdf_extract_images: boolean;
|
||||||
|
chunk: ChunkConfigForm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${RAG_API_BASE_URL}/chunk/update`, {
|
const res = await fetch(`${RAG_API_BASE_URL}/config/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chunk_size: size,
|
...payload
|
||||||
chunk_overlap: overlap
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -85,17 +94,49 @@ export const getRAGTemplate = async (token: string) => {
|
|||||||
return res?.template ?? '';
|
return res?.template ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateRAGTemplate = async (token: string, template: string) => {
|
export const getQuerySettings = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${RAG_API_BASE_URL}/template/update`, {
|
const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, {
|
||||||
|
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 QuerySettings = {
|
||||||
|
k: number | null;
|
||||||
|
template: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateQuerySettings = async (token: string, settings: QuerySettings) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template: template
|
...settings
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
@@ -183,7 +224,7 @@ export const queryDoc = async (
|
|||||||
token: string,
|
token: string,
|
||||||
collection_name: string,
|
collection_name: string,
|
||||||
query: string,
|
query: string,
|
||||||
k: number
|
k: number | null = null
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -220,7 +261,7 @@ export const queryCollection = async (
|
|||||||
token: string,
|
token: string,
|
||||||
collection_names: string,
|
collection_names: string,
|
||||||
query: string,
|
query: string,
|
||||||
k: number
|
k: number | null = null
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const downloadDatabase = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'webui.db';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class=" text-center text-6xl mb-3">📄</div>
|
<div class=" text-center text-6xl mb-3">📄</div>
|
||||||
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div>
|
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
|
||||||
Drop any files here to add to the conversation
|
{$i18n.t('Drop any files here to add to the conversation')}
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { Confetti } from 'svelte-confetti';
|
import { Confetti } from 'svelte-confetti';
|
||||||
|
|
||||||
import { WEBUI_NAME, config } from '$lib/stores';
|
import { WEBUI_NAME, config } from '$lib/stores';
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
import Modal from './common/Modal.svelte';
|
import Modal from './common/Modal.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
let changelog = null;
|
let changelog = null;
|
||||||
@@ -23,7 +25,8 @@
|
|||||||
<div class="px-5 py-4 dark:text-gray-300">
|
<div class="px-5 py-4 dark:text-gray-300">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="text-xl font-bold">
|
<div class="text-xl font-bold">
|
||||||
What’s New in {$WEBUI_NAME}
|
{$i18n.t('What’s New in')}
|
||||||
|
{$WEBUI_NAME}
|
||||||
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -45,7 +48,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1">
|
<div class="flex items-center mt-1">
|
||||||
<div class="text-sm dark:text-gray-200">Release Notes</div>
|
<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||||
<div class="text-sm dark:text-gray-200">
|
<div class="text-sm dark:text-gray-200">
|
||||||
v{WEBUI_VERSION}
|
v{WEBUI_VERSION}
|
||||||
@@ -108,7 +111,7 @@
|
|||||||
}}
|
}}
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
>
|
>
|
||||||
<span class="relative">Okay, Let's Go!</span>
|
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
import { updateUserById } from '$lib/apis/users';
|
import { updateUserById } from '$lib/apis/users';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">Edit User</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -84,7 +85,8 @@
|
|||||||
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')}
|
{$i18n.t('Created at')}
|
||||||
|
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
|
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Email</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -108,7 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Name</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -122,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">New Password</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -140,7 +142,7 @@
|
|||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { downloadDatabase } from '$lib/apis/utils';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// permissions = await getUserPermissions(localStorage.token);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={async () => {
|
||||||
|
saveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
// exportAllUserChats();
|
||||||
|
|
||||||
|
downloadDatabase(localStorage.token);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
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
|
||||||
|
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"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
</form>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||||
import {
|
import {
|
||||||
getDefaultUserRole,
|
getDefaultUserRole,
|
||||||
getJWTExpiresDuration,
|
getJWTExpiresDuration,
|
||||||
@@ -7,13 +8,17 @@
|
|||||||
updateDefaultUserRole,
|
updateDefaultUserRole,
|
||||||
updateJWTExpiresDuration
|
updateJWTExpiresDuration
|
||||||
} from '$lib/apis/auths';
|
} from '$lib/apis/auths';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
let signUpEnabled = true;
|
let signUpEnabled = true;
|
||||||
let defaultUserRole = 'pending';
|
let defaultUserRole = 'pending';
|
||||||
let JWTExpiresIn = '';
|
let JWTExpiresIn = '';
|
||||||
|
|
||||||
|
let webhookUrl = '';
|
||||||
|
|
||||||
const toggleSignUpEnabled = async () => {
|
const toggleSignUpEnabled = async () => {
|
||||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
||||||
};
|
};
|
||||||
@@ -26,27 +31,32 @@
|
|||||||
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateWebhookUrlHandler = async () => {
|
||||||
|
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||||
|
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||||
});
|
});
|
||||||
</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={() => {
|
||||||
// console.log('submit');
|
|
||||||
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
||||||
|
updateWebhookUrlHandler();
|
||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">General Settings</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Enable New Sign Ups</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -66,7 +76,7 @@
|
|||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ml-2 self-center">Enabled</span>
|
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -81,25 +91,25 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">Disabled</span>
|
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Default User Role</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={defaultUserRole}
|
bind:value={defaultUserRole}
|
||||||
placeholder="Select a theme"
|
placeholder="Select a theme"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
updateDefaultUserRoleHandler(e.target.value);
|
updateDefaultUserRoleHandler(e.target.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">{$i18n.t('pending')}</option>
|
||||||
<option value="user">User</option>
|
<option value="user">{$i18n.t('user')}</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">{$i18n.t('admin')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +118,24 @@
|
|||||||
|
|
||||||
<div class=" w-full justify-between">
|
<div class=" w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">JWT Expiration</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2 space-x-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
|
type="text"
|
||||||
|
placeholder={`https://example.com/webhook`}
|
||||||
|
bind:value={webhookUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
|
<div class=" w-full justify-between">
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 space-x-2">
|
<div class="flex mt-2 space-x-2">
|
||||||
@@ -121,8 +148,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">
|
||||||
Valid time units: <span class=" text-gray-300 font-medium"
|
{$i18n.t('Valid time units:')}
|
||||||
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
|
<span class=" text-gray-300 font-medium"
|
||||||
|
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +162,7 @@
|
|||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
|
||||||
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
||||||
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
|
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { models } from '$lib/stores';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let whitelistEnabled = false;
|
||||||
|
let whitelistModels = [''];
|
||||||
let permissions = {
|
let permissions = {
|
||||||
chat: {
|
chat: {
|
||||||
deletion: true
|
deletion: true
|
||||||
@@ -13,6 +20,13 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
permissions = await getUserPermissions(localStorage.token);
|
permissions = await getUserPermissions(localStorage.token);
|
||||||
|
|
||||||
|
const res = await getModelFilterConfig(localStorage.token);
|
||||||
|
if (res) {
|
||||||
|
whitelistEnabled = res.enabled;
|
||||||
|
|
||||||
|
whitelistModels = res.models.length > 0 ? res.models : [''];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -21,15 +35,17 @@
|
|||||||
on:submit|preventDefault={async () => {
|
on:submit|preventDefault={async () => {
|
||||||
// console.log('submit');
|
// console.log('submit');
|
||||||
await updateUserPermissions(localStorage.token, permissions);
|
await updateUserPermissions(localStorage.token, permissions);
|
||||||
|
|
||||||
|
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
|
||||||
saveHandler();
|
saveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2 text-sm font-medium">User Permissions</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Allow Chat Deletion</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -49,7 +65,7 @@
|
|||||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ml-2 self-center">Allow</span>
|
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -64,11 +80,112 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">Don't Allow</span>
|
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" dark:border-gray-700 my-2" />
|
||||||
|
|
||||||
|
<div class="mt-2 space-y-3 pr-1.5">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" space-y-3">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" text-xs font-medium text-gray-500"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
whitelistEnabled = !whitelistEnabled;
|
||||||
|
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if whitelistEnabled}
|
||||||
|
<div>
|
||||||
|
<div class=" space-y-1.5">
|
||||||
|
{#each whitelistModels as modelId, modelIdx}
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<select
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
|
bind:value={modelId}
|
||||||
|
placeholder="Select a model"
|
||||||
|
>
|
||||||
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
|
{#each $models.filter((model) => model.id) as model}
|
||||||
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
|
||||||
|
>{model.name}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if modelIdx === 0}
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
if (whitelistModels.at(-1) !== '') {
|
||||||
|
whitelistModels = [...whitelistModels, ''];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
whitelistModels.splice(modelIdx, 1);
|
||||||
|
whitelistModels = whitelistModels;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
|
||||||
|
<div class=" text-xs font-medium">
|
||||||
|
{whitelistModels.length}
|
||||||
|
{$i18n.t('Model(s) Whitelisted')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
@@ -76,7 +193,7 @@
|
|||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import Database from './Settings/Database.svelte';
|
||||||
|
|
||||||
import General from './Settings/General.svelte';
|
import General from './Settings/General.svelte';
|
||||||
import Users from './Settings/Users.svelte';
|
import Users from './Settings/Users.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
let selectedTab = 'general';
|
let selectedTab = 'general';
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">Admin Settings</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">General</div>
|
<div class=" self-center">{$i18n.t('General')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -84,7 +88,35 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Users</div>
|
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
|
'db'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-700'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'db';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
||||||
|
<path
|
||||||
|
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">{$i18n.t('Database')}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[380px]">
|
<div class="flex-1 md:min-h-[380px]">
|
||||||
@@ -100,6 +132,12 @@
|
|||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{:else if selectedTab === 'db'}
|
||||||
|
<Database
|
||||||
|
saveHandler={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, tick } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import { settings } from '$lib/stores';
|
import { settings } from '$lib/stores';
|
||||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
@@ -12,13 +12,16 @@
|
|||||||
import Documents from './MessageInput/Documents.svelte';
|
import Documents from './MessageInput/Documents.svelte';
|
||||||
import Models from './MessageInput/Models.svelte';
|
import Models from './MessageInput/Models.svelte';
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let submitPrompt: Function;
|
export let submitPrompt: Function;
|
||||||
export let stopResponse: Function;
|
export let stopResponse: Function;
|
||||||
|
|
||||||
export let suggestionPrompts = [];
|
export let suggestionPrompts = [];
|
||||||
export let autoScroll = true;
|
export let autoScroll = true;
|
||||||
|
let chatTextAreaElement: HTMLTextAreaElement;
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
|
|
||||||
let promptsElement;
|
let promptsElement;
|
||||||
@@ -42,11 +45,9 @@
|
|||||||
let speechRecognition;
|
let speechRecognition;
|
||||||
|
|
||||||
$: if (prompt) {
|
$: if (prompt) {
|
||||||
const chatInput = document.getElementById('chat-textarea');
|
if (chatTextAreaElement) {
|
||||||
|
chatTextAreaElement.style.height = '';
|
||||||
if (chatInput) {
|
chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
|
||||||
chatInput.style.height = '';
|
|
||||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +86,7 @@
|
|||||||
if (res) {
|
if (res) {
|
||||||
prompt = res.text;
|
prompt = res.text;
|
||||||
await tick();
|
await tick();
|
||||||
|
chatTextAreaElement?.focus();
|
||||||
const inputElement = document.getElementById('chat-textarea');
|
|
||||||
inputElement?.focus();
|
|
||||||
|
|
||||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
||||||
submitPrompt(prompt, user);
|
submitPrompt(prompt, user);
|
||||||
@@ -190,8 +189,7 @@
|
|||||||
prompt = `${prompt}${transcript}`;
|
prompt = `${prompt}${transcript}`;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
const inputElement = document.getElementById('chat-textarea');
|
chatTextAreaElement?.focus();
|
||||||
inputElement?.focus();
|
|
||||||
|
|
||||||
// Restart the inactivity timeout
|
// Restart the inactivity timeout
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
@@ -213,11 +211,11 @@
|
|||||||
// Event triggered when an error occurs
|
// Event triggered when an error occurs
|
||||||
speechRecognition.onerror = function (event) {
|
speechRecognition.onerror = function (event) {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
toast.error(`Speech recognition error: ${event.error}`);
|
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
||||||
isRecording = false;
|
isRecording = false;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
toast.error('SpeechRecognition API is not supported in this browser.');
|
toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,6 +291,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||||
|
|
||||||
const dropZone = document.querySelector('body');
|
const dropZone = document.querySelector('body');
|
||||||
|
|
||||||
const onDragOver = (e) => {
|
const onDragOver = (e) => {
|
||||||
@@ -335,12 +335,15 @@
|
|||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
$i18n.t(
|
||||||
|
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||||
|
{ file_type: file['type'] }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`File not found.`);
|
toast.error($i18n.t(`File not found.`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,12 +364,12 @@
|
|||||||
|
|
||||||
{#if dragged}
|
{#if dragged}
|
||||||
<div
|
<div
|
||||||
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
|
class="fixed lg:w-[calc(100%-260px)] w-full h-full flex z-50 touch-none pointer-events-none"
|
||||||
id="dropzone"
|
id="dropzone"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Drag and Drop Container"
|
aria-label="Drag and Drop Container"
|
||||||
>
|
>
|
||||||
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<AddFilesPlaceholder />
|
<AddFilesPlaceholder />
|
||||||
@@ -479,13 +482,16 @@
|
|||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
$i18n.t(
|
||||||
|
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||||
|
{ file_type: file['type'] }
|
||||||
|
)
|
||||||
);
|
);
|
||||||
uploadDoc(file);
|
uploadDoc(file);
|
||||||
filesInputElement.value = '';
|
filesInputElement.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`File not found.`);
|
toast.error($i18n.t(`File not found.`));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -572,7 +578,7 @@
|
|||||||
{file.name}
|
{file.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">Document</div>
|
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if file.type === 'collection'}
|
{:else if file.type === 'collection'}
|
||||||
@@ -600,7 +606,7 @@
|
|||||||
{file?.title ?? `#${file.name}`}
|
{file?.title ?? `#${file.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">Collection</div>
|
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -634,37 +640,40 @@
|
|||||||
<div class=" flex">
|
<div class=" flex">
|
||||||
{#if fileUploadEnabled}
|
{#if fileUploadEnabled}
|
||||||
<div class=" self-end mb-2 ml-1">
|
<div class=" self-end mb-2 ml-1">
|
||||||
<button
|
<Tooltip content={$i18n.t('Upload files')}>
|
||||||
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
<button
|
||||||
type="button"
|
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||||
on:click={() => {
|
type="button"
|
||||||
filesInputElement.click();
|
on:click={() => {
|
||||||
}}
|
filesInputElement.click();
|
||||||
>
|
}}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-[1.2rem] h-[1.2rem]"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
/>
|
viewBox="0 0 16 16"
|
||||||
</svg>
|
fill="currentColor"
|
||||||
</button>
|
class="w-[1.2rem] h-[1.2rem]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="chat-textarea"
|
id="chat-textarea"
|
||||||
|
bind:this={chatTextAreaElement}
|
||||||
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
|
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
|
||||||
? ''
|
? ''
|
||||||
: ' pl-4'} rounded-xl resize-none h-[48px]"
|
: ' pl-4'} rounded-xl resize-none h-[48px]"
|
||||||
placeholder={chatInputPlaceholder !== ''
|
placeholder={chatInputPlaceholder !== ''
|
||||||
? chatInputPlaceholder
|
? chatInputPlaceholder
|
||||||
: isRecording
|
: isRecording
|
||||||
? 'Listening...'
|
? $i18n.t('Listening...')
|
||||||
: 'Send a message'}
|
: $i18n.t('Send a Message')}
|
||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
on:keypress={(e) => {
|
on:keypress={(e) => {
|
||||||
if (e.keyCode == 13 && !e.shiftKey) {
|
if (e.keyCode == 13 && !e.shiftKey) {
|
||||||
@@ -803,87 +812,97 @@
|
|||||||
|
|
||||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||||
{#if speechRecognitionEnabled}
|
<Tooltip content={$i18n.t('Record voice')}>
|
||||||
|
{#if speechRecognitionEnabled}
|
||||||
|
<button
|
||||||
|
id="voice-input-button"
|
||||||
|
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
speechRecognitionHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if isRecording}
|
||||||
|
<svg
|
||||||
|
class=" w-5 h-5 translate-y-[0.5px]"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><style>
|
||||||
|
.spinner_qM83 {
|
||||||
|
animation: spinner_8HQG 1.05s infinite;
|
||||||
|
}
|
||||||
|
.spinner_oXPr {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.spinner_ZTLf {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
@keyframes spinner_8HQG {
|
||||||
|
0%,
|
||||||
|
57.14% {
|
||||||
|
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
28.57% {
|
||||||
|
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||||
|
class="spinner_qM83 spinner_oXPr"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="2.5"
|
||||||
|
/><circle
|
||||||
|
class="spinner_qM83 spinner_ZTLf"
|
||||||
|
cx="20"
|
||||||
|
cy="12"
|
||||||
|
r="2.5"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5 translate-y-[0.5px]"
|
||||||
|
>
|
||||||
|
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
|
||||||
|
<path
|
||||||
|
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Send message')}>
|
||||||
<button
|
<button
|
||||||
id="voice-input-button"
|
class="{prompt !== ''
|
||||||
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||||
type="button"
|
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
|
||||||
on:click={() => {
|
type="submit"
|
||||||
speechRecognitionHandler();
|
disabled={prompt === ''}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{#if isRecording}
|
<svg
|
||||||
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class=" w-5 h-5 translate-y-[0.5px]"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 24 24"
|
class="w-5 h-5"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
><style>
|
<path
|
||||||
.spinner_qM83 {
|
fill-rule="evenodd"
|
||||||
animation: spinner_8HQG 1.05s infinite;
|
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||||
}
|
clip-rule="evenodd"
|
||||||
.spinner_oXPr {
|
/>
|
||||||
animation-delay: 0.1s;
|
</svg>
|
||||||
}
|
|
||||||
.spinner_ZTLf {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
@keyframes spinner_8HQG {
|
|
||||||
0%,
|
|
||||||
57.14% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
28.57% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
|
||||||
transform: translateY(-6px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
|
||||||
class="spinner_qM83 spinner_oXPr"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="2.5"
|
|
||||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5 translate-y-[0.5px]"
|
|
||||||
>
|
|
||||||
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
|
|
||||||
<path
|
|
||||||
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
</Tooltip>
|
||||||
<button
|
|
||||||
class="{prompt !== ''
|
|
||||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
|
||||||
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
|
|
||||||
type="submit"
|
|
||||||
disabled={prompt === ''}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||||
@@ -908,7 +927,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
||||||
LLMs can make mistakes. Verify important information.
|
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
import { documents } from '$lib/stores';
|
import { documents } from '$lib/stores';
|
||||||
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
||||||
import { tick } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
|
|
||||||
@@ -89,16 +91,16 @@
|
|||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">#</div>
|
<div class=" text-lg font-semibold mt-2">#</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||||
{#each filteredItems as doc, docIdx}
|
{#each filteredItems as doc, docIdx}
|
||||||
<button
|
<button
|
||||||
class=" px-3 py-1.5 rounded-lg w-full text-left {docIdx === selectedIdx
|
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
|
||||||
? ' bg-gray-100 selected-command-option-button'
|
? ' bg-gray-100 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -117,7 +119,7 @@
|
|||||||
{doc?.title ?? `#${doc.name}`}
|
{doc?.title ?? `#${doc.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" font-medium text-black line-clamp-1">
|
<div class=" font-medium text-black line-clamp-1">
|
||||||
#{doc.name} ({doc.filename})
|
#{doc.name} ({doc.filename})
|
||||||
@@ -132,7 +134,7 @@
|
|||||||
|
|
||||||
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 rounded-lg w-full text-left bg-gray-100 selected-command-option-button"
|
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||||
@@ -140,7 +142,9 @@
|
|||||||
confirmSelectWeb(url);
|
confirmSelectWeb(url);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
$i18n.t(
|
||||||
|
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -149,7 +153,7 @@
|
|||||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 line-clamp-1">Web</div>
|
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
import { generatePrompt } from '$lib/apis/ollama';
|
import { generatePrompt } from '$lib/apis/ollama';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
import { splitStream } from '$lib/utils';
|
import { splitStream } from '$lib/utils';
|
||||||
import { tick } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let user = null;
|
export let user = null;
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
user = JSON.parse(JSON.stringify(model.name));
|
user = JSON.parse(JSON.stringify(model.name));
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
chatInputPlaceholder = `'${model.name}' is thinking...`;
|
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
|
||||||
|
|
||||||
const chatInputElement = document.getElementById('chat-textarea');
|
const chatInputElement = document.getElementById('chat-textarea');
|
||||||
|
|
||||||
@@ -79,14 +81,18 @@
|
|||||||
throw data;
|
throw data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.done == false) {
|
if ('id' in data) {
|
||||||
if (prompt == '' && data.response == '\n') {
|
console.log(data);
|
||||||
continue;
|
} else {
|
||||||
} else {
|
if (data.done == false) {
|
||||||
prompt += data.response;
|
if (prompt == '' && data.response == '\n') {
|
||||||
console.log(data.response);
|
continue;
|
||||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
} else {
|
||||||
await tick();
|
prompt += data.response;
|
||||||
|
console.log(data.response);
|
||||||
|
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +115,9 @@
|
|||||||
toast.error(error.error);
|
toast.error(error.error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
|
toast.error(
|
||||||
|
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,16 +129,16 @@
|
|||||||
|
|
||||||
{#if filteredModels.length > 0}
|
{#if filteredModels.length > 0}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">@</div>
|
<div class=" text-lg font-semibold mt-2">@</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||||
{#each filteredModels as model, modelIdx}
|
{#each filteredModels as model, modelIdx}
|
||||||
<button
|
<button
|
||||||
class=" px-3 py-1.5 rounded-lg w-full text-left {modelIdx === selectedIdx
|
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||||
? ' bg-gray-100 selected-command-option-button'
|
? ' bg-gray-100 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { prompts } from '$lib/stores';
|
import { prompts } from '$lib/stores';
|
||||||
import { findWordIndices } from '$lib/utils';
|
import { findWordIndices } from '$lib/utils';
|
||||||
import { tick } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
let selectedCommandIdx = 0;
|
let selectedCommandIdx = 0;
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
|
|
||||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||||
toast.error('Failed to read clipboard contents');
|
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||||
return '{{CLIPBOARD}}';
|
return '{{CLIPBOARD}}';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,16 +62,16 @@
|
|||||||
|
|
||||||
{#if filteredPromptCommands.length > 0}
|
{#if filteredPromptCommands.length > 0}
|
||||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
<div class="flex w-full px-2">
|
||||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">/</div>
|
<div class=" text-lg font-semibold mt-2">/</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||||
{#each filteredPromptCommands as command, commandIdx}
|
{#each filteredPromptCommands as command, commandIdx}
|
||||||
<button
|
<button
|
||||||
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
|
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
|
||||||
? ' bg-gray-100 selected-command-option-button'
|
? ' bg-gray-100 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1"
|
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-xl flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<svg
|
<svg
|
||||||
@@ -113,8 +115,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
|
{$i18n.t(
|
||||||
input after each replacement.
|
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
||||||
import { tick } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
|
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||||
|
|
||||||
import UserMessage from './Messages/UserMessage.svelte';
|
import UserMessage from './Messages/UserMessage.svelte';
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { imageGenerations } from '$lib/apis/images';
|
import { imageGenerations } from '$lib/apis/images';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
export let sendPrompt: Function;
|
export let sendPrompt: Function;
|
||||||
export let continueGeneration: Function;
|
export let continueGeneration: Function;
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
function () {
|
function () {
|
||||||
console.log('Async: Copying to clipboard was successful!');
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
toast.success('Copying to clipboard was successful!');
|
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||||
},
|
},
|
||||||
function (err) {
|
function (err) {
|
||||||
console.error('Async: Could not copy text: ', err);
|
console.error('Async: Could not copy text: ', err);
|
||||||
@@ -223,33 +225,80 @@
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: change delete behaviour
|
|
||||||
// const deleteMessageAndDescendants = async (messageId: string) => {
|
|
||||||
// if (history.messages[messageId]) {
|
|
||||||
// history.messages[messageId].deleted = true;
|
|
||||||
|
|
||||||
// for (const childId of history.messages[messageId].childrenIds) {
|
|
||||||
// await deleteMessageAndDescendants(childId);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const triggerDeleteMessageRecursive = async (messageId: string) => {
|
|
||||||
// await deleteMessageAndDescendants(messageId);
|
|
||||||
// await updateChatById(localStorage.token, chatId, { history });
|
|
||||||
// await chats.set(await getChatList(localStorage.token));
|
|
||||||
// };
|
|
||||||
|
|
||||||
const messageDeleteHandler = async (messageId) => {
|
const messageDeleteHandler = async (messageId) => {
|
||||||
if (history.messages[messageId]) {
|
const messageToDelete = history.messages[messageId];
|
||||||
history.messages[messageId].deleted = true;
|
const messageParentId = messageToDelete.parentId;
|
||||||
|
const messageChildrenIds = messageToDelete.childrenIds ?? [];
|
||||||
for (const childId of history.messages[messageId].childrenIds) {
|
const hasSibling = messageChildrenIds.some(
|
||||||
history.messages[childId].deleted = true;
|
(childId) => history.messages[childId]?.childrenIds?.length > 0
|
||||||
|
);
|
||||||
|
messageChildrenIds.forEach((childId) => {
|
||||||
|
const child = history.messages[childId];
|
||||||
|
if (child && child.childrenIds) {
|
||||||
|
if (child.childrenIds.length === 0 && !hasSibling) {
|
||||||
|
// if last prompt/response pair
|
||||||
|
history.messages[messageParentId].childrenIds = [];
|
||||||
|
history.currentId = messageParentId;
|
||||||
|
} else {
|
||||||
|
child.childrenIds.forEach((grandChildId) => {
|
||||||
|
if (history.messages[grandChildId]) {
|
||||||
|
history.messages[grandChildId].parentId = messageParentId;
|
||||||
|
history.messages[messageParentId].childrenIds.push(grandChildId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// remove response
|
||||||
await updateChatById(localStorage.token, chatId, { history });
|
history.messages[messageParentId].childrenIds = history.messages[
|
||||||
|
messageParentId
|
||||||
|
].childrenIds.filter((id) => id !== childId);
|
||||||
|
});
|
||||||
|
// remove prompt
|
||||||
|
history.messages[messageParentId].childrenIds = history.messages[
|
||||||
|
messageParentId
|
||||||
|
].childrenIds.filter((id) => id !== messageId);
|
||||||
|
await updateChatById(localStorage.token, chatId, {
|
||||||
|
messages: messages,
|
||||||
|
history: history
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const messageDeleteHandler = async (messageId) => {
|
||||||
|
// const message = history.messages[messageId];
|
||||||
|
// const parentId = message.parentId;
|
||||||
|
// const childrenIds = message.childrenIds ?? [];
|
||||||
|
// const grandchildrenIds = [];
|
||||||
|
|
||||||
|
// // Iterate through childrenIds to find grandchildrenIds
|
||||||
|
// for (const childId of childrenIds) {
|
||||||
|
// const childMessage = history.messages[childId];
|
||||||
|
// const grandChildrenIds = childMessage.childrenIds ?? [];
|
||||||
|
|
||||||
|
// for (const grandchildId of grandchildrenIds) {
|
||||||
|
// const childMessage = history.messages[grandchildId];
|
||||||
|
// childMessage.parentId = parentId;
|
||||||
|
// }
|
||||||
|
// grandchildrenIds.push(...grandChildrenIds);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
|
||||||
|
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
|
||||||
|
// (id) => id !== messageId
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Select latest message
|
||||||
|
// let currentMessageId = grandchildrenIds.at(-1);
|
||||||
|
// if (currentMessageId) {
|
||||||
|
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||||
|
// while (messageChildrenIds.length !== 0) {
|
||||||
|
// currentMessageId = messageChildrenIds.at(-1);
|
||||||
|
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||||
|
// }
|
||||||
|
// history.currentId = currentMessageId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await updateChatById(localStorage.token, chatId, { messages, history });
|
||||||
|
// };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if messages.length == 0}
|
{#if messages.length == 0}
|
||||||
@@ -258,103 +307,55 @@
|
|||||||
<div class=" pb-10">
|
<div class=" pb-10">
|
||||||
{#key chatId}
|
{#key chatId}
|
||||||
{#each messages as message, messageIdx}
|
{#each messages as message, messageIdx}
|
||||||
{#if !message.deleted}
|
<div class=" w-full">
|
||||||
<div class=" w-full">
|
<div
|
||||||
<div
|
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
? 'max-w-full'
|
||||||
? 'max-w-full'
|
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
>
|
||||||
>
|
{#if message.role === 'user'}
|
||||||
{#if message.role === 'user'}
|
<UserMessage
|
||||||
<UserMessage
|
on:delete={() => messageDeleteHandler(message.id)}
|
||||||
on:delete={() => messageDeleteHandler(message.id)}
|
user={$user}
|
||||||
user={$user}
|
{message}
|
||||||
{message}
|
isFirstMessage={messageIdx === 0}
|
||||||
isFirstMessage={messageIdx === 0}
|
siblings={message.parentId !== null
|
||||||
siblings={message.parentId !== null
|
? history.messages[message.parentId]?.childrenIds ?? []
|
||||||
? history.messages[message.parentId]?.childrenIds ?? []
|
: Object.values(history.messages)
|
||||||
: Object.values(history.messages)
|
.filter((message) => message.parentId === null)
|
||||||
.filter((message) => message.parentId === null)
|
.map((message) => message.id) ?? []}
|
||||||
.map((message) => message.id) ?? []}
|
{confirmEditMessage}
|
||||||
{confirmEditMessage}
|
{showPreviousMessage}
|
||||||
{showPreviousMessage}
|
{showNextMessage}
|
||||||
{showNextMessage}
|
{copyToClipboard}
|
||||||
{copyToClipboard}
|
/>
|
||||||
/>
|
{:else}
|
||||||
|
<ResponseMessage
|
||||||
|
{message}
|
||||||
|
modelfiles={selectedModelfiles}
|
||||||
|
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||||
|
isLastMessage={messageIdx + 1 === messages.length}
|
||||||
|
{confirmEditResponseMessage}
|
||||||
|
{showPreviousMessage}
|
||||||
|
{showNextMessage}
|
||||||
|
{rateMessage}
|
||||||
|
{copyToClipboard}
|
||||||
|
{continueGeneration}
|
||||||
|
{regenerateResponse}
|
||||||
|
on:save={async (e) => {
|
||||||
|
console.log('save', e);
|
||||||
|
|
||||||
{#if messages.length - 1 === messageIdx && processing !== ''}
|
const message = e.detail;
|
||||||
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
|
history.messages[message.id] = message;
|
||||||
<div class=" dark:text-blue-100">
|
await updateChatById(localStorage.token, chatId, {
|
||||||
<svg
|
messages: messages,
|
||||||
class=" w-4 h-4 translate-y-[0.5px]"
|
history: history
|
||||||
fill="currentColor"
|
});
|
||||||
viewBox="0 0 24 24"
|
}}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
/>
|
||||||
><style>
|
{/if}
|
||||||
.spinner_qM83 {
|
|
||||||
animation: spinner_8HQG 1.05s infinite;
|
|
||||||
}
|
|
||||||
.spinner_oXPr {
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
.spinner_ZTLf {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
@keyframes spinner_8HQG {
|
|
||||||
0%,
|
|
||||||
57.14% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
28.57% {
|
|
||||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
|
||||||
transform: translateY(-6px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
|
||||||
class="spinner_qM83 spinner_oXPr"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="2.5"
|
|
||||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class=" text-sm font-medium">
|
|
||||||
{processing}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<ResponseMessage
|
|
||||||
{message}
|
|
||||||
modelfiles={selectedModelfiles}
|
|
||||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
|
||||||
isLastMessage={messageIdx + 1 === messages.length}
|
|
||||||
{confirmEditResponseMessage}
|
|
||||||
{showPreviousMessage}
|
|
||||||
{showNextMessage}
|
|
||||||
{rateMessage}
|
|
||||||
{copyToClipboard}
|
|
||||||
{continueGeneration}
|
|
||||||
{regenerateResponse}
|
|
||||||
on:save={async (e) => {
|
|
||||||
console.log('save', e);
|
|
||||||
|
|
||||||
const message = e.detail;
|
|
||||||
history.messages[message.id] = message;
|
|
||||||
await updateChatById(localStorage.token, chatId, {
|
|
||||||
messages: messages,
|
|
||||||
history: history
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if bottomPadding}
|
{#if bottomPadding}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let models = [];
|
export let models = [];
|
||||||
export let modelfiles = [];
|
export let modelfiles = [];
|
||||||
@@ -31,7 +33,7 @@
|
|||||||
<img
|
<img
|
||||||
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
alt="modelfile"
|
alt="modelfile"
|
||||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
src={models.length === 1
|
src={models.length === 1
|
||||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||||
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
||||||
{#if modelfile}
|
{#if modelfile}
|
||||||
<span class=" capitalize">
|
<span class=" capitalize">
|
||||||
{modelfile.title}
|
{modelfile.title}
|
||||||
@@ -64,9 +66,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" line-clamp-1">Hello, {$user.name}</div>
|
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
|
||||||
|
|
||||||
<div>How can I help you today?</div>
|
<div>{$i18n.t('How can I help you today?')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { onMount, tick } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
import CodeBlock from './CodeBlock.svelte';
|
import CodeBlock from './CodeBlock.svelte';
|
||||||
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';
|
||||||
|
|
||||||
export let modelfiles = [];
|
export let modelfiles = [];
|
||||||
export let message;
|
export let message;
|
||||||
@@ -40,7 +44,7 @@
|
|||||||
|
|
||||||
let edit = false;
|
let edit = false;
|
||||||
let editedContent = '';
|
let editedContent = '';
|
||||||
|
let editTextAreaElement: HTMLTextAreaElement;
|
||||||
let tooltipInstance = null;
|
let tooltipInstance = null;
|
||||||
|
|
||||||
let sentencesAudio = {};
|
let sentencesAudio = {};
|
||||||
@@ -247,10 +251,9 @@
|
|||||||
editedContent = message.content;
|
editedContent = message.content;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
|
||||||
|
|
||||||
editElement.style.height = '';
|
editTextAreaElement.style.height = '';
|
||||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const editMessageConfirmHandler = async () => {
|
const editMessageConfirmHandler = async () => {
|
||||||
@@ -276,13 +279,15 @@
|
|||||||
|
|
||||||
const generateImage = async (message) => {
|
const generateImage = async (message) => {
|
||||||
generatingImage = true;
|
generatingImage = true;
|
||||||
const res = await imageGenerations(localStorage.token, message.content);
|
const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
});
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
message.files = res.images.map((image) => ({
|
message.files = res.map((image) => ({
|
||||||
type: 'image',
|
type: 'image',
|
||||||
url: `data:image/png;base64,${image}`
|
url: `${image.url}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch('save', message);
|
dispatch('save', message);
|
||||||
@@ -313,7 +318,7 @@
|
|||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||||
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
@@ -334,16 +339,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
|
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{#if edit === true}
|
{#if edit === true}
|
||||||
<div class=" w-full">
|
<div class=" w-full">
|
||||||
<textarea
|
<textarea
|
||||||
id="message-edit-{message.id}"
|
id="message-edit-{message.id}"
|
||||||
|
bind:this={editTextAreaElement}
|
||||||
class=" bg-transparent outline-none w-full resize-none"
|
class=" bg-transparent outline-none w-full resize-none"
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
|
e.target.style.height = '';
|
||||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -355,7 +362,7 @@
|
|||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -364,7 +371,7 @@
|
|||||||
cancelEditMessage();
|
cancelEditMessage();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{$i18n.t('Cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,7 +422,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">
|
<div class="flex self-center min-w-fit -mt-1">
|
||||||
<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={() => {
|
||||||
@@ -462,189 +469,125 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<Tooltip content="Edit" placement="bottom">
|
||||||
class="{isLastMessage
|
|
||||||
? 'visible'
|
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
|
||||||
on:click={() => {
|
|
||||||
editMessageHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="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>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="{isLastMessage
|
|
||||||
? 'visible'
|
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
|
|
||||||
on:click={() => {
|
|
||||||
copyToClipboard(message.content);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="{isLastMessage
|
|
||||||
? 'visible'
|
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
|
||||||
? '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="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>
|
|
||||||
<button
|
|
||||||
class="{isLastMessage
|
|
||||||
? 'visible'
|
|
||||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
|
||||||
? '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>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="speak-button-{message.id}"
|
|
||||||
class="{isLastMessage
|
|
||||||
? 'visible'
|
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
|
||||||
on:click={() => {
|
|
||||||
if (!loadingSpeech) {
|
|
||||||
toggleSpeakMessage(message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if loadingSpeech}
|
|
||||||
<svg
|
|
||||||
class=" w-4 h-4"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_S1WN {
|
|
||||||
animation: spinner_MGfb 0.8s linear infinite;
|
|
||||||
animation-delay: -0.8s;
|
|
||||||
}
|
|
||||||
.spinner_Km9P {
|
|
||||||
animation-delay: -0.65s;
|
|
||||||
}
|
|
||||||
.spinner_JApP {
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
}
|
|
||||||
@keyframes spinner_MGfb {
|
|
||||||
93.75%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
|
|
||||||
class="spinner_S1WN spinner_Km9P"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="3"
|
|
||||||
/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
|
|
||||||
>
|
|
||||||
{:else if speaking}
|
|
||||||
<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="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<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="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if $config.images}
|
|
||||||
<button
|
<button
|
||||||
class="{isLastMessage
|
class="{isLastMessage
|
||||||
? 'visible'
|
? 'visible'
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!generatingImage) {
|
editMessageHandler();
|
||||||
generateImage(message);
|
}}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Tooltip content="Copy" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
|
||||||
|
on:click={() => {
|
||||||
|
copyToClipboard(message.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Good Response" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||||
|
? '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="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.rating === -1
|
||||||
|
? '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>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Read Aloud" placement="bottom">
|
||||||
|
<button
|
||||||
|
id="speak-button-{message.id}"
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
on:click={() => {
|
||||||
|
if (!loadingSpeech) {
|
||||||
|
toggleSpeakMessage(message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if generatingImage}
|
{#if loadingSpeech}
|
||||||
<svg
|
<svg
|
||||||
class=" w-4 h-4"
|
class=" w-4 h-4"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -679,105 +622,193 @@
|
|||||||
r="3"
|
r="3"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{:else}
|
{:else if speaking}
|
||||||
<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="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<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
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if $config.images}
|
||||||
|
<Tooltip content="Generate Image" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="{isLastMessage
|
||||||
|
? 'visible'
|
||||||
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
on:click={() => {
|
||||||
|
if (!generatingImage) {
|
||||||
|
generateImage(message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if generatingImage}
|
||||||
|
<svg
|
||||||
|
class=" w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><style>
|
||||||
|
.spinner_S1WN {
|
||||||
|
animation: spinner_MGfb 0.8s linear infinite;
|
||||||
|
animation-delay: -0.8s;
|
||||||
|
}
|
||||||
|
.spinner_Km9P {
|
||||||
|
animation-delay: -0.65s;
|
||||||
|
}
|
||||||
|
.spinner_JApP {
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
@keyframes spinner_MGfb {
|
||||||
|
93.75%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
|
||||||
|
class="spinner_S1WN spinner_Km9P"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
/><circle
|
||||||
|
class="spinner_S1WN spinner_JApP"
|
||||||
|
cx="20"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<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
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.info}
|
{#if message.info}
|
||||||
<button
|
<Tooltip content="Generation Info" placement="bottom">
|
||||||
class=" {isLastMessage
|
<button
|
||||||
? 'visible'
|
class=" {isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
? 'visible'
|
||||||
on:click={() => {
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||||
console.log(message);
|
on:click={() => {
|
||||||
}}
|
console.log(message);
|
||||||
id="info-{message.id}"
|
}}
|
||||||
>
|
id="info-{message.id}"
|
||||||
<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
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage}
|
{#if isLastMessage}
|
||||||
<button
|
<Tooltip content="Continue Response" placement="bottom">
|
||||||
type="button"
|
<button
|
||||||
class="{isLastMessage
|
type="button"
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
? 'visible'
|
||||||
on:click={() => {
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||||
continueGeneration();
|
on:click={() => {
|
||||||
}}
|
continueGeneration();
|
||||||
>
|
}}
|
||||||
<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
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
<path
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
class="w-4 h-4"
|
||||||
stroke-linejoin="round"
|
>
|
||||||
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
<path
|
||||||
/>
|
stroke-linecap="round"
|
||||||
</svg>
|
stroke-linejoin="round"
|
||||||
</button>
|
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<button
|
<Tooltip content="Regenerate" placement="bottom">
|
||||||
type="button"
|
<button
|
||||||
class="{isLastMessage
|
type="button"
|
||||||
? 'visible'
|
class="{isLastMessage
|
||||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
? 'visible'
|
||||||
on:click={regenerateResponse}
|
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||||
>
|
on:click={regenerateResponse}
|
||||||
<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
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { tick, createEventDispatcher } from 'svelte';
|
import { tick, createEventDispatcher, getContext } from 'svelte';
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
import { modelfiles, settings } from '$lib/stores';
|
import { modelfiles, settings } from '$lib/stores';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -20,18 +23,17 @@
|
|||||||
|
|
||||||
let edit = false;
|
let edit = false;
|
||||||
let editedContent = '';
|
let editedContent = '';
|
||||||
|
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||||
const editMessageHandler = async () => {
|
const editMessageHandler = async () => {
|
||||||
edit = true;
|
edit = true;
|
||||||
editedContent = message.content;
|
editedContent = message.content;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
|
||||||
|
|
||||||
editElement.style.height = '';
|
messageEditTextAreaElement.style.height = '';
|
||||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
|
||||||
|
|
||||||
editElement?.focus();
|
messageEditTextAreaElement?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const editMessageConfirmHandler = async () => {
|
const editMessageConfirmHandler = async () => {
|
||||||
@@ -65,17 +67,18 @@
|
|||||||
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
||||||
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
||||||
{:else}
|
{:else}
|
||||||
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
{$i18n.t('You')}
|
||||||
|
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if $settings.showUsername}
|
{:else if $settings.showUsername}
|
||||||
{user.name}
|
{user.name}
|
||||||
{:else}
|
{:else}
|
||||||
You
|
{$i18n.t('You')}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||||
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Name>
|
</Name>
|
||||||
@@ -123,7 +126,7 @@
|
|||||||
{file.name}
|
{file.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">Document</div>
|
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if file.type === 'collection'}
|
{:else if file.type === 'collection'}
|
||||||
@@ -152,7 +155,7 @@
|
|||||||
{file?.title ?? `#${file.name}`}
|
{file?.title ?? `#${file.name}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-gray-500 text-sm">Collection</div>
|
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -165,9 +168,11 @@
|
|||||||
<div class=" w-full">
|
<div class=" w-full">
|
||||||
<textarea
|
<textarea
|
||||||
id="message-edit-{message.id}"
|
id="message-edit-{message.id}"
|
||||||
|
bind:this={messageEditTextAreaElement}
|
||||||
class=" bg-transparent outline-none w-full resize-none"
|
class=" bg-transparent outline-none w-full resize-none"
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
|
e.target.style.height = '';
|
||||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -179,7 +184,7 @@
|
|||||||
editMessageConfirmHandler();
|
editMessageConfirmHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save & Submit
|
{$i18n.t('Save & Submit')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -188,7 +193,7 @@
|
|||||||
cancelEditMessage();
|
cancelEditMessage();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{$i18n.t('Cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +203,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">
|
<div class="flex self-center -mt-1">
|
||||||
<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={() => {
|
||||||
@@ -245,72 +250,78 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<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"
|
|
||||||
on:click={() => {
|
|
||||||
editMessageHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="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>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
|
||||||
on:click={() => {
|
|
||||||
copyToClipboard(message.content);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if !isFirstMessage}
|
|
||||||
<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 edit-user-message-button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
deleteMessageHandler();
|
editMessageHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="Copy" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
on:click={() => {
|
||||||
|
copyToClipboard(message.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if !isFirstMessage}
|
||||||
|
<Tooltip content="Delete" placement="bottom">
|
||||||
|
<button
|
||||||
|
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||||
|
on:click={() => {
|
||||||
|
deleteMessageHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Select from '../common/Select.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let selectedModels = [''];
|
export let selectedModels = [''];
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
@@ -10,7 +13,7 @@
|
|||||||
const saveDefaultModel = async () => {
|
const saveDefaultModel = async () => {
|
||||||
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
||||||
if (hasEmptyModel.length) {
|
if (hasEmptyModel.length) {
|
||||||
toast.error('Choose a model before saving...');
|
toast.error($i18n.t('Choose a model before saving...'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.set({ ...$settings, models: selectedModels });
|
settings.set({ ...$settings, models: selectedModels });
|
||||||
@@ -20,7 +23,7 @@
|
|||||||
console.log('setting default models globally');
|
console.log('setting default models globally');
|
||||||
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
||||||
}
|
}
|
||||||
toast.success('Default model updated');
|
toast.success($i18n.t('Default model updated'));
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (selectedModels.length > 0 && $models.length > 0) {
|
$: if (selectedModels.length > 0 && $models.length > 0) {
|
||||||
@@ -30,28 +33,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col my-2">
|
<div class="flex flex-col my-2 w-full">
|
||||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||||
<div class="flex">
|
<div class="flex w-full">
|
||||||
<select
|
<div class="overflow-hidden w-full">
|
||||||
id="models"
|
<div class="mr-2 max-w-full">
|
||||||
class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
|
<Select
|
||||||
bind:value={selectedModel}
|
placeholder={$i18n.t('Select a model')}
|
||||||
{disabled}
|
items={$models
|
||||||
>
|
.filter((model) => model.name !== 'hr')
|
||||||
<option class=" text-gray-700" value="" selected disabled>Select a model</option>
|
.map((model) => ({
|
||||||
|
value: model.id,
|
||||||
{#each $models as model}
|
label:
|
||||||
{#if model.name === 'hr'}
|
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`
|
||||||
<hr />
|
}))}
|
||||||
{:else}
|
bind:value={selectedModel}
|
||||||
<option value={model.id} class="text-gray-700 text-lg"
|
/>
|
||||||
>{model.name +
|
</div>
|
||||||
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
</div>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{#if selectedModelIdx === 0}
|
{#if selectedModelIdx === 0}
|
||||||
<button
|
<button
|
||||||
@@ -132,6 +131,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-left mt-1.5 text-xs text-gray-500">
|
<div class="text-left mt-1.5 ml-1 text-xs text-gray-500">
|
||||||
<button on:click={saveDefaultModel}> Set as default</button>
|
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
import { WEBUI_VERSION } from '$lib/constants';
|
import { WEBUI_VERSION } from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
||||||
import { compareVersion } from '$lib/utils';
|
import { compareVersion } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let ollamaVersion = '';
|
let ollamaVersion = '';
|
||||||
|
|
||||||
@@ -43,7 +45,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||||
<div>
|
<div>
|
||||||
{$WEBUI_NAME} Version
|
{$WEBUI_NAME}
|
||||||
|
{$i18n.t('Version')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="flex w-full justify-between items-center">
|
||||||
@@ -56,10 +59,10 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{updateAvailable === null
|
{updateAvailable === null
|
||||||
? 'Checking for updates...'
|
? $i18n.t('Checking for updates...')
|
||||||
: updateAvailable
|
: updateAvailable
|
||||||
? `(v${version.latest} available!)`
|
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||||
: '(latest)'}
|
: $i18n.t('(latest)')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
showChangelog.set(true);
|
showChangelog.set(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>See what's new</div>
|
<div>{$i18n.t("See what's new")}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
checkForVersionUpdates();
|
checkForVersionUpdates();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Check for updates
|
{$i18n.t('Check for updates')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +91,7 @@
|
|||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||||
{ollamaVersion ?? 'N/A'}
|
{ollamaVersion ?? 'N/A'}
|
||||||
@@ -123,7 +126,8 @@
|
|||||||
</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">
|
||||||
Created by <a
|
{$i18n.t('Created by')}
|
||||||
|
<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"
|
||||||
target="_blank">Timothy J. Baek</a
|
target="_blank">Timothy J. Baek</a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount } 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 } from '$lib/apis/auths';
|
||||||
@@ -9,12 +9,15 @@
|
|||||||
import { getGravatarUrl } from '$lib/apis/utils';
|
import { getGravatarUrl } from '$lib/apis/utils';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
let profileImageUrl = '';
|
let profileImageUrl = '';
|
||||||
let name = '';
|
let name = '';
|
||||||
let showJWTToken = false;
|
let showJWTToken = false;
|
||||||
let JWTTokenCopied = false;
|
let JWTTokenCopied = false;
|
||||||
|
let profileImageInputElement: HTMLInputElement;
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||||
@@ -37,14 +40,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<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]">
|
||||||
<input
|
<input
|
||||||
id="profile-image-input"
|
id="profile-image-input"
|
||||||
|
bind:this={profileImageInputElement}
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
const files = e?.target?.files ?? [];
|
const files = profileImageInputElement.files ?? [];
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
let originalImageUrl = `${event.target.result}`;
|
let originalImageUrl = `${event.target.result}`;
|
||||||
@@ -86,7 +90,7 @@
|
|||||||
// Display the compressed image
|
// Display the compressed image
|
||||||
profileImageUrl = compressedSrc;
|
profileImageUrl = compressedSrc;
|
||||||
|
|
||||||
e.target.files = null;
|
profileImageInputElement.files = null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,7 +103,7 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">Profile</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-5">
|
<div class="flex space-x-5">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -108,7 +112,7 @@
|
|||||||
class="relative rounded-full dark:bg-gray-700"
|
class="relative rounded-full dark:bg-gray-700"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('profile-image-input')?.click();
|
profileImageInputElement.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -141,13 +145,13 @@
|
|||||||
const url = await getGravatarUrl($user.email);
|
const url = await getGravatarUrl($user.email);
|
||||||
|
|
||||||
profileImageUrl = url;
|
profileImageUrl = url;
|
||||||
}}>Use Gravatar</button
|
}}>{$i18n.t('Use Gravatar')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Name</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -168,7 +172,7 @@
|
|||||||
|
|
||||||
<div class=" w-full justify-between">
|
<div class=" w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">JWT Token</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
@@ -269,7 +273,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
const res = await submitHandler();
|
const res = await submitHandler();
|
||||||
|
|
||||||
@@ -278,7 +282,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { getContext } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
import { updateUserPassword } from '$lib/apis/auths';
|
import { updateUserPassword } from '$lib/apis/auths';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
let currentPassword = '';
|
let currentPassword = '';
|
||||||
let newPassword = '';
|
let newPassword = '';
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success('Successfully updated.');
|
toast.success($i18n.t('Successfully updated.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPassword = '';
|
currentPassword = '';
|
||||||
@@ -40,20 +43,20 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">Change Password</div>
|
<div class=" font-medium">{$i18n.t('Change Password')}</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
show = !show;
|
show = !show;
|
||||||
}}>{show ? 'Hide' : 'Show'}</button
|
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class=" py-2.5 space-y-1.5">
|
<div class=" py-2.5 space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">New Password</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -99,7 +102,7 @@
|
|||||||
<button
|
<button
|
||||||
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
|
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
|
||||||
>
|
>
|
||||||
Update password
|
{$i18n.t('Update password')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
|
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
@@ -55,14 +57,14 @@
|
|||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<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-80">
|
||||||
<div class=" text-sm font-medium">Parameters</div>
|
<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
|
||||||
|
|
||||||
<AdvancedParams bind:options />
|
<AdvancedParams bind:options />
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div class=" py-1 w-full justify-between">
|
<div class=" py-1 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -72,9 +74,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if keepAlive === null}
|
{#if keepAlive === null}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
<input
|
<input
|
||||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||||
bind:value={keepAlive}
|
bind:value={keepAlive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-1 flex w-full justify-between">
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">Request Mode</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -102,7 +104,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if requestFormat === ''}
|
{#if requestFormat === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
{:else if requestFormat === 'json'}
|
{:else if requestFormat === 'json'}
|
||||||
<!-- <svg
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -114,7 +116,7 @@
|
|||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
/>
|
/>
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<span class="ml-2 self-center"> JSON </span>
|
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let options = {
|
export let options = {
|
||||||
// Advanced
|
// Advanced
|
||||||
seed: 0,
|
seed: 0,
|
||||||
@@ -20,7 +24,7 @@
|
|||||||
<div class=" space-y-3 text-xs">
|
<div class=" space-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" w-20 text-xs font-medium self-center">Seed</div>
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
@@ -36,12 +40,12 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div>
|
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
|
||||||
<div class=" flex-1 self-center">
|
<div class=" flex-1 self-center">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter Stop Sequence"
|
placeholder={$i18n.t('Enter stop sequence')}
|
||||||
bind:value={options.stop}
|
bind:value={options.stop}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -51,7 +55,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Temperature</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -61,9 +65,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.temperature === ''}
|
{#if options.temperature === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +101,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Mirostat</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -107,9 +111,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat === ''}
|
{#if options.mirostat === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +147,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -153,9 +157,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat_eta === ''}
|
{#if options.mirostat_eta === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +193,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -199,9 +203,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.mirostat_tau === ''}
|
{#if options.mirostat_tau === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +239,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Top K</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -245,9 +249,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.top_k === ''}
|
{#if options.top_k === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +285,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Top P</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -291,9 +295,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.top_p === ''}
|
{#if options.top_p === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,7 +331,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -337,9 +341,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.repeat_penalty === ''}
|
{#if options.repeat_penalty === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +377,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Repeat Last N</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -383,9 +387,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.repeat_last_n === ''}
|
{#if options.repeat_last_n === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -419,7 +423,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Tfs Z</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -429,9 +433,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.tfs_z === ''}
|
{#if options.tfs_z === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -465,7 +469,7 @@
|
|||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Context Length</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -475,9 +479,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.num_ctx === ''}
|
{#if options.num_ctx === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -510,7 +514,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Max Tokens</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -520,9 +524,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if options.num_predict === ''}
|
{#if options.num_predict === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
@@ -101,32 +103,36 @@
|
|||||||
>
|
>
|
||||||
<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-80">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">STT Settings</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</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">Speech-to-Text Engine</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={STTEngine}
|
bind:value={STTEngine}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (e.target.value !== '') {
|
if (e.target.value !== '') {
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
||||||
toast.error(`Permission denied when accessing microphone: ${err}`);
|
toast.error(
|
||||||
|
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
||||||
|
error: err
|
||||||
|
})
|
||||||
|
);
|
||||||
STTEngine = '';
|
STTEngine = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Default (Web API)</option>
|
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||||
<option value="whisper-local">Whisper (Local)</option>
|
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">Conversation Mode</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -136,15 +142,17 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if conversationMode === true}
|
{#if conversationMode === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</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">Auto-send input after 3 sec.</div>
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Auto-send input after 3 sec.')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -154,22 +162,22 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if speechAutoSend === true}
|
{#if speechAutoSend === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">TTS Settings</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</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">Text-to-Speech Engine</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<select
|
<select
|
||||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={TTSEngine}
|
bind:value={TTSEngine}
|
||||||
placeholder="Select a mode"
|
placeholder="Select a mode"
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
@@ -182,14 +190,14 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Default (Web API)</option>
|
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||||
<option value="openai">Open AI</option>
|
<option value="openai">{$i18n.t('Open AI')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">Auto-playback response</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -199,9 +207,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if responseAutoPlayback === true}
|
{#if responseAutoPlayback === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +219,7 @@
|
|||||||
|
|
||||||
{#if TTSEngine === ''}
|
{#if TTSEngine === ''}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">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
|
<select
|
||||||
@@ -219,7 +227,7 @@
|
|||||||
bind:value={speaker}
|
bind:value={speaker}
|
||||||
placeholder="Select a voice"
|
placeholder="Select a voice"
|
||||||
>
|
>
|
||||||
<option value="" selected>Default</option>
|
<option value="" selected>{$i18n.t('Default')}</option>
|
||||||
{#each voices.filter((v) => v.localService === true) as voice}
|
{#each voices.filter((v) => v.localService === true) as voice}
|
||||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
||||||
>
|
>
|
||||||
@@ -230,7 +238,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if TTSEngine === 'openai'}
|
{:else if TTSEngine === 'openai'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">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
|
<select
|
||||||
@@ -251,10 +259,10 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
const { saveAs } = fileSaver;
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
import { resetVectorDB } from '$lib/apis/rag';
|
|
||||||
import { chats, user } from '$lib/stores';
|
import { chats, user } from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,15 +12,18 @@
|
|||||||
getChatList
|
getChatList
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
// Chats
|
// Chats
|
||||||
let saveChatHistory = true;
|
let saveChatHistory = true;
|
||||||
let importFiles;
|
let importFiles;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
let chatImportInputElement: HTMLInputElement;
|
||||||
|
|
||||||
$: if (importFiles) {
|
$: if (importFiles) {
|
||||||
console.log(importFiles);
|
console.log(importFiles);
|
||||||
@@ -75,7 +77,9 @@
|
|||||||
|
|
||||||
const deleteChats = async () => {
|
const deleteChats = async () => {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
await deleteAllChats(localStorage.token);
|
await deleteAllChats(localStorage.token).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
});
|
||||||
await chats.set(await getChatList(localStorage.token));
|
await chats.set(await getChatList(localStorage.token));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,13 +100,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
<div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]">
|
||||||
<div class=" space-y-2">
|
<div class=" space-y-2">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
|
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
|
||||||
>
|
>
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">Chat History</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center"> On </span>
|
<span class="ml-2 self-center"> {$i18n.t('On')} </span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -144,24 +148,31 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-left w-full font-medium mt-0.5">
|
<div class="text-xs text-left w-full font-medium mt-0.5">
|
||||||
This setting does not sync across browsers or devices.
|
{$i18n.t('This setting does not sync across browsers or devices.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden />
|
<input
|
||||||
|
id="chat-import-input"
|
||||||
|
bind:this={chatImportInputElement}
|
||||||
|
bind:files={importFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('chat-import-input').click();
|
chatImportInputElement.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
@@ -178,7 +189,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">Import Chats</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||||
@@ -200,7 +211,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">Export Chats</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -222,7 +233,7 @@
|
|||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Are you sure?</span>
|
<span>{$i18n.t('Are you sure?')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-1.5 items-center">
|
<div class="flex space-x-1.5 items-center">
|
||||||
@@ -286,7 +297,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">Delete Chats</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -314,39 +325,9 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
|
<div class=" self-center text-sm font-medium">
|
||||||
</button>
|
{$i18n.t('Export All Chats (All Users)')}
|
||||||
|
|
||||||
<hr class=" dark:border-gray-700" />
|
|
||||||
|
|
||||||
<button
|
|
||||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
|
||||||
on:click={() => {
|
|
||||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
|
||||||
toast.error(error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
toast.success('Success');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class=" self-center mr-3">
|
|
||||||
<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.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
|
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models, user } from '$lib/stores';
|
import { models, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama';
|
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
||||||
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
|
import {
|
||||||
import toast from 'svelte-french-toast';
|
getOpenAIKeys,
|
||||||
|
getOpenAIUrls,
|
||||||
|
updateOpenAIKeys,
|
||||||
|
updateOpenAIUrls
|
||||||
|
} from '$lib/apis/openai';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
|
||||||
// External
|
// External
|
||||||
let API_BASE_URL = '';
|
let OLLAMA_BASE_URL = '';
|
||||||
|
let OLLAMA_BASE_URLS = [''];
|
||||||
|
|
||||||
let OPENAI_API_KEY = '';
|
let OPENAI_API_KEY = '';
|
||||||
let OPENAI_API_BASE_URL = '';
|
let OPENAI_API_BASE_URL = '';
|
||||||
|
|
||||||
|
let OPENAI_API_KEYS = [''];
|
||||||
|
let OPENAI_API_BASE_URLS = [''];
|
||||||
|
|
||||||
let showOpenAI = false;
|
let showOpenAI = false;
|
||||||
let showLiteLLM = false;
|
|
||||||
|
|
||||||
const updateOpenAIHandler = async () => {
|
const updateOpenAIHandler = async () => {
|
||||||
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
|
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||||
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
|
||||||
|
|
||||||
await models.set(await getModels());
|
await models.set(await getModels());
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateOllamaAPIUrlHandler = async () => {
|
const updateOllamaUrlsHandler = async () => {
|
||||||
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
|
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||||
|
|
||||||
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
@@ -34,16 +44,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (ollamaVersion) {
|
if (ollamaVersion) {
|
||||||
toast.success('Server connection verified');
|
toast.success($i18n.t('Server connection verified'));
|
||||||
await models.set(await getModels());
|
await models.set(await getModels());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user.role === 'admin') {
|
if ($user.role === 'admin') {
|
||||||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
|
||||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -53,59 +63,92 @@
|
|||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
updateOpenAIHandler();
|
updateOpenAIHandler();
|
||||||
dispatch('save');
|
dispatch('save');
|
||||||
|
|
||||||
// saveSettings({
|
|
||||||
// OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
|
|
||||||
// OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
|
|
||||||
// });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
|
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3">
|
||||||
<div class=" space-y-3">
|
<div class=" space-y-3">
|
||||||
<div class="mt-2 space-y-2 pr-1.5">
|
<div class="mt-2 space-y-2 pr-1.5">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">OpenAI API</div>
|
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showOpenAI = !showOpenAI;
|
showOpenAI = !showOpenAI;
|
||||||
}}>{showOpenAI ? 'Hide' : 'Show'}</button
|
}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showOpenAI}
|
{#if showOpenAI}
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class=" mb-2.5 text-sm font-medium">API Key</div>
|
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-2">
|
||||||
<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"
|
||||||
placeholder="Enter OpenAI API Key"
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={OPENAI_API_KEY}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div class=" mb-2.5 text-sm font-medium">API Base URL</div>
|
<input
|
||||||
<div class="flex w-full">
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
<div class="flex-1">
|
placeholder={$i18n.t('API Key')}
|
||||||
<input
|
bind:value={OPENAI_API_KEYS[idx]}
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
autocomplete="off"
|
||||||
placeholder="Enter OpenAI API Base URL"
|
/>
|
||||||
bind:value={OPENAI_API_BASE_URL}
|
</div>
|
||||||
autocomplete="off"
|
<div class="self-center flex items-center">
|
||||||
/>
|
{#if idx === 0}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
|
||||||
|
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||||
|
(url, urlIdx) => idx !== urlIdx
|
||||||
|
);
|
||||||
|
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
{$i18n.t('WebUI will make requests to')}
|
||||||
WebUI will make requests to <span class=" text-gray-200"
|
<span class=" text-gray-200">'{url}/models'</span>
|
||||||
>'{OPENAI_API_BASE_URL}/chat'</span
|
</div>
|
||||||
>
|
{/each}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,45 +157,92 @@
|
|||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-1.5">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 flex flex-col gap-2">
|
||||||
<input
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
<div class="flex gap-1.5">
|
||||||
placeholder="Enter URL (e.g. http://localhost:11434/api)"
|
<input
|
||||||
bind:value={API_BASE_URL}
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
/>
|
placeholder="Enter URL (e.g. http://localhost:11434)"
|
||||||
|
bind:value={url}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="self-center flex items-center">
|
||||||
|
{#if idx === 0}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
<div class="">
|
||||||
on:click={() => {
|
<button
|
||||||
updateOllamaAPIUrlHandler();
|
class="p-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-lg transition"
|
||||||
}}
|
on:click={() => {
|
||||||
type="button"
|
updateOllamaUrlsHandler();
|
||||||
>
|
}}
|
||||||
<svg
|
type="button"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
fill-rule="evenodd"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
viewBox="0 0 20 20"
|
||||||
clip-rule="evenodd"
|
fill="currentColor"
|
||||||
/>
|
class="w-4 h-4"
|
||||||
</svg>
|
>
|
||||||
</button>
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</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">
|
||||||
Trouble accessing Ollama?
|
{$i18n.t('Trouble accessing Ollama?')}
|
||||||
<a
|
<a
|
||||||
class=" text-gray-300 font-medium"
|
class=" text-gray-300 font-medium underline"
|
||||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Click here for help.
|
{$i18n.t('Click here for help.')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,10 +250,10 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
|
import { getLanguages } from '$lib/i18n';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { models, user } from '$lib/stores';
|
import { models, user, theme } from '$lib/stores';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||||
|
|
||||||
@@ -12,7 +15,10 @@
|
|||||||
|
|
||||||
// General
|
// General
|
||||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
||||||
let theme = 'dark';
|
let selectedTheme = 'system';
|
||||||
|
|
||||||
|
let languages = [];
|
||||||
|
let lang = $i18n.language;
|
||||||
let notificationEnabled = false;
|
let notificationEnabled = false;
|
||||||
let system = '';
|
let system = '';
|
||||||
|
|
||||||
@@ -63,9 +69,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
selectedTheme = localStorage.theme ?? 'system';
|
||||||
|
|
||||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||||
|
languages = await getLanguages();
|
||||||
|
|
||||||
theme = localStorage.theme ?? 'dark';
|
|
||||||
notificationEnabled = settings.notificationEnabled ?? false;
|
notificationEnabled = settings.notificationEnabled ?? false;
|
||||||
system = settings.system ?? '';
|
system = settings.system ?? '';
|
||||||
|
|
||||||
@@ -81,77 +89,93 @@
|
|||||||
options = { ...options, ...settings.options };
|
options = { ...options, ...settings.options };
|
||||||
options.stop = (settings?.options?.stop ?? []).join(',');
|
options.stop = (settings?.options?.stop ?? []).join(',');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const applyTheme = (_theme: string) => {
|
||||||
|
let themeToApply = _theme;
|
||||||
|
|
||||||
|
if (_theme === 'system') {
|
||||||
|
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
themes
|
||||||
|
.filter((e) => e !== themeToApply)
|
||||||
|
.forEach((e) => {
|
||||||
|
e.split(' ').forEach((e) => {
|
||||||
|
document.documentElement.classList.remove(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
themeToApply.split(' ').forEach((e) => {
|
||||||
|
document.documentElement.classList.add(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(_theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeChangeHandler = (_theme: string) => {
|
||||||
|
theme.set(_theme);
|
||||||
|
localStorage.setItem('theme', _theme);
|
||||||
|
|
||||||
|
applyTheme(_theme);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full justify-between text-sm">
|
<div class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem]">
|
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Theme</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<div class=" absolute right-16">
|
|
||||||
{#if theme === 'dark'}
|
|
||||||
<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="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else if theme === 'light'}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4 self-center"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select
|
<select
|
||||||
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||||
bind:value={theme}
|
bind:value={selectedTheme}
|
||||||
placeholder="Select a theme"
|
placeholder="Select a theme"
|
||||||
on:change={(e) => {
|
on:change={() => themeChangeHandler(selectedTheme)}
|
||||||
localStorage.theme = theme;
|
|
||||||
|
|
||||||
themes
|
|
||||||
.filter((e) => e !== theme)
|
|
||||||
.forEach((e) => {
|
|
||||||
e.split(' ').forEach((e) => {
|
|
||||||
document.documentElement.classList.remove(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
theme.split(' ').forEach((e) => {
|
|
||||||
document.documentElement.classList.add(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(theme);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="dark">Dark</option>
|
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||||
<option value="light">Light</option>
|
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||||
<option value="rose-pine dark">Rosé Pine</option>
|
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||||
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
|
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||||
|
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||||
|
bind:value={lang}
|
||||||
|
placeholder="Select a language"
|
||||||
|
on:change={(e) => {
|
||||||
|
$i18n.changeLanguage(lang);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each languages as language}
|
||||||
|
<option value={language['code']}>{language['title']}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $i18n.language === 'en-US'}
|
||||||
|
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Couldn't find your language?
|
||||||
|
<a
|
||||||
|
class=" text-gray-300 font-medium underline"
|
||||||
|
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Help us translate Open WebUI!
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<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">Notification</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -161,9 +185,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if notificationEnabled === true}
|
{#if notificationEnabled === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,23 +197,23 @@
|
|||||||
<hr class=" dark:border-gray-700 my-3" />
|
<hr class=" dark:border-gray-700 my-3" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" my-2.5 text-sm font-medium">System Prompt</div>
|
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={system}
|
bind:value={system}
|
||||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="4"
|
rows="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 space-y-3 pr-1.5">
|
<div class="mt-2 space-y-3 pr-1.5">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">Advanced Parameters</div>
|
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
|
||||||
<button
|
<button
|
||||||
class=" text-xs font-medium text-gray-500"
|
class=" text-xs font-medium text-gray-500"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showAdvanced = !showAdvanced;
|
showAdvanced = !showAdvanced;
|
||||||
}}>{showAdvanced ? 'Hide' : 'Show'}</button
|
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +223,7 @@
|
|||||||
|
|
||||||
<div class=" py-1 w-full justify-between">
|
<div class=" py-1 w-full justify-between">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -209,9 +233,9 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if keepAlive === null}
|
{#if keepAlive === null}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center"> Custom </span>
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +245,7 @@
|
|||||||
<input
|
<input
|
||||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||||
bind:value={keepAlive}
|
bind:value={keepAlive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +254,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-1 flex w-full justify-between">
|
<div class=" py-1 flex w-full justify-between">
|
||||||
<div class=" self-center text-sm font-medium">Request Mode</div>
|
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -239,7 +263,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if requestFormat === ''}
|
{#if requestFormat === ''}
|
||||||
<span class="ml-2 self-center"> Default </span>
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
{:else if requestFormat === 'json'}
|
{:else if requestFormat === 'json'}
|
||||||
<!-- <svg
|
<!-- <svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -251,7 +275,7 @@
|
|||||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||||
/>
|
/>
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<span class="ml-2 self-center"> JSON </span>
|
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +286,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
saveSettings({
|
saveSettings({
|
||||||
system: system !== '' ? system : undefined,
|
system: system !== '' ? system : undefined,
|
||||||
@@ -286,7 +310,7 @@
|
|||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { config, user } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
getAUTOMATIC1111Url,
|
getImageGenerationModels,
|
||||||
getDefaultDiffusionModel,
|
getDefaultImageGenerationModel,
|
||||||
getDiffusionModels,
|
updateDefaultImageGenerationModel,
|
||||||
getImageGenerationEnabledStatus,
|
|
||||||
getImageSize,
|
getImageSize,
|
||||||
toggleImageGenerationEnabledStatus,
|
getImageGenerationConfig,
|
||||||
updateAUTOMATIC1111Url,
|
updateImageGenerationConfig,
|
||||||
updateDefaultDiffusionModel,
|
getImageGenerationEngineUrls,
|
||||||
|
updateImageGenerationEngineUrls,
|
||||||
updateImageSize,
|
updateImageSize,
|
||||||
getImageSteps,
|
getImageSteps,
|
||||||
updateImageSteps
|
updateImageSteps,
|
||||||
|
getOpenAIKey,
|
||||||
|
updateOpenAIKey
|
||||||
} from '$lib/apis/images';
|
} from '$lib/apis/images';
|
||||||
import { getBackendConfig } from '$lib/apis';
|
import { getBackendConfig } from '$lib/apis';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
|
let imageGenerationEngine = '';
|
||||||
let enableImageGeneration = false;
|
let enableImageGeneration = false;
|
||||||
|
|
||||||
let AUTOMATIC1111_BASE_URL = '';
|
let AUTOMATIC1111_BASE_URL = '';
|
||||||
|
let COMFYUI_BASE_URL = '';
|
||||||
|
|
||||||
|
let OPENAI_API_KEY = '';
|
||||||
|
|
||||||
let selectedModel = '';
|
let selectedModel = '';
|
||||||
let models = null;
|
let models = null;
|
||||||
@@ -33,62 +42,101 @@
|
|||||||
let steps = 50;
|
let steps = 50;
|
||||||
|
|
||||||
const getModels = async () => {
|
const getModels = async () => {
|
||||||
models = await getDiffusionModels(localStorage.token).catch((error) => {
|
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
selectedModel = await getDefaultDiffusionModel(localStorage.token).catch((error) => {
|
selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAUTOMATIC1111UrlHandler = async () => {
|
const updateUrlHandler = async () => {
|
||||||
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
|
if (imageGenerationEngine === 'comfyui') {
|
||||||
(error) => {
|
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||||
|
COMFYUI_BASE_URL: COMFYUI_BASE_URL
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
|
await getModels();
|
||||||
|
|
||||||
|
if (models) {
|
||||||
|
toast.success($i18n.t('Server connection verified'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||||
|
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL
|
||||||
|
}).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
|
||||||
|
|
||||||
|
await getModels();
|
||||||
|
|
||||||
|
if (models) {
|
||||||
|
toast.success($i18n.t('Server connection verified'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
AUTOMATIC1111_BASE_URL = res;
|
|
||||||
|
|
||||||
await getModels();
|
|
||||||
|
|
||||||
if (models) {
|
|
||||||
toast.success('Server connection verified');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const toggleImageGeneration = async () => {
|
const updateImageGeneration = async () => {
|
||||||
if (AUTOMATIC1111_BASE_URL) {
|
const res = await updateImageGenerationConfig(
|
||||||
enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
|
localStorage.token,
|
||||||
(error) => {
|
imageGenerationEngine,
|
||||||
toast.error(error);
|
enableImageGeneration
|
||||||
return false;
|
).catch((error) => {
|
||||||
}
|
toast.error(error);
|
||||||
);
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (enableImageGeneration) {
|
if (res) {
|
||||||
config.set(await getBackendConfig(localStorage.token));
|
imageGenerationEngine = res.engine;
|
||||||
getModels();
|
enableImageGeneration = res.enabled;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
enableImageGeneration = false;
|
if (enableImageGeneration) {
|
||||||
toast.error('AUTOMATIC1111_BASE_URL not provided');
|
config.set(await getBackendConfig(localStorage.token));
|
||||||
|
getModels();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user.role === 'admin') {
|
if ($user.role === 'admin') {
|
||||||
enableImageGeneration = await getImageGenerationEnabledStatus(localStorage.token);
|
const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
||||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
|
if (res) {
|
||||||
imageSize = await getImageSize(localStorage.token);
|
imageGenerationEngine = res.engine;
|
||||||
steps = await getImageSteps(localStorage.token);
|
enableImageGeneration = res.enabled;
|
||||||
|
}
|
||||||
|
const URLS = await getImageGenerationEngineUrls(localStorage.token);
|
||||||
|
|
||||||
|
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
||||||
|
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
||||||
|
|
||||||
|
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||||
|
|
||||||
|
imageSize = await getImageSize(localStorage.token);
|
||||||
|
steps = await getImageSteps(localStorage.token);
|
||||||
|
|
||||||
|
if (enableImageGeneration) {
|
||||||
getModels();
|
getModels();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +147,13 @@
|
|||||||
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={async () => {
|
on:submit|preventDefault={async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
await updateDefaultDiffusionModel(localStorage.token, selectedModel);
|
|
||||||
|
if (imageGenerationEngine === 'openai') {
|
||||||
|
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||||
|
|
||||||
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
@@ -113,25 +167,58 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[20.5rem]">
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1 text-sm font-medium">Image Settings</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<select
|
||||||
|
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||||
|
bind:value={imageGenerationEngine}
|
||||||
|
placeholder={$i18n.t('Select a mode')}
|
||||||
|
on:change={async () => {
|
||||||
|
await updateImageGeneration();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
||||||
|
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||||
|
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">Image Generation (Experimental)</div>
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Image Generation (Experimental)')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
toggleImageGeneration();
|
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
||||||
|
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
||||||
|
enableImageGeneration = false;
|
||||||
|
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
|
||||||
|
toast.error($i18n.t('ComfyUI Base URL is required.'));
|
||||||
|
enableImageGeneration = false;
|
||||||
|
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
|
||||||
|
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||||
|
enableImageGeneration = false;
|
||||||
|
} else {
|
||||||
|
enableImageGeneration = !enableImageGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImageGeneration();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if enableImageGeneration === true}
|
{#if enableImageGeneration === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,69 +226,110 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div>
|
{#if imageGenerationEngine === ''}
|
||||||
<div class="flex w-full">
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex w-full">
|
||||||
<input
|
<div class="flex-1 mr-2">
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
<input
|
||||||
placeholder="Enter URL (e.g. http://127.0.0.1:7860/)"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
bind:value={AUTOMATIC1111_BASE_URL}
|
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
/>
|
bind:value={AUTOMATIC1111_BASE_URL}
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
// updateOllamaAPIUrlHandler();
|
|
||||||
|
|
||||||
updateAUTOMATIC1111UrlHandler();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</div>
|
||||||
</button>
|
<button
|
||||||
</div>
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
updateUrlHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</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">
|
||||||
Include `--api` flag when running stable-diffusion-webui
|
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
|
||||||
<a
|
<a
|
||||||
class=" text-gray-300 font-medium"
|
class=" text-gray-300 font-medium"
|
||||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
(e.g. `sh webui.sh --api`)
|
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if imageGenerationEngine === 'comfyui'}
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<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('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||||
|
bind:value={COMFYUI_BASE_URL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
updateUrlHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if imageGenerationEngine === 'openai'}
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<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('Enter API Key')}
|
||||||
|
bind:value={OPENAI_API_KEY}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if enableImageGeneration}
|
{#if enableImageGeneration}
|
||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">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
|
<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={selectedModel}
|
bind:value={selectedModel}
|
||||||
placeholder="Select a model"
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
{#if !selectedModel}
|
{#if !selectedModel}
|
||||||
<option value="" disabled selected>Select a model</option>
|
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||||
{/if}
|
{/if}
|
||||||
{#each models ?? [] as model}
|
{#each models ?? [] as model}
|
||||||
<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
|
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||||
>{model.model_name}</option
|
|
||||||
>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,12 +337,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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"
|
||||||
placeholder="Enter Image Size (e.g. 512x512)"
|
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||||
bind:value={imageSize}
|
bind:value={imageSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,12 +350,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">Set Steps</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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"
|
||||||
placeholder="Enter Number of Steps (e.g. 50)"
|
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||||
bind:value={steps}
|
bind:value={steps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,13 +366,13 @@
|
|||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded flex flex-row space-x-1 items-center {loading
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
|
||||||
? ' cursor-not-allowed'
|
? ' cursor-not-allowed'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
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, user } from '$lib/stores';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveSettings: Function;
|
export let saveSettings: Function;
|
||||||
|
|
||||||
// Addons
|
// Addons
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveSettings({
|
saveSettings({
|
||||||
|
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||||
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -81,7 +84,9 @@
|
|||||||
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
||||||
titleGenerationPrompt =
|
titleGenerationPrompt =
|
||||||
settings.titleGenerationPrompt ??
|
settings.titleGenerationPrompt ??
|
||||||
`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}}`;
|
$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':"
|
||||||
|
) + ' {{prompt}}';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -92,13 +97,13 @@
|
|||||||
dispatch('save');
|
dispatch('save');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll 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">WebUI Add-ons</div>
|
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
|
||||||
|
|
||||||
<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">Title Auto-Generation</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -108,9 +113,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if titleAutoGenerate === true}
|
{#if titleAutoGenerate === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +123,9 @@
|
|||||||
|
|
||||||
<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">Response AutoCopy to Clipboard</div>
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Response AutoCopy to Clipboard')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -128,9 +135,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if responseAutoCopy === true}
|
{#if responseAutoCopy === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +145,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">Full Screen Mode</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -148,9 +155,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if fullScreenMode === true}
|
{#if fullScreenMode === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +166,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">
|
<div class=" self-center text-xs font-medium">
|
||||||
Display the username instead of "You" in the Chat
|
{$i18n.t('Display the username instead of You in the Chat')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -170,9 +177,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{#if showUsername === true}
|
{#if showUsername === true}
|
||||||
<span class="ml-2 self-center">On</span>
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ml-2 self-center">Off</span>
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,51 +189,31 @@
|
|||||||
<hr class=" dark:border-gray-700" />
|
<hr class=" dark:border-gray-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">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">
|
||||||
<div class="flex-1 mr-2">
|
<div class="flex-1 mr-2">
|
||||||
<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={titleAutoGenerateModel}
|
bind:value={titleAutoGenerateModel}
|
||||||
placeholder="Select a model"
|
placeholder={$i18n.t('Select a model')}
|
||||||
>
|
>
|
||||||
<option value="" selected>Current Model</option>
|
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||||
{#each $models.filter((m) => m.size != null) as model}
|
{#each $models as model}
|
||||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
{#if model.size != null}
|
||||||
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
|
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||||
>
|
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
|
|
||||||
on:click={() => {
|
|
||||||
saveSettings({
|
|
||||||
titleAutoGenerateModel:
|
|
||||||
titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
|
||||||
<div class=" mb-2.5 text-sm font-medium">Title Generation Prompt</div>
|
<div class="mt-3 mr-2">
|
||||||
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={titleGenerationPrompt}
|
bind:value={titleGenerationPrompt}
|
||||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +224,9 @@
|
|||||||
|
|
||||||
<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-80">
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
|
<div class=" self-center text-sm font-semibold">
|
||||||
|
{$i18n.t('Default Prompt Suggestions')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 px-3 text-xs flex rounded transition"
|
class="p-1 px-3 text-xs flex rounded transition"
|
||||||
@@ -310,19 +299,19 @@
|
|||||||
|
|
||||||
{#if promptSuggestions.length > 0}
|
{#if promptSuggestions.length > 0}
|
||||||
<div class="text-xs text-left w-full mt-2">
|
<div class="text-xs text-left w-full mt-2">
|
||||||
Adjusting these settings will apply changes universally to all users.
|
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { getContext } from 'svelte';
|
||||||
|
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 { getOllamaModels } from '$lib/apis/ollama';
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
import Connections from './Settings/Connections.svelte';
|
import Connections from './Settings/Connections.svelte';
|
||||||
import Images from './Settings/Images.svelte';
|
import Images from './Settings/Images.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
const saveSettings = async (updated) => {
|
const saveSettings = async (updated) => {
|
||||||
@@ -58,7 +61,7 @@
|
|||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">Settings</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -106,7 +109,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">General</div>
|
<div class=" self-center">{$i18n.t('General')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
@@ -131,7 +134,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Connections</div>
|
<div class=" self-center">{$i18n.t('Connections')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -157,7 +160,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Models</div>
|
<div class=" self-center">{$i18n.t('Models')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Interface</div>
|
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -211,7 +214,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Audio</div>
|
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $user.role === 'admin'}
|
{#if $user.role === 'admin'}
|
||||||
@@ -238,7 +241,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Images</div>
|
<div class=" self-center">{$i18n.t('Images')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -265,7 +268,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Chats</div>
|
<div class=" self-center">{$i18n.t('Chats')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -291,7 +294,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">Account</div>
|
<div class=" self-center">{$i18n.t('Account')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -317,16 +320,16 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">About</div>
|
<div class=" self-center">{$i18n.t('About')}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[380px]">
|
<div class="flex-1 md:min-h-[25rem]">
|
||||||
{#if selectedTab === 'general'}
|
{#if selectedTab === 'general'}
|
||||||
<General
|
<General
|
||||||
{getModels}
|
{getModels}
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'models'}
|
{:else if selectedTab === 'models'}
|
||||||
@@ -335,28 +338,28 @@
|
|||||||
<Connections
|
<Connections
|
||||||
{getModels}
|
{getModels}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'interface'}
|
{:else if selectedTab === 'interface'}
|
||||||
<Interface
|
<Interface
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'audio'}
|
{:else if selectedTab === 'audio'}
|
||||||
<Audio
|
<Audio
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'images'}
|
{:else if selectedTab === 'images'}
|
||||||
<Images
|
<Images
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'chats'}
|
{:else if selectedTab === 'chats'}
|
||||||
@@ -364,7 +367,7 @@
|
|||||||
{:else if selectedTab === 'account'}
|
{:else if selectedTab === 'account'}
|
||||||
<Account
|
<Account
|
||||||
saveHandler={() => {
|
saveHandler={() => {
|
||||||
show = false;
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'about'}
|
{:else if selectedTab === 'about'}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let downloadChat: Function;
|
export let downloadChat: Function;
|
||||||
export let shareChat: Function;
|
export let shareChat: Function;
|
||||||
|
|
||||||
@@ -17,11 +20,11 @@
|
|||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Share to OpenWebUI Community
|
{$i18n.t('Share to OpenWebUI Community')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex justify-center space-x-1 mt-1.5">
|
<div class="flex justify-center space-x-1 mt-1.5">
|
||||||
<div class=" self-center text-gray-400 text-xs font-medium">or</div>
|
<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 rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download as a File
|
{$i18n.t('Download as a File')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show>
|
<Modal bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">Keyboard shortcuts</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -32,7 +35,7 @@
|
|||||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
<div class="flex flex-col space-y-3 w-full self-start">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Open new chat</div>
|
<div class=" text-sm">{$i18n.t('Open new chat')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -56,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Focus chat input</div>
|
<div class=" text-sm">{$i18n.t('Focus chat input')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -74,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Copy last code block</div>
|
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -98,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Copy last response</div>
|
<div class=" text-sm">{$i18n.t('Copy last response')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -124,7 +127,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col space-y-3 w-full self-start">
|
<div class="flex flex-col space-y-3 w-full self-start">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Toggle settings</div>
|
<div class=" text-sm">{$i18n.t('Toggle settings')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -141,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Toggle sidebar</div>
|
<div class=" text-sm">{$i18n.t('Toggle sidebar')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -165,7 +168,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Delete chat</div>
|
<div class=" text-sm">{$i18n.t('Delete chat')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
@@ -188,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class=" text-sm">Show shortcuts</div>
|
<div class=" text-sm">{$i18n.t('Show shortcuts')}</div>
|
||||||
|
|
||||||
<div class="flex space-x-1 text-xs">
|
<div class="flex space-x-1 text-xs">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let state = 'unchecked';
|
||||||
|
export let indeterminate = false;
|
||||||
|
|
||||||
|
let _state = 'unchecked';
|
||||||
|
|
||||||
|
$: _state = state;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" outline -outline-offset-1 outline-[1.5px] outline-gray-200 dark:outline-gray-600 {state !==
|
||||||
|
'unchecked'
|
||||||
|
? 'bg-black outline-black '
|
||||||
|
: 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded inline-block w-3.5 h-3.5 relative"
|
||||||
|
on:click={() => {
|
||||||
|
if (_state === 'unchecked') {
|
||||||
|
_state = 'checked';
|
||||||
|
dispatch('change', _state);
|
||||||
|
} else if (_state === 'checked') {
|
||||||
|
_state = 'unchecked';
|
||||||
|
if (!indeterminate) {
|
||||||
|
dispatch('change', _state);
|
||||||
|
}
|
||||||
|
} else if (indeterminate) {
|
||||||
|
_state = 'checked';
|
||||||
|
dispatch('change', _state);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="top-0 left-0 absolute w-full flex justify-center">
|
||||||
|
{#if _state === 'checked'}
|
||||||
|
<svg
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="3"
|
||||||
|
d="m5 12 4.7 4.5 9.3-9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else if indeterminate}
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3.5 text-gray-800 dark:text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="3"
|
||||||
|
d="M5 12h14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- {checked} -->
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenu.Root
|
||||||
|
onOpenChange={(state) => {
|
||||||
|
dispatch('change', state);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<slot name="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
|
||||||
|
sideOffset={8}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
|
<div class="flex items-center">Profile</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
|
<div class="flex items-center">Profile</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
|
<div class="flex items-center">Profile</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</slot>
|
||||||
|
</DropdownMenu.Root>
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import ImagePreview from './ImagePreview.svelte';
|
import ImagePreview from './ImagePreview.svelte';
|
||||||
|
|
||||||
export let src = '';
|
export let src = '';
|
||||||
export let alt = '';
|
export let alt = '';
|
||||||
|
|
||||||
|
let _src = '';
|
||||||
|
|
||||||
|
$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
|
||||||
|
|
||||||
let showImagePreview = false;
|
let showImagePreview = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ImagePreview bind:show={showImagePreview} {src} {alt} />
|
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
console.log('image preview');
|
console.log('image preview');
|
||||||
showImagePreview = true;
|
showImagePreview = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img {src} {alt} class=" max-h-96 rounded-lg" draggable="false" />
|
<img src={_src} {alt} class=" max-h-96 rounded-lg" draggable="false" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
export let show = false;
|
export let show = false;
|
||||||
export let src = '';
|
export let src = '';
|
||||||
export let alt = '';
|
export let alt = '';
|
||||||
|
|
||||||
|
const downloadImage = (url, filename) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error downloading image:', error));
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
@@ -35,10 +51,7 @@
|
|||||||
<button
|
<button
|
||||||
class=" p-5"
|
class=" p-5"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const a = document.createElement('a');
|
downloadImage(src, 'Image.png');
|
||||||
a.href = src;
|
|
||||||
a.download = 'Image.png';
|
|
||||||
a.click();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
export let show = true;
|
export let show = true;
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
|
|
||||||
@@ -41,10 +43,10 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=" modal-content m-auto rounded-xl max-w-full {sizeToWidth(
|
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
|
||||||
size
|
size
|
||||||
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
||||||
in:fade={{ duration: 10 }}
|
in:flyAndScale
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select } from 'bits-ui';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import Check from '../icons/Check.svelte';
|
||||||
|
import Search from '../icons/Search.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let placeholder = 'Select a model';
|
||||||
|
export let items = [
|
||||||
|
{ value: 'mango', label: 'Mango' },
|
||||||
|
{ value: 'watermelon', label: 'Watermelon' },
|
||||||
|
{ value: 'apple', label: 'Apple' },
|
||||||
|
{ value: 'pineapple', label: 'Pineapple' },
|
||||||
|
{ value: 'orange', label: 'Orange' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let searchValue = '';
|
||||||
|
|
||||||
|
$: filteredItems = searchValue
|
||||||
|
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
|
||||||
|
: items;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root
|
||||||
|
{items}
|
||||||
|
onOpenChange={() => {
|
||||||
|
searchValue = '';
|
||||||
|
}}
|
||||||
|
selected={items.find((item) => item.value === value)}
|
||||||
|
onSelectedChange={(selectedItem) => {
|
||||||
|
value = selectedItem.value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="relative w-full" aria-label={placeholder}>
|
||||||
|
<Select.Value
|
||||||
|
class="inline-flex h-input px-0.5 w-full 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="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}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
|
||||||
|
<Search className="size-4" strokeWidth="2.5" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:value={searchValue}
|
||||||
|
class="w-full text-sm bg-transparent outline-none"
|
||||||
|
placeholder="Search a model"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800" />
|
||||||
|
|
||||||
|
<div class="px-3 my-2 max-h-80 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 data-[highlighted]:bg-muted"
|
||||||
|
value={item.value}
|
||||||
|
label={item.label}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
|
||||||
|
{#if value === item.value}
|
||||||
|
<div class="ml-auto">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Select.Item>
|
||||||
|
{:else}
|
||||||
|
<span class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
|
||||||
|
No results found
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Select.Content>
|
||||||
|
<Select.Input name="favoriteFruit" />
|
||||||
|
</Select.Root>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let showTagInput = false;
|
let showTagInput = false;
|
||||||
let tagName = '';
|
let tagName = '';
|
||||||
</script>
|
</script>
|
||||||
@@ -9,12 +11,6 @@
|
|||||||
<div class="flex space-x-1 pl-1.5">
|
<div class="flex space-x-1 pl-1.5">
|
||||||
{#if showTagInput}
|
{#if showTagInput}
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
|
||||||
bind:value={tagName}
|
|
||||||
class=" cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[4rem]"
|
|
||||||
placeholder="Add a tag"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -36,6 +32,11 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
bind:value={tagName}
|
||||||
|
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[8rem]"
|
||||||
|
placeholder={$i18n.t('Add a tag')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Tag Suggestions -->
|
<!-- TODO: Tag Suggestions -->
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
|
export let placement = 'top';
|
||||||
|
export let content = `I'm a tooltip!`;
|
||||||
|
export let touch = true;
|
||||||
|
|
||||||
|
let tooltipElement;
|
||||||
|
let tooltipInstance;
|
||||||
|
|
||||||
|
$: if (tooltipElement && content) {
|
||||||
|
if (tooltipInstance) {
|
||||||
|
tooltipInstance.setContent(content);
|
||||||
|
} else {
|
||||||
|
tooltipInstance = tippy(tooltipElement, {
|
||||||
|
content: content,
|
||||||
|
placement: placement,
|
||||||
|
allowHTML: true,
|
||||||
|
touch: touch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (tooltipInstance) {
|
||||||
|
tooltipInstance.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={tooltipElement} aria-label={content}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import { documents } from '$lib/stores';
|
||||||
|
import TagInput from '../common/Tags/TagInput.svelte';
|
||||||
|
import Tags from '../common/Tags.svelte';
|
||||||
|
import { addTagById } from '$lib/apis/chats';
|
||||||
|
import { uploadDocToVectorDB } from '$lib/apis/rag';
|
||||||
|
import { transformFileName } from '$lib/utils';
|
||||||
|
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let selectedDoc;
|
||||||
|
let uploadDocInputElement: HTMLInputElement;
|
||||||
|
let inputFiles;
|
||||||
|
let tags = [];
|
||||||
|
|
||||||
|
let doc = {
|
||||||
|
name: '',
|
||||||
|
title: '',
|
||||||
|
content: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadDoc = async (file) => {
|
||||||
|
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await createNewDoc(
|
||||||
|
localStorage.token,
|
||||||
|
res.collection_name,
|
||||||
|
res.filename,
|
||||||
|
transformFileName(res.filename),
|
||||||
|
res.filename,
|
||||||
|
tags.length > 0
|
||||||
|
? {
|
||||||
|
tags: tags
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
await documents.set(await getDocs(localStorage.token));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
if (inputFiles && inputFiles.length > 0) {
|
||||||
|
for (const file of inputFiles) {
|
||||||
|
console.log(file, file.name.split('.').at(-1));
|
||||||
|
if (
|
||||||
|
SUPPORTED_FILE_TYPE.includes(file['type']) ||
|
||||||
|
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
|
||||||
|
) {
|
||||||
|
uploadDoc(file);
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||||
|
);
|
||||||
|
uploadDoc(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFiles = null;
|
||||||
|
uploadDocInputElement.value = '';
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t(`File not found.`));
|
||||||
|
}
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
documents.set(await getDocs(localStorage.token));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTagHandler = async (tagName) => {
|
||||||
|
if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
|
||||||
|
tags = [...tags, { name: tagName }];
|
||||||
|
} else {
|
||||||
|
console.log('tag already exists');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTagHandler = async (tagName) => {
|
||||||
|
tags = tags.filter((tag) => tag.name !== tagName);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
|
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="mb-3 w-full">
|
||||||
|
<input
|
||||||
|
id="upload-doc-input"
|
||||||
|
bind:this={uploadDocInputElement}
|
||||||
|
hidden
|
||||||
|
bind:files={inputFiles}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 text-center rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
uploadDocInputElement.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if inputFiles}
|
||||||
|
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Click here to select documents.')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex flex-col space-y-1.5">
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
||||||
|
|
||||||
|
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-5 text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
/* display: none; <- Crashes Chrome on hover */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* for Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield; /* Firefox */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import toast from 'svelte-french-toast';
|
import { toast } from 'svelte-sonner';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
import Tags from '../common/Tags.svelte';
|
import Tags from '../common/Tags.svelte';
|
||||||
import { addTagById } from '$lib/apis/chats';
|
import { addTagById } from '$lib/apis/chats';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedDoc;
|
export let selectedDoc;
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@
|
|||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
<div class=" text-lg font-medium self-center">Edit Doc</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -105,7 +107,7 @@
|
|||||||
>
|
>
|
||||||
<div class=" flex flex-col space-y-1.5">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Name Tag</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div>
|
||||||
|
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div
|
<div
|
||||||
@@ -134,7 +136,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">Title</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1.5 text-xs text-gray-500">Tags</div>
|
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
|
||||||
|
|
||||||
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +161,7 @@
|
|||||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
{$i18n.t('Save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user