Compare commits

...
324 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek ac294a74e7 Merge pull request #1277 from open-webui/dev
0.1.115
2024-03-24 18:20:08 -05:00
Timothy J. Baekandrob 4c9598918a doc: changelog
Co-Author: Fixed the upload experimental feature
Co-Authored-By: rob <64106559+kroonen@users.noreply.github.com>
2024-03-24 16:12:07 -07:00
Timothy Jaeryang Baek ffa168211e Merge pull request #1237 from lainedfles/debug_print
Migrate to python logging module with env var control.
2024-03-24 18:04:27 -05:00
Timothy Jaeryang Baek 371dfc1143 Merge branch 'dev' into debug_print 2024-03-24 18:04:03 -05:00
Timothy Jaeryang Baek a1faa307b1 Merge pull request #1272 from anuraagdjain/feat/cancel-model-download
feat: cancel model download
2024-03-24 18:00:57 -05:00
Timothy J. Baek a4c4a0370e refac: transition 2024-03-24 15:55:03 -07:00
Timothy J. Baek 3e0aa29683 refac: transition 2024-03-24 15:43:03 -07:00
Timothy J. Baek df21a92542 fix: select 2024-03-24 15:31:09 -07:00
Timothy J. Baek b218b02d93 feat: custom model selector 2024-03-24 15:28:36 -07:00
Timothy J. Baek ff8a55a861 refac: rag api 2024-03-24 00:41:41 -07:00
Timothy J. Baek 7e0ea8f77d feat: RAG text ingestion(store) api 2024-03-24 00:40:27 -07:00
Timothy J. Baek c2d6d3230b refac: reset vector db 2024-03-23 20:50:57 -07:00
Timothy J. Baek 8d108060d5 refac: styling 2024-03-23 18:08:33 -07:00
Timothy J. Baek 5cb6e9b0b4 refac: styling 2024-03-23 18:07:42 -07:00
Timothy J. Baek cea74eaf52 feat: light mode theme update 2024-03-23 18:05:36 -07:00
Timothy Jaeryang Baek adf9ccb5eb Merge pull request #1275 from open-webui/comfyui
feat: comfyui support
2024-03-23 19:01:51 -05:00
Timothy J. Baek 98624a406f feat: comfyui integration 2024-03-23 17:01:13 -07:00
Timothy J. Baek 862c96fcef feat: comfyui support 2024-03-23 15:38:59 -07:00
Timothy J. Baek cbaaacfb75 fix: styling 2024-03-23 13:36:11 -07:00
Timothy Jaeryang Baek 3e0d9ad74f Merge branch 'dev' into feat/cancel-model-download 2024-03-23 15:16:06 -05:00
Timothy J. Baek e008738f30 feat: cancel download from backend 2024-03-23 13:12:54 -07:00
Timothy J. Baek 244f34c24e refac: cancel download 2024-03-23 13:12:23 -07:00
Timothy J. Baek 642ca5a94a refac: ui 2024-03-23 12:46:06 -07:00
Timothy J. Baek 85187deb4b fix: languages order 2024-03-23 12:43:31 -07:00
Timothy Jaeryang Baek 3305ac5928 Merge pull request #1269 from pazoff/dev
added Italian, Portuguese and Japanese translations
2024-03-23 14:34:54 -05:00
Timothy Jaeryang Baek 51a4e49ae1 Merge pull request #1247 from changchiyou/enhance/zh_TW_translation
Enhance/zh tw translation
2024-03-23 14:34:28 -05:00
Timothy Jaeryang Baek 6c9f552733 Merge pull request #1271 from Axodouble/main
Added nl-NL (Dutch) Translations.
2024-03-23 14:33:54 -05:00
Anuraag Jain 7e6d499486 refac: remove conosle.log 2024-03-23 21:00:01 +02:00
Anuraag Jain d291821bf3 refac: delete button UI 2024-03-23 20:56:18 +02:00
Anuraag Jain 45311bfa15 Merge branch 'main' into feat/cancel-model-download
# Conflicts:
#	src/lib/components/chat/Settings/Models.svelte
2024-03-23 18:10:35 +02:00
changchiyou b03f4371d6 enhance: setting 2024-03-23 23:55:42 +08:00
Axodouble f662aab9e8 Finished Dutch translation. 2024-03-23 13:05:13 +01:00
Axodouble 129253d90d Added partial Dutch (Netherlands, NL) translations. 2024-03-23 12:23:05 +01:00
changchiyou 2d1b67561b enhance: routes/auth 2024-03-23 17:13:43 +08:00
changchiyou dca3dbbce6 enhance: admin 2024-03-23 17:07:35 +08:00
changchiyou e9f1a714e8 enhance: playground 2024-03-23 16:19:14 +08:00
Plamen Vatev d5b5155c20 add ja-JP (Japanese) translation 2024-03-23 07:29:07 +02:00
Plamen Vatev 6e79530e0e add Japanese translation 2024-03-23 07:27:24 +02:00
Plamen Vatev b0a2eb31ee add Portuguese translation 2024-03-23 07:00:40 +02:00
Plamen Vatev c0549498bf add Portuguese translation 2024-03-23 07:00:08 +02:00
Plamen Vatev f8a922e966 Merge branch 'open-webui:dev' into dev 2024-03-23 06:52:51 +02:00
Timothy J. Baek 5c1e3a460e fix: remove aiofiles dependency 2024-03-22 21:47:53 -07:00
Plamen Vatev bc04553a7c add Italian translation 2024-03-23 06:28:26 +02:00
Plamen Vatev c296e9061e add Italian translation 2024-03-23 06:22:53 +02:00
changchiyou e14ad508c9 enhance: e9a198d8ed 2024-03-23 10:14:47 +08:00
changchiyou e9a198d8ed enhance: routes/documents & components/documents/Settings/General.svelte 2024-03-23 10:12:11 +08:00
changchiyou cb0e858c04 enhance: routes/prompts 2024-03-23 09:44:56 +08:00
Timothy Jaeryang Baek 848d2382bc Merge pull request #1267 from Michelklingler/langfuse_dependency
Added langfuse dependency to the requierements.txt file.
2024-03-22 16:36:21 -05:00
Timothy Jaeryang Baek 106727109e Merge pull request #1258 from que-nguyen/main
Fixed Vietnamese translation for improved language quality
2024-03-22 16:20:39 -05:00
Timothy Jaeryang Baek c97051e6eb Merge pull request #1262 from pazoff/dev
Fixed spelling in Bulgarian translation
2024-03-22 16:20:11 -05:00
Michel K 4140e1b84a Added langfuse dependency to the requierements.txt file. 2024-03-22 14:01:39 -04:00
Plamen Vatev fe8a3ac2d7 Fix spelling 2024-03-22 17:49:25 +02:00
changchiyou b5111c4b34 enhance: routes/modelfiles
- `Modelfile` is a proprietary term of Ollama. `模型文件` is the Simplified Chinese translation used in their community but not common used in zh-TW community.
- `Prompt`'s better zh-TW translation is `提示詞`(refert to https://docs.aws.amazon.com/zh_tw/bedrock/latest/userguide/what-is-prompt-engineering.html ) instead of `提示`, which is easily misunderstood as a verb `建議`
2024-03-22 17:17:22 +08:00
Timothy J. Baek 92cc87d908 fix: ollama upload url 2024-03-22 00:58:38 -07:00
Timothy J. Baek 9e37d6edcc refac: gguf upload ui 2024-03-22 00:55:59 -07:00
Timothy J. Baek 77ec296ac3 chore: print removed 2024-03-22 00:11:17 -07:00
Timothy J. Baek bc80f1438c fix: ollama gguf upload 2024-03-22 00:10:55 -07:00
Timothy J. Baek af4caec4f5 refac: ollama gguf upload 2024-03-21 23:45:00 -07:00
Que Nguyen c5e9ceeb06 Merge branch 'open-webui:main' into main 2024-03-22 10:43:03 +07:00
Que Nguyen 47a04a7e3c Fixed Vietnamese translation for improved language quality 2024-03-22 10:42:38 +07:00
Timothy Jaeryang Baek c54fb92263 Merge pull request #1249 from pazoff/dev
added Bulgarian Translation
2024-03-21 16:29:25 -05:00
Timothy J. Baek f3c113cb04 fix: languages order 2024-03-21 14:29:05 -07:00
Plamen Vatev 5f26b2a9e3 added bg-BG (Bulgarian) 2024-03-21 17:43:31 +02:00
Plamen Vatev 8935dca279 added Bulgarian Translation 2024-03-21 17:14:59 +02:00
changchiyou f675a18b05 enhance: i18n's zh-TW translation for About.svelte 2024-03-21 19:38:42 +08:00
Self Denial f74f2ea765 Merge conflicts 2024-03-21 00:14:13 -06:00
Timothy Jaeryang Baek 5a90e90c0b Merge pull request #1241 from 7a6ac0/duplicate_def
fix: delete duplicate function
2024-03-20 22:01:13 -05:00
Timothy Jaeryang Baek 2fa94956f4 Merge pull request #1209 from open-webui/dev
0.1.114
2024-03-20 21:34:22 -05:00
Timothy J. Baek 8e52ba8b91 refac: auth required for litellm in dev 2024-03-20 19:33:27 -07:00
Timothy J. Baek ed0d4b5a2a chore: comment clean up 2024-03-20 19:32:25 -07:00
tabaco.wang a1412d0b55 fix: delete duplicate function 2024-03-21 10:31:25 +08:00
Timothy J. Baek 1a240e103e doc: changelog 2024-03-20 19:31:15 -07:00
Timothy J. Baek 93c90dc186 feat: litellm model filter support 2024-03-20 19:28:33 -07:00
Timothy J. Baek 8cb7127fbc doc: changelog 2024-03-20 19:01:20 -07:00
Timothy J. Baek 4b91ac352a chore: i18n alphabetically ordering 2024-03-20 18:52:24 -07:00
Timothy Jaeryang Baek 09dc3b7b67 Merge pull request #1226 from marionguyen/main
Added Vietnamese translation files
2024-03-20 20:49:27 -05:00
Timothy J. Baek bdc51b45ec Update translation.json 2024-03-20 18:49:08 -07:00
Timothy Jaeryang Baek 6313a98287 Merge branch 'dev' into main 2024-03-20 20:48:26 -05:00
Timothy Jaeryang Baek ae347bb5e7 Merge pull request #1239 from open-webui/webhook
feat: webhook
2024-03-20 20:47:46 -05:00
Timothy J. Baek fa65be2ad2 refac: post webhook 2024-03-20 18:47:13 -07:00
Timothy J. Baek 2481e48a3a feat: webhook settings frontend 2024-03-20 18:35:54 -07:00
Timothy J. Baek 2c6e2d5e8a feat: webhook backend 2024-03-20 18:35:02 -07:00
Self Denial e6dd0bfbe0 Migrate to python logging module with env var control. 2024-03-20 17:11:36 -06:00
Que Nguyen b22ed64770 Added Vietnamese translation files 2024-03-20 11:31:44 +07:00
Timothy Jaeryang Baek 16fe0ee167 Merge pull request #1151 from changchiyou/feat/custom_default_user_permissions_chat_deletion
Feat/custom default user permissions chat deletion
2024-03-19 16:23:23 -05:00
Timothy J. Baek 87344adf29 refac 2024-03-19 14:21:46 -07:00
Timothy J. Baek 349fd0c87d refac: config.json 2024-03-19 14:03:46 -07:00
Timothy Jaeryang Baek d4a11ca33f Merge pull request #1220 from justinh-rahb/fix-messsage
Fix spelling
2024-03-19 15:17:06 -05:00
Justin Hayes bd84ee0c6a Fix spelling 2024-03-19 15:06:45 -04:00
Timothy J. Baek a9e411fdb9 fix: languages 2024-03-19 11:46:48 -07:00
Timothy Jaeryang Baek de8de7e008 Merge pull request #1217 from justinh-rahb/french
French (Canada) translations added, some French (France) strings updated/changed
2024-03-19 13:45:53 -05:00
Timothy Jaeryang Baek 63edaae8d8 Merge pull request #1219 from djismgaming/spanish-translation
Spanish translation
2024-03-19 13:44:47 -05:00
Timothy Jaeryang Baek d3cf9a7535 Merge pull request #1215 from granludo/main
Added Catalan translation
2024-03-19 13:42:53 -05:00
Ismael 7c8727151e spanish translation of the webui 2024-03-19 14:12:04 -04:00
Justin Hayes c3f805b809 Fix capitalization of words 2024-03-19 12:24:12 -04:00
Justin Hayes 8508319c23 Revert "Change some fr-FR strings"
This reverts commit b21091bc01.
2024-03-19 12:07:16 -04:00
Justin Hayes b9382f7051 Update languages.json 2024-03-19 11:51:52 -04:00
Justin Hayes 53fb3d9c3e Add fr-CA translations.json 2024-03-19 11:51:36 -04:00
Justin Hayes b21091bc01 Change some fr-FR strings 2024-03-19 11:51:21 -04:00
Ludo (Marc Alier) 4de591b930 added catalan to "languages.json" added proper code "ca-ES" 2024-03-19 15:26:02 +01:00
Ludo (Marc Alier) 492d4f3403 Added Catalan translation 2024-03-19 11:27:46 +01:00
Timothy J. Baek 0b163c2c2d Update translation.json 2024-03-19 00:30:44 -07:00
Timothy Jaeryang Baek 7bb8e91edb Merge pull request #1207 from officialsahyaboutorabi/patch-4
Update languages.json
2024-03-19 02:28:53 -05:00
Timothy Jaeryang Baek 49faff497e Merge pull request #1206 from officialsahyaboutorabi/patch-3
Create translation.json
2024-03-19 02:28:14 -05:00
Timothy Jaeryang Baek 33888ffaf8 Merge pull request #1205 from adan89lion/patch-1
Enhance Traditional Chinese translation
2024-03-19 02:27:17 -05:00
officialsahyaboutorabi b10eb0dacc Update languages.json 2024-03-19 16:14:34 +11:00
officialsahyaboutorabi 7f197a70f9 Create translation.json
For Russian language.
2024-03-19 16:13:20 +11:00
Ismael 61b4ab1e63 spanish translation of the webui 2024-03-18 22:44:38 -04:00
Ismael 51342179ad feat: Add support for Spanish language option in WebUI Settings 2024-03-18 22:44:05 -04:00
Seohyun Joo f1925841e2 Enhance Traditional Chinese translation
Correct some machine translation mistakes, and convert Mainland China's regional expression to Taiwan.
2024-03-19 10:17:54 +08:00
Timothy J. Baek e86db74333 refac: stream default value 2024-03-18 18:14:05 -07:00
Timothy J. Baek c569280806 fix: styling 2024-03-18 17:46:16 -07:00
Timothy J. Baek bc5d322cbb fix: add doc modal styling 2024-03-18 17:03:08 -07:00
Timothy Jaeryang Baek d865b9fe59 Merge pull request #1199 from open-webui/dev
fix: openai proxy issue
2024-03-18 13:04:53 -05:00
Timothy J. Baek b188143f01 fix: openai proxy issue 2024-03-18 11:04:22 -07:00
Timothy Jaeryang Baek 621719c6ac Merge pull request #1187 from open-webui/dev
0.1.113
2024-03-18 13:02:46 -05:00
Timothy J. Baek ec9895a14a fix: openai proxy issue 2024-03-18 11:02:09 -07:00
Timothy J. Baek 212e051683 doc: changelog 2024-03-18 11:00:38 -07:00
Timothy J. Baek 47081011d6 doc: readme 2024-03-18 10:51:45 -07:00
Timothy J. Baek c94d72bca8 fix: sidebar menu 2024-03-18 10:49:39 -07:00
Timothy Jaeryang Baek c3dd11de16 Merge pull request #1196 from fbirlik/patch-autoscrolldisable
fix: auto scroll fails to stop with small scroll amounts
2024-03-18 11:57:34 -05:00
Timothy Jaeryang Baek 1f0dcb4fce Merge pull request #1115 from dannyl1u/feat/system-wide-theme
feat: system os light/dark mode option 🔦
2024-03-18 11:55:45 -05:00
Timothy J. Baek 1bfcd801b7 fix: multiple openai issue 2024-03-18 01:11:48 -07:00
Timothy J. Baek 19f69c821f refac: code convention 2024-03-18 00:31:43 -07:00
Timothy J. Baek 8f8a68e002 refac: unnecessary code 2024-03-18 00:30:08 -07:00
Timothy J. Baek 94c845d71f refac 2024-03-18 00:27:48 -07:00
Firat Birlik adbcb06892 fix: auto scroll fails to stop with small scroll amounts 2024-03-18 00:08:45 -05:00
Danny Liu 3c01932a78 run prettier 2024-03-17 01:00:38 -07:00
Danny Liu a57586a2b7 use IIFE in app.html to prevent FOUC 2024-03-17 00:25:33 -07:00
Danny Liu 41cb6d7a54 remove duplicate "System" in select menu 2024-03-17 00:06:59 -07:00
Danny Liu 549408d808 Add setContext import to +layout.svelte 2024-03-16 23:59:09 -07:00
Danny Liu f25a967a0b add system theme ⚙️ option to select menu 2024-03-16 23:54:18 -07:00
Danny Liu f55dae3027 fix merge conflict 2024-03-16 23:27:08 -07:00
Danny Liu db0712aefd Merge branch 'dev' into feat/system-wide-theme 2024-03-16 23:23:56 -07:00
Danny Liu f1716f45b9 add event listener in app.html to handle system theme changes 2024-03-16 23:14:38 -07:00
Danny Liu 6f3acb347d update theme handling and persist selection using Svelte store 2024-03-16 23:14:13 -07:00
Timothy J. Baek e414b9ea6d fix: typo 2024-03-16 03:28:02 -07:00
Timothy J. Baek 1e03871748 fix: openai issue 2024-03-16 03:25:20 -07:00
Timothy J. Baek f90236f521 doc: demo.gif 2024-03-16 03:22:37 -07:00
Timothy J. Baek cf82aa9268 Update README.md 2024-03-16 03:01:43 -07:00
Timothy Jaeryang Baek ddb654f293 Merge pull request #1099 from zhecho/feat/helm-labels-and-lb-class
feat(helm): adding lb class and labels for using it with Cilium CNI l…
2024-03-16 04:50:49 -05:00
Timothy J. Baek ed964260ba fix: openai urls issue 2024-03-16 02:45:24 -07:00
Timothy J. Baek a636e784bc fix: styling 2024-03-16 02:43:51 -07:00
Timothy J. Baek ffeae78b88 feat: chat menu tooltip added 2024-03-16 02:35:40 -07:00
Timothy J. Baek 79a02b85a8 refac: chat menu styling 2024-03-16 02:34:27 -07:00
Timothy Jaeryang Baek 36a5588957 Merge pull request #926 from asedmammad/feat/add-i18n
Add i18n
2024-03-16 04:30:22 -05:00
Timothy J. Baek e664c38377 refac: settings styling 2024-03-16 02:24:32 -07:00
Timothy J. Baek 264eb28455 fix: styling 2024-03-16 02:16:24 -07:00
Timothy J. Baek 06f7cba401 Update package-lock.json 2024-03-16 02:10:19 -07:00
Timothy J. Baek df55ced0c0 Merge branch 'dev' into pr/926 2024-03-16 02:08:02 -07:00
Timothy J. Baek 71def5c4cb feat: sidebar ui update 2024-03-16 01:57:26 -07:00
Timothy J. Baek d6905d6297 feat: sidebar ui update 2024-03-16 00:27:29 -07:00
Timothy J. Baek 91efd6cb63 fix: file upload encoding issue 2024-03-15 23:52:37 -07:00
Timothy Jaeryang Baek 5ce421e7fa Merge pull request #1144 from open-webui/dev
0.1.112
2024-03-15 15:49:52 -05:00
Timothy J. Baek 51ef5cfa45 doc: changelog 2024-03-15 13:46:43 -07:00
Timothy J. Baek 072b499a50 fix: backslash rag content issue 2024-03-15 13:34:52 -07:00
Ased Mammad d6e89d074d Update Ukranian translation 2024-03-15 15:14:07 +03:30
Timothy J. Baek cd92cf0da5 fix: image settings save issue 2024-03-15 02:40:44 -07:00
Ased Mammad f9bc35a11c fix: Ternary expression workaround for i18n:parse 2024-03-15 12:30:49 +03:30
Timothy Jaeryang Baek 7cef008b44 Merge pull request #1163 from joequant/main
fix typo in uploading blob to ollama
2024-03-14 15:48:49 -05:00
Ased Mammad b85385385a Update uk translation 2024-03-14 23:42:30 +03:30
Ased Mammad aeb265c789 Update persian translation + run i18n:parse 2024-03-14 23:35:09 +03:30
Jannik Streidl 1e590b79cf added uk keys 2024-03-14 20:52:52 +01:00
Jannik Streidl c0de658dac uk key dix 2024-03-14 20:32:59 +01:00
Jannik Streidl a65d042c73 title fix + new translation keys 2024-03-14 20:30:30 +01:00
Jannik Streidl e99ab95987 maybe this matches better.. im done 2024-03-14 17:29:37 +01:00
Jannik Streidl dd22f1aebc ..and the icon 2024-03-14 17:28:00 +01:00
Jannik Streidl 81fba4774b typo 2024-03-14 17:20:56 +01:00
Jannik Streidl 8a23704138 added changelog entry 2024-03-14 17:19:39 +01:00
Jannik Streidl 5d6182eb7f sorted 2024-03-14 14:43:17 +01:00
Jannik Streidl fafe42e487 more missing translation keys 2024-03-14 14:40:52 +01:00
Jannik Streidl ccc1c81256 added missing translation keys 2024-03-14 14:38:05 +01:00
Jannik Streidl dbdc602791 added simplified + traditional chinese, updates uk keys, changed standart language code format to xx-XX 2024-03-14 12:10:04 +01:00
Joseph C Wang 9ee0feae32 fix typo in uploading blob to ollama 2024-03-14 17:17:52 +08:00
Jannik Streidl aa3985e879 merged from main and added new translation keys 2024-03-14 09:42:37 +01:00
Jannik Streidl 2a5bc2b2ad added chinese 2024-03-14 09:29:02 +01:00
changchiyou c6e14ce327 refactor: restrict the scope of USER_PERMISSIONS_CHAT_DELETION
it can easily confuse people before becuase when people accidentally set `env.USER_PERMISSIONS_CHAT_DELETION` to `true/yes/Yes`, `USER_PERMISSIONS["chat"]["deletion"]` would become `False`, which is unexpectedly and hard to notice.
2024-03-14 00:01:46 +08:00
changchiyou 04e3b168a6 config: allow custom default user permissions - chat deletion
Allow Chat Deletion
2024-03-13 23:17:22 +08:00
Timothy Jaeryang Baek 75607541c6 Merge pull request #1138 from Silentoplayz/patch-1
Update tailwind.css
2024-03-12 15:57:51 -05:00
Timothy J. Baek 3790790a18 fix: possible fix for #1139 2024-03-12 13:47:49 -07:00
Timothy J. Baek 5a567ce4d0 fix: message after image generation 2024-03-12 13:43:49 -07:00
Timothy J. Baek 7f78e58488 refac: image generation error handling 2024-03-12 13:35:30 -07:00
Silentoplayz cc4b82a3f3 Update tailwind.css
Fix spacing
2024-03-12 06:08:03 -04:00
Silentoplayz 97842d037e Update tailwind.css
- Combined font-family declaration: Instead of repeating the font-family declaration for both `html` and `pre`, it's combined into one declaration to save space and avoid repetition.

- Removed unnecessary `pre` styling: The `pre` styling for `font-family` is removed as the font-family is already defined in the `html` rule.

The code is more concise and easier to read.
2024-03-12 05:34:00 -04:00
Timothy J. Baek 26a187f5ac refac: print removed 2024-03-12 00:26:53 -07:00
Timothy J. Baek be3ab88c88 fix: error handling 2024-03-12 00:26:14 -07:00
Ased Mammad 1091ee5a9b fix: Typo 2024-03-11 18:54:31 +03:30
Timothy Jaeryang Baek 7ae4669f35 Merge pull request #1131 from open-webui/dev
fix: env var migration issue
2024-03-10 21:29:20 -05:00
Timothy J. Baek e3fc97241d fix: env var migration issue 2024-03-10 19:26:06 -07:00
Timothy J. Baek b4c770d74b fix: disable openai model by default 2024-03-10 19:00:43 -07:00
Timothy Jaeryang Baek 11ca2703b0 Merge pull request #1130 from open-webui/dev
fix: rag
2024-03-10 20:41:58 -05:00
Timothy J. Baek 8df6b137cb fix: rag 2024-03-10 18:40:50 -07:00
Timothy Jaeryang Baek 89634046e7 Merge pull request #1107 from open-webui/dev
0.1.111
2024-03-10 17:00:56 -05:00
Timothy J. Baek 88d324b52d doc: changelog 2024-03-10 14:55:17 -07:00
Ased Mammad 78f79ef670 Update persian translation with new dev features 2024-03-11 01:04:22 +03:30
Timothy J. Baek 64afde6c0e refac: config versioning 2024-03-10 14:15:32 -07:00
Timothy J. Baek 1e9d40af42 feat: update all models 2024-03-10 14:10:32 -07:00
Timothy J. Baek 98948814fd feat: toggle pdf ocr 2024-03-10 13:32:34 -07:00
Ased Mammad 7545a717ff Add more translation keys 2024-03-10 22:31:10 +03:30
Ased Mammad b8902072fd Merge branch 'dev' into feat/add-i18n 2024-03-10 22:02:16 +03:30
Timothy J. Baek 96ada23272 Update requirements.txt 2024-03-10 00:00:16 -08:00
Timothy J. Baek a02ac67b5c Update requirements.txt 2024-03-09 23:59:17 -08:00
Timothy J. Baek 350489e600 fix: requirements.txt 2024-03-09 23:49:07 -08:00
Timothy J. Baek 8a08ba0791 refac: settings save 2024-03-09 22:02:27 -08:00
Timothy J. Baek c6667510c4 refac: confusing icon 2024-03-09 21:55:13 -08:00
Timothy J. Baek bd84753c6b feat: title auto-generate for openai apis 2024-03-09 21:52:19 -08:00
Timothy J. Baek a4ca1fc5c4 feat: model filter list env var 2024-03-09 21:47:01 -08:00
Timothy Jaeryang Baek bcabd3df84 Merge pull request #1117 from open-webui/model-whitelist
feat: model filter (whitelist)
2024-03-10 00:30:43 -05:00
Timothy J. Baek 81daf4ceb9 feat: model filter frontend integration 2024-03-09 21:29:04 -08:00
Timothy J. Baek b550e23bf6 feat: model filter backend 2024-03-09 21:19:20 -08:00
Timothy J. Baek 83a900bd4a fix: dragged layover 2024-03-09 20:27:54 -08:00
Timothy J. Baek 1a93021cc4 fix: log modified messages 2024-03-09 20:12:32 -08:00
Danny Liu 27153ddb03 remove unused variable 2024-03-09 13:12:15 -08:00
Danny Liu b94dc767ff implement system wide light/dark mode option 🔦 2024-03-09 12:17:47 -08:00
Ased Mammad 6e57fda8f4 fix: Whisper French translation 2024-03-09 15:18:14 +03:30
Timothy J. Baek 6d5ff8d469 feat: frontend whitelist model selector 2024-03-09 01:50:31 -08:00
Timothy J. Baek 219466374d feat: model whitelist 2024-03-09 01:43:02 -08:00
Timothy J. Baek d40edc09e1 feat: hide litellm by default 2024-03-09 00:50:48 -08:00
Timothy Jaeryang Baek d99114518c Merge pull request #1113 from open-webui/rag
feat: rag api
2024-03-09 02:22:40 -05:00
Timothy J. Baek 784ee6f521 fix: error handling 2024-03-08 23:21:00 -08:00
Timothy J. Baek d936353da0 fix: message type edge case 2024-03-08 23:19:20 -08:00
Timothy J. Baek 9f58ed5afa fix 2024-03-08 22:52:42 -08:00
Timothy J. Baek dfcc314283 fix: only edit body with whitelisted paths 2024-03-08 22:51:42 -08:00
Timothy J. Baek 6c58bb59be feat: rag docs as payload field 2024-03-08 22:43:06 -08:00
Timothy J. Baek c49491e516 refac: rag to backend 2024-03-08 22:34:47 -08:00
Timothy J. Baek 6ba62cf25d fix: dalle3 issue 2024-03-08 19:54:47 -08:00
Timothy Jaeryang Baek 19700e9074 Merge pull request #1109 from ther0bster/fix-openai-env-var-parsing
fix: parsing of OPENAI_API_BASE_URLS env var
2024-03-08 22:31:57 -05:00
Timothy J. Baek 7e5e2c42c9 refac: rag routes 2024-03-08 19:26:39 -08:00
Roberto Gudelj 9235864f56 fix: parsing of OPENAI_API_BASE_URLS env var 2024-03-09 04:11:39 +01:00
Ased Mammad f221e39c24 Merge remote-tracking branch 'upstream/dev' into feat/add-i18n 2024-03-09 05:26:39 +03:30
Timothy Jaeryang Baek 30503b5958 Merge pull request #1108 from open-webui/dall-e
feat: dall-e integration
2024-03-08 20:41:03 -05:00
Timothy J. Baek fe7610d380 fix: disable dall-e image generation w/o key 2024-03-08 17:40:30 -08:00
Timothy J. Baek 0221acd163 feat: dall-e integration 2024-03-08 17:38:10 -08:00
Ased Mammad c7ca7d03ee Add more translations 2024-03-09 04:03:53 +03:30
Timothy J. Baek dd3a4b3889 refac: image generation 2024-03-08 16:22:19 -08:00
Ased Mammad 90513941bc feat: add i18n:parser script to package.json 2024-03-09 03:40:32 +03:30
Ased Mammad e8ffb2c023 fix: configure i18next to not return empty strings 2024-03-09 03:33:20 +03:30
Ased Mammad 48e8b45dc9 fix: issue with nbsp in i18next-parser 2024-03-09 03:11:45 +03:30
Timothy J. Baek f1c8e7f5d1 Update README.md 2024-03-08 14:47:34 -08:00
Timothy J. Baek f4bd442495 fix: consistent stroke width for icons 2024-03-08 14:22:23 -08:00
Timothy J. Baek f2c7db7637 fix: empty title issue 2024-03-08 14:04:08 -08:00
Timothy J. Baek 404aa62a8a fix: chat list issues 2024-03-08 13:58:56 -08:00
Timothy J. Baek 9881022b11 fix 2024-03-08 13:45:21 -08:00
Timothy J. Baek 9a83bde7e5 feat: max token option for litellm models 2024-03-08 13:41:38 -08:00
Timothy J. Baek 2b84af878a refac: litellm 2024-03-08 13:33:56 -08:00
Zhecho Zhechev 3c4cc60277 feat(helm): adding lb class and labels for using it with Cilium CNI l2 annoncements mode 2024-03-08 10:09:21 +02:00
Ased Mammad f605bd96be Update translations 2024-03-07 23:24:15 +03:30
Timothy Jaeryang Baek 171084ea0b Merge pull request #1088 from pandego/patch-1
Update Dockerfile
2024-03-07 14:35:59 -05:00
Timothy Jaeryang Baek 712f0e3501 Merge pull request #1084 from jannikstdl/patch-4
fix: show lates changes in releases
2024-03-07 14:35:29 -05:00
Timothy J. Baek 04ddbf4306 fix: button click issue 2024-03-07 11:07:27 -08:00
Ased Mammad f73cae2a73 format some files with prettier 2024-03-07 22:21:00 +03:30
Ased Mammad 3fa5144cf6 fix: Add missing english translations 2024-03-07 20:39:43 +03:30
Jannik Streidl 275a8256bb second merge 2024-03-07 18:04:50 +01:00
Jannik Streidl d4c8a7dab2 merge 2024-03-07 17:58:43 +01:00
Ased Mammad 1f2d704f76 fix: Add missing translations in admin setting 2024-03-07 20:27:52 +03:30
Jannik Streidl 12c74b2cd1 select fix + uk update 2024-03-07 17:56:57 +01:00
Ased Mammad fcc6189091 fix: Update uk translations 2024-03-07 20:24:44 +03:30
Jannik Streidl 93ed5851ca spelling + changed to hardcoded text 2024-03-07 16:37:37 +01:00
Jannik Streidl d40fbcd06b added margin 2024-03-07 16:21:56 +01:00
Jannik Streidl 8c596ea2fc only show the info when language = en (makes more sense to me) 2024-03-07 16:02:14 +01:00
Jannik Streidl 0d127ffef1 merged conflicts 2024-03-07 15:47:15 +01:00
Jannik Streidl f5afb0ca52 contribution info in the settings 2024-03-07 15:41:18 +01:00
pandego 203cd84922 Update Dockerfile
Tiny typo.
2024-03-07 15:13:51 +01:00
Ased Mammad f7e10e1fb6 Update translations 2024-03-07 16:35:56 +03:30
Ased Mammad 03690de773 fix: disable i18next debug 2024-03-07 16:23:17 +03:30
Ased Mammad 80e2125266 add missing types 2024-03-07 16:23:17 +03:30
Ased Mammad 7031aa14e8 Merge branch 'dev' into feat/add-i18n 2024-03-07 15:41:42 +03:30
Ased Mammad 08aa60b33e feat: auto add translation keys to json
Uses i18next-parser to automatically extract translation keys
from js and svelte files.
2024-03-07 15:21:37 +03:30
Jannik S bbf9deabf0 fix: show lates changes in releases
fix for ##1048
2024-03-07 11:41:23 +01:00
Ased Mammad f55525c680 fix: load languages in an exported function 2024-03-07 14:10:30 +03:30
Timothy J. Baek a901031896 fix: accept attribute 2024-03-07 02:17:57 -08:00
Ased Mammad c724e19e62 add new persian translations 2024-03-07 13:27:09 +03:30
Ased Mammad b76eb46d86 docs: Update contributing.md 2024-03-07 13:20:00 +03:30
Ased Mammad f4a1885298 feat: Get available languages from json file 2024-03-07 13:08:37 +03:30
Ased Mammad 41378748b8 refac: Move store to i18n index 2024-03-07 13:08:37 +03:30
Timothy Jaeryang Baek ef9a799e77 Merge pull request #1080 from officialsahyaboutorabi/patch-1
feat: additional file types for uploading models
2024-03-07 00:02:23 -05:00
Timothy Jaeryang Baek 8ed5759d0e Merge pull request #1071 from open-webui/dev
0.1.110
2024-03-06 23:32:24 -05:00
Timothy Jaeryang Baek 6c4e368ac7 Merge pull request #1077 from fbirlik/patch-listblocks
fix: numbered lists with an additional line between items are rendered incorrectly
2024-03-06 23:28:28 -05:00
Timothy J. Baek df49e4e1cc fix: default openai api value 2024-03-06 20:25:24 -08:00
officialsahyaboutorabi 14c38d31a7 Update Models.svelte 2024-03-07 15:06:12 +11:00
Firat Birlik 3823908f37 fix: numbered lists with an additional line between items are rendered incorrectly 2024-03-06 21:15:18 -06:00
Timothy J. Baek 35918cefcc doc: changelog 2024-03-06 18:54:52 -08:00
Timothy J. Baek 34c7b6783c fix: ocr issue 2024-03-06 18:49:35 -08:00
Timothy J. Baek 0476c5f30b fix: add doc modal issue 2024-03-06 18:39:33 -08:00
Timothy J. Baek 53adc6a0ca fix: rag issue 2024-03-06 18:37:40 -08:00
Timothy J. Baek b88c64f80e fix: ocr issue 2024-03-06 17:54:42 -08:00
Timothy Jaeryang Baek dc6e91a23d Merge pull request #1074 from open-webui/main
dev
2024-03-06 20:07:37 -05:00
Timothy J. Baek bb98c10abb revert: ocr feature 2024-03-06 17:04:40 -08:00
Timothy Jaeryang Baek e17e2e7c17 Merge pull request #1070 from open-webui/multi-openai
feat: multiple openai apis
2024-03-06 19:14:03 -05:00
Timothy J. Baek c255cba198 feat: multiple openai apis 2024-03-06 16:13:25 -08:00
Timothy J. Baek 51d509bafb Update config.py 2024-03-06 13:18:17 -08:00
Timothy Jaeryang Baek df298e1001 Merge pull request #1066 from open-webui/dev
refac: semicolon as delimiter
2024-03-06 15:42:38 -05:00
Timothy J. Baek 0142c2de43 refac: semicolon as delimiter 2024-03-06 12:42:14 -08:00
Jannik Streidl 6a271a1691 fix: spelling 2024-03-06 20:55:07 +01:00
Ased Mammad 213e08a8a5 feat: add French translation 2024-03-06 20:03:33 +03:30
Ased Mammad 25e0f0de42 Merge remote-tracking branch 'upstream/dev' into feat/add-i18n 2024-03-06 01:25:29 +03:30
Ased Mammad df8aeb39c6 docs: Add translations section to contributing.md 2024-03-05 18:17:52 +03:30
Jannik Streidl 2b3c8194eb fix: type 2024-03-05 15:39:57 +01:00
Jannik Streidl 71dba592bf changed some german spelling 2024-03-05 14:24:03 +01:00
Jannik Streidl 31fc9a04b6 Merge branch 'feat/add-i18n' of https://github.com/asedmammad/open-webui into feat/add-i18n 2024-03-05 14:06:47 +01:00
Jannik Streidl 53f69ace6c fixed you var + added ua translation for the new playground 2024-03-05 14:06:27 +01:00
Ased Mammad 66342140a3 fix: use translation key as fallback + more translations 2024-03-05 15:28:54 +03:30
Jannik Streidl b5b860f575 add: ukrainian translation 2024-03-05 11:52:07 +01:00
Jannik Streidl 7f9049c3a1 add: added Playground + AddFilesPlaceholder translation 2024-03-05 11:44:37 +01:00
Jannik Streidl b9f53abf37 fix: show translated user role in admin panel 2024-03-05 09:30:00 +01:00
Jannik Streidl fcb991db63 fix: darkmode for selects 2024-03-05 09:15:22 +01:00
Ased Mammad 5f5b6ec141 feat: add persian translation for new keys 2024-03-05 00:47:43 +03:30
Jannik Streidl 4d67dd977d sidebar tooltop translation 2024-03-04 14:53:34 +01:00
Ased Mammad ec8f229f5a fix: make translation key naming consistent 2024-03-04 16:48:08 +03:30
Jannik Streidl d4fd6c5a57 merged new updates 2024-03-04 11:53:09 +01:00
Jannik Streidl 7a79aab5d4 (probably) last transition vars + Theme select element bg and icon fix 2024-03-04 11:15:54 +01:00
Jannik Streidl 3fe5f00114 more translations 2024-03-04 09:53:56 +01:00
Jannik Streidl 304006aa8c typo 2024-03-03 12:22:54 +01:00
Jannik Streidl 893b738cbf german translations + new vars 2024-03-03 12:22:29 +01:00
Ased Mammad 670b65c066 fix: add missed imports 2024-03-03 13:34:26 +03:30
Ased Mammad 6d2f462090 fix: add missed translations 2024-03-03 13:31:34 +03:30
Ased Mammad da0202851f feat: add farsi translation file v1 2024-03-03 01:45:18 +03:30
Ased Mammad 45f6908154 fix: Minor fixes in english translation file 2024-03-03 01:43:49 +03:30
Ased Mammad fbed07f651 fix: Merge similar string literals 2024-03-03 01:23:41 +03:30
Ased Mammad 511b52f642 feat: Add translation file for english 2024-03-03 01:12:32 +03:30
Ased Mammad 3c471ee2ca feat: Migrate hardcoded strings to i18n calls 2024-03-03 00:56:24 +03:30
Ased Mammad 7a77f3c2c0 feat: Change translation keys to fallback strings 2024-03-02 14:47:41 +03:30
Ased Mammad fab89a76b1 feat: WIP: Initial setup for i18next 2024-03-02 14:47:37 +03:30
Anuraag Jain ae97a96379 WIP feat: cancel model download 2024-02-18 21:29:47 +02:00
132 changed files with 14422 additions and 5115 deletions
-1
View File
@@ -7,7 +7,6 @@ node_modules
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
__pycache__
+5 -5
View File
@@ -29,11 +29,11 @@ jobs:
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk '/^## \[/{n++} n==1' CHANGELOG.md)
echo "CHANGELOG_CONTENT<<EOF"
echo "$CHANGELOG_CONTENT"
echo "EOF"
echo "::set-output name=content::${CHANGELOG_CONTENT}"
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
uses: actions/github-script@v5
+1 -1
View File
@@ -166,7 +166,7 @@ cython_debug/
# 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
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# Logs
logs
+92
View File
@@ -5,6 +5,98 @@ 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/),
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
+4 -2
View File
@@ -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)
# IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
# device type for whisper tts and 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_DIR="/app/backend/data/cache/embedding/models"
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
@@ -53,6 +53,8 @@ WORKDIR /app/backend
# install python dependencies
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 -r requirements.txt --no-cache-dir
@@ -79,4 +81,4 @@ COPY --from=build /app/package.json /app/package.json
# copy backend files
COPY ./backend .
CMD [ "bash", "start.sh"]
CMD [ "bash", "start.sh"]
+13 -3
View File
@@ -11,7 +11,7 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](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/).
![Open WebUI Demo](./demo.gif)
@@ -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.
- 🤝 **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.
- 📜 **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.
- 🎨🤖 **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.
- 🔀 **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.
- 🔒 **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.
## 🔗 Also Check Out Open WebUI Community!
+8 -4
View File
@@ -1,4 +1,5 @@
import os
import logging
from fastapi import (
FastAPI,
Request,
@@ -21,7 +22,10 @@ from utils.utils import (
)
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.add_middleware(
@@ -38,7 +42,7 @@ def transcribe(
file: UploadFile = File(...),
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"]:
raise HTTPException(
@@ -62,7 +66,7 @@ def transcribe(
)
segments, info = model.transcribe(file_path, beam_size=5)
print(
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
@@ -72,7 +76,7 @@ def transcribe(
return {"text": transcript.strip()}
except Exception as e:
print(e)
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
+292 -58
View File
@@ -18,10 +18,25 @@ from utils.utils import (
get_current_user,
get_admin_user,
)
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
from utils.misc import calculate_sha256
from typing import Optional
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.add_middleware(
@@ -32,46 +47,104 @@ app.add_middleware(
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.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.IMAGE_SIZE = "512x512"
app.state.IMAGE_STEPS = 50
@app.get("/enabled", response_model=bool)
async def get_enable_status(request: Request, user=Depends(get_admin_user)):
return app.state.ENABLED
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
@app.get("/enabled/toggle", response_model=bool)
async def toggle_enabled(request: Request, user=Depends(get_admin_user)):
try:
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 ConfigUpdateForm(BaseModel):
engine: str
enabled: bool
class UrlUpdateForm(BaseModel):
url: str
@app.post("/config/update")
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")
async def get_openai_url(user=Depends(get_admin_user)):
return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL}
async def get_engine_url(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
}
@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
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 {
"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,
}
@@ -132,9 +205,34 @@ async def update_image_size(
@app.get("/models")
def get_models(user=Depends(get_current_user)):
try:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models")
models = r.json()
return models
if app.state.ENGINE == "openai":
return [
{"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:
app.state.ENABLED = False
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")
async def get_default_model(user=Depends(get_admin_user)):
try:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
options = r.json()
return {"model": options["sd_model_checkpoint"]}
if app.state.ENGINE == "openai":
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
elif app.state.ENGINE == "comfyui":
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:
app.state.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@@ -157,16 +259,23 @@ class UpdateModelForm(BaseModel):
def set_model_handler(model: str):
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
options = r.json()
if app.state.ENGINE == "openai":
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"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
)
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
)
return options
return options
@app.post("/models/default/update")
@@ -181,45 +290,170 @@ class GenerateImageForm(BaseModel):
model: Optional[str] = None
prompt: str
n: int = 1
size: str = "512x512"
size: 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")
def generate_image(
form_data: GenerateImageForm,
user=Depends(get_current_user),
):
print(form_data)
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
r = None
try:
if form_data.model:
set_model_handler(form_data.model)
if app.state.ENGINE == "openai":
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 = {
"prompt": form_data.prompt,
"batch_size": form_data.n,
"width": width,
"height": height,
}
data = {
"model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
"prompt": form_data.prompt,
"n": form_data.n,
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
"response_format": "b64_json",
}
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
r = requests.post(
url=f"https://api.openai.com/v1/images/generations",
json=data,
headers=headers,
)
if form_data.negative_prompt != None:
data["negative_prompt"] = form_data.negative_prompt
r.raise_for_status()
res = r.json()
print(data)
images = []
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
)
for image in res["data"]:
image_id = save_b64_image(image["b64_json"])
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:
print(e)
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
error = 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))
+228
View File
@@ -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
+100
View File
@@ -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)
+324 -62
View File
@@ -1,24 +1,43 @@
from fastapi import FastAPI, Request, Response, HTTPException, Depends, status
from fastapi import (
FastAPI,
Request,
Response,
HTTPException,
Depends,
status,
UploadFile,
File,
BackgroundTasks,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, ConfigDict
import os
import copy
import random
import requests
import json
import uuid
import aiohttp
import asyncio
import logging
from urllib.parse import urlparse
from typing import Optional, List, Union
from apps.web.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import decode_token, get_current_user, get_admin_user
from config import OLLAMA_BASE_URLS
from typing import Optional, List, Union
from config import SRC_LOG_LEVELS, OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR
from utils.misc import calculate_sha256
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
app = FastAPI()
app.add_middleware(
@@ -29,6 +48,10 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.MODELS = {}
@@ -65,7 +88,7 @@ class UrlUpdateForm(BaseModel):
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
app.state.OLLAMA_BASE_URLS = form_data.urls
print(app.state.OLLAMA_BASE_URLS)
log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
@@ -86,7 +109,7 @@ async def fetch_url(url):
return await response.json()
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
log.error(f"Connection error: {e}")
return None
@@ -94,13 +117,14 @@ def merge_models_lists(model_lists):
merged_models = {}
for idx, model_list in enumerate(model_lists):
for model in model_list:
digest = model["digest"]
if digest not in merged_models:
model["urls"] = [idx]
merged_models[digest] = model
else:
merged_models[digest]["urls"].append(idx)
if model_list is not None:
for model in model_list:
digest = model["digest"]
if digest not in merged_models:
model["urls"] = [idx]
merged_models[digest] = model
else:
merged_models[digest]["urls"].append(idx)
return list(merged_models.values())
@@ -109,16 +133,16 @@ def merge_models_lists(model_lists):
async def get_all_models():
print("get_all_models")
log.info("get_all_models()")
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses))
models = {
"models": merge_models_lists(
map(lambda response: response["models"], responses)
map(lambda response: response["models"] if response else None, responses)
)
}
app.state.MODELS = {model["model"]: model for model in models["models"]}
return models
@@ -129,9 +153,19 @@ async def get_all_models():
async def get_ollama_tags(
url_idx: Optional[int] = None, user=Depends(get_current_user)
):
if url_idx == None:
return await get_all_models()
models = await get_all_models()
if app.state.MODEL_FILTER_ENABLED:
if user.role == "user":
models["models"] = list(
filter(
lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
models["models"],
)
)
return models
return models
else:
url = app.state.OLLAMA_BASE_URLS[url_idx]
try:
@@ -140,7 +174,7 @@ async def get_ollama_tags(
return r.json()
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -167,11 +201,17 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses))
lowest_version = min(
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
)
if len(responses) > 0:
lowest_version = min(
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
)
return {"version": lowest_version["version"]}
return {"version": lowest_version["version"]}
else:
raise HTTPException(
status_code=500,
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
)
else:
url = app.state.OLLAMA_BASE_URLS[url_idx]
try:
@@ -180,7 +220,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
return r.json()
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -206,23 +246,38 @@ async def pull_model(
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
):
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
r = None
def get_request():
nonlocal url
nonlocal r
request_id = str(uuid.uuid4())
try:
REQUEST_POOL.append(request_id)
def stream_content():
for chunk in r.iter_content(chunk_size=8192):
yield chunk
try:
yield json.dumps({"id": request_id, "done": False}) + "\n"
for chunk in r.iter_content(chunk_size=8192):
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
break
finally:
if hasattr(r, "close"):
r.close()
if request_id in REQUEST_POOL:
REQUEST_POOL.remove(request_id)
r = requests.request(
method="POST",
url=f"{url}/api/pull",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
stream=True,
)
@@ -238,8 +293,9 @@ async def pull_model(
try:
return await run_in_threadpool(get_request)
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -278,7 +334,7 @@ async def push_model(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.debug(f"url: {url}")
r = None
@@ -294,7 +350,7 @@ async def push_model(
r = requests.request(
method="POST",
url=f"{url}/api/push",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
@@ -310,7 +366,7 @@ async def push_model(
try:
return await run_in_threadpool(get_request)
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -338,9 +394,9 @@ class CreateModelForm(BaseModel):
async def create_model(
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
):
print(form_data)
log.debug(f"form_data: {form_data}")
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
r = None
@@ -356,13 +412,13 @@ async def create_model(
r = requests.request(
method="POST",
url=f"{url}/api/create",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
stream=True,
)
r.raise_for_status()
print(r)
log.debug(f"r: {r}")
return StreamingResponse(
stream_content(),
@@ -375,7 +431,7 @@ async def create_model(
try:
return await run_in_threadpool(get_request)
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -413,21 +469,21 @@ async def copy_model(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
try:
r = requests.request(
method="POST",
url=f"{url}/api/copy",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
print(r.text)
log.debug(f"r.text: {r.text}")
return True
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -460,21 +516,21 @@ async def delete_model(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
try:
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
print(r.text)
log.debug(f"r.text: {r.text}")
return True
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -500,19 +556,19 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
try:
r = requests.request(
method="POST",
url=f"{url}/api/show",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -552,19 +608,19 @@ async def generate_embeddings(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -612,7 +668,7 @@ async def generate_completion(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
r = None
@@ -633,7 +689,7 @@ async def generate_completion(
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
@@ -644,7 +700,7 @@ async def generate_completion(
r = requests.request(
method="POST",
url=f"{url}/api/generate",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
stream=True,
)
@@ -688,7 +744,7 @@ class GenerateChatCompletionForm(BaseModel):
format: Optional[str] = None
options: Optional[dict] = None
template: Optional[str] = None
stream: Optional[bool] = True
stream: Optional[bool] = None
keep_alive: Optional[Union[int, str]] = None
@@ -710,11 +766,11 @@ async def generate_chat_completion(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
r = None
print(form_data.model_dump_json(exclude_none=True))
log.debug("form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(form_data.model_dump_json(exclude_none=True).encode()))
def get_request():
nonlocal form_data
@@ -733,7 +789,7 @@ async def generate_chat_completion(
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
@@ -744,7 +800,7 @@ async def generate_chat_completion(
r = requests.request(
method="POST",
url=f"{url}/api/chat",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
stream=True,
)
@@ -756,6 +812,7 @@ async def generate_chat_completion(
headers=dict(r.headers),
)
except Exception as e:
log.exception(e)
raise e
try:
@@ -809,7 +866,7 @@ async def generate_openai_chat_completion(
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
print(url)
log.info(f"url: {url}")
r = None
@@ -832,7 +889,7 @@ async def generate_openai_chat_completion(
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
@@ -843,7 +900,7 @@ async def generate_openai_chat_completion(
r = requests.request(
method="POST",
url=f"{url}/v1/chat/completions",
data=form_data.model_dump_json(exclude_none=True),
data=form_data.model_dump_json(exclude_none=True).encode(),
stream=True,
)
@@ -875,6 +932,211 @@ async def generate_openai_chat_completion(
)
class UrlForm(BaseModel):
url: str
class UploadBlobForm(BaseModel):
filename: str
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(
ollama_url, file_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(file_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_url}/api/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."
# def number_generator():
# for i in range(1, 101):
# yield f"data: {i}\n"
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
@app.post("/models/download")
@app.post("/models/download/{url_idx}")
async def download_model(
form_data: UrlForm,
url_idx: Optional[int] = None,
):
if url_idx == None:
url_idx = 0
url = app.state.OLLAMA_BASE_URLS[url_idx]
file_name = parse_huggingface_url(form_data.url)
if file_name:
file_path = f"{UPLOAD_DIR}/{file_name}"
return StreamingResponse(
download_file_stream(url, form_data.url, file_path, file_name),
)
else:
return None
@app.post("/models/upload")
@app.post("/models/upload/{url_idx}")
def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
if url_idx == None:
url_idx = 0
ollama_url = app.state.OLLAMA_BASE_URLS[url_idx]
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():
nonlocal ollama_url
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_url}/api/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")
# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
# if url_idx == None:
# url_idx = 0
# url = app.state.OLLAMA_BASE_URLS[url_idx]
# file_location = os.path.join(UPLOAD_DIR, file.filename)
# total_size = file.size
# async def file_upload_generator(file):
# print(file)
# try:
# async with aiofiles.open(file_location, "wb") as f:
# completed_size = 0
# while True:
# chunk = await file.read(1024*1024)
# if not chunk:
# break
# await f.write(chunk)
# completed_size += len(chunk)
# progress = (completed_size / total_size) * 100
# print(progress)
# yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
# except Exception as e:
# print(e)
# yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
# finally:
# await file.close()
# print("done")
# yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
# return StreamingResponse(
# file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
# )
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
url = app.state.OLLAMA_BASE_URLS[0]
@@ -925,7 +1187,7 @@ async def deprecated_proxy(path: str, request: Request, user=Depends(get_current
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
+233 -94
View File
@@ -3,7 +3,11 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
import requests
import aiohttp
import asyncio
import json
import logging
from pydantic import BaseModel
@@ -15,11 +19,23 @@ from utils.utils import (
get_verified_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
from pathlib import Path
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
@@ -29,122 +45,247 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = OPENAI_API_KEY
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
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):
url: str
@app.middleware("http")
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):
key: str
class UrlsUpdateForm(BaseModel):
urls: List[str]
@app.get("/url")
async def get_openai_url(user=Depends(get_admin_user)):
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
class KeysUpdateForm(BaseModel):
keys: List[str]
@app.post("/url/update")
async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
app.state.OPENAI_API_BASE_URL = form_data.url
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
@app.get("/urls")
async def get_openai_urls(user=Depends(get_admin_user)):
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
@app.get("/key")
async def get_openai_key(user=Depends(get_admin_user)):
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
@app.post("/urls/update")
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
app.state.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
@app.post("/key/update")
async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)):
app.state.OPENAI_API_KEY = form_data.key
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
@app.get("/keys")
async def get_openai_keys(user=Depends(get_admin_user)):
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
@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")
async def speech(request: Request, user=Depends(get_verified_user)):
target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech"
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"
idx = None
try:
print("openai")
r = requests.post(
url=target_url,
data=body,
headers=headers,
stream=True,
)
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
body = await request.body()
name = hashlib.sha256(body).hexdigest()
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
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
# Check if the file already exists in the cache
if file_path.is_file():
return FileResponse(file_path)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
headers["Content-Type"] = "application/json"
# Return the saved file
return FileResponse(file_path)
r = None
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:
print(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}"
# Handle connection error here
log.error(f"Connection error: {e}")
return None
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"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}"
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)
idx = 0
body = await request.body()
# 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:
body = body.decode("utf-8")
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
# This is a workaround until OpenAI fixes the issue with this model
if body.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in body:
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:
@@ -156,12 +297,22 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
# Convert the modified body back to JSON
body = json.dumps(body)
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["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.request(
method=request.method,
@@ -181,24 +332,10 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
headers=dict(r.headers),
)
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()
if "api.openai.com" 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
except Exception as e:
print(e)
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
@@ -208,4 +345,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
except:
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
)
+133 -132
View File
@@ -8,7 +8,7 @@ from fastapi import (
Form,
)
from fastapi.middleware.cors import CORSMiddleware
import os, shutil
import os, shutil, logging
from pathlib import Path
from typing import List
@@ -44,6 +44,8 @@ from apps.web.models.documents import (
DocumentResponse,
)
from apps.rag.utils import query_doc, query_collection
from utils.misc import (
calculate_sha256,
calculate_sha256_string,
@@ -52,6 +54,7 @@ from utils.misc import (
)
from utils.utils import get_current_user, get_admin_user
from config import (
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
RAG_EMBEDDING_MODEL,
@@ -64,6 +67,9 @@ from config import (
from constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
#
# if RAG_EMBEDDING_MODEL:
# sentence_transformer_ef = SentenceTransformer(
@@ -75,6 +81,7 @@ from constants import ERROR_MESSAGES
app = FastAPI()
app.state.PDF_EXTRACT_IMAGES = False
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_TEMPLATE = RAG_TEMPLATE
@@ -107,40 +114,6 @@ class CollectionNameForm(BaseModel):
class StoreWebForm(CollectionNameForm):
url: str
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
)
docs = text_splitter.split_documents(data)
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
@app.get("/")
async def get_status():
return {
@@ -182,12 +155,15 @@ async def update_embedding_model(
}
@app.get("/chunk")
async def get_chunk_params(user=Depends(get_admin_user)):
@app.get("/config")
async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
},
}
@@ -196,17 +172,24 @@ class ChunkParamUpdateForm(BaseModel):
chunk_overlap: int
@app.post("/chunk/update")
async def update_chunk_params(
form_data: ChunkParamUpdateForm, user=Depends(get_admin_user)
):
app.state.CHUNK_SIZE = form_data.chunk_size
app.state.CHUNK_OVERLAP = form_data.chunk_overlap
class ConfigUpdateForm(BaseModel):
pdf_extract_images: bool
chunk: ChunkParamUpdateForm
@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 {
"status": True,
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
},
}
@@ -248,23 +231,20 @@ class QueryDocForm(BaseModel):
@app.post("/query/doc")
def query_doc(
def query_doc_handler(
form_data: QueryDocForm,
user=Depends(get_current_user),
):
try:
# if you use docker use the model from the environment variable
collection = CHROMA_CLIENT.get_collection(
name=form_data.collection_name,
return query_doc(
collection_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,
)
result = collection.query(
query_texts=[form_data.query],
n_results=form_data.k if form_data.k else app.state.TOP_K,
)
return result
except Exception as e:
print(e)
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
@@ -277,76 +257,16 @@ class QueryCollectionsForm(BaseModel):
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")
def query_collection(
def query_collection_handler(
form_data: QueryCollectionsForm,
user=Depends(get_current_user),
):
results = []
for collection_name in form_data.collection_names:
try:
# if you use docker use the model from the environment variable
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 if form_data.k else app.state.TOP_K,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(
results, form_data.k if form_data.k else app.state.TOP_K
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
@@ -368,13 +288,63 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
"filename": form_data.url,
}
except Exception as e:
print(e)
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
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):
file_ext = filename.split(".")[-1].lower()
known_type = True
@@ -425,7 +395,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
]
if file_ext == "pdf":
loader = PyPDFLoader(file_path, extract_images=True)
loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
@@ -450,9 +420,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path)
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path)
loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False
return loader, known_type
@@ -466,7 +436,7 @@ def store_doc(
):
# "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:
filename = file.filename
file_path = f"{UPLOAD_DIR}/{filename}"
@@ -497,7 +467,7 @@ def store_doc(
detail=ERROR_MESSAGES.DEFAULT(),
)
except Exception as e:
print(e)
log.exception(e)
if "No pandoc was found" in str(e):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -510,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")
def scan_docs_dir(user=Depends(get_admin_user)):
for path in Path(DOCS_DIR).rglob("./**/*"):
@@ -562,7 +563,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
)
except Exception as e:
print(e)
log.exception(e)
return True
@@ -583,11 +584,11 @@ def reset(user=Depends(get_admin_user)) -> bool:
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
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:
CHROMA_CLIENT.reset()
except Exception as e:
print(e)
log.exception(e)
return True
+188
View File
@@ -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
+5 -2
View File
@@ -1,13 +1,16 @@
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")
print("File renamed successfully.")
log.info("File renamed successfully.")
else:
pass
+2
View File
@@ -19,6 +19,7 @@ from config import (
DEFAULT_USER_ROLE,
ENABLE_SIGNUP,
USER_PERMISSIONS,
WEBHOOK_URL,
)
app = FastAPI()
@@ -32,6 +33,7 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.USER_PERMISSIONS = USER_PERMISSIONS
app.state.WEBHOOK_URL = WEBHOOK_URL
app.add_middleware(
+7 -2
View File
@@ -2,6 +2,7 @@ from pydantic import BaseModel
from typing import List, Union, Optional
import time
import uuid
import logging
from peewee import *
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 config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# DB MODEL
####################
@@ -86,7 +91,7 @@ class AuthsTable:
def insert_new_auth(
self, email: str, password: str, name: str, role: str = "pending"
) -> Optional[UserModel]:
print("insert_new_auth")
log.info("insert_new_auth")
id = str(uuid.uuid4())
@@ -103,7 +108,7 @@ class AuthsTable:
return None
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
print("authenticate_user", email)
log.info(f"authenticate_user: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
-14
View File
@@ -95,20 +95,6 @@ class ChatTable:
except:
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(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
+7 -2
View File
@@ -3,6 +3,7 @@ from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
import logging
from utils.utils import decode_token
from utils.misc import get_gravatar_url
@@ -11,6 +12,10 @@ from apps.web.internal.db import DB
import json
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Documents DB Schema
####################
@@ -118,7 +123,7 @@ class DocumentsTable:
doc = Document.get(Document.name == form_data.name)
return DocumentModel(**model_to_dict(doc))
except Exception as e:
print(e)
log.exception(e)
return None
def update_doc_content_by_name(
@@ -138,7 +143,7 @@ class DocumentsTable:
doc = Document.get(Document.name == name)
return DocumentModel(**model_to_dict(doc))
except Exception as e:
print(e)
log.exception(e)
return None
def delete_doc_by_name(self, name: str) -> bool:
+9 -4
View File
@@ -6,9 +6,14 @@ from playhouse.shortcuts import model_to_dict
import json
import uuid
import time
import logging
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
####################
@@ -173,7 +178,7 @@ class TagTable:
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
)
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)
if tag_count == 0:
@@ -185,7 +190,7 @@ class TagTable:
return True
except Exception as e:
print("delete_tag", e)
log.error(f"delete_tag: {e}")
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id(
@@ -198,7 +203,7 @@ class TagTable:
& (ChatIdTag.user_id == user_id)
)
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)
if tag_count == 0:
@@ -210,7 +215,7 @@ class TagTable:
return True
except Exception as e:
print("delete_tag", e)
log.error(f"delete_tag: {e}")
return False
def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool:
+13 -1
View File
@@ -27,7 +27,8 @@ from utils.utils import (
create_token,
)
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()
@@ -155,6 +156,17 @@ async def signup(request: Request, form_data: SignupForm):
)
# 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 {
"token": token,
"token_type": "Bearer",
+7 -2
View File
@@ -5,6 +5,7 @@ from utils.utils import get_current_user, get_admin_user
from fastapi import APIRouter
from pydantic import BaseModel
import json
import logging
from apps.web.models.users import Users
from apps.web.models.chats import (
@@ -27,6 +28,10 @@ from apps.web.models.tags import (
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
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)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
except Exception as e:
print(e)
log.exception(e)
raise HTTPException(
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)
return tags
except Exception as e:
print(e)
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
+6 -1
View File
@@ -7,6 +7,7 @@ from fastapi import APIRouter
from pydantic import BaseModel
import time
import uuid
import logging
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
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 constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
@@ -83,7 +88,7 @@ async def update_user_by_id(
if 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_email_by_id(user_id, form_data.email.lower())
-149
View File
@@ -21,155 +21,6 @@ from constants import ERROR_MESSAGES
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_BASE_URLS[0]}/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_BASE_URLS[0]}/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")
async def get_gravatar(
email: str,
+69 -10
View File
@@ -1,4 +1,6 @@
import os
import sys
import logging
import chromadb
from chromadb import Settings
from base64 import b64encode
@@ -21,7 +23,7 @@ try:
load_dotenv(find_dotenv("../.env"))
except ImportError:
print("dotenv not installed, skipping...")
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = "Open WebUI"
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
@@ -100,6 +102,34 @@ for version in soup.find_all("h2"):
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
####################################
@@ -125,7 +155,7 @@ if CUSTOM_NAME:
WEBUI_NAME = data["name"]
except Exception as e:
print(e)
log.exception(e)
pass
@@ -194,9 +224,9 @@ def create_config_file(file_path):
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
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)
print("Config file created successfully.")
log.info("Config file created successfully.")
####################################
@@ -209,10 +239,6 @@ OLLAMA_API_BASE_URL = os.environ.get(
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = (
@@ -221,10 +247,15 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
else OLLAMA_API_BASE_URL
)
if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama":
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(",")]
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
####################################
@@ -234,9 +265,25 @@ OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(",")]
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
if OPENAI_API_BASE_URL == "":
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
@@ -273,8 +320,19 @@ DEFAULT_PROMPT_SUGGESTIONS = (
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
@@ -348,3 +406,4 @@ WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models"
####################################
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
+11 -1
View File
@@ -5,6 +5,13 @@ class MESSAGES(str, Enum):
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):
def __str__(self) -> str:
return super().__str__()
@@ -41,12 +48,15 @@ class ERROR_MESSAGES(str, Enum):
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."
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."
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"
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
View File
@@ -1,4 +1,5 @@
{
"version": 0,
"ui": {
"prompt_suggestions": [
{
+145 -40
View File
@@ -4,33 +4,50 @@ import markdown
import time
import os
import sys
import logging
import requests
from fastapi import FastAPI, Request, Depends, status
from fastapi.staticfiles import StaticFiles
from fastapi import HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware
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.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.images.main import app as images_app
from apps.rag.main import app as rag_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 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):
async def get_response(self, path: str, scope):
@@ -43,25 +60,72 @@ class SPAStaticFiles(StaticFiles):
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.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.WEBHOOK_URL = WEBHOOK_URL
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(
CORSMiddleware,
allow_origins=origins,
@@ -71,11 +135,6 @@ app.add_middleware(
)
@app.on_event("startup")
async def on_startup():
await startup()
@app.middleware("http")
async def check_url(request: Request, call_next):
start_time = int(time.time())
@@ -86,19 +145,9 @@ async def check_url(request: Request, call_next):
return response
@litellm_app.middleware("http")
async def auth_middleware(request: Request, call_next):
auth_header = request.headers.get("Authorization", "")
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.on_event("startup")
async def on_startup():
await litellm_app_startup()
app.mount("/api/v1", webui_app)
@@ -125,6 +174,61 @@ 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():
@@ -156,6 +260,7 @@ async def get_app_latest_release_version():
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
app.mount(
+5 -1
View File
@@ -16,7 +16,8 @@ aiohttp
peewee
bcrypt
litellm
litellm==1.30.7
argon2-cffi
apscheduler
google-generativeai
@@ -34,6 +35,8 @@ pandas
openpyxl
pyxlsb
xlrd
opencv-python-headless
rapidocr-onnxruntime
faster-whisper
@@ -42,3 +45,4 @@ PyJWT
pyjwt[crypto]
black
langfuse
+20
View File
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

+12
View File
@@ -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.
### 🌐 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
Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help!
+38
View File
@@ -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
};
+11 -6
View File
@@ -4,6 +4,9 @@ metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.webui.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
@@ -11,14 +14,16 @@ metadata:
spec:
selector:
{{- include "open-webui.selectorLabels" . | nindent 4 }}
{{- with .Values.webui.service }}
type: {{ .type }}
type: {{ .Values.webui.service.type | default "ClusterIP" }}
ports:
- protocol: TCP
name: http
port: {{ .port }}
port: {{ .Values.webui.service.port }}
targetPort: http
{{- if .nodePort }}
nodePort: {{ .nodePort | int }}
{{- if .Values.webui.service.nodePort }}
nodePort: {{ .Values.webui.service.nodePort | int }}
{{- end }}
{{- end }}
{{- if .Values.webui.service.loadBalancerClass }}
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
{{- end }}
+2
View File
@@ -70,3 +70,5 @@ webui:
port: 80
containerPort: 8080
nodePort: ""
labels: {}
loadBalancerClass: ""
+3116 -2945
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.1.109",
"version": "0.1.115",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -13,7 +13,8 @@
"lint:types": "npm run check",
"lint:backend": "pylint backend/",
"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": {
"@sveltejs/adapter-auto": "^2.0.0",
@@ -27,6 +28,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"i18next-parser": "^8.13.0",
"postcss": "^8.4.31",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
@@ -42,9 +44,13 @@
"dependencies": {
"@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"dayjs": "^1.11.10",
"file-saver": "^2.0.5",
"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",
"js-sha256": "^0.10.1",
"katex": "^0.16.9",
+4
View File
@@ -43,6 +43,10 @@ ol > li {
font-weight: 400;
}
li p {
display: inline;
}
::-webkit-scrollbar-thumb {
--tw-border-opacity: 1;
background-color: rgba(217, 217, 227, 0.8);
+28 -11
View File
@@ -8,18 +8,35 @@
<meta name="robots" content="noindex,nofollow" />
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (
localStorage.theme === 'light' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) {
document.documentElement.classList.add('light');
} else if (localStorage.theme) {
localStorage.theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
(() => {
if (
localStorage.theme === 'light' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) {
document.documentElement.classList.add('light');
} else if (localStorage.theme && localStorage.theme !== 'system') {
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>
%sveltekit.head%
+88 -13
View File
@@ -1,9 +1,9 @@
import { IMAGES_API_BASE_URL } from '$lib/constants';
export const getImageGenerationEnabledStatus = async (token: string = '') => {
export const getImageGenerationConfig = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/config`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -32,10 +32,50 @@ export const getImageGenerationEnabledStatus = async (token: string = '') => {
return res;
};
export const toggleImageGenerationEnabledStatus = async (token: string = '') => {
export const updateImageGenerationConfig = async (
token: string = '',
engine: string,
enabled: boolean
) => {
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',
headers: {
Accept: 'application/json',
@@ -61,10 +101,45 @@ export const toggleImageGenerationEnabledStatus = async (token: string = '') =>
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;
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
@@ -93,10 +168,10 @@ export const getAUTOMATIC1111Url = async (token: string = '') => {
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;
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}` })
},
body: JSON.stringify({
url: url
...urls
})
})
.then(async (res) => {
@@ -128,7 +203,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
throw error;
}
return res.AUTOMATIC1111_BASE_URL;
return res;
};
export const getImageSize = async (token: string = '') => {
@@ -263,7 +338,7 @@ export const updateImageSteps = async (token: string = '', steps: number) => {
return res.IMAGE_STEPS;
};
export const getDiffusionModels = async (token: string = '') => {
export const getImageGenerationModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
@@ -295,7 +370,7 @@ export const getDiffusionModels = async (token: string = '') => {
return res;
};
export const getDefaultDiffusionModel = async (token: string = '') => {
export const getDefaultImageGenerationModel = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
@@ -327,7 +402,7 @@ export const getDefaultDiffusionModel = async (token: string = '') => {
return res.model;
};
export const updateDefaultDiffusionModel = async (token: string = '', model: string) => {
export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
+119
View File
@@ -77,3 +77,122 @@ export const getVersionUpdates = async () => {
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;
};
+3 -1
View File
@@ -77,6 +77,7 @@ type AddLiteLLMModelForm = {
api_base: string;
api_key: string;
rpm: string;
max_tokens: string;
};
export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => {
@@ -95,7 +96,8 @@ export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMMod
model: payload.model,
...(payload.api_base === '' ? {} : { api_base: payload.api_base }),
...(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 })
}
})
})
+68 -1
View File
@@ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
return [res, controller];
};
export const cancelChatCompletion = async (token: string = '', requestId: string) => {
export const cancelOllamaRequest = async (token: string = '', requestId: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, {
@@ -390,6 +390,73 @@ export const pullModel = async (token: string, tagName: string, urlIdx: string |
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) => {
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
// method: 'POST',
+14 -14
View File
@@ -1,9 +1,9 @@
import { OPENAI_API_BASE_URL } from '$lib/constants';
export const getOpenAIUrl = async (token: string = '') => {
export const getOpenAIUrls = async (token: string = '') => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/url`, {
const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => {
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;
const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, {
const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
@@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url
urls: urls
})
})
.then(async (res) => {
@@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
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;
const res = await fetch(`${OPENAI_API_BASE_URL}/key`, {
const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => {
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;
const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, {
const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
@@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
key: key
keys: keys
})
})
.then(async (res) => {
@@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
throw error;
}
return res.OPENAI_API_KEY;
return res.OPENAI_API_KEYS;
};
export const getOpenAIModels = async (token: string = '') => {
+16 -7
View File
@@ -1,9 +1,9 @@
import { RAG_API_BASE_URL } from '$lib/constants';
export const getChunkParams = async (token: string) => {
export const getRAGConfig = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/chunk`, {
const res = await fetch(`${RAG_API_BASE_URL}/config`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => {
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;
const res = await fetch(`${RAG_API_BASE_URL}/chunk/update`, {
const res = await fetch(`${RAG_API_BASE_URL}/config/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
chunk_size: size,
chunk_overlap: overlap
...payload
})
})
.then(async (res) => {
@@ -252,7 +261,7 @@ export const queryCollection = async (
token: string,
collection_names: string,
query: string,
k: number
k: number | null = null
) => {
let error = 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 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
><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>
</slot>
+7 -4
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { Confetti } from 'svelte-confetti';
import { WEBUI_NAME, config } from '$lib/stores';
@@ -9,6 +9,8 @@
import Modal from './common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
let changelog = null;
@@ -23,7 +25,8 @@
<div class="px-5 py-4 dark:text-gray-300">
<div class="flex justify-between items-start">
<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]} />
</div>
<button
@@ -45,7 +48,7 @@
</button>
</div>
<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="text-sm dark:text-gray-200">
v{WEBUI_VERSION}
@@ -108,7 +111,7 @@
}}
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>
</div>
</div>
@@ -2,11 +2,12 @@
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { updateUserById } from '$lib/apis/users';
import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
@@ -42,7 +43,7 @@
<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">Edit User</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
<button
class="self-center"
on:click={() => {
@@ -84,7 +85,8 @@
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<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>
@@ -93,7 +95,7 @@
<div class=" flex flex-col space-y-1.5">
<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">
<input
@@ -108,7 +110,7 @@
</div>
<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">
<input
@@ -122,7 +124,7 @@
</div>
<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">
<input
@@ -140,7 +142,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -1,6 +1,8 @@
<script lang="ts">
import { downloadDatabase } from '$lib/apis/utils';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
@@ -17,10 +19,10 @@
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div class=" mb-2 text-sm font-medium">Database</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">Allow Chat Deletion</div> -->
<!-- <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"
@@ -46,7 +48,7 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Download Database</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button>
</div>
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import {
getDefaultUserRole,
getJWTExpiresDuration,
@@ -7,13 +8,17 @@
updateDefaultUserRole,
updateJWTExpiresDuration
} from '$lib/apis/auths';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let signUpEnabled = true;
let defaultUserRole = 'pending';
let JWTExpiresIn = '';
let webhookUrl = '';
const toggleSignUpEnabled = async () => {
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
};
@@ -26,27 +31,32 @@
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
};
const updateWebhookUrlHandler = async () => {
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
};
onMount(async () => {
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token);
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
webhookUrl = await getWebhookUrl(localStorage.token);
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
// console.log('submit');
updateJWTExpiresDurationHandler(JWTExpiresIn);
updateWebhookUrlHandler();
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<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=" 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
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"
/>
</svg>
<span class="ml-2 self-center">Enabled</span>
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -81,25 +91,25 @@
/>
</svg>
<span class="ml-2 self-center">Disabled</span>
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
{/if}
</button>
</div>
<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">
<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}
placeholder="Select a theme"
on:change={(e) => {
updateDefaultUserRoleHandler(e.target.value);
}}
>
<option value="pending">Pending</option>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="pending">{$i18n.t('pending')}</option>
<option value="user">{$i18n.t('user')}</option>
<option value="admin">{$i18n.t('admin')}</option>
</select>
</div>
</div>
@@ -108,7 +118,24 @@
<div class=" 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 class="flex mt-2 space-x-2">
@@ -121,8 +148,9 @@
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Valid time units: <span class=" text-gray-300 font-medium"
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
{$i18n.t('Valid time units:')}
<span class=" text-gray-300 font-medium"
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
>
</div>
</div>
@@ -134,7 +162,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
+123 -6
View File
@@ -1,10 +1,17 @@
<script lang="ts">
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
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;
let whitelistEnabled = false;
let whitelistModels = [''];
let permissions = {
chat: {
deletion: true
@@ -13,6 +20,13 @@
onMount(async () => {
permissions = await getUserPermissions(localStorage.token);
const res = await getModelFilterConfig(localStorage.token);
if (res) {
whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : [''];
}
});
</script>
@@ -21,15 +35,17 @@
on:submit|preventDefault={async () => {
// console.log('submit');
await updateUserPermissions(localStorage.token, permissions);
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<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=" 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
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"
/>
</svg>
<span class="ml-2 self-center">Allow</span>
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -64,11 +80,112 @@
/>
</svg>
<span class="ml-2 self-center">Don't Allow</span>
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
{/if}
</button>
</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 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"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -1,10 +1,13 @@
<script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte';
const i18n = getContext('i18n');
export let show = false;
let selectedTab = 'general';
@@ -13,7 +16,7 @@
<Modal bind:show>
<div>
<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
class="self-center"
on:click={() => {
@@ -61,7 +64,7 @@
/>
</svg>
</div>
<div class=" self-center">General</div>
<div class=" self-center">{$i18n.t('General')}</div>
</button>
<button
@@ -85,7 +88,7 @@
/>
</svg>
</div>
<div class=" self-center">Users</div>
<div class=" self-center">{$i18n.t('Users')}</div>
</button>
<button
@@ -113,7 +116,7 @@
/>
</svg>
</div>
<div class=" self-center">Database</div>
<div class=" self-center">{$i18n.t('Database')}</div>
</button>
</div>
<div class="flex-1 md:min-h-[380px]">
+26 -18
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
@@ -14,12 +14,14 @@
import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let stopResponse: Function;
export let suggestionPrompts = [];
export let autoScroll = true;
let chatTextAreaElement:HTMLTextAreaElement
let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement;
let promptsElement;
@@ -209,11 +211,11 @@
// Event triggered when an error occurs
speechRecognition.onerror = function (event) {
console.log(event);
toast.error(`Speech recognition error: ${event.error}`);
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
isRecording = false;
};
} else {
toast.error('SpeechRecognition API is not supported in this browser.');
toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
}
}
}
@@ -333,12 +335,15 @@
uploadDoc(file);
} else {
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);
}
} else {
toast.error(`File not found.`);
toast.error($i18n.t(`File not found.`));
}
}
@@ -359,12 +364,12 @@
{#if dragged}
<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"
role="region"
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="max-w-md">
<AddFilesPlaceholder />
@@ -477,13 +482,16 @@
filesInputElement.value = '';
} else {
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);
filesInputElement.value = '';
}
} else {
toast.error(`File not found.`);
toast.error($i18n.t(`File not found.`));
}
}}
/>
@@ -570,7 +578,7 @@
{file.name}
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</div>
{:else if file.type === 'collection'}
@@ -598,7 +606,7 @@
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</div>
{/if}
@@ -632,7 +640,7 @@
<div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1">
<Tooltip content="Upload files">
<Tooltip content={$i18n.t('Upload files')}>
<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"
type="button"
@@ -664,8 +672,8 @@
placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder
: isRecording
? 'Listening...'
: 'Send a message'}
? $i18n.t('Listening...')
: $i18n.t('Send a Message')}
bind:value={prompt}
on:keypress={(e) => {
if (e.keyCode == 13 && !e.shiftKey) {
@@ -804,7 +812,7 @@
<div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true}
<Tooltip content="Record voice">
<Tooltip content={$i18n.t('Record voice')}>
{#if speechRecognitionEnabled}
<button
id="voice-input-button"
@@ -873,7 +881,7 @@
{/if}
</Tooltip>
<Tooltip content="Send message">
<Tooltip content={$i18n.t('Send message')}>
<button
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
@@ -919,7 +927,7 @@
</form>
<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>
@@ -3,9 +3,11 @@
import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
const dispatch = createEventDispatcher();
@@ -117,7 +119,7 @@
{doc?.title ?? `#${doc.name}`}
</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}
<div class=" font-medium text-black line-clamp-1">
#{doc.name} ({doc.filename})
@@ -140,7 +142,9 @@
confirmSelectWeb(url);
} else {
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)}
</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>
{/if}
</div>
@@ -2,9 +2,11 @@
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let user = null;
@@ -41,7 +43,7 @@
user = JSON.parse(JSON.stringify(model.name));
await tick();
chatInputPlaceholder = `'${model.name}' is thinking...`;
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
const chatInputElement = document.getElementById('chat-textarea');
@@ -113,7 +115,9 @@
toast.error(error.error);
}
} 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' })
);
}
}
@@ -1,9 +1,11 @@
<script lang="ts">
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
let selectedCommandIdx = 0;
let filteredPromptCommands = [];
@@ -29,7 +31,7 @@
if (command.content.includes('{{CLIPBOARD}}')) {
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}}';
});
@@ -113,8 +115,9 @@
</div>
<div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
input after each replacement.
{$i18n.t(
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div>
</div>
</div>
+4 -2
View File
@@ -2,7 +2,7 @@
import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
@@ -13,6 +13,8 @@
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
const i18n = getContext('i18n');
export let chatId = '';
export let sendPrompt: Function;
export let continueGeneration: Function;
@@ -67,7 +69,7 @@
navigator.clipboard.writeText(text).then(
function () {
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) {
console.error('Async: Could not copy text: ', err);
@@ -1,7 +1,9 @@
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let models = [];
export let modelfiles = [];
@@ -31,7 +33,7 @@
<img
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
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"
/>
{:else}
@@ -39,7 +41,7 @@
src={models.length === 1
? `${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"
draggable="false"
/>
@@ -64,9 +66,9 @@
</div>
{/if}
{: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}
</div>
</div>
@@ -6,8 +6,11 @@
import auto_render from 'katex/dist/contrib/auto-render.mjs';
import 'katex/dist/katex.min.css';
import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { onMount, tick } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
@@ -276,13 +279,15 @@
const generateImage = async (message) => {
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);
if (res) {
message.files = res.images.map((image) => ({
message.files = res.map((image) => ({
type: 'image',
url: `data:image/png;base64,${image}`
url: `${image.url}`
}));
dispatch('save', message);
@@ -313,7 +318,7 @@
{#if message.timestamp}
<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>
{/if}
</Name>
@@ -357,7 +362,7 @@
editMessageConfirmHandler();
}}
>
Save
{$i18n.t('Save')}
</button>
<button
@@ -366,7 +371,7 @@
cancelEditMessage();
}}
>
Cancel
{$i18n.t('Cancel')}
</button>
</div>
</div>
@@ -417,7 +422,7 @@
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
>
{#if siblings.length > 1}
<div class="flex self-center min-w-fit">
<div class="flex self-center min-w-fit -mt-1">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
@@ -477,7 +482,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -503,7 +508,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -622,7 +627,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -637,7 +642,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -703,7 +708,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -733,7 +738,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -762,7 +767,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -792,7 +797,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -1,12 +1,14 @@
<script lang="ts">
import dayjs from 'dayjs';
import { tick, createEventDispatcher } from 'svelte';
import { tick, createEventDispatcher, getContext } from 'svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let user;
@@ -65,17 +67,18 @@
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{: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}
{:else if $settings.showUsername}
{user.name}
{:else}
You
{$i18n.t('You')}
{/if}
{#if message.timestamp}
<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>
{/if}
</Name>
@@ -123,7 +126,7 @@
{file.name}
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</button>
{:else if file.type === 'collection'}
@@ -152,7 +155,7 @@
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</button>
{/if}
@@ -181,7 +184,7 @@
editMessageConfirmHandler();
}}
>
Save & Submit
{$i18n.t('Save & Submit')}
</button>
<button
@@ -190,7 +193,7 @@
cancelEditMessage();
}}
>
Cancel
{$i18n.t('Cancel')}
</button>
</div>
</div>
@@ -200,7 +203,7 @@
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
{#if siblings.length > 1}
<div class="flex self-center">
<div class="flex self-center -mt-1">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
@@ -258,7 +261,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -282,7 +285,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -307,7 +310,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
+25 -26
View File
@@ -1,8 +1,11 @@
<script lang="ts">
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Select from '../common/Select.svelte';
const i18n = getContext('i18n');
export let selectedModels = [''];
export let disabled = false;
@@ -10,7 +13,7 @@
const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) {
toast.error('Choose a model before saving...');
toast.error($i18n.t('Choose a model before saving...'));
return;
}
settings.set({ ...$settings, models: selectedModels });
@@ -20,7 +23,7 @@
console.log('setting default models globally');
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) {
@@ -30,28 +33,24 @@
}
</script>
<div class="flex flex-col my-2">
<div class="flex flex-col my-2 w-full">
{#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex">
<select
id="models"
class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
bind:value={selectedModel}
{disabled}
>
<option class=" text-gray-700" value="" selected disabled>Select a model</option>
{#each $models as model}
{#if model.name === 'hr'}
<hr />
{:else}
<option value={model.id} class="text-gray-700 text-lg"
>{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
>
{/if}
{/each}
</select>
<div class="flex w-full">
<div class="overflow-hidden w-full">
<div class="mr-2 max-w-full">
<Select
placeholder={$i18n.t('Select a model')}
items={$models
.filter((model) => model.name !== 'hr')
.map((model) => ({
value: model.id,
label:
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`
}))}
bind:value={selectedModel}
/>
</div>
</div>
{#if selectedModelIdx === 0}
<button
@@ -132,6 +131,6 @@
{/each}
</div>
<div class="text-left mt-1.5 text-xs text-gray-500">
<button on:click={saveDefaultModel}> Set as default</button>
<div class="text-left mt-1.5 ml-1 text-xs text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
+13 -9
View File
@@ -4,7 +4,9 @@
import { WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let ollamaVersion = '';
@@ -43,7 +45,8 @@
<div>
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
<div>
{$WEBUI_NAME} Version
{$WEBUI_NAME}
{$i18n.t('Version')}
</div>
</div>
<div class="flex w-full justify-between items-center">
@@ -56,10 +59,10 @@
target="_blank"
>
{updateAvailable === null
? 'Checking for updates...'
? $i18n.t('Checking for updates...')
: updateAvailable
? `(v${version.latest} available!)`
: '(latest)'}
? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')}
</a>
</div>
@@ -69,7 +72,7 @@
showChangelog.set(true);
}}
>
<div>See what's new</div>
<div>{$i18n.t("See what's new")}</div>
</button>
</div>
@@ -79,7 +82,7 @@
checkForVersionUpdates();
}}
>
Check for updates
{$i18n.t('Check for updates')}
</button>
</div>
</div>
@@ -88,7 +91,7 @@
<hr class=" dark:border-gray-700" />
<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-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'}
@@ -123,7 +126,8 @@
</div>
<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"
href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a
@@ -1,6 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths';
@@ -9,6 +9,8 @@
import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils';
const i18n = getContext('i18n');
export let saveHandler: Function;
let profileImageUrl = '';
@@ -38,7 +40,7 @@
</script>
<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
id="profile-image-input"
bind:this={profileImageInputElement}
@@ -101,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 flex-col">
@@ -109,7 +111,9 @@
<button
class="relative rounded-full dark:bg-gray-700"
type="button"
on:click={profileImageInputElement.click}
on:click={() => {
profileImageInputElement.click();
}}
>
<img
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
@@ -141,13 +145,13 @@
const url = await getGravatarUrl($user.email);
profileImageUrl = url;
}}>Use Gravatar</button
}}>{$i18n.t('Use Gravatar')}</button
>
</div>
<div class="flex-1">
<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">
<input
@@ -168,7 +172,7 @@
<div class=" 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 class="flex mt-2">
@@ -269,7 +273,7 @@
<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"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={async () => {
const res = await submitHandler();
@@ -278,7 +282,7 @@
}
}}
>
Save
{$i18n.t('Save')}
</button>
</div>
</div>
@@ -1,7 +1,10 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { updateUserPassword } from '$lib/apis/auths';
const i18n = getContext('i18n');
let show = false;
let currentPassword = '';
let newPassword = '';
@@ -17,7 +20,7 @@
);
if (res) {
toast.success('Successfully updated.');
toast.success($i18n.t('Successfully updated.'));
}
currentPassword = '';
@@ -40,20 +43,20 @@
}}
>
<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
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
show = !show;
}}>{show ? 'Hide' : 'Show'}</button
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
{#if show}
<div class=" py-2.5 space-y-1.5">
<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">
<input
@@ -67,7 +70,7 @@
</div>
<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">
<input
@@ -81,7 +84,7 @@
</div>
<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">
<input
@@ -99,7 +102,7 @@
<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"
>
Update password
{$i18n.t('Update password')}
</button>
</div>
{/if}
@@ -1,8 +1,10 @@
<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();
import AdvancedParams from './Advanced/AdvancedParams.svelte';
export let saveSettings: Function;
// Advanced
@@ -55,14 +57,14 @@
<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=" text-sm font-medium">Parameters</div>
<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
<AdvancedParams bind:options />
<hr class=" dark:border-gray-700" />
<div class=" py-1 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -72,9 +74,9 @@
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
@@ -84,7 +86,7 @@
<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={`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}
/>
</div>
@@ -93,7 +95,7 @@
<div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -102,7 +104,7 @@
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else if requestFormat === 'json'}
<!-- <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"
/>
</svg> -->
<span class="ml-2 self-center"> JSON </span>
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
{/if}
</button>
</div>
@@ -147,7 +149,7 @@
dispatch('save');
}}
>
Save
{$i18n.t('Save')}
</button>
</div>
</div>
@@ -1,4 +1,8 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let options = {
// Advanced
seed: 0,
@@ -20,7 +24,7 @@
<div class=" space-y-3 text-xs">
<div>
<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">
<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"
@@ -36,12 +40,12 @@
<div>
<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">
<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="Enter Stop Sequence"
placeholder={$i18n.t('Enter stop sequence')}
bind:value={options.stop}
autocomplete="off"
/>
@@ -51,7 +55,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -61,9 +65,9 @@
}}
>
{#if options.temperature === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
@@ -97,7 +101,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -107,9 +111,9 @@
}}
>
{#if options.mirostat === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -143,7 +147,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -153,9 +157,9 @@
}}
>
{#if options.mirostat_eta === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -189,7 +193,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -199,9 +203,9 @@
}}
>
{#if options.mirostat_tau === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
@@ -235,7 +239,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -245,9 +249,9 @@
}}
>
{#if options.top_k === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -281,7 +285,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -291,9 +295,9 @@
}}
>
{#if options.top_p === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -327,7 +331,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -337,9 +341,9 @@
}}
>
{#if options.repeat_penalty === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -373,7 +377,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -383,9 +387,9 @@
}}
>
{#if options.repeat_last_n === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -419,7 +423,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -429,9 +433,9 @@
}}
>
{#if options.tfs_z === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -465,7 +469,7 @@
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -475,9 +479,9 @@
}}
>
{#if options.num_ctx === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
@@ -510,7 +514,7 @@
</div>
<div class=" py-0.5 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -520,9 +524,9 @@
}}
>
{#if options.num_predict === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
+34 -26
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
// Audio
@@ -101,32 +103,36 @@
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<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=" 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">
<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}
placeholder="Select a mode"
on:change={(e) => {
if (e.target.value !== '') {
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 = '';
});
}
}}
>
<option value="">Default (Web API)</option>
<option value="whisper-local">Whisper (Local)</option>
<option value="">{$i18n.t('Default (Web API)')}</option>
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
</select>
</div>
</div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -136,15 +142,17 @@
type="button"
>
{#if conversationMode === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -154,22 +162,22 @@
type="button"
>
{#if speechAutoSend === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</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=" 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">
<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}
placeholder="Select a mode"
on:change={(e) => {
@@ -182,14 +190,14 @@
}
}}
>
<option value="">Default (Web API)</option>
<option value="openai">Open AI</option>
<option value="">{$i18n.t('Default (Web API)')}</option>
<option value="openai">{$i18n.t('Open AI')}</option>
</select>
</div>
</div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -199,9 +207,9 @@
type="button"
>
{#if responseAutoPlayback === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -211,7 +219,7 @@
{#if TTSEngine === ''}
<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-1">
<select
@@ -219,7 +227,7 @@
bind:value={speaker}
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}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
>
@@ -230,7 +238,7 @@
</div>
{:else if TTSEngine === 'openai'}
<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-1">
<select
@@ -251,10 +259,10 @@
<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"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
+17 -44
View File
@@ -2,7 +2,6 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { resetVectorDB } from '$lib/apis/rag';
import { chats, user } from '$lib/stores';
import {
@@ -13,10 +12,12 @@
getChatList
} from '$lib/apis/chats';
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let saveSettings: Function;
// Chats
let saveChatHistory = true;
@@ -99,13 +100,13 @@
});
</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="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=" self-center text-sm font-medium">Chat History</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
@@ -129,7 +130,7 @@
/>
</svg>
<span class="ml-2 self-center"> On </span>
<span class="ml-2 self-center"> {$i18n.t('On')} </span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -147,13 +148,13 @@
/>
</svg>
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
<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>
@@ -170,7 +171,9 @@
/>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={chatImportInputElement.click}
on:click={() => {
chatImportInputElement.click();
}}
>
<div class=" self-center mr-3">
<svg
@@ -186,7 +189,7 @@
/>
</svg>
</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
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
@@ -208,7 +211,7 @@
/>
</svg>
</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>
</div>
@@ -230,7 +233,7 @@
clip-rule="evenodd"
/>
</svg>
<span>Are you sure?</span>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
@@ -294,7 +297,7 @@
/>
</svg>
</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>
{/if}
@@ -322,39 +325,9 @@
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
</button>
<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 class=" self-center text-sm font-medium">
{$i18n.t('Export All Chats (All Users)')}
</div>
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
</button>
{/if}
</div>
@@ -1,12 +1,19 @@
<script lang="ts">
import { models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher();
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import {
getOpenAIKeys,
getOpenAIUrls,
updateOpenAIKeys,
updateOpenAIUrls
} from '$lib/apis/openai';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let getModels: Function;
// External
@@ -16,12 +23,14 @@
let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = '';
let OPENAI_API_KEYS = [''];
let OPENAI_API_BASE_URLS = [''];
let showOpenAI = false;
let showLiteLLM = false;
const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
await models.set(await getModels());
};
@@ -35,7 +44,7 @@
});
if (ollamaVersion) {
toast.success('Server connection verified');
toast.success($i18n.t('Server connection verified'));
await models.set(await getModels());
}
};
@@ -43,8 +52,8 @@
onMount(async () => {
if ($user.role === 'admin') {
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
}
});
</script>
@@ -56,52 +65,90 @@
dispatch('save');
}}
>
<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="mt-2 space-y-2 pr-1.5">
<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
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showOpenAI = !showOpenAI;
}}>{showOpenAI ? 'Hide' : 'Show'}</button
}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
{#if showOpenAI}
<div>
<div class=" mb-2.5 text-sm font-medium">API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Key"
bind:value={OPENAI_API_KEY}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="flex flex-col gap-1">
{#each OPENAI_API_BASE_URLS as url, idx}
<div class="flex w-full gap-2">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
/>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Base URL"
bind:value={OPENAI_API_BASE_URL}
autocomplete="off"
/>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={OPENAI_API_KEYS[idx]}
autocomplete="off"
/>
</div>
<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 class="mt-2 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200"
>'{OPENAI_API_BASE_URL}/chat'</span
>
</div>
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('WebUI will make requests to')}
<span class=" text-gray-200">'{url}/models'</span>
</div>
{/each}
</div>
{/if}
</div>
@@ -110,7 +157,7 @@
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
<div class="flex w-full gap-1.5">
<div class="flex-1 flex flex-col gap-2">
{#each OLLAMA_BASE_URLS as url, idx}
@@ -189,13 +236,13 @@
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Trouble accessing Ollama?
{$i18n.t('Trouble accessing Ollama?')}
<a
class=" text-gray-300 font-medium"
class=" text-gray-300 font-medium underline"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
Click here for help.
{$i18n.t('Click here for help.')}
</a>
</div>
</div>
@@ -203,10 +250,10 @@
<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"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
+99 -75
View File
@@ -1,9 +1,12 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { getLanguages } from '$lib/i18n';
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';
@@ -12,7 +15,10 @@
// General
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 system = '';
@@ -63,9 +69,11 @@
};
onMount(async () => {
selectedTheme = localStorage.theme ?? 'system';
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
languages = await getLanguages();
theme = localStorage.theme ?? 'dark';
notificationEnabled = settings.notificationEnabled ?? false;
system = settings.system ?? '';
@@ -81,77 +89,93 @@
options = { ...options, ...settings.options };
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>
<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=" 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=" self-center text-xs font-medium">Theme</div>
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
<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
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={theme}
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={selectedTheme}
placeholder="Select a theme"
on:change={(e) => {
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);
}}
on:change={() => themeChangeHandler(selectedTheme)}
>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="rose-pine dark">Rosé Pine</option>
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
<option value="system">⚙️ {$i18n.t('System')}</option>
<option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
</select>
</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 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -161,9 +185,9 @@
type="button"
>
{#if notificationEnabled === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -173,23 +197,23 @@
<hr class=" dark:border-gray-700 my-3" />
<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
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"
/>
</div>
<div class="mt-2 space-y-3 pr-1.5">
<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
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showAdvanced = !showAdvanced;
}}>{showAdvanced ? 'Hide' : 'Show'}</button
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
@@ -199,7 +223,7 @@
<div class=" py-1 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -209,9 +233,9 @@
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<span class="ml-2 self-center"> Custom </span>
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
@@ -221,7 +245,7 @@
<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={`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}
/>
</div>
@@ -230,7 +254,7 @@
<div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -239,7 +263,7 @@
}}
>
{#if requestFormat === ''}
<span class="ml-2 self-center"> Default </span>
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else if requestFormat === 'json'}
<!-- <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"
/>
</svg> -->
<span class="ml-2 self-center"> JSON </span>
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
{/if}
</button>
</div>
@@ -262,7 +286,7 @@
<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"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={() => {
saveSettings({
system: system !== '' ? system : undefined,
@@ -286,7 +310,7 @@
dispatch('save');
}}
>
Save
{$i18n.t('Save')}
</button>
</div>
</div>
+237 -109
View File
@@ -1,30 +1,39 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { config, user } from '$lib/stores';
import {
getAUTOMATIC1111Url,
getDefaultDiffusionModel,
getDiffusionModels,
getImageGenerationEnabledStatus,
getImageGenerationModels,
getDefaultImageGenerationModel,
updateDefaultImageGenerationModel,
getImageSize,
toggleImageGenerationEnabledStatus,
updateAUTOMATIC1111Url,
updateDefaultDiffusionModel,
getImageGenerationConfig,
updateImageGenerationConfig,
getImageGenerationEngineUrls,
updateImageGenerationEngineUrls,
updateImageSize,
getImageSteps,
updateImageSteps
updateImageSteps,
getOpenAIKey,
updateOpenAIKey
} from '$lib/apis/images';
import { getBackendConfig } from '$lib/apis';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let loading = false;
let imageGenerationEngine = '';
let enableImageGeneration = false;
let AUTOMATIC1111_BASE_URL = '';
let COMFYUI_BASE_URL = '';
let OPENAI_API_KEY = '';
let selectedModel = '';
let models = null;
@@ -33,62 +42,101 @@
let steps = 50;
const getModels = async () => {
models = await getDiffusionModels(localStorage.token).catch((error) => {
models = await getImageGenerationModels(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
selectedModel = await getDefaultDiffusionModel(localStorage.token).catch((error) => {
selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
return '';
});
};
const updateAUTOMATIC1111UrlHandler = async () => {
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
(error) => {
const updateUrlHandler = async () => {
if (imageGenerationEngine === 'comfyui') {
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);
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 () => {
if (AUTOMATIC1111_BASE_URL) {
enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
(error) => {
toast.error(error);
return false;
}
);
const updateImageGeneration = async () => {
const res = await updateImageGenerationConfig(
localStorage.token,
imageGenerationEngine,
enableImageGeneration
).catch((error) => {
toast.error(error);
return null;
});
if (enableImageGeneration) {
config.set(await getBackendConfig(localStorage.token));
getModels();
}
} else {
enableImageGeneration = false;
toast.error('AUTOMATIC1111_BASE_URL not provided');
if (res) {
imageGenerationEngine = res.engine;
enableImageGeneration = res.enabled;
}
if (enableImageGeneration) {
config.set(await getBackendConfig(localStorage.token));
getModels();
}
};
onMount(async () => {
if ($user.role === 'admin') {
enableImageGeneration = await getImageGenerationEnabledStatus(localStorage.token);
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
imageSize = await getImageSize(localStorage.token);
steps = await getImageSteps(localStorage.token);
if (res) {
imageGenerationEngine = res.engine;
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();
}
}
@@ -99,7 +147,13 @@
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
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) => {
toast.error(error);
return null;
@@ -113,25 +167,58 @@
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 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 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
class="p-1 px-3 text-xs flex rounded transition"
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"
>
{#if enableImageGeneration === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -139,69 +226,110 @@
</div>
<hr class=" dark:border-gray-700" />
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="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"
{#if imageGenerationEngine === ''}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 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={AUTOMATIC1111_BASE_URL}
/>
</svg>
</button>
</div>
</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>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Include `--api` flag when running stable-diffusion-webui
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank"
>
(e.g. `sh webui.sh --api`)
</a>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank"
>
{$i18n.t('(e.g. `sh webui.sh --api`)')}
</a>
</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}
<hr class=" dark:border-gray-700" />
<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-1 mr-2">
<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}
placeholder="Select a model"
placeholder={$i18n.t('Select a model')}
required
>
{#if !selectedModel}
<option value="" disabled selected>Select a model</option>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each models ?? [] as model}
<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
</div>
@@ -209,12 +337,12 @@
</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-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter Image Size (e.g. 512x512)"
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 Image Size (e.g. 512x512)')}
bind:value={imageSize}
/>
</div>
@@ -222,12 +350,12 @@
</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-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter Number of Steps (e.g. 50)"
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 Number of Steps (e.g. 50)')}
bind:value={steps}
/>
</div>
@@ -238,13 +366,13 @@
<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 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'
: ''}"
type="submit"
disabled={loading}
>
Save
{$i18n.t('Save')}
{#if loading}
<div class="ml-2 self-center">
@@ -2,10 +2,12 @@
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
// Addons
@@ -63,6 +65,7 @@
}
saveSettings({
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
});
};
@@ -81,7 +84,9 @@
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
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>
@@ -92,13 +97,13 @@
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 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 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -108,9 +113,9 @@
type="button"
>
{#if titleAutoGenerate === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -118,7 +123,9 @@
<div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -128,9 +135,9 @@
type="button"
>
{#if responseAutoCopy === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -138,7 +145,7 @@
<div>
<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
class="p-1 px-3 text-xs flex rounded transition"
@@ -148,9 +155,9 @@
type="button"
>
{#if fullScreenMode === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -159,7 +166,7 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<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>
<button
@@ -170,9 +177,9 @@
type="button"
>
{#if showUsername === true}
<span class="ml-2 self-center">On</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">Off</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
@@ -182,15 +189,15 @@
<hr class=" dark:border-gray-700" />
<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-1 mr-2">
<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}
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 as model}
{#if model.size != null}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
@@ -200,35 +207,13 @@
{/each}
</select>
</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 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
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"
/>
</div>
@@ -239,7 +224,9 @@
<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=" 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
class="p-1 px-3 text-xs flex rounded transition"
@@ -312,19 +299,19 @@
{#if promptSuggestions.length > 0}
<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>
{/if}
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end text-sm font-medium">
<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"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
File diff suppressed because it is too large Load Diff
+20 -17
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores';
@@ -17,6 +18,8 @@
import Connections from './Settings/Connections.svelte';
import Images from './Settings/Images.svelte';
const i18n = getContext('i18n');
export let show = false;
const saveSettings = async (updated) => {
@@ -58,7 +61,7 @@
<Modal bind:show>
<div>
<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
class="self-center"
on:click={() => {
@@ -106,7 +109,7 @@
/>
</svg>
</div>
<div class=" self-center">General</div>
<div class=" self-center">{$i18n.t('General')}</div>
</button>
{#if $user?.role === 'admin'}
@@ -131,7 +134,7 @@
/>
</svg>
</div>
<div class=" self-center">Connections</div>
<div class=" self-center">{$i18n.t('Connections')}</div>
</button>
<button
@@ -157,7 +160,7 @@
/>
</svg>
</div>
<div class=" self-center">Models</div>
<div class=" self-center">{$i18n.t('Models')}</div>
</button>
{/if}
@@ -184,7 +187,7 @@
/>
</svg>
</div>
<div class=" self-center">Interface</div>
<div class=" self-center">{$i18n.t('Interface')}</div>
</button>
<button
@@ -211,7 +214,7 @@
/>
</svg>
</div>
<div class=" self-center">Audio</div>
<div class=" self-center">{$i18n.t('Audio')}</div>
</button>
{#if $user.role === 'admin'}
@@ -238,7 +241,7 @@
/>
</svg>
</div>
<div class=" self-center">Images</div>
<div class=" self-center">{$i18n.t('Images')}</div>
</button>
{/if}
@@ -265,7 +268,7 @@
/>
</svg>
</div>
<div class=" self-center">Chats</div>
<div class=" self-center">{$i18n.t('Chats')}</div>
</button>
<button
@@ -291,7 +294,7 @@
/>
</svg>
</div>
<div class=" self-center">Account</div>
<div class=" self-center">{$i18n.t('Account')}</div>
</button>
<button
@@ -317,16 +320,16 @@
/>
</svg>
</div>
<div class=" self-center">About</div>
<div class=" self-center">{$i18n.t('About')}</div>
</button>
</div>
<div class="flex-1 md:min-h-[380px]">
<div class="flex-1 md:min-h-[25rem]">
{#if selectedTab === 'general'}
<General
{getModels}
{saveSettings}
on:save={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'models'}
@@ -335,28 +338,28 @@
<Connections
{getModels}
on:save={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'interface'}
<Interface
{saveSettings}
on:save={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'audio'}
<Audio
{saveSettings}
on:save={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'images'}
<Images
{saveSettings}
on:save={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'chats'}
@@ -364,7 +367,7 @@
{:else if selectedTab === 'account'}
<Account
saveHandler={() => {
show = false;
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'about'}
@@ -1,6 +1,9 @@
<script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
export let downloadChat: Function;
export let shareChat: Function;
@@ -17,11 +20,11 @@
show = false;
}}
>
Share to OpenWebUI Community
{$i18n.t('Share to OpenWebUI Community')}
</button>
<div class="flex justify-center space-x-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">or</div>
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
@@ -31,7 +34,7 @@
show = false;
}}
>
Download as a File
{$i18n.t('Download as a File')}
</button>
</div>
</div>
+12 -9
View File
@@ -1,13 +1,16 @@
<script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
</script>
<Modal bind:show>
<div>
<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
class="self-center"
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 space-y-3 w-full self-start">
<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
@@ -56,7 +59,7 @@
</div>
<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
@@ -74,7 +77,7 @@
</div>
<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
@@ -98,7 +101,7 @@
</div>
<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
@@ -124,7 +127,7 @@
<div class="flex flex-col space-y-3 w-full self-start">
<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
@@ -141,7 +144,7 @@
</div>
<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
@@ -165,7 +168,7 @@
</div>
<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
@@ -188,7 +191,7 @@
</div>
<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
+40
View File
@@ -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>
+7 -2
View File
@@ -1,18 +1,23 @@
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import ImagePreview from './ImagePreview.svelte';
export let src = '';
export let alt = '';
let _src = '';
$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
let showImagePreview = false;
</script>
<ImagePreview bind:show={showImagePreview} {src} {alt} />
<ImagePreview bind:show={showImagePreview} src={_src} {alt} />
<button
on:click={() => {
console.log('image preview');
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>
+17 -4
View File
@@ -2,6 +2,22 @@
export let show = false;
export let src = '';
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>
{#if show}
@@ -35,10 +51,7 @@
<button
class=" p-5"
on:click={() => {
const a = document.createElement('a');
a.href = src;
a.download = 'Image.png';
a.click();
downloadImage(src, 'Image.png');
}}
>
<svg
+4 -2
View File
@@ -2,6 +2,8 @@
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions';
export let show = true;
export let size = 'md';
@@ -41,10 +43,10 @@
}}
>
<div
class=" modal-content m-auto rounded-2xl max-w-full {sizeToWidth(
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
in:fade={{ duration: 10 }}
in:flyAndScale
on:click={(e) => {
e.stopPropagation();
}}
+87
View File
@@ -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">
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, getContext } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
let showTagInput = false;
let tagName = '';
</script>
@@ -9,12 +11,6 @@
<div class="flex space-x-1 pl-1.5">
{#if showTagInput}
<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
type="button"
on:click={() => {
@@ -36,6 +32,11 @@
/>
</svg>
</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>
<!-- TODO: Tag Suggestions -->
@@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte';
@@ -13,6 +13,8 @@
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;
@@ -71,7 +73,7 @@
inputFiles = null;
uploadDocInputElement.value = '';
} else {
toast.error(`File not found.`);
toast.error($i18n.t(`File not found.`));
}
show = false;
@@ -96,7 +98,7 @@
<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">Add Docs</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
<button
class="self-center"
on:click={() => {
@@ -136,21 +138,23 @@
/>
<button
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
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}
on:click={() => {
uploadDocInputElement.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
Click here to select documents.
{$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">Tags</div>
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div>
@@ -161,7 +165,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -1,7 +1,7 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte';
@@ -10,6 +10,8 @@
import Tags from '../common/Tags.svelte';
import { addTagById } from '$lib/apis/chats';
const i18n = getContext('i18n');
export let show = false;
export let selectedDoc;
@@ -74,7 +76,7 @@
<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">Edit Doc</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div>
<button
class="self-center"
on:click={() => {
@@ -105,7 +107,7 @@
>
<div class=" flex flex-col space-y-1.5">
<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
@@ -134,7 +136,7 @@
</div>
<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">
<input
@@ -148,7 +150,7 @@
</div>
<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} />
</div>
@@ -159,7 +161,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -1,22 +1,29 @@
<script lang="ts">
import { getDocs } from '$lib/apis/documents';
import {
getChunkParams,
getRAGConfig,
updateRAGConfig,
getQuerySettings,
scanDocs,
updateChunkParams,
updateQuerySettings
updateQuerySettings,
resetVectorDB
} from '$lib/apis/rag';
import { documents } from '$lib/stores';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let saveHandler: Function;
let loading = false;
let showResetConfirm = false;
let chunkSize = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
let querySettings = {
template: '',
@@ -30,21 +37,29 @@
if (res) {
await documents.set(await getDocs(localStorage.token));
toast.success('Scan complete!');
toast.success($i18n.t('Scan complete!'));
}
};
const submitHandler = async () => {
const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap);
const res = await updateRAGConfig(localStorage.token, {
pdf_extract_images: pdfExtractImages,
chunk: {
chunk_overlap: chunkOverlap,
chunk_size: chunkSize
}
});
querySettings = await updateQuerySettings(localStorage.token, querySettings);
};
onMount(async () => {
const res = await getChunkParams(localStorage.token);
const res = await getRAGConfig(localStorage.token);
if (res) {
chunkSize = res.chunk_size;
chunkOverlap = res.chunk_overlap;
pdfExtractImages = res.pdf_extract_images;
chunkSize = res.chunk.chunk_size;
chunkOverlap = res.chunk.chunk_overlap;
}
querySettings = await getQuerySettings(localStorage.token);
@@ -60,10 +75,12 @@
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<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=" self-center text-xs font-medium">Scan for documents from '/data/docs'</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading
@@ -76,7 +93,7 @@
type="button"
disabled={loading}
>
<div class="self-center font-medium">Scan</div>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
@@ -125,17 +142,17 @@
<hr class=" dark:border-gray-700" />
<div class=" ">
<div class=" text-sm font-medium">Chunk Params</div>
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">Chunk Size</div>
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div>
<div class="self-center p-3">
<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="number"
placeholder="Enter Chunk Size"
placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize}
autocomplete="off"
min="0"
@@ -144,13 +161,13 @@
</div>
<div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
<div class=" self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Overlap')}</div>
<div class="self-center p-3">
<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="number"
placeholder="Enter Chunk Overlap"
placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap}
autocomplete="off"
min="0"
@@ -159,17 +176,33 @@
</div>
</div>
<div class=" text-sm font-medium">Query Params</div>
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
pdfExtractImages = !pdfExtractImages;
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
</div>
<div>
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium flex-1">Top K</div>
<div class="self-center text-xs font-medium flex-1">{$i18n.t('Top K')}</div>
<div class="self-center p-3">
<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="number"
placeholder="Enter Top K"
placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k}
autocomplete="off"
min="0"
@@ -178,23 +211,23 @@
</div>
<!-- <div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
<div class="self-center p-3">
<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="number"
placeholder="Enter Chunk Overlap"
bind:value={chunkOverlap}
autocomplete="off"
min="0"
/>
</div>
</div> -->
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
<div class="self-center p-3">
<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="number"
placeholder="Enter Chunk Overlap"
bind:value={chunkOverlap}
autocomplete="off"
min="0"
/>
</div>
</div> -->
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">RAG Template</div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea
bind:value={querySettings.template}
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
@@ -202,6 +235,100 @@
/>
</div>
</div>
<hr class=" dark:border-gray-700" />
{#if showResetConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-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-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
showResetConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class="hover:text-white transition"
on:click={() => {
showResetConfirm = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
</div>
{:else}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetConfirm = true;
}}
>
<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 class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
@@ -209,7 +336,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
{$i18n.t('Save')}
</button>
</div>
</form>
@@ -1,7 +1,10 @@
<script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import General from './Settings/General.svelte';
const i18n = getContext('i18n');
export let show = false;
let selectedTab = 'general';
@@ -10,7 +13,7 @@
<Modal bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Document Settings</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Document Settings')}</div>
<button
class="self-center"
on:click={() => {
@@ -58,7 +61,7 @@
/>
</svg>
</div>
<div class=" self-center">General</div>
<div class=" self-center">{$i18n.t('General')}</div>
</button>
</div>
<div class="flex-1 md:min-h-[380px]">
+15
View File
@@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
@@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<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>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
/>
</svg>
+6 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
@@ -9,6 +10,8 @@
import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let initNewChat: Function;
export let title: string = $WEBUI_NAME;
export let shareEnabled: boolean = false;
@@ -26,7 +29,7 @@
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat);
toast.success('Redirecting you to OpenWebUI Community');
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
@@ -66,10 +69,7 @@
</script>
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
<nav
id="nav"
class=" sticky py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-900/90 dark:text-gray-200 backdrop-blur-xl z-30"
>
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div
class=" flex {$settings?.fullScreenMode ?? null
? 'max-w-full'
@@ -110,7 +110,7 @@
<Tags {tags} {deleteTag} {addTag} />
<button
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition border dark:border-gray-600"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition"
on:click={async () => {
showShareChatModal = !showShareChatModal;
+213 -203
View File
@@ -7,7 +7,10 @@
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import {
deleteChatById,
getChatList,
@@ -17,9 +20,10 @@
getAllChatTags
} from '$lib/apis/chats';
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
import { fade, slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
let show = false;
let navElement;
@@ -27,6 +31,8 @@
let title: string = 'UI';
let search = '';
let selectedChatId = null;
let chatDeleteId = null;
let chatTitleEditId = null;
let chatTitle = '';
@@ -61,12 +67,16 @@
};
const editChatTitle = async (id, _title) => {
title = _title;
if (_title === '') {
toast.error('Title cannot be an empty string.');
} else {
title = _title;
await updateChatById(localStorage.token, id, {
title: _title
});
await chats.set(await getChatList(localStorage.token));
await updateChatById(localStorage.token, id, {
title: _title
});
await chats.set(await getChatList(localStorage.token));
}
};
const deleteChat = async (id) => {
@@ -78,7 +88,10 @@
});
if (res) {
goto('/');
if ($chatId === id) {
goto('/');
}
await chats.set(await getChatList(localStorage.token));
}
};
@@ -94,7 +107,7 @@
bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show
? 'lg:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-black text-gray-200 shadow-2xl text-sm transition z-40 fixed top-0 left-0
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition z-40 fixed top-0 left-0
"
>
<div
@@ -105,9 +118,11 @@
<div class="px-2 flex justify-center space-x-2">
<a
id="sidebar-new-chat-button"
class="flex-grow flex justify-between rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex justify-between rounded-xl px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
href="/"
on:click={async () => {
selectedChatId = null;
await goto('/');
const newChatButton = document.getElementById('new-chat-button');
setTimeout(() => {
@@ -119,12 +134,12 @@
<div class="self-center mr-1.5">
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" w-7 -translate-x-1.5 rounded-full"
class=" size-6 -translate-x-1.5 rounded-full"
alt="logo"
/>
</div>
<div class=" self-center font-medium text-sm">New Chat</div>
<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
</div>
<div class="self-center">
@@ -148,15 +163,19 @@
{#if $user?.role === 'admin'}
<div class="px-2 flex justify-center mt-0.5">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
href="/modelfiles"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -169,22 +188,26 @@
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">Modelfiles</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
href="/prompts"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -197,22 +220,26 @@
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">Prompts</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center mb-1">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-200 dark:hover:bg-gray-900 transition"
href="/documents"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
@@ -225,7 +252,7 @@
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">Documents</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
</div>
</a>
</div>
@@ -233,13 +260,15 @@
<div class="relative flex flex-col flex-1 overflow-y-auto">
{#if !($settings.saveChatHistory ?? true)}
<div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
<div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center">
<div class=" text-left px-5 py-2">
<div class=" font-medium">Chat History is off for this browser.</div>
<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
<div class="text-xs mt-2">
When history is turned off, new chats on this browser won't appear in your history on
any of your devices. <span class=" font-semibold"
>This setting does not sync across browsers or devices.</span
{$i18n.t(
"When history is turned off, new chats on this browser won't appear in your history on any of your devices."
)}
<span class=" font-semibold"
>{$i18n.t('This setting does not sync across browsers or devices.')}</span
>
</div>
@@ -266,7 +295,7 @@
/>
</svg>
<div>Enable Chat History</div>
<div>{$i18n.t('Enable Chat History')}</div>
</button>
</div>
</div>
@@ -275,7 +304,7 @@
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l-xl bg-gray-950">
<div class="self-center pl-3 py-2 rounded-l-xl bg-white dark:bg-gray-950">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -291,37 +320,20 @@
</div>
<input
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
placeholder="Search"
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300 dark:bg-gray-950 outline-none"
placeholder={$i18n.t('Search')}
bind:value={search}
on:focus={() => {
enrichChatsWithContent($chats);
}}
/>
<!-- <div class="self-center pr-3 py-2 bg-gray-900">
<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="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
</div> -->
</div>
</div>
{#if $tags.length > 0}
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
<button
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
await chats.set(await getChatList(localStorage.token));
}}
@@ -330,7 +342,7 @@
</button>
{#each $tags as tag}
<button
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
@@ -366,180 +378,174 @@
return title.includes(query) || contentMatches;
}
}) as chat, i}
<div class=" w-full pr-2 relative">
<div class=" w-full pr-2 relative group">
{#if chatTitleEditId === chat.id}
<div
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
chat.id === chatTitleEditId ||
chat.id === chatDeleteId
? 'bg-gray-300 dark:bg-gray-900'
: chat.id === selectedChatId
? 'bg-gray-100 dark:bg-gray-950'
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
>
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
</div>
{:else}
<a
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
chat.id === chatTitleEditId ||
chat.id === chatDeleteId
? 'bg-gray-300 dark:bg-gray-900'
: chat.id === selectedChatId
? 'bg-gray-100 dark:bg-gray-950'
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
href="/c/{chat.id}"
on:click={() => {
selectedChatId = chat.id;
if (window.innerWidth < 1024) {
show = false;
}
}}
draggable="false"
>
<div class=" flex self-center flex-1 w-full">
<div
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[160px]'
: 'w-full'} "
>
<div class=" text-left self-center overflow-hidden w-full h-[20px]">
{chat.title}
</div>
</div>
</a>
{/if}
{#if chat.id === $chatId}
<div class=" absolute right-[22px] top-[10px]">
{#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5">
<div
class="
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
? 'from-gray-300 dark:from-gray-900'
: chat.id === selectedChatId
? 'from-gray-100 dark:from-gray-950'
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
to-transparent"
>
{#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5 z-10">
<button
class=" self-center dark:hover:text-white transition"
on:click={() => {
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitle = '';
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center dark:hover:text-white transition"
on:click={() => {
chatTitleEditId = null;
chatTitle = '';
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5 z-10">
<button
class=" self-center dark:hover:text-white transition"
on:click={() => {
deleteChat(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center dark:hover:text-white transition"
on:click={() => {
chatDeleteId = null;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{:else}
<div class="flex self-center space-x-1.5 z-10">
<ChatMenu
renameHandler={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
}}
deleteHandler={() => {
chatDeleteId = chat.id;
}}
onClose={() => {
selectedChatId = null;
}}
>
<button
class=" self-center hover:text-white transition"
aria-label="Chat Menu"
class=" self-center dark:hover:text-white transition"
on:click={() => {
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitle = '';
selectedChatId = chat.id;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitleEditId = null;
chatTitle = '';
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5">
<button
class=" self-center hover:text-white transition"
on:click={() => {
deleteChat(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = null;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<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>
{:else}
<div class="flex self-center space-x-1.5">
<button
id="delete-chat-button"
class=" hidden"
on:click={() => {
deleteChat(chat.id);
}}
/>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitle = chat.title;
chatTitleEditId = chat.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
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=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = chat.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
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
{/if}
</div>
{/if}
</ChatMenu>
</div>
{/if}
</div>
</div>
{/each}
</div>
@@ -551,7 +557,7 @@
<div class="flex flex-col">
{#if $user !== undefined}
<button
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-900 transition"
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-900 transition"
on:click={() => {
showDropdown = !showDropdown;
}}
@@ -569,13 +575,13 @@
{#if showDropdown}
<div
id="dropdownDots"
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-gray-900"
in:slide={{ duration: 150 }}
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-white dark:bg-gray-900"
transition:fade|slide={{ duration: 100 }}
>
<div class="py-2 w-full">
{#if $user.role === 'admin'}
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/admin');
showDropdown = false;
@@ -597,11 +603,11 @@
/>
</svg>
</div>
<div class=" self-center font-medium">Admin Panel</div>
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button>
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/playground');
showDropdown = false;
@@ -623,12 +629,12 @@
/>
</svg>
</div>
<div class=" self-center font-medium">Playground</div>
<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
</button>
{/if}
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
on:click={async () => {
await showSettings.set(true);
showDropdown = false;
@@ -655,15 +661,15 @@
/>
</svg>
</div>
<div class=" self-center font-medium">Settings</div>
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
</button>
</div>
<hr class=" border-gray-700 m-0 p-0" />
<hr class=" dark:border-gray-700 m-0 p-0" />
<div class="py-2 w-full">
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition"
on:click={() => {
localStorage.removeItem('token');
location.href = '/auth';
@@ -689,7 +695,7 @@
/>
</svg>
</div>
<div class=" self-center font-medium">Sign Out</div>
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
</button>
</div>
</div>
@@ -702,7 +708,11 @@
<div
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
<Tooltip placement="right" content={`${show ? 'Close' : 'Open'} sidebar`} touch={false}>
<Tooltip
placement="right"
content={`${show ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
touch={false}
>
<button
id="sidebar-toggle-button"
class=" group"
@@ -711,7 +721,7 @@
}}
><span class="" data-state="closed"
><div
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
>
<div class="flex h-6 w-6 flex-col items-center">
<div
@@ -0,0 +1,56 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let renameHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
</script>
<Dropdown
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<Tooltip content="More">
<slot />
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="start"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
renameHandler();
}}
>
<Pencil strokeWidth="2" />
<div class="flex items-center">Rename</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
deleteHandler();
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">Delete</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>
@@ -1,5 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let messages = [];
let textAreaElement: HTMLTextAreaElement;
@@ -19,16 +21,20 @@
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition"
on:click={() => {
message.role = message.role === 'user' ? 'assistant' : 'user';
}}>{message.role}</button
}}>{$i18n.t(message.role)}</button
>
</div>
<div class="flex-1">
<!-- $i18n.t('a user') -->
<!-- $i18n.t('an assistant') -->
<textarea
id="{message.role}-{idx}-textarea"
bind:this={textAreaElement}
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here"
placeholder={$i18n.t(`Enter {{role}} message here`, {
role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant')
})}
rows="1"
on:input={(e) => {
textAreaElement.style.height = '';
@@ -100,6 +106,6 @@
</svg>
</div>
<div class=" text-sm font-medium">Add message</div>
<div class=" text-sm font-medium">{$i18n.t('Add message')}</div>
</button>
</div>
+73
View File
@@ -0,0 +1,73 @@
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { i18n as i18nType } from 'i18next';
import { writable } from 'svelte/store';
const createI18nStore = (i18n: i18nType) => {
const i18nWritable = writable(i18n);
i18n.on('initialized', () => {
i18nWritable.set(i18n);
});
i18n.on('loaded', () => {
i18nWritable.set(i18n);
});
i18n.on('added', () => i18nWritable.set(i18n));
i18n.on('languageChanged', () => {
i18nWritable.set(i18n);
});
return i18nWritable;
};
const createIsLoadingStore = (i18n: i18nType) => {
const isLoading = writable(false);
// if loaded resources are empty || {}, set loading to true
i18n.on('loaded', (resources) => {
// console.log('loaded:', resources);
Object.keys(resources).length !== 0 && isLoading.set(false);
});
// if resources failed loading, set loading to true
i18n.on('failedLoading', () => {
isLoading.set(true);
});
return isLoading;
};
i18next
.use(
resourcesToBackend(
(language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)
)
)
.use(LanguageDetector)
.init({
debug: false,
detection: {
order: ['querystring', 'localStorage', 'navigator'],
caches: ['localStorage'],
lookupQuerystring: 'lang',
lookupLocalStorage: 'locale'
},
fallbackLng: {
default: ['en-US']
},
ns: 'translation',
returnEmptyString: false,
interpolation: {
escapeValue: false // not needed for svelte as it escapes by default
}
});
const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next);
export const getLanguages = async () => {
const languages = (await import(`./locales/languages.json`)).default;
return languages;
};
export default i18n;
export const isLoading = isLoadingStore;
+363
View File
@@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'с', 'м', 'ч', 'д', 'с' или '-1' за неограничен срок.",
"(Beta)": "(Бета)",
"(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)",
"(latest)": "(последна)",
"{{modelName}} is thinking...": "{{modelName}} мисли ...",
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
"a user": "потребител",
"About": "Относно",
"Account": "Акаунт",
"Action": "Действие",
"Add a model": "Добавяне на модел",
"Add a model tag name": "Добавяне на име на таг за модел",
"Add a short description about what this modelfile does": "Добавяне на кратко описание за това какво прави този модфайл",
"Add a short title for this prompt": "Добавяне на кратко заглавие за този промпт",
"Add a tag": "Добавяне на таг",
"Add Docs": "Добавяне на Документи",
"Add Files": "Добавяне на Файлове",
"Add message": "Добавяне на съобщение",
"add tags": "добавяне на тагове",
"Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.",
"admin": "админ",
"Admin Panel": "Панел на Администратор",
"Admin Settings": "Настройки на Администратор",
"Advanced Parameters": "Разширени Параметри",
"all": "всички",
"All Users": "Всички Потребители",
"Allow": "Позволи",
"Allow Chat Deletion": "Позволи Изтриване на Чат",
"alphanumeric characters and hyphens": "алфанумерични знаци и тире",
"Already have an account?": "Вече имате акаунт? ",
"an assistant": "асистент",
"and": "и",
"API Base URL": "API Базов URL",
"API Key": "API Ключ",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "са разрешени - Активирайте тази команда чрез въвеждане",
"Are you sure?": "Сигурни ли сте?",
"Audio": "Аудио",
"Auto-playback response": "Аувтоматично възпроизвеждане на Отговора",
"Auto-send input after 3 sec.": "Аувтоматично изпращане на входа след 3 сек.",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.",
"available!": "наличен!",
"Back": "Назад",
"Builder Mode": "Режим на Създаване",
"Cancel": "Отказ",
"Categories": "Категории",
"Change Password": "Промяна на Парола",
"Chat": "Чат",
"Chat History": "Чат История",
"Chat History is off for this browser.": "Чат История е изключен за този браузър.",
"Chats": "Чатове",
"Check Again": "Проверете Още Веднъж",
"Check for updates": "Проверка за актуализации",
"Checking for updates...": "Проверка за актуализации...",
"Choose a model before saving...": "Изберете модел преди запазване...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Params",
"Chunk Size": "Chunk Size",
"Click here for help.": "Натиснете тук за помощ.",
"Click here to check other modelfiles.": "Натиснете тук за проверка на други моделфайлове.",
"Click here to select": "Натиснете тук, за да изберете",
"Click here to select documents.": "Натиснете тук, за да изберете документи.",
"click here.": "натиснете тук.",
"Click on the user role button to change a user's role.": "Натиснете върху бутона за промяна на ролята на потребителя.",
"Close": "Затвори",
"Collection": "Колекция",
"Command": "Команда",
"Confirm Password": "Потвърди Парола",
"Connections": "Връзки",
"Content": "Съдържание",
"Context Length": "Дължина на Контекста",
"Conversation Mode": "Режим на Чат",
"Copy last code block": "Копиране на последен код блок",
"Copy last response": "Копиране на последен отговор",
"Copying to clipboard was successful!": "Копирането в клипборда беше успешно!",
"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':": "Създайте кратка фраза от 3-5 думи като заглавие за следващото запитване, като стриктно спазвате ограничението от 3-5 думи и избягвате използването на думата 'заглавие':",
"Create a modelfile": "Създаване на модфайл",
"Create Account": "Създаване на Акаунт",
"Created at": "Създадено на",
"Created by": "Създадено от",
"Current Model": "Текущ модел",
"Current Password": "Текуща Парола",
"Custom": "Персонализиран",
"Customize Ollama models for a specific purpose": "Персонализиране на Ollama моделите за конкретна цел",
"Dark": "Тъмен",
"Database": "База данни",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "По подразбиране",
"Default (Automatic1111)": "По подразбиране (Automatic1111)",
"Default (Web API)": "По подразбиране (Web API)",
"Default model updated": "Моделът по подразбиране е обновен",
"Default Prompt Suggestions": "Промпт Предложения по подразбиране",
"Default User Role": "Роля на потребителя по подразбиране",
"delete": "изтриване",
"Delete a model": "Изтриване на модел",
"Delete chat": "Изтриване на чат",
"Delete Chats": "Изтриване на Чатове",
"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
"Deleted {tagName}": "Изтрито {tagName}",
"Description": "Описание",
"Desktop Notifications": "Десктоп Известия",
"Disabled": "Деактивиран",
"Discover a modelfile": "Откриване на модфайл",
"Discover a prompt": "Откриване на промпт",
"Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове",
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
"Document": "Документ",
"Document Settings": "Документ Настройки",
"Documents": "Документи",
"does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.",
"Don't Allow": "Не Позволявай",
"Don't have an account?": "Нямате акаунт?",
"Download as a File": "Сваляне като Файл",
"Download Database": "Сваляне на база данни",
"Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.",
"Edit Doc": "Редактиране на документ",
"Edit User": "Редактиране на потребител",
"Email": "Имейл",
"Enable Chat History": "Вклюване на Чат История",
"Enable New Sign Ups": "Вклюване на Нови Потребители",
"Enabled": "Включено",
"Enter {{role}} message here": "Въведете съобщение за {{role}} тук",
"Enter API Key": "Въведете API ключ",
"Enter Chunk Overlap": "Въведете Chunk Overlap",
"Enter Chunk Size": "Въведете Chunk Size",
"Enter Image Size (e.g. 512x512)": "Въведете размер на изображението (напр. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Въведете LiteLLM API Base URL (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Въведете LiteLLM API Key (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Въведете LiteLLM API RPM (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "Въведете LiteLLM Model (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Въведете Max Tokens (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
"Enter stop sequence": "Въведете стоп последователност",
"Enter Top K": "Въведете Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)",
"Enter Your Email": "Въведете имейл",
"Enter Your Full Name": "Въведете вашето пълно име",
"Enter Your Password": "Въведете вашата парола",
"Experimental": "Експериментално",
"Export All Chats (All Users)": "Експортване на всички чатове (За всички потребители)",
"Export Chats": "Експортване на чатове",
"Export Documents Mapping": "Експортване на документен мапинг",
"Export Modelfiles": "Експортване на модфайлове",
"Export Prompts": "Експортване на промптове",
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
"File Mode": "Файл Мод",
"File not found.": "Файл не е намерен.",
"Focus chat input": "Фокусиране на чат вход",
"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
"From (Base Model)": "От (Базов модел)",
"Full Screen Mode": "На Цял екран",
"General": "Основни",
"General Settings": "Основни Настройки",
"Hello, {{name}}": "Здравей, {{name}}",
"Hide": "Скрий",
"Hide Additional Params": "Скрий допълнителни параметри",
"How can I help you today?": "Как мога да ви помогна днес?",
"Image Generation (Experimental)": "Генерация на изображения (Експериментално)",
"Image Generation Engine": "Двигател за генериране на изображения",
"Image Settings": "Настройки на изображения",
"Images": "Изображения",
"Import Chats": "Импортване на чатове",
"Import Documents Mapping": "Импортване на документен мапинг",
"Import Modelfiles": "Импортване на модфайлове",
"Import Prompts": "Импортване на промптове",
"Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui",
"Interface": "Интерфейс",
"join our Discord for help.": "свържете се с нашия Discord за помощ.",
"JSON": "JSON",
"JWT Expiration": "JWT Expiration",
"JWT Token": "JWT Token",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "Клавиши за бърз достъп",
"Language": "Език",
"Light": "Светъл",
"Listening...": "Слушам...",
"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
"Make sure to enclose them with": "Уверете се, че са заключени с",
"Manage LiteLLM Models": "Управление на LiteLLM Моделите",
"Manage Models": "Управление на Моделите",
"Manage Ollama Models": "Управление на Ollama Моделите",
"Max Tokens": "Max Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.",
"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
"Model {{modelName}} already exists.": "Моделът {{modelName}} вече съществува.",
"Model Name": "Име на модел",
"Model not selected": "Не е избран модел",
"Model Tag Name": "Име на таг на модел",
"Model Whitelisting": "Модел Whitelisting",
"Model(s) Whitelisted": "Модели Whitelisted",
"Modelfile": "Модфайл",
"Modelfile Advanced Settings": "Разширени настройки на модфайл",
"Modelfile Content": "Съдържание на модфайл",
"Modelfiles": "Модфайлове",
"Models": "Модели",
"My Documents": "Мои документи",
"My Modelfiles": "Мои модфайлове",
"My Prompts": "Мои промптове",
"Name": "Име",
"Name Tag": "Име Таг",
"Name your modelfile": "Име на модфайла",
"New Chat": "Нов чат",
"New Password": "Нова парола",
"Not sure what to add?": "Не сте сигурни, какво да добавите?",
"Not sure what to write? Switch to": "Не сте сигурни, какво да напишете? Превключете към",
"Off": "Изкл.",
"Okay, Let's Go!": "ОК, Нека започваме!",
"Ollama Base URL": "Ollama Базов URL",
"Ollama Version": "Ollama Версия",
"On": "Вкл.",
"Only": "Само",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерични знаци и тире са разрешени в командния низ.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Упс! Задръжте! Файловете ви все още са в пещта за обработка. Готвим ги до съвършенство. Моля, бъдете търпеливи и ще ви уведомим, когато са готови.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
"Open": "Отвори",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Отвори нов чат",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API ключ",
"OpenAI API Key is required.": "OpenAI API ключ е задължителен.",
"or": "или",
"Parameters": "Параметри",
"Password": "Парола",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
"pending": "в очакване",
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
"Playground": "Плейграунд",
"Profile": "Профил",
"Prompt Content": "Съдържание на промпта",
"Prompt suggestions": "Промпт предложения",
"Prompts": "Промптове",
"Pull a model from Ollama.com": "Издърпайте модел от Ollama.com",
"Pull Progress": "Прогрес на издърпването",
"Query Params": "Query Параметри",
"RAG Template": "RAG Шаблон",
"Raw Format": "Raw Формат",
"Record voice": "Записване на глас",
"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
"Release Notes": "Бележки по изданието",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "Request Mode",
"Reset Vector Storage": "Ресет Vector Storage",
"Response AutoCopy to Clipboard": "Аувтоматично копиране на отговор в клипборда",
"Role": "Роля",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Запис",
"Save & Create": "Запис & Създаване",
"Save & Submit": "Запис & Изпращане",
"Save & Update": "Запис & Актуализиране",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
"Scan": "Сканиране",
"Scan complete!": "Сканиране завършено!",
"Scan for documents from {{path}}": "Сканиране за документи в {{path}}",
"Search": "Търси",
"Search Documents": "Търси Документи",
"Search Prompts": "Търси Промптове",
"See readme.md for instructions": "Виж readme.md за инструкции",
"See what's new": "Виж какво е новото",
"Seed": "Seed",
"Select a mode": "Изберете режим",
"Select a model": "Изберете модел",
"Select an Ollama instance": "Изберете Ollama инстанция",
"Send a Message": "Изпращане на Съобщение",
"Send message": "Изпращане на съобщение",
"Server connection verified": "Server connection verified",
"Set as default": "Задай като по подразбиране",
"Set Default Model": "Задай Модел По Подразбиране",
"Set Image Size": "Задай Размер на Изображението",
"Set Steps": "Задай Стъпки",
"Set Title Auto-Generation Model": "Задай Модел за Автоматично Генериране на Заглавие",
"Set Voice": "Задай Глас",
"Settings": "Настройки",
"Settings saved successfully!": "Настройките са запазени успешно!",
"Share to OpenWebUI Community": "Споделите с OpenWebUI Общността",
"short-summary": "short-summary",
"Show": "Покажи",
"Show Additional Params": "Покажи допълнителни параметри",
"Show shortcuts": "Покажи",
"sidebar": "sidebar",
"Sign in": "Вписване",
"Sign Out": "Изход",
"Sign up": "Регистрация",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
"Speech-to-Text Engine": "Speech-to-Text Engine",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
"Stop Sequence": "Stop Sequence",
"STT Settings": "STT Настройки",
"Submit": "Изпращане",
"Success": "Успех",
"Successfully updated.": "Успешно обновено.",
"Sync All": "Синхронизиране на всички",
"System": "Система",
"System Prompt": "Системен Промпт",
"Tags": "Тагове",
"Temperature": "Температура",
"Template": "Шаблон",
"Text Completion": "Text Completion",
"Text-to-Speech Engine": "Text-to-Speech Engine",
"Tfs Z": "Tfs Z",
"Theme": "Тема",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Това гарантира, че ценните ви разговори се запазват сигурно във вашата бекенд база данни. Благодарим ви!",
"This setting does not sync across browsers or devices.": "Тази настройка не се синхронизира между браузъри или устройства.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.",
"Title": "Заглавие",
"Title Auto-Generation": "Автоматично Генериране на Заглавие",
"Title Generation Prompt": "Промпт за Генериране на Заглавие",
"to": "до",
"To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,",
"To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,",
"to chat input.": "към чат входа.",
"Toggle settings": "Toggle settings",
"Toggle sidebar": "Toggle sidebar",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?",
"TTS Settings": "TTS Настройки",
"Type Hugging Face Resolve (Download) URL": "Въведете Hugging Face Resolve (Download) URL",
"Uh-oh! There was an issue connecting to {{provider}}.": "О, не! Възникна проблем при свързването с {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Непознат файлов тип '{{file_type}}', но се приема и обработва като текст",
"Update password": "Обновяване на парола",
"Upload a GGUF model": "Качване на GGUF модел",
"Upload files": "Качване на файлове",
"Upload Progress": "Прогрес на качването",
"URL Mode": "URL Mode",
"Use '#' in the prompt input to load and select your documents.": "Използвайте '#' във промпта за да заредите и изберете вашите документи.",
"Use Gravatar": "Използвайте Gravatar",
"user": "потребител",
"User Permissions": "Права на потребителя",
"Users": "Потребители",
"Utilize": "Използване",
"Valid time units:": "Валидни единици за време:",
"variable": "променлива",
"variable to have them replaced with clipboard content.": "променливи да се заменят съдържанието от клипборд.",
"Version": "Версия",
"Web": "Уеб",
"WebUI Add-ons": "WebUI Добавки",
"WebUI Settings": "WebUI Настройки",
"WebUI will make requests to": "WebUI ще направи заявки към",
"What’s New in": "Какво е новото в",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когато историята е изключена, нови чатове в този браузър ще не се показват в историята на никои от вашия профил.",
"Whisper (Local)": "Whisper (Локален)",
"Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].",
"You": "Вие",
"You're a helpful assistant.": "Вие сте полезен асистент.",
"You're now logged in.": "Сега, вие влязохте в системата."
}
+363
View File
@@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' per no caduca mai.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(p. ex. `sh webui.sh --api`)",
"(latest)": "(últim)",
"{{modelName}} is thinking...": "{{modelName}} està pensant...",
"{{webUIName}} Backend Required": "Es requereix Backend de {{webUIName}}",
"a user": "un usuari",
"About": "Sobre",
"Account": "Compte",
"Action": "Acció",
"Add a model": "Afegeix un model",
"Add a model tag name": "Afegeix un nom d'etiqueta de model",
"Add a short description about what this modelfile does": "Afegeix una descripció curta del que fa aquest arxiu de model",
"Add a short title for this prompt": "Afegeix un títol curt per aquest prompt",
"Add a tag": "Afegeix una etiqueta",
"Add Docs": "Afegeix Documents",
"Add Files": "Afegeix Arxius",
"Add message": "Afegeix missatge",
"add tags": "afegeix etiquetes",
"Adjusting these settings will apply changes universally to all users.": "Ajustar aquests paràmetres aplicarà canvis de manera universal a tots els usuaris.",
"admin": "administrador",
"Admin Panel": "Panell d'Administració",
"Admin Settings": "Configuració d'Administració",
"Advanced Parameters": "Paràmetres Avançats",
"all": "tots",
"All Users": "Tots els Usuaris",
"Allow": "Permet",
"Allow Chat Deletion": "Permet la Supressió del Xat",
"alphanumeric characters and hyphens": "caràcters alfanumèrics i guions",
"Already have an account?": "Ja tens un compte?",
"an assistant": "un assistent",
"and": "i",
"API Base URL": "URL Base de l'API",
"API Key": "Clau de l'API",
"API RPM": "RPM de l'API",
"are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint",
"Are you sure?": "Estàs segur?",
"Audio": "Àudio",
"Auto-playback response": "Resposta de reproducció automàtica",
"Auto-send input after 3 sec.": "Enviar entrada automàticament després de 3 segons",
"AUTOMATIC1111 Base URL": "URL Base AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base AUTOMATIC1111.",
"available!": "disponible!",
"Back": "Enrere",
"Builder Mode": "Mode Constructor",
"Cancel": "Cancel·la",
"Categories": "Categories",
"Change Password": "Canvia la Contrasenya",
"Chat": "Xat",
"Chat History": "Històric del Xat",
"Chat History is off for this browser.": "L'historial de xat està desactivat per a aquest navegador.",
"Chats": "Xats",
"Check Again": "Comprova-ho de Nou",
"Check for updates": "Comprova si hi ha actualitzacions",
"Checking for updates...": "Comprovant actualitzacions...",
"Choose a model before saving...": "Tria un model abans de guardar...",
"Chunk Overlap": "Solapament de Blocs",
"Chunk Params": "Paràmetres de Blocs",
"Chunk Size": "Mida del Bloc",
"Click here for help.": "Fes clic aquí per ajuda.",
"Click here to check other modelfiles.": "Fes clic aquí per comprovar altres fitxers de model.",
"Click here to select": "Fes clic aquí per seleccionar",
"Click here to select documents.": "Fes clic aquí per seleccionar documents.",
"click here.": "fes clic aquí.",
"Click on the user role button to change a user's role.": "Fes clic al botó de rol d'usuari per canviar el rol d'un usuari.",
"Close": "Tanca",
"Collection": "Col·lecció",
"Command": "Comanda",
"Confirm Password": "Confirma la Contrasenya",
"Connections": "Connexions",
"Content": "Contingut",
"Context Length": "Longitud del Context",
"Conversation Mode": "Mode de Conversa",
"Copy last code block": "Copia l'últim bloc de codi",
"Copy last response": "Copia l'última resposta",
"Copying to clipboard was successful!": "La còpia al porta-retalls ha estat exitosa!",
"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':": "Crea una frase concisa de 3-5 paraules com a capçalera per a la següent consulta, seguint estrictament el límit de 3-5 paraules i evitant l'ús de la paraula 'títol':",
"Create a modelfile": "Crea un fitxer de model",
"Create Account": "Crea un Compte",
"Created at": "Creat el",
"Created by": "Creat per",
"Current Model": "Model Actual",
"Current Password": "Contrasenya Actual",
"Custom": "Personalitzat",
"Customize Ollama models for a specific purpose": "Personalitza els models Ollama per a un propòsit específic",
"Dark": "Fosc",
"Database": "Base de Dades",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Per defecte",
"Default (Automatic1111)": "Per defecte (Automatic1111)",
"Default (Web API)": "Per defecte (Web API)",
"Default model updated": "Model per defecte actualitzat",
"Default Prompt Suggestions": "Suggeriments de Prompt Per Defecte",
"Default User Role": "Rol d'Usuari Per Defecte",
"delete": "esborra",
"Delete a model": "Esborra un model",
"Delete chat": "Esborra xat",
"Delete Chats": "Esborra Xats",
"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
"Deleted {tagName}": "Esborrat {tagName}",
"Description": "Descripció",
"Desktop Notifications": "Notificacions d'Escriptori",
"Disabled": "Desactivat",
"Discover a modelfile": "Descobreix un fitxer de model",
"Discover a prompt": "Descobreix un prompt",
"Discover, download, and explore custom prompts": "Descobreix, descarrega i explora prompts personalitzats",
"Discover, download, and explore model presets": "Descobreix, descarrega i explora presets de models",
"Display the username instead of You in the Chat": "Mostra el nom d'usuari en lloc de 'Tu' al Xat",
"Document": "Document",
"Document Settings": "Configuració de Documents",
"Documents": "Documents",
"does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.",
"Don't Allow": "No Permetre",
"Don't have an account?": "No tens un compte?",
"Download as a File": "Descarrega com a Arxiu",
"Download Database": "Descarrega Base de Dades",
"Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.",
"Edit Doc": "Edita Document",
"Edit User": "Edita Usuari",
"Email": "Correu electrònic",
"Enable Chat History": "Activa Historial de Xat",
"Enable New Sign Ups": "Permet Noves Inscripcions",
"Enabled": "Activat",
"Enter {{role}} message here": "Introdueix aquí el missatge de {{role}}",
"Enter API Key": "Introdueix la Clau API",
"Enter Chunk Overlap": "Introdueix el Solapament de Blocs",
"Enter Chunk Size": "Introdueix la Mida del Bloc",
"Enter Image Size (e.g. 512x512)": "Introdueix la Mida de la Imatge (p. ex. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Introdueix l'URL Base de LiteLLM API (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Introdueix la Clau de LiteLLM API (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Introdueix RPM de LiteLLM API (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "Introdueix el Model de LiteLLM (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Introdueix el Màxim de Tokens (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Introdueix el Nombre de Passos (p. ex. 50)",
"Enter stop sequence": "Introdueix la seqüència de parada",
"Enter Top K": "Introdueix Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)",
"Enter Your Email": "Introdueix el Teu Correu Electrònic",
"Enter Your Full Name": "Introdueix el Teu Nom Complet",
"Enter Your Password": "Introdueix la Teva Contrasenya",
"Experimental": "Experimental",
"Export All Chats (All Users)": "Exporta Tots els Xats (Tots els Usuaris)",
"Export Chats": "Exporta Xats",
"Export Documents Mapping": "Exporta el Mapatge de Documents",
"Export Modelfiles": "Exporta Fitxers de Model",
"Export Prompts": "Exporta Prompts",
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
"File Mode": "Mode Arxiu",
"File not found.": "Arxiu no trobat.",
"Focus chat input": "Enfoca l'entrada del xat",
"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
"From (Base Model)": "Des de (Model Base)",
"Full Screen Mode": "Mode de Pantalla Completa",
"General": "General",
"General Settings": "Configuració General",
"Hello, {{name}}": "Hola, {{name}}",
"Hide": "Amaga",
"Hide Additional Params": "Amaga Paràmetres Addicionals",
"How can I help you today?": "Com et puc ajudar avui?",
"Image Generation (Experimental)": "Generació d'Imatges (Experimental)",
"Image Generation Engine": "Motor de Generació d'Imatges",
"Image Settings": "Configuració d'Imatges",
"Images": "Imatges",
"Import Chats": "Importa Xats",
"Import Documents Mapping": "Importa el Mapa de Documents",
"Import Modelfiles": "Importa Fitxers de Model",
"Import Prompts": "Importa Prompts",
"Include `--api` flag when running stable-diffusion-webui": "Inclou la bandera `--api` quan executis stable-diffusion-webui",
"Interface": "Interfície",
"join our Discord for help.": "uneix-te al nostre Discord per ajuda.",
"JSON": "JSON",
"JWT Expiration": "Expiració de JWT",
"JWT Token": "Token JWT",
"Keep Alive": "Mantén Actiu",
"Keyboard shortcuts": "Dreceres de Teclat",
"Language": "Idioma",
"Light": "Clar",
"Listening...": "Escoltant...",
"LLMs can make mistakes. Verify important information.": "Els LLMs poden cometre errors. Verifica la informació important.",
"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
"Manage LiteLLM Models": "Gestiona Models LiteLLM",
"Manage Models": "Gestiona Models",
"Manage Ollama Models": "Gestiona Models Ollama",
"Max Tokens": "Màxim de Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es poden descarregar un màxim de 3 models simultàniament. Si us plau, prova-ho més tard.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Eta de Mirostat",
"Mirostat Tau": "Tau de Mirostat",
"MMMM DD, YYYY": "DD de MMMM, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat amb èxit.",
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
"Model {{modelId}} not found": "Model {{modelId}} no trobat",
"Model {{modelName}} already exists.": "El model {{modelName}} ja existeix.",
"Model Name": "Nom del Model",
"Model not selected": "Model no seleccionat",
"Model Tag Name": "Nom de l'Etiqueta del Model",
"Model Whitelisting": "Llista Blanca de Models",
"Model(s) Whitelisted": "Model(s) a la Llista Blanca",
"Modelfile": "Fitxer de Model",
"Modelfile Advanced Settings": "Configuració Avançada de Fitxers de Model",
"Modelfile Content": "Contingut del Fitxer de Model",
"Modelfiles": "Fitxers de Model",
"Models": "Models",
"My Documents": "Els Meus Documents",
"My Modelfiles": "Els Meus Fitxers de Model",
"My Prompts": "Els Meus Prompts",
"Name": "Nom",
"Name Tag": "Etiqueta de Nom",
"Name your modelfile": "Nomena el teu fitxer de model",
"New Chat": "Xat Nou",
"New Password": "Nova Contrasenya",
"Not sure what to add?": "No estàs segur del que afegir?",
"Not sure what to write? Switch to": "No estàs segur del que escriure? Canvia a",
"Off": "Desactivat",
"Okay, Let's Go!": "D'acord, Anem!",
"Ollama Base URL": "URL Base d'Ollama",
"Ollama Version": "Versió d'Ollama",
"On": "Activat",
"Only": "Només",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Només es permeten caràcters alfanumèrics i guions en la cadena de comandes.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Ui! Aguanta! Els teus fitxers encara estan en el forn de processament. Els estem cuinant a la perfecció. Si us plau, tingues paciència i t'avisarem quan estiguin llestos.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL és invàlida. Si us plau, revisa-ho i prova de nou.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
"Open": "Obre",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Obre un nou xat",
"OpenAI API": "API d'OpenAI",
"OpenAI API Key": "Clau API d'OpenAI",
"OpenAI API Key is required.": "Es requereix la Clau API d'OpenAI.",
"or": "o",
"Parameters": "Paràmetres",
"Password": "Contrasenya",
"PDF Extract Images (OCR)": "Extreu Imatges de PDF (OCR)",
"pending": "pendent",
"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
"Playground": "Zona de Jocs",
"Profile": "Perfil",
"Prompt Content": "Contingut del Prompt",
"Prompt suggestions": "Suggeriments de Prompt",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Treu un model d'Ollama.com",
"Pull Progress": "Progrés de Tracció",
"Query Params": "Paràmetres de Consulta",
"RAG Template": "Plantilla RAG",
"Raw Format": "Format Brut",
"Record voice": "Enregistra veu",
"Redirecting you to OpenWebUI Community": "Redirigint-te a la Comunitat OpenWebUI",
"Release Notes": "Notes de la Versió",
"Repeat Last N": "Repeteix Últim N",
"Repeat Penalty": "Penalització de Repetició",
"Request Mode": "Mode de Sol·licitud",
"Reset Vector Storage": "Reinicia l'Emmagatzematge de Vectors",
"Response AutoCopy to Clipboard": "Resposta AutoCopiar al Portapapers",
"Role": "Rol",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Albada Rosé Pine",
"Save": "Guarda",
"Save & Create": "Guarda i Crea",
"Save & Submit": "Guarda i Envia",
"Save & Update": "Guarda i Actualitza",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Guardar registres de xat directament a l'emmagatzematge del teu navegador ja no és suportat. Si us plau, pren un moment per descarregar i eliminar els teus registres de xat fent clic al botó de sota. No et preocupis, pots reimportar fàcilment els teus registres de xat al backend a través de",
"Scan": "Escaneja",
"Scan complete!": "Escaneig completat!",
"Scan for documents from {{path}}": "Escaneja documents des de {{path}}",
"Search": "Cerca",
"Search Documents": "Cerca Documents",
"Search Prompts": "Cerca Prompts",
"See readme.md for instructions": "Consulta el readme.md per a instruccions",
"See what's new": "Veure novetats",
"Seed": "Llavor",
"Select a mode": "Selecciona un mode",
"Select a model": "Selecciona un model",
"Select an Ollama instance": "Selecciona una instància d'Ollama",
"Send a Message": "Envia un Missatge",
"Send message": "Envia missatge",
"Server connection verified": "Connexió al servidor verificada",
"Set as default": "Estableix com a predeterminat",
"Set Default Model": "Estableix Model Predeterminat",
"Set Image Size": "Estableix Mida de la Imatge",
"Set Steps": "Estableix Passos",
"Set Title Auto-Generation Model": "Estableix Model d'Auto-Generació de Títol",
"Set Voice": "Estableix Veu",
"Settings": "Configuracions",
"Settings saved successfully!": "Configuracions guardades amb èxit!",
"Share to OpenWebUI Community": "Comparteix amb la Comunitat OpenWebUI",
"short-summary": "resum curt",
"Show": "Mostra",
"Show Additional Params": "Mostra Paràmetres Addicionals",
"Show shortcuts": "Mostra dreceres",
"sidebar": "barra lateral",
"Sign in": "Inicia sessió",
"Sign Out": "Tanca sessió",
"Sign up": "Registra't",
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
"Speech-to-Text Engine": "Motor de Veu a Text",
"SpeechRecognition API is not supported in this browser.": "L'API de Reconèixer Veu no és compatible amb aquest navegador.",
"Stop Sequence": "Atura Seqüència",
"STT Settings": "Configuracions STT",
"Submit": "Envia",
"Success": "Èxit",
"Successfully updated.": "Actualitzat amb èxit.",
"Sync All": "Sincronitza Tot",
"System": "Sistema",
"System Prompt": "Prompt del Sistema",
"Tags": "Etiquetes",
"Temperature": "Temperatura",
"Template": "Plantilla",
"Text Completion": "Completació de Text",
"Text-to-Speech Engine": "Motor de Text a Veu",
"Tfs Z": "Tfs Z",
"Theme": "Tema",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden segurament guardades a la teva base de dades backend. Gràcies!",
"This setting does not sync across browsers or devices.": "Aquesta configuració no es sincronitza entre navegadors ni dispositius.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza diversos espais de variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
"Title": "Títol",
"Title Auto-Generation": "Auto-Generació de Títol",
"Title Generation Prompt": "Prompt de Generació de Títol",
"to": "a",
"To access the available model names for downloading,": "Per accedir als noms dels models disponibles per descarregar,",
"To access the GGUF models available for downloading,": "Per accedir als models GGUF disponibles per descarregar,",
"to chat input.": "a l'entrada del xat.",
"Toggle settings": "Commuta configuracions",
"Toggle sidebar": "Commuta barra lateral",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Problemes accedint a Ollama?",
"TTS Settings": "Configuracions TTS",
"Type Hugging Face Resolve (Download) URL": "Escriu URL de Resolució (Descàrrega) de Hugging Face",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uf! Hi va haver un problema connectant-se a {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipus d'Arxiu Desconegut '{{file_type}}', però acceptant i tractant com a text pla",
"Update password": "Actualitza contrasenya",
"Upload a GGUF model": "Puja un model GGUF",
"Upload files": "Puja arxius",
"Upload Progress": "Progrés de Càrrega",
"URL Mode": "Mode URL",
"Use '#' in the prompt input to load and select your documents.": "Utilitza '#' a l'entrada del prompt per carregar i seleccionar els teus documents.",
"Use Gravatar": "Utilitza Gravatar",
"user": "usuari",
"User Permissions": "Permisos d'Usuari",
"Users": "Usuaris",
"Utilize": "Utilitza",
"Valid time units:": "Unitats de temps vàlides:",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
"Version": "Versió",
"Web": "Web",
"WebUI Add-ons": "Complements de WebUI",
"WebUI Settings": "Configuració de WebUI",
"WebUI will make requests to": "WebUI farà peticions a",
"What’s New in": "Què hi ha de Nou en",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quan l'historial està desactivat, els nous xats en aquest navegador no apareixeran en el teu historial en cap dels teus dispositius.",
"Whisper (Local)": "Whisper (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència de prompt (p. ex. Qui ets tu?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
"You": "Tu",
"You're a helpful assistant.": "Ets un assistent útil.",
"You're now logged in.": "Ara estàs connectat."
}
+363
View File
@@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' oder '-1' für kein Ablaufdatum.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(z.B. `sh webui.sh --api`)",
"(latest)": "(neueste)",
"{{modelName}} is thinking...": "{{modelName}} denkt nach...",
"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
"a user": "",
"About": "Über",
"Account": "Account",
"Action": "Aktion",
"Add a model": "Füge ein Modell hinzu",
"Add a model tag name": "Benenne dein Modell-Tag",
"Add a short description about what this modelfile does": "Füge eine kurze Beschreibung hinzu, was dieses Modelfile kann",
"Add a short title for this prompt": "Füge einen kurzen Titel für diesen Prompt hinzu",
"Add a tag": "Tag hinzugügen",
"Add Docs": "Dokumente hinzufügen",
"Add Files": "Dateien hinzufügen",
"Add message": "Nachricht eingeben",
"add tags": "Tags hinzufügen",
"Adjusting these settings will apply changes universally to all users.": "Das Anpassen dieser Einstellungen wirkt sich universell auf alle Benutzer aus.",
"admin": "Administrator",
"Admin Panel": "Admin Panel",
"Admin Settings": "Admin Einstellungen",
"Advanced Parameters": "Erweiterte Parameter",
"all": "Alle",
"All Users": "Alle Benutzer",
"Allow": "Erlauben",
"Allow Chat Deletion": "Chat Löschung erlauben",
"alphanumeric characters and hyphens": "alphanumerische Zeichen und Bindestriche",
"Already have an account?": "Hast du vielleicht schon ein Account?",
"an assistant": "",
"and": "und",
"API Base URL": "API Basis URL",
"API Key": "API Key",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "sind erlaubt - Aktiviere diesen Befehl, indem du",
"Are you sure?": "",
"Audio": "Audio",
"Auto-playback response": "Automatische Wiedergabe der Antwort",
"Auto-send input after 3 sec.": "Automatisches Senden der Eingabe nach 3 Sek",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Basis URL",
"AUTOMATIC1111 Base URL is required.": "",
"available!": "verfügbar!",
"Back": "Zurück",
"Builder Mode": "Builder Modus",
"Cancel": "Abbrechen",
"Categories": "Kategorien",
"Change Password": "Passwort ändern",
"Chat": "Chat",
"Chat History": "Chat Verlauf",
"Chat History is off for this browser.": "Chat Verlauf ist für diesen Browser ausgeschaltet.",
"Chats": "Chats",
"Check Again": "Erneut überprüfen",
"Check for updates": "Nach Updates suchen",
"Checking for updates...": "Nach Updates suchen...",
"Choose a model before saving...": "Wähle bitte zuerst ein Modell, bevor du speicherst...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Parameter",
"Chunk Size": "Chunk Size",
"Click here for help.": "Klicke hier für Hilfe.",
"Click here to check other modelfiles.": "Klicke hier, um andere Modelfiles zu überprüfen.",
"Click here to select": "",
"Click here to select documents.": "",
"click here.": "hier klicken.",
"Click on the user role button to change a user's role.": "Klicke auf die Benutzerrollenschaltfläche, um die Rolle eines Benutzers zu ändern.",
"Close": "Schließe",
"Collection": "Kollektion",
"Command": "Befehl",
"Confirm Password": "Passwort bestätigen",
"Connections": "Verbindungen",
"Content": "Inhalt",
"Context Length": "Context Length",
"Conversation Mode": "Konversationsmodus",
"Copy last code block": "Letzten Codeblock kopieren",
"Copy last response": "Letzte Antwort kopieren",
"Copying to clipboard was successful!": "Das Kopieren in die Zwischenablage war erfolgreich!",
"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':": "Erstelle einen prägnanten Satz mit 3-5 Wörtern als Überschrift für die folgende Abfrage. Halte dich dabei strikt an die 3-5-Wort-Grenze und vermeide die Verwendung des Wortes Titel:",
"Create a modelfile": "Modelfiles erstellen",
"Create Account": "Konto erstellen",
"Created at": "Erstellt am",
"Created by": "Erstellt von",
"Current Model": "Aktuelles Modell",
"Current Password": "Aktuelles Passwort",
"Custom": "Benutzerdefiniert",
"Customize Ollama models for a specific purpose": "Ollama-Modelle für einen bestimmten Zweck anpassen",
"Dark": "Dunkel",
"Database": "Datenbank",
"DD/MM/YYYY HH:mm": "DD.MM.YYYY HH:mm",
"Default": "Standard",
"Default (Automatic1111)": "",
"Default (Web API)": "Standard (Web-API)",
"Default model updated": "Standardmodell aktualisiert",
"Default Prompt Suggestions": "Standard-Prompt-Vorschläge",
"Default User Role": "Standardbenutzerrolle",
"delete": "löschen",
"Delete a model": "Ein Modell löschen",
"Delete chat": "Chat löschen",
"Delete Chats": "Chats löschen",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht",
"Deleted {tagName}": "{tagName} gelöscht",
"Description": "Beschreibung",
"Desktop Notifications": "Desktop-Benachrichtigungen",
"Disabled": "Deaktiviert",
"Discover a modelfile": "Eine Modelfiles entdecken",
"Discover a prompt": "Einen Prompt entdecken",
"Discover, download, and explore custom prompts": "Benutzerdefinierte Prompts entdecken, herunterladen und erkunden",
"Discover, download, and explore model presets": "Modellvorgaben entdecken, herunterladen und erkunden",
"Display the username instead of You in the Chat": "Den Benutzernamen anstelle von 'Du' im Chat anzeigen",
"Document": "Dokument",
"Document Settings": "Dokumenteinstellungen",
"Documents": "Dokumente",
"does not make any external connections, and your data stays securely on your locally hosted server.": "stellt keine externen Verbindungen her, und Deine Daten bleiben sicher auf Deinen lokal gehosteten Server.",
"Don't Allow": "Nicht erlauben",
"Don't have an account?": "Hast du vielleicht noch kein Account?",
"Download as a File": "Als Datei herunterladen",
"Download Database": "Datenbank herunterladen",
"Drop any files here to add to the conversation": "Lasse Dateien hier fallen, um sie dem Chat anzuhängen",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z.B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.",
"Edit Doc": "Dokument bearbeiten",
"Edit User": "Benutzer bearbeiten",
"Email": "E-Mail",
"Enable Chat History": "Chat-Verlauf aktivieren",
"Enable New Sign Ups": "Neue Anmeldungen aktivieren",
"Enabled": "Aktiviert",
"Enter {{role}} message here": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Stop-Sequenz eingeben",
"Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Geben Deine E-Mail-Adresse ein",
"Enter Your Full Name": "Gebe Deinen vollständigen Namen ein",
"Enter Your Password": "Gebe Dein Passwort ein",
"Experimental": "Experimentell",
"Export All Chats (All Users)": "Alle Chats exportieren (alle Benutzer)",
"Export Chats": "Chats exportieren",
"Export Documents Mapping": "Dokumentenmapping exportieren",
"Export Modelfiles": "Modelfiles exportieren",
"Export Prompts": "Prompts exportieren",
"Failed to read clipboard contents": "Fehler beim Lesen des Zwischenablageninhalts",
"File Mode": "File Mode",
"File not found.": "Datei nicht gefunden.",
"Focus chat input": "Chat-Eingabe fokussieren",
"Format your variables using square brackets like this:": "Formatiere Deine Variablen mit eckigen Klammern wie folgt:",
"From (Base Model)": "Von (Basismodell)",
"Full Screen Mode": "Vollbildmodus",
"General": "Allgemein",
"General Settings": "Allgemeine Einstellungen",
"Hello, {{name}}": "Hallo, {{name}}",
"Hide": "Verbergen",
"Hide Additional Params": "Hide Additional Params",
"How can I help you today?": "Wie kann ich Dir heute helfen?",
"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
"Image Generation Engine": "",
"Image Settings": "Bildeinstellungen",
"Images": "Bilder",
"Import Chats": "Chats importieren",
"Import Documents Mapping": "Dokumentenmapping importieren",
"Import Modelfiles": "Modelfiles importieren",
"Import Prompts": "Prompts importieren",
"Include `--api` flag when running stable-diffusion-webui": "Füge das `--api`-Flag hinzu, wenn Du stable-diffusion-webui nutzt",
"Interface": "Benutzeroberfläche",
"join our Discord for help.": "Trete unserem Discord bei, um Hilfe zu erhalten.",
"JSON": "JSON",
"JWT Expiration": "JWT-Ablauf",
"JWT Token": "JWT-Token",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "Tastenkürzel",
"Language": "Sprache",
"Light": "Hell",
"Listening...": "Hören...",
"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
"Made by OpenWebUI Community": "Von der OpenWebUI-Community",
"Make sure to enclose them with": "Formatiere Deine Variablen mit:",
"Manage LiteLLM Models": "LiteLLM-Modelle verwalten",
"Manage Models": "Modelle verwalten",
"Manage Ollama Models": "Ollama-Modelle verwalten",
"Max Tokens": "Maximale Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es können maximal 3 Modelle gleichzeitig heruntergeladen werden. Bitte versuche es später erneut.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "DD.MM.YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Modell '{{modelName}}' wurde erfolgreich heruntergeladen.",
"Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.",
"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
"Model {{modelName}} already exists.": "Modell {{modelName}} existiert bereits.",
"Model Name": "Modellname",
"Model not selected": "Modell nicht ausgewählt",
"Model Tag Name": "Modell-Tag-Name",
"Model Whitelisting": "Modell-Whitelisting",
"Model(s) Whitelisted": "Modell(e) auf der Whitelist",
"Modelfile": "Modelfiles",
"Modelfile Advanced Settings": "Erweiterte Modelfileseinstellungen",
"Modelfile Content": "Modelfile Content",
"Modelfiles": "Modelfiles",
"Models": "Modelle",
"My Documents": "Meine Dokumente",
"My Modelfiles": "Meine Modelfiles",
"My Prompts": "Meine Prompts",
"Name": "Name",
"Name Tag": "Namens-Tag",
"Name your modelfile": "Name your modelfile",
"New Chat": "Neuer Chat",
"New Password": "Neues Passwort",
"Not sure what to add?": "Nicht sicher, was hinzugefügt werden soll?",
"Not sure what to write? Switch to": "Nicht sicher, was Du schreiben sollst? Wechsel zu",
"Off": "Aus",
"Okay, Let's Go!": "Okay, los geht's!",
"Ollama Base URL": "",
"Ollama Version": "Ollama-Version",
"On": "Ein",
"Only": "Nur",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Nur alphanumerische Zeichen und Bindestriche sind im Befehlsstring erlaubt.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Hoppla! Warte noch einen Moment! Die Dateien sind noch im der Verarbeitung. Bitte habe etwas Geduld und wir informieren Dich, sobald sie bereit sind.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.",
"Open": "Öffne",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open new chat": "Neuen Chat öffnen",
"OpenAI API": "OpenAI-API",
"OpenAI API Key": "",
"OpenAI API Key is required.": "",
"or": "oder",
"Parameters": "Parameter",
"Password": "Passwort",
"PDF Extract Images (OCR)": "Text von Bilder aus PDFs extrahieren (OCR)",
"pending": "ausstehend",
"Permission denied when accessing microphone: {{error}}": "Zugriff auf das Mikrofon verweigert: {{error}}",
"Playground": "Playground",
"Profile": "Profil",
"Prompt Content": "Prompt-Inhalt",
"Prompt suggestions": "Prompt-Vorschläge",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Ein Modell von Ollama.com abrufen",
"Pull Progress": "Fortschritt abrufen",
"Query Params": "Query Parameter",
"RAG Template": "RAG-Vorlage",
"Raw Format": "Rohformat",
"Record voice": "Stimme aufnehmen",
"Redirecting you to OpenWebUI Community": "Du wirst zur OpenWebUI-Community weitergeleitet",
"Release Notes": "Versionshinweise",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "Request-Modus",
"Reset Vector Storage": "Vektorspeicher zurücksetzen",
"Response AutoCopy to Clipboard": "Antwort automatisch in die Zwischenablage kopieren",
"Role": "Rolle",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Speichern",
"Save & Create": "Speichern und erstellen",
"Save & Submit": "Speichern und senden",
"Save & Update": "Speichern und aktualisieren",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Das direkte Speichern von Chat-Protokollen im Browser-Speicher wird nicht mehr unterstützt. Bitte nehme Dir einen Moment Zeit, um Deine Chat-Protokolle herunterzuladen und zu löschen, indem Du auf die Schaltfläche unten klickst. Keine Sorge, Du kannst Deine Chat-Protokolle problemlos über das Backend wieder importieren.",
"Scan": "Scannen",
"Scan complete!": "Scan abgeschlossen!",
"Scan for documents from {{path}}": "Dokumente von {{path}} scannen",
"Search": "Suchen",
"Search Documents": "Dokumente suchen",
"Search Prompts": "Prompts suchen",
"See readme.md for instructions": "Anleitung in readme.md anzeigen",
"See what's new": "Was gibt's Neues",
"Seed": "Seed",
"Select a mode": "",
"Select a model": "Ein Modell auswählen",
"Select an Ollama instance": "",
"Send a Message": "Eine Nachricht senden",
"Send message": "Nachricht senden",
"Server connection verified": "Serververbindung überprüft",
"Set as default": "Als Standard festlegen",
"Set Default Model": "Standardmodell festlegen",
"Set Image Size": "Bildgröße festlegen",
"Set Steps": "Schritte festlegen",
"Set Title Auto-Generation Model": "Modell für automatische Titelgenerierung festlegen",
"Set Voice": "Stimme festlegen",
"Settings": "Einstellungen",
"Settings saved successfully!": "Einstellungen erfolgreich gespeichert!",
"Share to OpenWebUI Community": "Mit OpenWebUI Community teilen",
"short-summary": "kurze-zusammenfassung",
"Show": "Anzeigen",
"Show Additional Params": "Show Additional Params",
"Show shortcuts": "Verknüpfungen anzeigen",
"sidebar": "Seitenleiste",
"Sign in": "Anmelden",
"Sign Out": "Abmelden",
"Sign up": "Registrieren",
"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
"Speech-to-Text Engine": "Sprache-zu-Text-Engine",
"SpeechRecognition API is not supported in this browser.": "Die SpeechRecognition-API wird in diesem Browser nicht unterstützt.",
"Stop Sequence": "Stop Sequence",
"STT Settings": "STT-Einstellungen",
"Submit": "Senden",
"Success": "Erfolg",
"Successfully updated.": "Erfolgreich aktualisiert.",
"Sync All": "Alles synchronisieren",
"System": "System",
"System Prompt": "System-Prompt",
"Tags": "Tags",
"Temperature": "Temperatur",
"Template": "Vorlage",
"Text Completion": "Textvervollständigung",
"Text-to-Speech Engine": "Text-zu-Sprache-Engine",
"Tfs Z": "Tfs Z",
"Theme": "Design",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dadurch werden Deine wertvollen Unterhaltungen sicher in der Backend-Datenbank gespeichert. Vielen Dank!",
"This setting does not sync across browsers or devices.": "Diese Einstellung wird nicht zwischen Browsern oder Geräten synchronisiert.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.",
"Title": "Titel",
"Title Auto-Generation": "Automatische Titelgenerierung",
"Title Generation Prompt": "Prompt für Titelgenerierung",
"to": "für",
"To access the available model names for downloading,": "Um auf die verfügbaren Modellnamen zum Herunterladen zuzugreifen,",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,",
"to chat input.": "to chat input.",
"Toggle settings": "Einstellungen umschalten",
"Toggle sidebar": "Seitenleiste umschalten",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Probleme beim Zugriff auf Ollama?",
"TTS Settings": "TTS-Einstellungen",
"Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Ups! Es gab ein Problem bei der Verbindung mit {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text",
"Update password": "Passwort aktualisieren",
"Upload a GGUF model": "Upload a GGUF model",
"Upload files": "Dateien hochladen",
"Upload Progress": "Upload Progress",
"URL Mode": "URL Mode",
"Use '#' in the prompt input to load and select your documents.": "Verwende '#' in der Prompt-Eingabe, um Deine Dokumente zu laden und auszuwählen.",
"Use Gravatar": "",
"user": "Benutzer",
"User Permissions": "Benutzerberechtigungen",
"Users": "Benutzer",
"Utilize": "Nutze die",
"Valid time units:": "Gültige Zeiteinheiten:",
"variable": "Variable",
"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
"Version": "Version",
"Web": "Web",
"WebUI Add-ons": "WebUI-Add-Ons",
"WebUI Settings": "WebUI-Einstellungen",
"WebUI will make requests to": "Wenn aktiviert sendet WebUI externe Anfragen an",
"What’s New in": "Was gibt's Neues in",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Wenn die Historie ausgeschaltet ist, werden neue Chats nicht in Deiner Historie auf Deine Geräte angezeigt.",
"Whisper (Local)": "Whisper (Lokal)",
"Write a prompt suggestion (e.g. Who are you?)": "Gebe einen Prompt-Vorschlag ein (z.B. Wer bist du?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.",
"You": "Du",
"You're a helpful assistant.": "Du bist ein hilfreicher Assistent.",
"You're now logged in.": "Du bist nun eingeloggt."
}
+363
View File
@@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)",
"(latest)": "(latest)",
"{{modelName}} is thinking...": "{{modelName}} is thinking...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Required",
"a user": "",
"About": "About",
"Account": "Account",
"Action": "Action",
"Add a model": "Add a model",
"Add a model tag name": "Add a model tag name",
"Add a short description about what this modelfile does": "Add a short description about what this modelfile does",
"Add a short title for this prompt": "Add a short title for this prompt",
"Add a tag": "Add a tag",
"Add Docs": "Add Docs",
"Add Files": "Add Files",
"Add message": "Add message",
"add tags": "add tags",
"Adjusting these settings will apply changes universally to all users.": "Adjusting these settings will apply changes universally to all users.",
"admin": "admin",
"Admin Panel": "Admin Panel",
"Admin Settings": "Admin Settings",
"Advanced Parameters": "Advanced Parameters",
"all": "all",
"All Users": "All Users",
"Allow": "Allow",
"Allow Chat Deletion": "Allow Chat Deletion",
"alphanumeric characters and hyphens": "alphanumeric characters and hyphens",
"Already have an account?": "Already have an account?",
"an assistant": "",
"and": "and",
"API Base URL": "API Base URL",
"API Key": "API Key",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "are allowed - Activate this command by typing",
"Are you sure?": "",
"Audio": "Audio",
"Auto-playback response": "Auto-playback response",
"Auto-send input after 3 sec.": "Auto-send input after 3 sec.",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "",
"available!": "available!",
"Back": "Back",
"Builder Mode": "Builder Mode",
"Cancel": "Cancel",
"Categories": "Categories",
"Change Password": "Change Password",
"Chat": "Chat",
"Chat History": "Chat History",
"Chat History is off for this browser.": "Chat History is off for this browser.",
"Chats": "Chats",
"Check Again": "Check Again",
"Check for updates": "Check for updates",
"Checking for updates...": "Checking for updates...",
"Choose a model before saving...": "Choose a model before saving...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Params",
"Chunk Size": "Chunk Size",
"Click here for help.": "Click here for help.",
"Click here to check other modelfiles.": "Click here to check other modelfiles.",
"Click here to select": "",
"Click here to select documents.": "",
"click here.": "click here.",
"Click on the user role button to change a user's role.": "Click on the user role button to change a user's role.",
"Close": "Close",
"Collection": "Collection",
"Command": "Command",
"Confirm Password": "Confirm Password",
"Connections": "Connections",
"Content": "Content",
"Context Length": "Context Length",
"Conversation Mode": "Conversation Mode",
"Copy last code block": "Copy last code block",
"Copy last response": "Copy last response",
"Copying to clipboard was successful!": "Copying to clipboard was successful!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':",
"Create a modelfile": "Create a modelfile",
"Create Account": "Create Account",
"Created at": "Created at",
"Created by": "Created by",
"Current Model": "Current Model",
"Current Password": "Current Password",
"Custom": "Custom",
"Customize Ollama models for a specific purpose": "Customize Ollama models for a specific purpose",
"Dark": "Dark",
"Database": "Database",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Default",
"Default (Automatic1111)": "",
"Default (Web API)": "Default (Web API)",
"Default model updated": "Default model updated",
"Default Prompt Suggestions": "Default Prompt Suggestions",
"Default User Role": "Default User Role",
"delete": "delete",
"Delete a model": "Delete a model",
"Delete chat": "Delete chat",
"Delete Chats": "Delete Chats",
"Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}",
"Deleted {tagName}": "Deleted {tagName}",
"Description": "Description",
"Desktop Notifications": "Notification",
"Disabled": "Disabled",
"Discover a modelfile": "Discover a modelfile",
"Discover a prompt": "Discover a prompt",
"Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts",
"Discover, download, and explore model presets": "Discover, download, and explore model presets",
"Display the username instead of You in the Chat": "Display the username instead of 'You' in the Chat",
"Document": "Document",
"Document Settings": "Document Settings",
"Documents": "Documents",
"does not make any external connections, and your data stays securely on your locally hosted server.": "does not make any external connections, and your data stays securely on your locally hosted server.",
"Don't Allow": "Don't Allow",
"Don't have an account?": "Don't have an account?",
"Download as a File": "Download as a File",
"Download Database": "Download Database",
"Drop any files here to add to the conversation": "Drop any files here to add to the conversation",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.",
"Edit Doc": "Edit Doc",
"Edit User": "Edit User",
"Email": "Email",
"Enable Chat History": "Enable Chat History",
"Enable New Sign Ups": "Enable New Sign Ups",
"Enabled": "Enabled",
"Enter {{role}} message here": "",
"Enter API Key": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Image Size (e.g. 512x512)": "",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "",
"Enter LiteLLM API Key (litellm_params.api_key)": "",
"Enter LiteLLM API RPM (litellm_params.rpm)": "",
"Enter LiteLLM Model (litellm_params.model)": "",
"Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Enter stop sequence",
"Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Enter Your Email",
"Enter Your Full Name": "Enter Your Full Name",
"Enter Your Password": "Enter Your Password",
"Experimental": "Experimental",
"Export All Chats (All Users)": "Export All Chats (All Users)",
"Export Chats": "Export Chats",
"Export Documents Mapping": "Export Documents Mapping",
"Export Modelfiles": "Export Modelfiles",
"Export Prompts": "Export Prompts",
"Failed to read clipboard contents": "Failed to read clipboard contents",
"File Mode": "File Mode",
"File not found.": "File not found.",
"Focus chat input": "Focus chat input",
"Format your variables using square brackets like this:": "Format your variables using square brackets like this:",
"From (Base Model)": "From (Base Model)",
"Full Screen Mode": "Full Screen Mode",
"General": "General",
"General Settings": "General Settings",
"Hello, {{name}}": "Hello, {{name}}",
"Hide": "Hide",
"Hide Additional Params": "Hide Additional Params",
"How can I help you today?": "How can I help you today?",
"Image Generation (Experimental)": "Image Generation (Experimental)",
"Image Generation Engine": "",
"Image Settings": "Image Settings",
"Images": "Images",
"Import Chats": "Import Chats",
"Import Documents Mapping": "Import Documents Mapping",
"Import Modelfiles": "Import Modelfiles",
"Import Prompts": "Import Prompts",
"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui",
"Interface": "Interface",
"join our Discord for help.": "join our Discord for help.",
"JSON": "JSON",
"JWT Expiration": "JWT Expiration",
"JWT Token": "JWT Token",
"Keep Alive": "Keep Alive",
"Keyboard shortcuts": "Keyboard shortcuts",
"Language": "Language",
"Light": "Light",
"Listening...": "Listening...",
"LLMs can make mistakes. Verify important information.": "LLMs can make mistakes. Verify important information.",
"Made by OpenWebUI Community": "Made by OpenWebUI Community",
"Make sure to enclose them with": "Make sure to enclose them with",
"Manage LiteLLM Models": "Manage LiteLLM Models",
"Manage Models": "Manage Models",
"Manage Ollama Models": "Manage Ollama Models",
"Max Tokens": "Max Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximum of 3 models can be downloaded simultaneously. Please try again later.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.",
"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.",
"Model {{modelId}} not found": "Model {{modelId}} not found",
"Model {{modelName}} already exists.": "Model {{modelName}} already exists.",
"Model Name": "Model Name",
"Model not selected": "Model not selected",
"Model Tag Name": "Model Tag Name",
"Model Whitelisting": "Model Whitelisting",
"Model(s) Whitelisted": "Model(s) Whitelisted",
"Modelfile": "Modelfile",
"Modelfile Advanced Settings": "Modelfile Advanced Settings",
"Modelfile Content": "Modelfile Content",
"Modelfiles": "Modelfiles",
"Models": "Models",
"My Documents": "My Documents",
"My Modelfiles": "My Modelfiles",
"My Prompts": "My Prompts",
"Name": "Name",
"Name Tag": "Name Tag",
"Name your modelfile": "Name your modelfile",
"New Chat": "New Chat",
"New Password": "New Password",
"Not sure what to add?": "Not sure what to add?",
"Not sure what to write? Switch to": "Not sure what to write? Switch to",
"Off": "Off",
"Okay, Let's Go!": "Okay, Let's Go!",
"Ollama Base URL": "",
"Ollama Version": "Ollama Version",
"On": "On",
"Only": "Only",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Only alphanumeric characters and hyphens are allowed in the command string.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.",
"Open": "Open",
"Open AI": "Open AI",
"Open AI (Dall-E)": "",
"Open new chat": "Open new chat",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "",
"OpenAI API Key is required.": "",
"or": "or",
"Parameters": "Parameters",
"Password": "Password",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
"pending": "pending",
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
"Playground": "Playground",
"Profile": "Profile",
"Prompt Content": "Prompt Content",
"Prompt suggestions": "Prompt suggestions",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Pull a model from Ollama.com",
"Pull Progress": "Pull Progress",
"Query Params": "Query Params",
"RAG Template": "RAG Template",
"Raw Format": "Raw Format",
"Record voice": "Record voice",
"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community",
"Release Notes": "Release Notes",
"Repeat Last N": "Repeat Last N",
"Repeat Penalty": "Repeat Penalty",
"Request Mode": "Request Mode",
"Reset Vector Storage": "Reset Vector Storage",
"Response AutoCopy to Clipboard": "Response AutoCopy to Clipboard",
"Role": "Role",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Save",
"Save & Create": "Save & Create",
"Save & Submit": "Save & Submit",
"Save & Update": "Save & Update",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through",
"Scan": "Scan",
"Scan complete!": "Scan complete!",
"Scan for documents from {{path}}": "Scan for documents from {{path}}",
"Search": "Search",
"Search Documents": "Search Documents",
"Search Prompts": "Search Prompts",
"See readme.md for instructions": "See readme.md for instructions",
"See what's new": "See what's new",
"Seed": "Seed",
"Select a mode": "",
"Select a model": "Select a model",
"Select an Ollama instance": "",
"Send a Message": "Send a Message",
"Send message": "Send message",
"Server connection verified": "Server connection verified",
"Set as default": "Set as default",
"Set Default Model": "Set Default Model",
"Set Image Size": "Set Image Size",
"Set Steps": "Set Steps",
"Set Title Auto-Generation Model": "Set Title Auto-Generation Model",
"Set Voice": "Set Voice",
"Settings": "Settings",
"Settings saved successfully!": "Settings saved successfully!",
"Share to OpenWebUI Community": "Share to OpenWebUI Community",
"short-summary": "short-summary",
"Show": "Show",
"Show Additional Params": "Show Additional Params",
"Show shortcuts": "Show shortcuts",
"sidebar": "sidebar",
"Sign in": "Sign in",
"Sign Out": "Sign Out",
"Sign up": "Sign up",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
"Speech-to-Text Engine": "Speech-to-Text Engine",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
"Stop Sequence": "Stop Sequence",
"STT Settings": "STT Settings",
"Submit": "Submit",
"Success": "Success",
"Successfully updated.": "Successfully updated.",
"Sync All": "Sync All",
"System": "System",
"System Prompt": "System Prompt",
"Tags": "Tags",
"Temperature": "Temperature",
"Template": "Template",
"Text Completion": "Text Completion",
"Text-to-Speech Engine": "Text-to-Speech Engine",
"Tfs Z": "Tfs Z",
"Theme": "Theme",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "This ensures that your valuable conversations are securely saved to your backend database. Thank you!",
"This setting does not sync across browsers or devices.": "This setting does not sync across browsers or devices.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.",
"Title": "Title",
"Title Auto-Generation": "Title Auto-Generation",
"Title Generation Prompt": "Title Generation Prompt",
"to": "to",
"To access the available model names for downloading,": "To access the available model names for downloading,",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,",
"to chat input.": "to chat input.",
"Toggle settings": "Toggle settings",
"Toggle sidebar": "Toggle sidebar",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Trouble accessing Ollama?",
"TTS Settings": "TTS Settings",
"Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text",
"Update password": "Update password",
"Upload a GGUF model": "Upload a GGUF model",
"Upload files": "Upload files",
"Upload Progress": "Upload Progress",
"URL Mode": "URL Mode",
"Use '#' in the prompt input to load and select your documents.": "Use '#' in the prompt input to load and select your documents.",
"Use Gravatar": "",
"user": "user",
"User Permissions": "User Permissions",
"Users": "Users",
"Utilize": "Utilize",
"Valid time units:": "Valid time units:",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content.",
"Version": "Version",
"Web": "Web",
"WebUI Add-ons": "WebUI Add-ons",
"WebUI Settings": "WebUI Settings",
"WebUI will make requests to": "WebUI will make requests to",
"What’s New in": "What’s New in",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "When history is turned off, new chats on this browser won't appear in your history on any of your devices.",
"Whisper (Local)": "Whisper (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword].",
"You": "You",
"You're a helpful assistant.": "You're a helpful assistant.",
"You're now logged in.": "You're now logged in."
}

Some files were not shown because too many files have changed in this diff Show More