Compare commits

..

538 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek be5534c655 Merge pull request #2376 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.1.125
2024-05-19 10:16:36 -10:00
Timothy J. Baek bcc2bab623 refac 2024-05-19 13:15:41 -07:00
Timothy J. Baek 0260c714d9 refac 2024-05-19 13:11:56 -07:00
Timothy Jaeryang Baek 009e85d56e Merge pull request #2394 from Yanyutin753/tem-tran
 add translations in shortcut
2024-05-19 09:20:55 -10:00
Timothy J. Baek 4d27ecef1e fix 2024-05-19 12:06:58 -07:00
Timothy J. Baek 6e6302a4a2 fix 2024-05-19 12:00:10 -07:00
Timothy J. Baek 2abbc9092e fix 2024-05-19 11:54:55 -07:00
Timothy J. Baek 4922b65753 Update Personalization.svelte 2024-05-19 11:51:32 -07:00
Timothy J. Baek 8163902ad1 fix: styling 2024-05-19 11:43:13 -07:00
Yanyutin753 0f4492114f fix Fomat & Build Frontend 2024-05-20 01:11:50 +08:00
Yanyutin753 bb6b363875 add translations in shortcut 2024-05-20 01:11:50 +08:00
Timothy J. Baek e36be75754 Update CodeBlock.svelte 2024-05-19 10:07:43 -07:00
Timothy J. Baek ec93fd15c3 Update CHANGELOG.md 2024-05-19 09:48:22 -07:00
Timothy J. Baek 5961fc84f4 feat: seaborn added to pyodide
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:46:43 -07:00
Timothy J. Baek a5fa881d4a Update CHANGELOG.md
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:34:47 -07:00
Timothy J. Baek 4fe237da58 Update CHANGELOG.md
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:30:56 -07:00
Timothy J. Baek 6139d775ef feat: clear memory 2024-05-19 09:26:24 -07:00
Timothy J. Baek 8f83f51c68 chore: format 2024-05-19 09:18:30 -07:00
Timothy J. Baek 4754e5c062 doc: changelog 2024-05-19 09:17:11 -07:00
Timothy J. Baek 5b08dc56af chore: format 2024-05-19 09:01:48 -07:00
Timothy J. Baek 8ec3934599 fix: memory 2024-05-19 08:53:28 -07:00
Timothy J. Baek 0d07d10bd8 fix 2024-05-19 08:47:37 -07:00
Timothy J. Baek bbdfcd18e1 fix 2024-05-19 08:46:39 -07:00
Timothy J. Baek febab58821 feat: memory integration 2024-05-19 08:40:46 -07:00
Timothy J. Baek 2638ae6a93 chore: format 2024-05-19 08:15:44 -07:00
Timothy J. Baek a8c4588fb5 feat: memory integration 2024-05-19 08:14:04 -07:00
Timothy J. Baek 288d8a3e32 feat: memory backend 2024-05-19 08:00:07 -07:00
Timothy J. Baek 1fb5ef99e2 refac 2024-05-19 07:40:54 -07:00
Timothy J. Baek ae35c68956 refac: styling 2024-05-19 07:37:14 -07:00
Timothy J. Baek 9ff74038b5 refac: styling 2024-05-19 07:32:19 -07:00
Timothy J. Baek 8c64f0627c fix: styling 2024-05-19 07:05:38 -07:00
Timothy J. Baek 5376525777 refac 2024-05-19 06:51:32 -07:00
Timothy J. Baek 82bd1eb155 refac 2024-05-19 06:51:21 -07:00
Timothy J. Baek 120f8d7786 refac: regenerate behaviour 2024-05-19 05:53:43 -07:00
Timothy Jaeryang Baek b9b25f872d Merge pull request #2391 from que-nguyen/dev
Fixed translation errors in Vietnamese locale file
2024-05-19 02:36:31 -10:00
Timothy Jaeryang Baek 123620bcc4 Merge branch 'dev' into dev 2024-05-19 02:36:18 -10:00
Timothy J. Baek dbdf83c55f refac: styling 2024-05-19 05:22:09 -07:00
Timothy J. Baek 066789d423 refac: pyodide worker error handling 2024-05-19 05:19:48 -07:00
Timothy J. Baek 78cbdbc63f fix: code block execution 2024-05-19 05:16:31 -07:00
Timothy J. Baek d2249921aa refac: styling 2024-05-19 05:06:13 -07:00
Timothy J. Baek f8815b0749 chore: format 2024-05-19 05:01:57 -07:00
Timothy J. Baek 68104358d0 fix: many model regeneration 2024-05-19 05:01:29 -07:00
Timothy J. Baek 676a4dffd0 feat: many model interaction ui 2024-05-19 04:46:35 -07:00
Timothy Jaeryang Baek 0ede1d1483 Merge pull request #2388 from Yanyutin753/uploadImage
 Expend upload image file types
2024-05-19 01:40:08 -10:00
Yanyutin753 c5f13cfdda Expend upload image file types 2024-05-19 19:10:00 +08:00
Que Nguyen 99dd81d700 "Fixed translation errors in Vietnamese locale file
Fixed translation errors in Vietnamese locale file locales/vi-VN/translation.json.
2024-05-19 16:24:40 +07:00
Timothy J. Baek 22d23afbaf feat: memory ui frontend 2024-05-19 00:17:40 -07:00
Timothy J. Baek b521a06b5f fix: styling
Co-Authored-By: Jannik S. <69747628+jannikstdl@users.noreply.github.com>
2024-05-19 00:14:23 -07:00
Timothy Jaeryang Baek 6d2785d58a Merge pull request #2384 from kostich/main
Update Serbian translation
2024-05-18 21:11:17 -10:00
Марко М. Костић (Marko M. Kostić) 3cd0141957 Update Serbian translation 2024-05-19 09:02:30 +02:00
Timothy Jaeryang Baek 4edef53169 Merge pull request #2374 from Silentoplayz/Silentoplayz-Ineedtonamethesebetter
Update pull_request_template.md once more
2024-05-18 13:13:23 -10:00
Timothy J. Baek 9a92ae78c8 fix: hide show username interface option 2024-05-18 16:11:28 -07:00
Timothy J. Baek 8dac4e91a5 fix: share chat issue 2024-05-18 15:23:36 -07:00
Timothy J. Baek 1e6ddb2fb3 chore: format 2024-05-18 15:02:47 -07:00
Silentoplayz e5207e098e Update pull_request_template.md
Note to first-time contributors to open a discussion post before submitting a PR
2024-05-18 21:46:38 +00:00
Timothy J. Baek 1b6700425a fix 2024-05-18 14:29:39 -07:00
Timothy J. Baek 8766d2699a refac: include packages 2024-05-18 14:28:16 -07:00
Timothy J. Baek 7f90d813bd feat: more libs for pyodide 2024-05-18 14:24:32 -07:00
Timothy J. Baek a8b92e5e9d fix: display username in shared chats 2024-05-18 14:19:48 -07:00
Silentoplayz 0349c86448 Update pull_request_template.md
Added Screenshots or Videos section
2024-05-18 21:17:42 +00:00
Timothy J. Baek 75491b2f87 fix: rtl styling 2024-05-18 14:04:35 -07:00
Timothy Jaeryang Baek 9e60d03911 Merge pull request #2318 from idomamia/feat/rtl-layout-chat-support
Add RTL layout chat support
2024-05-18 10:57:35 -10:00
Timothy Jaeryang Baek 04925407b2 Merge pull request #2369 from kostich/main
Adds the Serbian translation
2024-05-18 10:54:40 -10:00
Timothy J. Baek a121ae2c5d chore: format 2024-05-18 13:54:13 -07:00
Timothy Jaeryang Baek 86eafdf5e4 Merge pull request #2370 from sime2408/feature/translation-croatian
translation to croatian
2024-05-18 10:47:41 -10:00
Timothy Jaeryang Baek 12d59b6184 Merge pull request #2366 from cheahjs/refac/pyscript-npm
refac: fetch pyodide deps at build time, not checked into git
2024-05-18 10:47:34 -10:00
Timothy Jaeryang Baek 6f6b18e727 Merge pull request #2368 from cheahjs/fix/case-insensitive-model-search
fix: case insensitive model search
2024-05-18 10:44:07 -10:00
sime2408 719a8a9327 translation to croatian 2024-05-18 21:31:44 +02:00
Марко М. Костић (Marko M. Kostić) b8b9cab529 More fixes 2024-05-18 17:29:41 +02:00
Марко М. Костић (Marko M. Kostić) 8a0fa0b493 Fix translation 2024-05-18 17:27:40 +02:00
Марко М. Костић (Marko M. Kostić) 0176edd4c7 Initial Serbian Translation 2024-05-18 14:56:08 +02:00
Jun Siang Cheah f9587032de fix: case insensitive model search 2024-05-18 19:59:29 +08:00
Марко М. Костић (Marko M. Kostić) 4e43d31604 Initial new translation 2024-05-18 13:39:48 +02:00
Ido Henri Mamia 27c59d985f Merge branch 'dev' into feat/rtl-layout-chat-support 2024-05-18 13:58:53 +03:00
Jun Siang Cheah 781c904c91 refac: fetch pyodide deps at build time, not checked into git 2024-05-18 14:30:42 +08:00
Timothy J. Baek 400bfa5a02 fix: rag config.json 2024-05-17 19:53:38 -07:00
Timothy J. Baek 7facfa33d0 fix: litellm model filtering 2024-05-17 19:47:52 -07:00
Timothy J. Baek 3aa6b0fea9 fix: model filter issue 2024-05-17 19:11:14 -07:00
Timothy Jaeryang Baek 3890ea1490 Merge pull request #2240 from justinh-rahb/i18n-punjabi
Add Punjabi (India) language
2024-05-17 08:18:00 -10:00
Timothy J. Baek af5b2ddb01 fix: typo 2024-05-17 11:17:17 -07:00
Timothy J. Baek 5b2369da5d fix: #2332 2024-05-17 10:56:16 -07:00
Timothy J. Baek 014e52c072 fix: connection timeout 2024-05-17 10:49:12 -07:00
Timothy J. Baek fa54eb687e refac: switch styling 2024-05-17 10:45:50 -07:00
Timothy J. Baek 8181ae3918 fix 2024-05-17 10:35:33 -07:00
Timothy J. Baek cdbabdfa5a refac 2024-05-17 10:30:22 -07:00
Justin Hayes 400deb90b4 Reverting to mispelling from source 2024-05-17 10:55:23 -04:00
Justin Hayes 2c44443f93 Added new strings 2024-05-17 10:52:23 -04:00
Ido Henri Mamia e5d574307e Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 13:04:03 +03:00
Ido Henri Mamia 92c694e8ac Fix arrows to be in LTR layout for all layouts 2024-05-17 13:00:31 +03:00
Timothy J. Baek 6e19e46b07 refac: styling 2024-05-16 23:20:21 -10:00
Timothy J. Baek aa9f3d0632 refac: styling 2024-05-16 23:19:31 -10:00
Timothy J. Baek 85b2a63b6d fix: matplotlib 2024-05-16 23:15:39 -10:00
Timothy J. Baek ef300248ba feat: matplotlib support 2024-05-16 22:54:37 -10:00
Timothy J. Baek 548160c739 refac: time limit to 60sec 2024-05-16 22:39:46 -10:00
Timothy J. Baek a4630a9825 feat: code execution time limit 2024-05-16 22:39:07 -10:00
Timothy J. Baek c7221983a5 fix: input prompt message 2024-05-16 21:09:53 -10:00
Timothy J. Baek c7e1d3dbc7 fix 2024-05-16 21:03:59 -10:00
Timothy J. Baek 5e3c801c44 refac: no output 2024-05-16 20:53:50 -10:00
Timothy J. Baek b27b035caa refac: disable run button when code is running 2024-05-16 20:47:13 -10:00
Timothy Jaeryang Baek a2933bf89d Merge pull request #2328 from cheahjs/fix/image-gen-after-config-refac
fix: image generation broken by config persistence
2024-05-16 20:39:21 -10:00
Timothy J. Baek 302a17b3ed feat: code input support 2024-05-16 20:35:25 -10:00
Timothy J. Baek 01a656cc80 feat: numpy, pandas support 2024-05-16 20:25:55 -10:00
Jun Siang Cheah 51a41c8db4 fix: image generation broken by config persistence 2024-05-17 13:52:48 +08:00
Timothy J. Baek 6dedf9885f fix: code execution error handling 2024-05-16 19:30:09 -10:00
Timothy J. Baek 4efb8638ff refac 2024-05-16 19:22:10 -10:00
Timothy J. Baek 8f8ce26948 refac: migrated to pyodide from pyscript 2024-05-16 19:21:08 -10:00
Timothy J. Baek 0a9092156c chore: format 2024-05-16 18:21:19 -10:00
Timothy Jaeryang Baek 4950feded4 Merge pull request #2325 from open-webui/pyscript
feat: python code execution
2024-05-16 18:18:03 -10:00
Timothy Jaeryang Baek 7db4baeffa Merge pull request #2322 from cheahjs/feat/unsupported-nonroot-images
feat: optional UID/GID docker build args
2024-05-16 18:13:46 -10:00
Timothy J. Baek 18a7634f5c fix: python code execution 2024-05-16 18:05:43 -10:00
Timothy J. Baek 325fd2b97b refac: styling 2024-05-16 17:55:27 -10:00
Timothy J. Baek 1140036f20 feat: python error handling 2024-05-16 17:55:07 -10:00
Timothy J. Baek 58a13aec7e feat: python code execution 2024-05-16 17:49:28 -10:00
Jun Siang Cheah 1dc80786c1 feat: optional UID/GID docker build args 2024-05-17 09:44:52 +08:00
Timothy J. Baek ba61f87a2f feat: settings personalization tab ui 2024-05-16 14:25:48 -10:00
Ido Henri Mamia 34dd32d48f Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 01:22:46 +03:00
Ido Henri Mamia 3f4eb6caa2 Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 01:22:23 +03:00
Ido Henri Mamia 2e77ad87cc Make the margin and avatar support both LTR and RTL 2024-05-16 23:29:33 +03:00
Ido Henri Mamia 97f1d1665a Add to the i18n the chat direction text 2024-05-16 23:28:44 +03:00
Ido Henri Mamia 4ccb5d39c7 Code blocks should be in LTR layout for all layouts 2024-05-16 23:27:33 +03:00
Ido Henri Mamia 16b218e686 Add layout direction to all the chat components 2024-05-16 23:26:36 +03:00
Ido Henri Mamia a3a832a935 Add chat direction toggle button in the interface settings 2024-05-16 23:22:30 +03:00
Ido Henri Mamia 9c389df727 Add chat direction to the settings store 2024-05-16 23:14:54 +03:00
Timothy J. Baek e29a999dc9 fix: add user modal error handling 2024-05-15 19:01:02 -10:00
Justin Hayes b423e442d7 run i18next-parser 2024-05-15 23:23:29 -04:00
Justin Hayes d7ce488c42 Add newline 2024-05-15 23:12:46 -04:00
Justin Hayes e7a7caf911 Add pa-IN to languages.json 2024-05-15 23:12:45 -04:00
Justin Hayes 4a549f0c76 Add Punjabi (India) translations 2024-05-15 23:12:45 -04:00
Timothy Jaeryang Baek e979584298 Merge pull request #2296 from ChasLui/dev
feat: Add Chinese Simplified Translation
2024-05-15 17:06:56 -10:00
Timothy Jaeryang Baek 19203a8ed7 Merge pull request #2295 from cheahjs/fix/missing-jq
fix: add missing jq in non-ollama images
2024-05-15 17:06:46 -10:00
ChasLui 630c4d8bc6 fix: i18n zh-CN 2024-05-16 11:02:27 +08:00
Jun Siang Cheah b6d36ab638 fix: add missing jq in non-ollama images 2024-05-16 10:45:00 +08:00
Timothy J. Baek e6d20df463 refac: styling 2024-05-15 16:42:55 -10:00
Timothy J. Baek de0f31680c Update General.svelte
#2292
2024-05-15 15:24:57 -10:00
Timothy J. Baek 39f91e39a7 feat: openwebui community backward compatibility 2024-05-15 14:40:52 -10:00
Timothy J. Baek 967277f1d5 refac: styling 2024-05-15 12:55:13 -10:00
Timothy J. Baek f7fcf95689 Update UserMessage.svelte
c
2024-05-15 12:42:43 -10:00
Timothy J. Baek f2378be6f9 feat: toggleable chat bubble 2024-05-15 12:39:41 -10:00
Timothy J. Baek db158fc9e1 Update Dockerfile 2024-05-15 08:44:10 -10:00
Timothy Jaeryang Baek e30166118c Merge pull request #2264 from iits-jonathan-rohde/fix/share-chat-reactive-loop
fix: prevent loop when fetching shared chat id
2024-05-15 08:40:40 -10:00
Timothy Jaeryang Baek 9381113f60 Update Menu.svelte 2024-05-15 13:40:25 -05:00
Timothy Jaeryang Baek 85dbdd23fc Merge branch 'dev' into fix/share-chat-reactive-loop 2024-05-15 08:38:59 -10:00
Timothy Jaeryang Baek ea812a3bb8 Merge pull request #2281 from cheahjs/fix/settings-cypress
fix: settings cypress test after style changes
2024-05-15 08:34:17 -10:00
Timothy J. Baek 240cbda06c fix: draggable items in sidebar 2024-05-15 08:27:52 -10:00
Timothy J. Baek d4c193060e fix: styling 2024-05-15 08:20:36 -10:00
Timothy J. Baek 7d62993007 refac: healthcheck endpoint 2024-05-15 08:17:18 -10:00
Timothy J. Baek 8f5c671e4c refac: sidebar breakpoint 2024-05-15 08:13:14 -10:00
Jun Siang Cheah 8d4c9c77bf fix: settings cypress test after style changes 2024-05-15 23:49:26 +08:00
Timothy J. Baek b263e24e56 refac: styling 2024-05-15 00:19:10 -10:00
Timothy J. Baek 290b4e56c8 fix: styling 2024-05-14 23:57:32 -10:00
Timothy J. Baek cf9dbea370 fix: styling 2024-05-14 23:50:50 -10:00
Timothy J. Baek 0722e4b690 refac: user message styling 2024-05-14 23:45:27 -10:00
Timothy J. Baek 308a2e1de0 refac: styling 2024-05-14 22:22:15 -10:00
Timothy J. Baek 13019acce6 refac: styling 2024-05-14 22:09:32 -10:00
Timothy J. Baek aaaef3efd8 fix: unselectable sidebar
Co-Authored-By: Javier Leandro Arancibia <arancibiajav@gmail.com>
Co-Authored-By: Maksym Dmukhovskyy <25328501+maxdyy@users.noreply.github.com>
2024-05-14 21:29:22 -10:00
Timothy J. Baek 5079d5ec87 refac: styling 2024-05-14 21:26:54 -10:00
Timothy J. Baek 1d98099099 refac: admin panel styling 2024-05-14 20:48:46 -10:00
Timothy J. Baek e9decf2dda refac: styling 2024-05-14 20:31:22 -10:00
Timothy J. Baek 210456e424 refac: styling 2024-05-14 20:21:22 -10:00
Timothy J. Baek c45bb64de7 refac: styling 2024-05-14 20:16:22 -10:00
Jonathan Rohde ea6f1a0e57 Merge branch 'dev' into fix/share-chat-reactive-loop 2024-05-15 07:59:45 +02:00
Timothy J. Baek a57a01a598 refac: navbar styling 2024-05-14 14:50:24 -10:00
Timothy J. Baek 170361c27e refac: styling 2024-05-14 14:39:48 -10:00
Timothy J. Baek c489d473b9 refac: styling 2024-05-14 14:38:37 -10:00
Timothy J. Baek de75679ffb fix: sidebar model selector issue 2024-05-14 12:26:53 -10:00
Timothy J. Baek ad67339426 fix: sidebar 2024-05-14 12:17:56 -10:00
Timothy J. Baek d88cc6007c refac: styling 2024-05-14 12:15:01 -10:00
Timothy J. Baek 370f182430 refac: styling 2024-05-14 12:11:57 -10:00
Timothy J. Baek 39fdd95a0c refac: styling 2024-05-14 12:09:30 -10:00
Timothy Jaeryang Baek e6ff5af38a Merge pull request #2267 from maxdyy/fix/it-translation-update
fix: added missing translations for italian it-IT
2024-05-14 08:41:13 -10:00
Timothy Jaeryang Baek 767ae3be5b Merge pull request #2161 from cheahjs/fix/handlebars-harden
feat: add {{prompt:middletruncate:<length>}} to title gen
2024-05-14 08:21:39 -10:00
Timothy Jaeryang Baek 564a3a293f Merge branch 'dev' into fix/handlebars-harden 2024-05-14 08:21:26 -10:00
Maksym Dmukhovskyy 9ad4605e57 fix: added missing translations for italian 2024-05-14 20:56:04 +04:00
Jonathan Rohde 33b9629164 fix: prevent loop when fetching shared chat id 2024-05-14 16:19:37 +02:00
Timothy Jaeryang Baek 2290eefcb0 Merge pull request #2249 from cheahjs/fix/config-refac-broken
fix: rag, chat deletion, and webhook after config persistence change
2024-05-13 21:07:17 -10:00
Jun Siang Cheah 5369023e34 fix: rag, chat deletion, and webhook after config persistence change 2024-05-14 14:30:15 +08:00
Timothy Jaeryang Baek 233dcb2d95 Merge pull request #2242 from bo3bdo/main
Update Arabic language
2024-05-13 14:34:15 -10:00
Timothy Jaeryang Baek edbe5673c5 Merge branch 'dev' into main 2024-05-13 14:33:51 -10:00
HAMAD ABDULLA 1114f9625b Update translation.json 2024-05-14 01:59:17 +03:00
HAMAD ABDULLA f42819c6ad Update Arabic language 2024-05-14 01:49:04 +03:00
Timothy Jaeryang Baek 8b0144cd06 Merge pull request #2156 from cheahjs/feat/save-config
feat: save UI config changes to config.json
2024-05-13 11:45:30 -10:00
Timothy J. Baek 0c033b5b7b refac: rename 2024-05-13 11:32:21 -10:00
Timothy J. Baek 7e0d3496b5 fix: 1st #2208 2024-05-13 11:27:58 -10:00
Timothy Jaeryang Baek 1888327a79 Merge pull request #2150 from derek-palmer/docker-compose-version-obsolete
Removed version syntax as its no longer needed per Docker Docs
2024-05-13 10:59:54 -10:00
Timothy Jaeryang Baek fabcbc300f Merge pull request #2211 from arsaboo/header
chore: add HTTP-Referer and X-Title headers for Open WebUI
2024-05-13 10:58:48 -10:00
Timothy Jaeryang Baek 4f87512bbc Merge pull request #2236 from akx/error-page-fix
fix: load i18n before showing error page
2024-05-13 10:58:31 -10:00
Timothy J. Baek b5c759a059 chore: format 2024-05-13 10:56:30 -10:00
Aarni Koskela df167d8785 fix: load i18n for /error page 2024-05-13 18:00:30 +03:00
Alok Saboo bd82334647 chore: add additional headers only for Openrouter 2024-05-13 08:28:24 -04:00
Timothy Jaeryang Baek 8e5f96e4c3 Merge pull request #2157 from joecryptotoo/healthcheck
add healthcheck to docker
2024-05-12 14:28:59 -10:00
Timothy Jaeryang Baek 88a94cc50d Merge pull request #2183 from cheahjs/fix/streaming-error
feat: better handle openai errors, add error message to message
2024-05-12 14:28:13 -10:00
Timothy Jaeryang Baek ba13321728 Merge pull request #2186 from cheahjs/fix/windows-prettier
fix: prettier scripts work on windows
2024-05-12 14:27:48 -10:00
Timothy Jaeryang Baek 4756511aea Merge pull request #2192 from xraysight/fix/pl-translation-update
fix: added missing translations and corrected some spelling errors
2024-05-12 14:27:14 -10:00
Timothy Jaeryang Baek 543f2652c2 Merge branch 'dev' into fix/pl-translation-update 2024-05-12 14:27:00 -10:00
Timothy Jaeryang Baek 4bd999af81 Merge pull request #2196 from Silentoplayz/PR-template-update
style: update pull_request_template.md again
2024-05-12 14:26:11 -10:00
Timothy Jaeryang Baek 807be35d9f Merge pull request #2195 from not-a-ethan/not-a-ethan-patch-1
Fixed some formatting and a link
2024-05-12 14:25:26 -10:00
Timothy Jaeryang Baek 4997c8259b Merge pull request #2201 from liorkesos/main
Hebrew language support
2024-05-12 14:23:56 -10:00
Timothy Jaeryang Baek 227a3a0d86 Merge pull request #2202 from aguvener/dev
fix: tr_TR translation synchronized with the latest updates.
2024-05-12 14:23:40 -10:00
Timothy Jaeryang Baek e483aae1b8 Merge branch 'dev' into dev 2024-05-12 14:23:33 -10:00
Timothy Jaeryang Baek 7b864d967d Merge pull request #2203 from simonaszilinskas/patch-2
Addition of the Lithuanian language
2024-05-12 14:20:23 -10:00
Timothy Jaeryang Baek ca985cdb07 Merge pull request #2154 from Yanyutin753/citation-translate
🎖️Added translations for Citation and No source available
2024-05-12 14:19:42 -10:00
Timothy Jaeryang Baek eb87de9a63 Merge pull request #2124 from tcztzy/main
changed: replace deprecated `on_event` with `lifespan`
2024-05-12 14:18:35 -10:00
Alok Saboo 6671e5f170 chore: add HTTP-Referer and X-Title headers for Open WebUI 2024-05-12 14:05:38 -04:00
Simonas 5e5473c792 Create and fill translation.json for Lithuanian 2024-05-12 15:13:41 +02:00
aguvener 09357b6e81 fix: update tr_TR translation with the latest updates. 2024-05-12 12:50:03 +03:00
aguvener b63aca534b Update translation.json 2024-05-12 12:41:08 +03:00
Lior Kesos 31cb6c9857 Adding hebrew translation 2024-05-12 10:59:47 +03:00
Jun Siang Cheah 5d64822c84 refac: rename WrappedConfig to PersistedConfig 2024-05-12 13:28:40 +08:00
Silentoplayz 8e94db0d1b Update pull_request_template.md
Update Open WebUI pull request template to the latest version, striving to follow KeepaChangelog and Conventional Commits guidelines.
2024-05-12 02:38:40 +00:00
Ethan 4fa8d2aa5a Fixed link/formatting 2024-05-12 01:33:49 +00:00
bulek 3a4f3e97e7 Added missing translations and corrected some spelling errors 2024-05-11 21:31:31 +02:00
Jun Siang Cheah 55060045da feat: add .gitignore to .prettierignore 2024-05-11 23:55:06 +08:00
Jun Siang Cheah e3e037e187 fix: windows invocations of prettier 2024-05-11 17:43:53 +08:00
Jun Siang Cheah 3113825895 feat: better handle openai errors, add error message to message 2024-05-11 16:34:04 +08:00
Yanyutin753 d2954f8eb6 feat format 2024-05-11 09:33:31 +08:00
Jun Siang Cheah a789b785b4 feat: add unit tests for prompt templates 2024-05-10 20:41:22 +08:00
Jun Siang Cheah 34a6d55505 feat: add {{prompt:middletruncate:<length>}} to title gen
also harden the replacement logic
2024-05-10 20:17:41 +08:00
Jun Siang Cheah a0dceb06a5 fix: nested WrappedConfig breaks things 2024-05-10 15:20:22 +08:00
Jun Siang Cheah 298e6848b3 feat: switch to config proxy, remove config_get/set 2024-05-10 15:03:24 +08:00
Jun Siang Cheah f712c90019 feat: raise an exception if a WrappedConfig is used as a response 2024-05-10 14:18:39 +08:00
Jun Siang Cheah 058eb76568 feat: save UI config changes to config.json 2024-05-10 13:51:50 +08:00
Yanyutin753 a374405911 🎖️Added translations for Citation and No source available 2024-05-10 12:23:12 +08:00
joecryptotoo ef8070d35d added healthcheck 2024-05-09 12:25:26 -07:00
Derek Palmer (Creative) 93aea0a4d4 Removed version synatax as its no longer needed per Docker Docs 2024-05-09 14:54:26 -04:00
Timothy J. Baek 9a95767062 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-05-08 23:20:53 -10:00
Timothy J. Baek c1fc7bc764 fix: citation styling 2024-05-09 00:36:29 -07:00
Timothy J. Baek 038b6af813 refac: user menu 2024-05-09 00:34:57 -07:00
Tang Ziya 1d9c745b35 fix: replace deprecated on_event with lifespan 2024-05-09 12:04:09 +08:00
Timothy Jaeryang Baek df3675aaf7 Merge pull request #2104 from Silentoplayz/translation-update
Updated uk-UA/translation.json
2024-05-08 17:42:07 -07:00
Timothy J. Baek 685ee9b714 chore: format 2024-05-08 17:41:55 -07:00
Timothy Jaeryang Baek 90503be2ed Merge pull request #2118 from open-webui/dev
fix
2024-05-08 17:17:38 -07:00
Timothy J. Baek 26ee547f71 fix 2024-05-08 17:17:21 -07:00
Timothy Jaeryang Baek 734e62b865 Merge pull request #2117 from open-webui/dev
fix
2024-05-08 17:16:09 -07:00
Timothy J. Baek 01e020d1ad fix 2024-05-08 17:15:54 -07:00
Timothy Jaeryang Baek 943c49a07a Merge pull request #2116 from open-webui/dev
fix
2024-05-08 16:43:08 -07:00
Timothy J. Baek 88e8f31c07 fix: webui_auth permission issue 2024-05-08 16:42:41 -07:00
Timothy J. Baek 1c2aa10796 refac: disable enable_signup if webui_auth false 2024-05-08 16:33:34 -07:00
Timothy Jaeryang Baek 78c2f16c3c Merge pull request #2115 from open-webui/dev
enhancement: youtu.be url support
2024-05-08 16:30:41 -07:00
Timothy J. Baek bcd5853ce7 enhancement: youtu.be url support 2024-05-08 16:29:25 -07:00
Timothy Jaeryang Baek 61ffbf5b5b Merge pull request #2114 from open-webui/dev
fix: trusted header
2024-05-08 16:20:43 -07:00
Timothy J. Baek 58cfa5b9c1 fix: trusted header 2024-05-08 16:19:59 -07:00
Silentoplayz b3c5ebb323 Updated uk-UA/translation.json 2024-05-08 20:06:52 +00:00
Timothy Jaeryang Baek 9f2b846ea7 Merge pull request #2101 from open-webui/dev
doc
2024-05-08 11:31:31 -07:00
Timothy J. Baek 1b2b05d2ea Update README.md 2024-05-08 11:29:42 -07:00
Timothy J. Baek 0db71e97bb Update README.md 2024-05-08 11:28:42 -07:00
Timothy J. Baek c6053476ca Update dependabot.yml 2024-05-08 11:02:26 -07:00
Timothy Jaeryang Baek b8d7fdf16e Merge pull request #1965 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.1.124
2024-05-08 10:54:33 -07:00
Timothy J. Baek b44ae536b9 Update CHANGELOG.md 2024-05-08 10:53:38 -07:00
Timothy J. Baek c99e351688 Update CHANGELOG.md 2024-05-08 10:52:44 -07:00
Timothy J. Baek 37abc1513c doc: fix 2024-05-08 10:52:20 -07:00
Timothy J. Baek 73689bb26c doc: changelog 2024-05-08 10:51:51 -07:00
Timothy J. Baek 06cbe337de feat: youtube loader language env var 2024-05-08 10:51:29 -07:00
Timothy J. Baek d3822f782c feat: non-english youtube support 2024-05-08 10:47:05 -07:00
Timothy J. Baek 87daf122db refac: styling 2024-05-08 09:43:49 -07:00
Timothy J. Baek 6023f86cf7 chore: format 2024-05-08 09:34:36 -07:00
Timothy J. Baek c40e634391 feat: better error message 2024-05-08 09:30:14 -07:00
Timothy J. Baek 01472c071b feat: support webui_auth false
#929
2024-05-08 08:40:18 -07:00
Timothy Jaeryang Baek 0d633c0d17 Merge pull request #2079 from akx/fi
feat: add Finnish localization 🇫🇮
2024-05-08 08:04:44 -07:00
Timothy Jaeryang Baek 84ea10ab45 Merge pull request #2061 from AnkurSachdeva22/main
Added Hindi Translation and language JSON updated
2024-05-08 08:04:16 -07:00
Timothy Jaeryang Baek 675328c881 Merge pull request #2002 from Yanyutin753/addAudio
🤩 Added custom openai tts models and role variables
2024-05-08 08:03:20 -07:00
Timothy Jaeryang Baek 421a964a9b Merge pull request #2082 from cheahjs/fix/reword-secret-key
fix: reword WEBUI_SECRET_KEY generation log line
2024-05-08 08:02:49 -07:00
Jun Siang Cheah ceecfb66a5 fix: reword WEBUI_SECRET_KEY generation log line 2024-05-08 20:03:03 +08:00
Aarni Koskela 20c4ada998 feat: add Finnish localization 🇫🇮 2024-05-08 13:33:25 +03:00
AnkurSachdeva22 9fefddd844 Merge branch 'main' of https://github.com/AnkurSachdeva22/open-webui-hindi 2024-05-08 14:32:32 +05:30
AnkurSachdeva22 7dade0715d language.json sorted by language code 2024-05-08 14:25:28 +05:30
Timothy J. Baek 9c190b56ae feat: /v1/models endpoint added for ollama proxy 2024-05-07 23:34:12 -07:00
Timothy J. Baek 72675ec15f refac: citation styling 2024-05-07 23:13:37 -07:00
Timothy Jaeryang Baek 1ea91f9060 Merge pull request #2067 from cheahjs/feat/dependabot-updates
feat: update dependabot for python and github actions
2024-05-07 22:43:21 -07:00
Jun Siang Cheah 34632b7aac fix: formatting 2024-05-08 10:50:55 +08:00
Jun Siang Cheah 2969918d19 feat: enable dependabot for github actions 2024-05-08 10:48:54 +08:00
Jun Siang Cheah 32f74c9fc7 fix: don't group dependabot updates because of conflicts 2024-05-08 10:47:59 +08:00
Ankur 0ed5405571 Merge branch 'dev' into main 2024-05-08 04:54:14 +05:30
AnkurSachdeva22 6d99ef8b4b Hindi Translation file Added and language JSON updated 2024-05-08 01:39:30 +05:30
Timothy J. Baek c63176402c chore: format 2024-05-07 11:41:17 -07:00
Timothy J. Baek ea69c24bf7 fix: allow message switch during regeneration
Co-Authored-By: Pandazki <onlrrr@gmail.com>
2024-05-07 11:39:32 -07:00
Timothy Jaeryang Baek a32a752791 Merge pull request #2049 from ihavecoke/main
Adjust text formatting per Chinese copywriting guidelines
2024-05-07 11:05:13 -07:00
Timothy Jaeryang Baek 68c182db96 Merge branch 'dev' into main 2024-05-07 11:05:00 -07:00
Timothy Jaeryang Baek 876853f2b0 Merge pull request #2040 from akx/uuid4
fix: do not use hardware ID in document ID generation
2024-05-07 11:02:21 -07:00
Timothy Jaeryang Baek 838c6f8217 Merge pull request #2033 from jannikstdl/update-chat-timestamp-locale
Update chat timestamp locale
2024-05-07 11:00:40 -07:00
Timothy Jaeryang Baek 1b5d88d0c0 Merge branch 'dev' into update-chat-timestamp-locale 2024-05-07 11:00:31 -07:00
Timothy Jaeryang Baek 39bc3b508c Merge pull request #2032 from jannikstdl/update-de-locale
Added new translation strings + more missing i18n keys
2024-05-07 10:59:40 -07:00
Timothy Jaeryang Baek 5661de3221 Merge pull request #2039 from akx/py-requirement-fixes
chore: Small Python requirement fixes
2024-05-07 10:47:44 -07:00
Timothy Jaeryang Baek 3f14d8bf0b Merge pull request #2038 from akx/pr-branch
doc: note PRs should target `dev`
2024-05-07 10:47:15 -07:00
ihavecoke 242e70d181 Adjust text formatting per Chinese copywriting guidelines
Refined text formatting to follow the recommendations from: https://sparanoid.com/note/chinese-copywriting-guidelines/
2024-05-07 23:47:47 +08:00
Jannik Streidl 7ce157a8b8 fix german spelling 2024-05-07 12:00:03 +02:00
Jannik Streidl a2d0f9ce1d fix german spelling 2024-05-07 11:56:41 +02:00
Jannik Streidl 06683f5ac0 changed german spelling 2024-05-07 11:54:34 +02:00
Jannik Streidl 9f4a71029f more keys 2024-05-07 11:44:35 +02:00
Jannik Streidl cc72341861 found more tooltops without i18n keys 2024-05-07 11:34:15 +02:00
Aarni Koskela 61bb1f1dc8 fix: do not use hardware ID in document ID generation 2024-05-07 11:42:05 +03:00
Aarni Koskela cbd1929f36 chore: remove uuid requirement (it's a standard library module) 2024-05-07 11:39:01 +03:00
Aarni Koskela 3723f42f1d chore: remove duplicate requirements 2024-05-07 11:39:01 +03:00
Aarni Koskela 03955d6a42 chore: pin youtube-transcript-api like the rest 2024-05-07 11:39:01 +03:00
Aarni Koskela d27f80542c doc: note PRs should target dev 2024-05-07 11:18:40 +03:00
Jannik Streidl f32094dd41 Added keys as comments so they dont get removed by the parser 2024-05-07 09:15:23 +02:00
Jannik Streidl 3404b8dd13 added some missing keys 2024-05-07 08:43:25 +02:00
Jannik Streidl 11dcbb2e15 fix Submit button not beeing a i18n key 2024-05-07 08:18:58 +02:00
Jannik Streidl 8d5f35a404 addition 2024-05-07 08:11:48 +02:00
Jannik Streidl 755dd85a20 Added localisation for the new chat timestamps 2024-05-07 07:59:09 +02:00
Jannik Streidl 55321a3b12 Added new translation strings + 1 i18n kkey 2024-05-07 07:34:42 +02:00
Timothy Jaeryang Baek ab43bd17f6 Merge pull request #2030 from 0xThresh/remove-helm
Remove Helm charts, which have moved to a separate repo
2024-05-06 20:35:45 -07:00
0xThresh.eth bfb8b08723 Remove Helm charts, which have moved to a separate repo 2024-05-06 20:39:10 -06:00
Timothy J. Baek 04a9115254 feat: esc image preview 2024-05-06 19:12:43 -07:00
Timothy J. Baek 7874bea5f4 fix: code block rendering issue 2024-05-06 19:04:36 -07:00
Timothy J. Baek e3a49c8e9b fix: styling 2024-05-06 18:50:50 -07:00
Timothy J. Baek f58eb0d266 feat: browser search engine support 2024-05-06 17:29:16 -07:00
Yanyutin753 5d6517c537 update the name 2024-05-07 08:28:34 +08:00
Timothy J. Baek 66e7e577df refac: citation styling 2024-05-06 17:02:04 -07:00
Timothy J. Baek 9b59039388 refac: styling 2024-05-06 17:00:50 -07:00
Timothy J. Baek 9aa689ea56 feat: title trim quotation marks 2024-05-06 16:59:54 -07:00
Timothy J. Baek 7f617225a0 refac: styling 2024-05-06 16:29:57 -07:00
Timothy J. Baek bf604bc07c refac: styling 2024-05-06 16:19:48 -07:00
Timothy J. Baek 485469683e chore: format 2024-05-06 16:18:11 -07:00
Timothy Jaeryang Baek e98cb45238 Merge pull request #2011 from cheahjs/feat/rag-citations
feat: show RAG query results as citations
2024-05-06 16:17:08 -07:00
Timothy J. Baek c84e0aa2a3 fix: .env load 2024-05-06 16:16:26 -07:00
Timothy J. Baek aef2a514d1 refac: citation rendering 2024-05-06 16:16:26 -07:00
Timothy J. Baek 64ed0d1089 refac: include source name to citation 2024-05-06 16:16:26 -07:00
Timothy J. Baek 4c490132ba refac: styling 2024-05-06 16:16:26 -07:00
Timothy J. Baek 38590da0b1 fix: styling 2024-05-06 16:16:26 -07:00
Timothy J. Baek e70cdf118b refac: styling 2024-05-06 16:16:26 -07:00
Jun Siang Cheah 4c6567c46f feat: group citations by source 2024-05-06 16:16:26 -07:00
Jun Siang Cheah c3425f3bf1 fix: update translation files 2024-05-06 16:16:26 -07:00
Jun Siang Cheah 2571f3c43b fix: remove stray debug logs 2024-05-06 16:14:10 -07:00
Jun Siang Cheah 0872bea790 feat: show RAG query results as citations 2024-05-06 16:14:10 -07:00
Timothy Jaeryang Baek ba09fcd548 Merge pull request #2026 from cheahjs/fix/integration-test-wait-ollama
fix: wait for Ollama to come up in integration test
2024-05-06 16:01:32 -07:00
Jun Siang Cheah 546d553803 fix: wait for Ollama to come up in integration test 2024-05-07 06:17:10 +08:00
Timothy J. Baek aa0bf79330 chore: format 2024-05-06 14:51:53 -07:00
Timothy J. Baek cecb87b8c2 feat: web_loader_ssl_verification setting 2024-05-06 14:50:55 -07:00
Timothy J. Baek 07b39d1cb9 refac: document settings 2024-05-06 14:36:36 -07:00
Timothy J. Baek 95f579cabe feat: rag ssl verification env var
Co-Authored-By: Tobias Steidle <tobias.steidle@softwaredev.de>
2024-05-06 13:12:08 -07:00
Timothy J. Baek 7f1acf9c2d chore: format 2024-05-06 12:58:24 -07:00
Timothy Jaeryang Baek 6fa88854e3 Merge pull request #1874 from jannikstdl/update-i18n
Added some i18n keys & updated german locale
2024-05-06 12:57:42 -07:00
Jannik Streidl 2dcd9e75a1 resetted parser - only new keys 2024-05-06 21:10:46 +02:00
Yanyutin753 5f7188b480 fix python test 2024-05-06 15:33:29 +08:00
Yanyutin753 761c66a8d8 🤩 Added custom openai tts models and role variables 2024-05-06 15:23:27 +08:00
Timothy Jaeryang Baek 0334ab7c5d Merge pull request #2001 from cheahjs/feat/limit-title-gen-tokens
feat: restrict title gen output to 50 tokens
2024-05-06 00:05:53 -07:00
Jannik Streidl e31cf08188 merged conflicts in PL locale 2024-05-06 08:57:18 +02:00
Jun Siang Cheah b8c97fa012 feat: restrict title gen output to 50 tokens 2024-05-06 14:46:50 +08:00
Timothy J. Baek e92311b4eb fix: image download issue
Co-Authored-By: Clivia <132346501+Yanyutin753@users.noreply.github.com>
2024-05-05 22:46:22 -07:00
Timothy J. Baek 594f2f264f chore: formatting 2024-05-04 14:53:21 -07:00
Timothy Jaeryang Baek c2f429425e Merge pull request #1973 from bo3bdo/main
Adding language Arabic
2024-05-04 14:52:35 -07:00
HAMAD ABDULLA 2fec591b59 Update translation.json 2024-05-05 00:39:04 +03:00
HAMAD ABDULLA 9c1f224219 Update translation.json 2024-05-04 23:30:16 +03:00
Timothy J. Baek af1d8470cd fix: disable template variables in prompt 2024-05-04 13:21:35 -07:00
Timothy J. Baek 3a2f1d56c8 fix: promptTemplate infinite loop 2024-05-04 13:11:58 -07:00
Timothy J. Baek a341cfe3fd refac: getTimeRange 2024-05-04 12:59:23 -07:00
Timothy J. Baek 36d8442c92 fix: chat list time range 2024-05-04 12:53:01 -07:00
HAMAD ABDULLA cb2e989408 Adding language Arabic
The translation has been added and the previous errors have been corrected. i hope i good now. Thank you
2024-05-04 22:15:02 +03:00
Timothy J. Baek c0fbf1e7d0 refac: styling 2024-05-04 02:11:23 -07:00
Timothy J. Baek 1c39e5cff9 refac: styling 2024-05-04 02:05:09 -07:00
Timothy J. Baek 24c0dbec0d fix: pending permission issue 2024-05-04 01:31:08 -07:00
Timothy J. Baek 0a8dae633b refac: styling 2024-05-04 01:24:43 -07:00
Timothy J. Baek 53dae39042 feat: chat list group by time range 2024-05-04 01:23:02 -07:00
Timothy J. Baek 84ec58873a fix: tag delete issue 2024-05-03 17:27:48 -07:00
Timothy J. Baek d4db3dd593 fix: tag delete issue 2024-05-03 16:18:11 -07:00
Timothy J. Baek fa3c7bc218 fix 2024-05-03 15:34:08 -07:00
Timothy J. Baek facb6edfdc refac: chat tags 2024-05-03 15:25:41 -07:00
Timothy J. Baek e8085f80a7 fix: litellm warning message
>:(
2024-05-03 15:07:12 -07:00
Timothy J. Baek ecde76270c chore: format 2024-05-03 14:25:40 -07:00
Timothy J. Baek 70ae053a46 fix: ascii art issue
#1918
2024-05-03 14:23:38 -07:00
Timothy J. Baek 3e6b94dbc1 fix: sidebar handle 2024-05-03 14:10:00 -07:00
Timothy Jaeryang Baek e3a3a034c1 Merge pull request #1944 from aguvener/dev
fix: tr_TR translation synchronized with the latest updates.
2024-05-03 14:05:07 -07:00
Timothy Jaeryang Baek 46027cc390 Merge branch 'dev' into dev 2024-05-03 14:04:54 -07:00
Timothy Jaeryang Baek e4c77628be Merge pull request #1946 from Yanyutin753/shortcuts-dev
add Symbol startup instructions in ShortcutsModal
2024-05-03 14:03:44 -07:00
Timothy J. Baek ecfb6891f6 refac 2024-05-03 14:03:22 -07:00
Yanyutin753 115c3a38a7 add Symbol startup prompt startup instructions in ShortcutsModal 2024-05-03 17:49:58 +08:00
aguvener 9d8c23b3b2 fix: synchronized with the latest updates. 2024-05-03 11:01:37 +03:00
Timothy Jaeryang Baek 30b053116d Merge pull request #1942 from open-webui/dev
fix: drag and drop styling issue
2024-05-03 00:16:59 -07:00
Timothy J. Baek 0921317b2b fix: drag and drop styling issue 2024-05-03 00:16:43 -07:00
Timothy Jaeryang Baek f1963c95c9 Merge pull request #1937 from open-webui/dev
fix: selector styling issue
2024-05-02 18:56:44 -07:00
Timothy J. Baek c2c89525bb fix: selector styling issue 2024-05-02 18:56:19 -07:00
Timothy Jaeryang Baek 47565065f6 Merge pull request #1936 from open-webui/dev
fix: styling issue
2024-05-02 18:50:48 -07:00
Timothy J. Baek ecc841c57d fix: styling issue 2024-05-02 18:50:20 -07:00
Timothy Jaeryang Baek eaaa75387f Merge pull request #1933 from open-webui/dev
fix: selector styling
2024-05-02 17:14:59 -07:00
Timothy J. Baek 61a8f73291 fix: selector styling 2024-05-02 17:14:33 -07:00
Timothy Jaeryang Baek 9df4227db4 Merge pull request #1930 from open-webui/dev
fix
2024-05-02 16:00:05 -07:00
Timothy J. Baek 683e6ca337 fix 2024-05-02 15:59:45 -07:00
Timothy Jaeryang Baek b1eb08b44a Merge pull request #1929 from open-webui/dev
fix
2024-05-02 15:56:02 -07:00
Timothy J. Baek 989a4d3165 fix 2024-05-02 15:55:42 -07:00
Timothy Jaeryang Baek 443a256f72 Merge pull request #1928 from open-webui/dev
fix: image gen
2024-05-02 15:54:50 -07:00
Timothy J. Baek 482f010d22 fix: image gen 2024-05-02 15:54:31 -07:00
Timothy Jaeryang Baek fc86b20350 Merge pull request #1926 from open-webui/dev
fix
2024-05-02 15:48:49 -07:00
Timothy J. Baek 1f98c091f0 fix 2024-05-02 15:48:18 -07:00
Timothy Jaeryang Baek dc187d35a1 Merge pull request #1925 from open-webui/dev
refac: mobile detection
2024-05-02 15:44:54 -07:00
Timothy J. Baek e0908f9f26 refac: mobile detection 2024-05-02 15:44:31 -07:00
Timothy Jaeryang Baek 75911f6e4e Merge pull request #1920 from open-webui/dev
fix
2024-05-02 15:11:46 -07:00
Timothy J. Baek a2ebbd02ad fix: styling 2024-05-02 15:11:20 -07:00
Timothy J. Baek a19d4267f7 fix: integration test 2024-05-02 14:20:53 -07:00
Timothy Jaeryang Baek 444f20198a Merge pull request #1917 from open-webui/dev
fix: selector input issue
2024-05-02 13:26:08 -07:00
Timothy J. Baek a79618ee3e fix: selector input issue 2024-05-02 13:25:44 -07:00
Timothy Jaeryang Baek 38ff3209ad Merge pull request #1881 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Failing after 54s
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Failing after 1m2s
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Failing after 58s
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been skipped
0.1.123
2024-05-02 13:10:28 -07:00
Timothy J. Baek 2789102d27 Update chat.cy.ts 2024-05-02 12:54:28 -07:00
Timothy J. Baek c1f88eb0ad Update chat.cy.ts 2024-05-02 12:49:25 -07:00
Jannik Streidl e57c0c301c resolved conflicts #2 2024-05-02 21:46:24 +02:00
Timothy J. Baek 5cf621396d refac: navbar selector styling 2024-05-02 12:33:04 -07:00
Timothy J. Baek 2bece7b4c5 fix: styling 2024-05-02 03:16:08 -07:00
Timothy J. Baek 7a96893093 refac: mobile device detection 2024-05-02 03:11:56 -07:00
Timothy J. Baek 180fd44a4d refac: gesture sensitivity 2024-05-02 03:02:15 -07:00
Timothy J. Baek a2edd2d911 refac: styling 2024-05-02 02:59:05 -07:00
Timothy J. Baek e6cf552aad refac: styling 2024-05-02 02:54:33 -07:00
Timothy J. Baek be6cc1c53a chore: format 2024-05-02 02:48:56 -07:00
Timothy J. Baek f50ffc07d7 doc: changelog 2024-05-02 02:47:16 -07:00
Timothy Jaeryang Baek e700552f93 Merge pull request #1905 from 7a6ac0/r_score_type
fix: change r_score type to float
2024-05-02 02:22:59 -07:00
Timothy J. Baek d680d52b85 feat:'@' model support 2024-05-02 02:20:57 -07:00
Timothy J. Baek bf35297e4a refac: default role updated to user for add user modal 2024-05-02 01:35:00 -07:00
Timothy J. Baek c91b9af0e0 Create logo.png 2024-05-02 01:21:44 -07:00
Timothy J. Baek 82a61e72e1 refac: hide model set as default button 2024-05-02 01:15:58 -07:00
Timothy J. Baek 52fb09ff95 fix: chat input variable styling issue 2024-05-02 00:48:07 -07:00
Timothy J. Baek 572cd22b4d feat: prompt variables from suggestions 2024-05-02 00:45:04 -07:00
Timothy J. Baek 9ee39aa07f chore: format 2024-05-02 00:35:50 -07:00
Timothy J. Baek 372284b419 refac 2024-05-02 00:34:07 -07:00
Timothy J. Baek 714bdca3f3 refac: styling 2024-05-02 00:23:32 -07:00
Timothy J. Baek 5613af032d fix: styling 2024-05-02 00:07:04 -07:00
Timothy J. Baek 8fb5e22e43 refac: placeholder fade in effect 2024-05-01 23:41:12 -07:00
Timothy J. Baek b19e05669e fix: styling 2024-05-01 23:11:16 -07:00
Timothy J. Baek 35b5d5ba35 refac: suggestions scroll snap 2024-05-01 23:01:00 -07:00
Timothy J. Baek 84cd1b8cb8 refac: styling 2024-05-01 22:55:42 -07:00
tabacoWang fffd283b0c fix:
fix: Change the type from int to float
2024-05-02 13:45:19 +08:00
Timothy J. Baek 6d378844ae refac: styling 2024-05-01 22:39:09 -07:00
Timothy J. Baek 68cfccedee feat: super-admin (first one to signup) 2024-05-01 19:59:05 -07:00
Timothy J. Baek a433315346 feat: disable all actions for admins 2024-05-01 19:50:23 -07:00
Timothy J. Baek 77cc3b5470 feat: disable admin chatlist by default 2024-05-01 19:47:54 -07:00
Timothy J. Baek dc412d21f7 refac: scrollbar styling 2024-05-01 19:37:12 -07:00
Timothy J. Baek 96d9d3447b fix: pwa icon
#1886
2024-05-01 19:32:36 -07:00
Timothy J. Baek aed078b580 chore: format 2024-05-01 19:30:03 -07:00
Timothy J. Baek 33c337785c chore: i18n format 2024-05-01 19:29:11 -07:00
Timothy J. Baek 20a6dbbe65 feat: csv bulk import 2024-05-01 19:28:35 -07:00
Timothy J. Baek bd3b5f1edb feat: csv user import frontend 2024-05-01 19:02:25 -07:00
Timothy J. Baek e5703a588f fix: styling 2024-05-01 18:13:45 -07:00
Timothy J. Baek 3431a93499 fix: safari styling 2024-05-01 18:10:49 -07:00
Timothy J. Baek 8745091a16 chore: i18n 2024-05-01 18:07:29 -07:00
Timothy J. Baek e6bcdba5ad feat: add user from admin panel 2024-05-01 18:06:02 -07:00
Timothy J. Baek 96af34f240 chore: i18n 2024-05-01 17:56:39 -07:00
Timothy J. Baek b7fcf14f6e refac: styling 2024-05-01 17:55:18 -07:00
Timothy J. Baek ebc6a269d3 chore: i18n 2024-05-01 17:20:21 -07:00
Timothy J. Baek 0595c04909 feat: youtube rag 2024-05-01 17:17:00 -07:00
Timothy J. Baek e60c87d750 fix: delete chat shortcut 2024-05-01 16:08:06 -07:00
Timothy J. Baek d5bcae6182 chore: format 2024-05-01 15:41:39 -07:00
Timothy Jaeryang Baek dbc3aa7234 Merge pull request #1892 from Joakim-T/patch-1
Patch 1
2024-05-01 15:01:18 -07:00
Joakim e626fb2dbf Update languages.json
Moved sv-SE to correct position alphabetically
2024-05-01 20:42:50 +02:00
Joakim 34be6de4d8 Update translation.json sv-SE
Minor changes to Swedish translations
2024-05-01 14:43:12 +02:00
Joakim 20be93217b Update languages.json to add sv-SE
Swedish translation
2024-05-01 14:35:17 +02:00
Joakim c580d29a5d Create translation.json for sv-SE
Translation to Swedish
2024-05-01 14:30:16 +02:00
Timothy Jaeryang Baek f337ddb267 Merge pull request #1880 from Silentoplayz/saynototelemetry
feat: Added Environment Variable ANONYMIZED_TELEMETRY=false
2024-04-30 21:19:35 -07:00
Timothy J. Baek 1e05caf809 fix: response profile image 2024-04-30 17:10:12 -07:00
Timothy J. Baek 44884a8886 feat: randomised suggestion 2024-04-30 17:07:03 -07:00
Timothy J. Baek a01fd15812 refac: sidebar styling 2024-04-30 16:58:07 -07:00
Timothy J. Baek d513629984 fix: styling 2024-04-30 16:55:32 -07:00
Timothy J. Baek f1acf68bd0 fix: styling 2024-04-30 16:52:19 -07:00
Timothy J. Baek 3c9fc7858b fix: styling 2024-04-30 16:34:29 -07:00
Timothy J. Baek bf2ff47df0 refac: styling 2024-04-30 16:00:44 -07:00
Timothy J. Baek 6318282873 refac: styling 2024-04-30 15:31:50 -07:00
Timothy J. Baek 3043e43418 refac: styling 2024-04-30 15:18:58 -07:00
Timothy J. Baek 595ebd11ac refac: styling 2024-04-30 15:17:05 -07:00
Timothy J. Baek 1157b8e12d refac: styling 2024-04-30 15:12:58 -07:00
Timothy J. Baek f653944849 refac: snap-center removed 2024-04-30 15:11:17 -07:00
Timothy J. Baek 01c077da3d refac 2024-04-30 15:10:39 -07:00
Timothy J. Baek 27ff386115 fix: horizontal scroll issue on mobile
#1854
2024-04-30 15:08:34 -07:00
Timothy J. Baek 35437fb3a3 refac: styling 2024-04-30 14:58:11 -07:00
Timothy J. Baek 18e5c2e4cb refac: styling 2024-04-30 14:56:06 -07:00
Timothy Jaeryang Baek 2674656b49 Merge pull request #1884 from cheahjs/feat/freeze-py-and-dependabot
feat: pin all python packages and setup dependabot
2024-04-30 14:22:46 -07:00
Jun Siang Cheah fe56c0a7ac feat: add dependabot config for keeping python packages updated 2024-04-30 21:23:52 +01:00
Jun Siang Cheah ddac5284fe feat: specify versions for python packages 2024-04-30 21:21:00 +01:00
Silentoplayz b2020383dd Update Dockerfile
fix
2024-04-30 20:05:11 +00:00
Silentoplayz 563377a58a fix 2024-04-30 20:04:30 +00:00
Timothy Jaeryang Baek eadb671414 Merge pull request #1882 from cheahjs/feat/harden-streaming-parser
feat: use spec compliant SSE parser for OpenAI responses
2024-04-30 13:02:44 -07:00
Jun Siang Cheah b3ccabd2fe fix: add missing type for splitLargeChunks 2024-04-30 20:56:58 +01:00
Jun Siang Cheah e8bf596959 feat: switch OpenAI SSE parsing to eventsource-parser 2024-04-30 20:56:58 +01:00
Timothy J. Baek abc9b6b3f3 fix: version pinning 2024-04-30 12:41:35 -07:00
Timothy J. Baek 79bbc2be23 chore: formatting 2024-04-30 12:34:56 -07:00
Silentoplayz b763535c92 Added: Environment Variable ANONYMIZED_TELEMETRY=False to .env.example & Dockerfile files to prevent/disable the creation of telemetry_user_id file created by Chroma in Docker installation methods. 2024-04-30 19:10:44 +00:00
Timothy Jaeryang Baek de62153d49 Merge pull request #1815 from Yanyutin753/new-dev
 expend the image format type after the file is downloaded
2024-04-30 11:52:30 -07:00
Timothy J. Baek 80413a7628 refac 2024-04-30 11:52:08 -07:00
Timothy Jaeryang Baek 3f0fae1d10 Merge pull request #1868 from Yanyutin753/rag-dev
📌 fixed a bug where RAG would not reply after not reading the file correctly
2024-04-30 11:40:47 -07:00
Jannik Streidl dd52ea9d3d added and improved some german translations 2024-04-30 11:39:14 +02:00
Jannik Streidl 30d1048099 resolved the conflicts 😍 2024-04-30 11:33:05 +02:00
Jannik Streidl 142f82bae5 Added some i18n keys & updated german locale 2024-04-30 10:46:16 +02:00
Yanyutin753 c0bb32d768 📌 fixed a bug where RAG would not reply after not reading the file correctly 2024-04-30 13:51:30 +08:00
Timothy J. Baek 5e168e6fef refac: remove num_ctx limit 2024-04-29 16:10:57 -07:00
Timothy Jaeryang Baek 4bf6e64c35 Merge pull request #1865 from cheahjs/feat/run-i18n-in-ci
feat: run i18next in CI to format and catch translations diffs
2024-04-29 15:54:38 -07:00
Timothy Jaeryang Baek 1afc49c1e4 Merge pull request #1862 from cheahjs/feat/filter-local-rag-fetch
feat: add ENABLE_LOCAL_WEB_FETCH to protect against SSRF attacks
2024-04-29 15:51:17 -07:00
Timothy Jaeryang Baek eebabd84ef Merge pull request #1864 from cheahjs/fix/format
fix: formatting
2024-04-29 15:48:03 -07:00
Timothy Jaeryang Baek 6b04380c85 Merge pull request #1863 from cheahjs/fix/make-dev-executable
fix: make backend/fix.sh executable
2024-04-29 15:47:44 -07:00
Timothy Jaeryang Baek f3199c6510 Merge pull request #1858 from buroa/buroa/fixes
fix: various rag api calls and ui cleanup
2024-04-29 15:46:29 -07:00
Timothy Jaeryang Baek c523126bae Merge pull request #1853 from domsleee/fix/modal-closing-on-mouseup
fix: modal should not close when dragging from inside of modal
2024-04-29 15:45:46 -07:00
Jun Siang Cheah 45f34924b0 feat: run i18next in CI to format and catch translations diffs 2024-04-29 21:10:37 +01:00
Jun Siang Cheah 25af07a0c7 fix: formatting 2024-04-29 21:07:06 +01:00
Jun Siang Cheah bac28222fb fix: make backend/fix.sh executable 2024-04-29 21:01:54 +01:00
Jun Siang Cheah 1c4e63f71e feat: add ENABLE_LOCAL_WEB_FETCH to protect against SSRF attacks 2024-04-29 20:55:17 +01:00
Timothy J. Baek e8abaa8bea fix: styling 2024-04-29 12:08:36 -07:00
Steven Kreitzer 5b8fd14470 fix: various api rag results 2024-04-29 12:17:36 -05:00
Dom Slee d4339fe769 fix: modal should not close when dragging from inside of modal 2024-04-29 18:30:12 +10:00
Timothy J. Baek 877ed69004 refac: token length limit 2024-04-29 00:36:09 -07:00
Timothy Jaeryang Baek e360d57203 Merge pull request #1839 from cheahjs/fix/fluid-setting-not-reflecting-current-setting
fix: fluid stream setting not reflecting current state on load
2024-04-29 00:31:37 -07:00
Timothy Jaeryang Baek 0235b0f6f3 Merge pull request #1838 from Maximilian-Pichler/german-locale
German locale
2024-04-29 00:21:42 -07:00
Timothy Jaeryang Baek a128dfa06b Merge branch 'dev' into german-locale 2024-04-29 00:21:08 -07:00
Timothy Jaeryang Baek ffc91ba6b6 Merge pull request #1841 from Yanyutin753/translate
🎖️Added translations for Fluidly stream large external response chunks and OLED Dark
2024-04-29 00:20:02 -07:00
Yanyutin753 24c000739c 🎖️Added translations for Fluidly stream large external response chunks 2024-04-29 10:26:16 +08:00
Yanyutin753 00367ef23e 🎖️Added translations for Fluidly stream large external response chunks 2024-04-29 10:04:34 +08:00
Jun Siang Cheah 527a0b329b fix: fluid stream setting not reflecting current state on load 2024-04-29 00:06:32 +01:00
Maximilian Pichler 2e4bba2374 Update translation.json 2024-04-28 22:54:20 +02:00
Maximilian Pichler 4cafea0b97 Merge branch 'dev' into german-locale 2024-04-28 22:48:33 +02:00
Maximilian Pichler cfc9cc8a10 Merge pull request #2 from jannikstdl/german-locale
German locale -  ran i18n parser
2024-04-28 21:58:18 +02:00
Timothy Jaeryang Baek c7fa024b8f Merge pull request #1807 from insoutt/improve-es-lang
Improve 'es' translations
2024-04-28 10:27:40 -07:00
Timothy Jaeryang Baek cc50746043 Merge pull request #1821 from Yanyutin753/translate
🎖️Added translations for Playground and Archived Chats
2024-04-28 10:26:33 -07:00
Timothy Jaeryang Baek bf0ea4dc5a Merge pull request #1830 from cheahjs/feat/db-migration-tests
feat: add tests for db migration on sqlite and postgres
2024-04-28 10:26:17 -07:00
Timothy Jaeryang Baek 9ddc243c52 Merge pull request #1829 from cheahjs/fix/fluid-streaming-background
fix: fluid streaming was "pausing" when tab was not visible
2024-04-28 10:24:30 -07:00
Timothy J. Baek 9832e6edba enhancement: OpenAI API
api.together.xyz, api.replicate.com
2024-04-28 10:20:40 -07:00
Jannik Streidl afc78b0277 ran i18n parser to add the new translation keys 2024-04-28 18:47:12 +02:00
Jun Siang Cheah f561e94244 fix: refactor db migration tests 2024-04-28 17:00:31 +01:00
Jun Siang Cheah eed6c7194b fix: disable mysql tests 2024-04-28 16:55:53 +01:00
Jun Siang Cheah 167b5712ea feat: add tests for db migration on sqlite, postgres, and mysql 2024-04-28 16:55:16 +01:00
Timothy Jaeryang Baek 7bc17da534 Merge pull request #1825 from cheahjs/feat/allow-backend-to-run-without-frontend 2024-04-28 08:53:18 -07:00
Timothy Jaeryang Baek 24497592a8 Merge pull request #1827 from justinh-rahb/update-litellm 2024-04-28 08:52:31 -07:00
Jun Siang Cheah ed9e99e946 fix: fluid streaming was "pausing" when tab was not visible 2024-04-28 16:51:36 +01:00
Justin Hayes d42517b7ed Update LiteLLM (and Gunicorn) 2024-04-28 11:43:57 -04:00
Maximilian Pichler 93d4f987e7 Merge pull request #1 from jannikstdl/german-locale
German locale additions
2024-04-28 17:24:56 +02:00
Jun Siang Cheah 6a1d60b1b3 feat: warn but not exit if frontend build does not exist 2024-04-28 16:03:30 +01:00
Jannik Streidl 19843e39fd Added more german translations + added more i18n keys 2024-04-28 14:39:06 +02:00
Yanyutin753 ec18489723 🎖️Added translations for Playground and Archived Chats 2024-04-28 19:53:27 +08:00
Maximilian Pichler 9130428a55 Update translation.json 2024-04-28 13:12:41 +02:00
Maximilian Pichler 236ec28491 Update translation.json
added and corrected some translations
2024-04-28 12:58:13 +02:00
Timothy J. Baek f070e8b7f9 chore: format 2024-04-27 21:02:15 -07:00
Yanyutin753 3321a1b922 expend the image format type after the file is downloaded 2024-04-28 12:00:52 +08:00
Timothy Jaeryang Baek f935acc6bf Merge pull request #1813 from open-webui/doge
feat: much wow doge
2024-04-27 20:55:10 -07:00
Timothy J. Baek 6d90d130bd feat: doge i18n response message profile image 2024-04-27 20:54:31 -07:00
Timothy J. Baek bfd33ec0df feat: doge i18n placeholder 2024-04-27 20:53:42 -07:00
Timothy Jaeryang Baek 9af6c5300b Merge pull request #1811 from open-webui/doge
feat: doge i18n
2024-04-27 20:48:20 -07:00
Timothy J. Baek 5c887d0709 feat: doge translation 2024-04-27 20:46:52 -07:00
Timothy Jaeryang Baek c9589e2118 Merge pull request #1810 from open-webui/dev
fix: wording
2024-04-27 20:11:10 -07:00
Timothy J. Baek 75cbbbe52d fix: wording 2024-04-27 20:10:20 -07:00
Timothy Jaeryang Baek 0455b80604 Merge pull request #1809 from open-webui/dev
fix
2024-04-27 20:07:26 -07:00
Timothy J. Baek 71a75bccf5 fix 2024-04-27 20:06:07 -07:00
Timothy Jaeryang Baek 97609970b2 Merge pull request #1806 from open-webui/dev
fix
2024-04-27 19:46:05 -07:00
Timothy J. Baek 48ed701960 fix 2024-04-27 19:44:36 -07:00
Timothy Jaeryang Baek d401d77cbd Merge pull request #1805 from open-webui/dev
fix
2024-04-27 19:42:51 -07:00
Timothy J. Baek 5e97c9927b fix 2024-04-27 19:42:19 -07:00
insoutt 76660f3e44 Improve translation 2024-04-27 21:01:25 -05:00
210 changed files with 22451 additions and 7968 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ OPENAI_API_KEY=''
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
ANONYMIZED_TELEMETRY=false
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
LITELLM_LOCAL_MODEL_COST_MAP="True"
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: weekly
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: 'weekly'
+40 -18
View File
@@ -1,37 +1,56 @@
## Pull Request Checklist
# Pull Request Checklist
- [ ] **Description:** Briefly describe the changes in this pull request.
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
**Before submitting, make sure you've checked the following:**
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests for the changes?
- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
- **chore**: Refactor, cleanup, or other non-functional code changes
- **docs**: Documentation update or addition
- **feat**: Introduces a new feature or enhancement to the codebase
- **fix**: Bug fix or error correction
- **i18n**: Internationalization or localization changes
- **perf**: Performance improvement
- **refactor**: Code restructuring for better maintainability, readability, or scalability
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
- **test**: Adding missing tests or correcting existing tests
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
---
# Changelog Entry
## Description
### Description
[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
---
### Changelog Entry
- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
### Added
- [List any new features, functionalities, or additions]
### Fixed
- [List any fixes, corrections, or bug fixes]
### Changed
- [List any changes, updates, refactorings, or optimizations]
### Deprecated
- [List any deprecated functionality or features that have been removed]
### Removed
- [List any removed features, files, or deprecated functionalities]
- [List any removed features, files, or functionalities]
### Fixed
- [List any fixes, corrections, or bug fixes]
### Security
@@ -39,12 +58,15 @@
### Breaking Changes
- [List any breaking changes affecting compatibility or functionality]
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
---
### Additional Information
- [Insert any additional context, notes, or explanations for the changes]
- [Reference any related issues, commits, or other relevant information]
- [Reference any related issues, commits, or other relevant information]
### Screenshots or Videos
- [Attach any relevant screenshots or videos demonstrating the changes]
@@ -29,8 +29,29 @@ jobs:
- name: Format Frontend
run: npm run format
- name: Run i18next
run: npm run i18n:parse
- name: Check for Changes After Format
run: git diff --exit-code
- name: Build Frontend
run: npm run build
test-frontend:
name: 'Frontend Unit Tests'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Run vitest
run: npm run test:frontend
+141 -1
View File
@@ -20,7 +20,16 @@ jobs:
- name: Build and run Compose Stack
run: |
docker compose up --detach --build
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
- name: Preload Ollama model
run: |
@@ -53,3 +62,134 @@ jobs:
name: compose-logs
path: compose-logs.txt
if-no-files-found: ignore
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: yezz123/setup-uv@v4
with:
uv-venv: venv
- name: Activate virtualenv
run: |
. venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
- name: Install dependencies
run: |
uv pip install -r backend/requirements.txt
- name: Test backend with SQLite
id: sqlite
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
run: |
cd backend
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
- name: Test backend with Postgres
if: success() || steps.sqlite.conclusion == 'failure'
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
run: |
cd backend
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
curl -s http://localhost:8081/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
# - name: Test backend with MySQL
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
# env:
# WEBUI_SECRET_KEY: secret-key
# GLOBAL_LOG_LEVEL: debug
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
# run: |
# cd backend
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
# UVICORN_PID=$!
# # Wait up to 20 seconds for the server to start
# for i in {1..20}; do
# curl -s http://localhost:8083/api/config > /dev/null && break
# sleep 1
# if [ $i -eq 20 ]; then
# echo "Server failed to start"
# kill -9 $UVICORN_PID
# exit 1
# fi
# done
# # Check that the server is still running after 5 seconds
# sleep 5
# if ! kill -0 $UVICORN_PID; then
# echo "Server has stopped"
# exit 1
# fi
+4
View File
@@ -16,6 +16,10 @@ __pycache__/
# C extensions
*.so
# Pyodide distribution
static/pyodide/*
!static/pyodide/pyodide-lock.json
# Distribution / packaging
.Python
build/
+306 -6
View File
@@ -1,3 +1,11 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
kubernetes/
# Copy of .gitignore
.DS_Store
node_modules
/build
@@ -6,11 +14,303 @@ node_modules
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
# C extensions
*.so
# Ignore kubernetes files
kubernetes
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# 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/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# cypress artifacts
cypress/videos
cypress/screenshots
/static/*
+67
View File
@@ -5,6 +5,73 @@ 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.125] - 2024-05-19
### Added
- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
- **🩺 Health Check Endpoint**: Added for Docker deployment.
- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
### Changed
- **👤 Shared Chat Update**: Shared chat now includes creator user information.
## [0.1.124] - 2024-05-08
### Added
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
### Fixed
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
- **⚠️ Warning Messages**: Resolved backend warning messages.
### Changed
- **📝 Title Generation**: Limited output to 50 tokens.
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
## [0.1.123] - 2024-05-02
### Added
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
### Fixed
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
### Changed
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
## [0.1.122] - 2024-04-27
### Added
+59 -34
View File
@@ -11,6 +11,9 @@ ARG USE_CUDA_VER=cu121
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
# Override at your own risk - non-root configurations are untested
ARG UID=0
ARG GID=0
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
@@ -32,6 +35,8 @@ ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
ARG UID
ARG GID
## Basis ##
ENV ENV=prod \
@@ -51,7 +56,8 @@ ENV OLLAMA_BASE_URL="/ollama" \
ENV OPENAI_API_KEY="" \
WEBUI_SECRET_KEY="" \
SCARF_NO_ANALYTICS=true \
DO_NOT_TRACK=true
DO_NOT_TRACK=true \
ANONYMIZED_TELEMETRY=false
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
@@ -74,43 +80,58 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models"
WORKDIR /app/backend
ENV HOME /root
# Create user and group if not root
RUN if [ $UID -ne 0 ]; then \
if [ $GID -ne 0 ]; then \
addgroup --gid $GID app; \
fi; \
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
fi
RUN mkdir -p $HOME/.cache/chroma
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
# Make sure the user has access to the app and root directory
RUN chown -R $UID:$GID /app $HOME
RUN if [ "$USE_OLLAMA" = "true" ]; then \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
apt-get install -y --no-install-recommends curl && \
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
apt-get install -y --no-install-recommends curl jq && \
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
else \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
fi
# install python dependencies
COPY ./backend/requirements.txt ./requirements.txt
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
RUN pip3 install uv && \
if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
fi
@@ -120,13 +141,17 @@ RUN pip3 install uv && \
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
# copy built frontend files
COPY --from=build /app/build /app/build
COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
COPY --from=build /app/package.json /app/package.json
COPY --chown=$UID:$GID --from=build /app/build /app/build
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
# copy backend files
COPY ./backend .
COPY --chown=$UID:$GID ./backend .
EXPOSE 8080
CMD [ "bash", "start.sh"]
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
USER $UID:$GID
CMD [ "bash", "start.sh"]
+53 -17
View File
@@ -120,22 +120,66 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
> [!TIP]
> If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system.
**If Ollama is on your computer**, use this command:
### Installation with Default Configuration
```bash
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
- **If Ollama is on your computer**, use this command:
**If Ollama is on a Different Server**, use this command:
```bash
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
- **If Ollama is on a Different Server**, use this command:
```bash
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
```bash
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
- **To run Open WebUI with Nvidia GPU support**, use this command:
```bash
docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
```
### Installation for OpenAI API Usage Only
- **If you're only using OpenAI API**, use this command:
```bash
docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
### Installing Open WebUI with Bundled Ollama Support
This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup:
- **With GPU Support**:
Utilize GPU resources by running the following command:
```bash
docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
```
- **For CPU Only**:
If you're not using a GPU, use this command instead:
```bash
docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
```
Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly.
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
### Other Installation Methods
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
#### Open WebUI: Server Connection Error
If you're experiencing connection issues, its often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
@@ -146,14 +190,6 @@ If you're experiencing connection issues, its often due to the WebUI docker c
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
### Other Installation Methods
We offer various installation alternatives, including non-Docker methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
### Keeping Your Docker Installation Up-to-Date
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
+24 -11
View File
@@ -43,6 +43,9 @@ from config import (
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
AUDIO_OPENAI_API_MODEL,
AUDIO_OPENAI_API_VOICE,
AppConfig,
)
log = logging.getLogger(__name__)
@@ -57,9 +60,11 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config = AppConfig()
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -72,13 +77,17 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
model: str
speaker: str
@app.get("/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
}
@@ -89,13 +98,17 @@ async def update_openai_config(
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.OPENAI_API_BASE_URL = form_data.url
app.state.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_MODEL = form_data.model
app.state.config.OPENAI_API_VOICE = form_data.speaker
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
}
@@ -112,13 +125,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
+140 -92
View File
@@ -24,6 +24,7 @@ from utils.misc import calculate_sha256
from typing import Optional
from pydantic import BaseModel
from pathlib import Path
import mimetypes
import uuid
import base64
import json
@@ -41,6 +42,7 @@ from config import (
IMAGE_GENERATION_MODEL,
IMAGE_SIZE,
IMAGE_STEPS,
AppConfig,
)
@@ -59,26 +61,31 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.ENGINE = IMAGE_GENERATION_ENGINE
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.config = AppConfig()
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
app.state.MODEL = IMAGE_GENERATION_MODEL
app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.IMAGE_SIZE = IMAGE_SIZE
app.state.IMAGE_STEPS = IMAGE_STEPS
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class ConfigUpdateForm(BaseModel):
@@ -88,9 +95,12 @@ class ConfigUpdateForm(BaseModel):
@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}
app.state.config.ENGINE = form_data.engine
app.state.config.ENABLED = form_data.enabled
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class EngineUrlUpdateForm(BaseModel):
@@ -101,8 +111,8 @@ class EngineUrlUpdateForm(BaseModel):
@app.get("/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,
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
}
@@ -112,29 +122,29 @@ async def update_engine_url(
):
if form_data.AUTOMATIC1111_BASE_URL == None:
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
else:
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
try:
r = requests.head(url)
app.state.AUTOMATIC1111_BASE_URL = url
app.state.config.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
app.state.config.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
app.state.config.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,
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"status": True,
}
@@ -147,8 +157,8 @@ class OpenAIConfigUpdateForm(BaseModel):
@app.get("/openai/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@@ -159,13 +169,13 @@ async def update_openai_config(
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.OPENAI_API_BASE_URL = form_data.url
app.state.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@@ -175,7 +185,7 @@ class ImageSizeUpdateForm(BaseModel):
@app.get("/size")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
@app.post("/size/update")
@@ -184,9 +194,9 @@ async def update_image_size(
):
pattern = r"^\d+x\d+$" # Regular expression pattern
if re.match(pattern, form_data.size):
app.state.IMAGE_SIZE = form_data.size
app.state.config.IMAGE_SIZE = form_data.size
return {
"IMAGE_SIZE": app.state.IMAGE_SIZE,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"status": True,
}
else:
@@ -202,7 +212,7 @@ class ImageStepsUpdateForm(BaseModel):
@app.get("/steps")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
@app.post("/steps/update")
@@ -210,9 +220,9 @@ async def update_image_size(
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
):
if form_data.steps >= 0:
app.state.IMAGE_STEPS = form_data.steps
app.state.config.IMAGE_STEPS = form_data.steps
return {
"IMAGE_STEPS": app.state.IMAGE_STEPS,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
"status": True,
}
else:
@@ -225,14 +235,14 @@ async def update_image_size(
@app.get("/models")
def get_models(user=Depends(get_current_user)):
try:
if app.state.ENGINE == "openai":
if app.state.config.ENGINE == "openai":
return [
{"id": "dall-e-2", "name": "DALL·E 2"},
{"id": "dall-e-3", "name": "DALL·E 3"},
]
elif app.state.ENGINE == "comfyui":
elif app.state.config.ENGINE == "comfyui":
r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
info = r.json()
return list(
@@ -244,7 +254,7 @@ def get_models(user=Depends(get_current_user)):
else:
r = requests.get(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
)
models = r.json()
return list(
@@ -254,23 +264,29 @@ def get_models(user=Depends(get_current_user)):
)
)
except Exception as e:
app.state.ENABLED = False
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@app.get("/models/default")
async def get_default_model(user=Depends(get_admin_user)):
try:
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 ""}
if app.state.config.ENGINE == "openai":
return {
"model": (
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
)
}
elif app.state.config.ENGINE == "comfyui":
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
else:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
)
options = r.json()
return {"model": options["sd_model_checkpoint"]}
except Exception as e:
app.state.ENABLED = False
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@@ -279,20 +295,20 @@ class UpdateModelForm(BaseModel):
def set_model_handler(model: str):
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
if app.state.config.ENGINE in ["openai", "comfyui"]:
app.state.config.MODEL = model
return app.state.config.MODEL
else:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
r = requests.get(
url=f"{app.state.config.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
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
)
return options
@@ -315,35 +331,61 @@ class GenerateImageForm(BaseModel):
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)
image_id = str(uuid.uuid4())
# Write the image data to a file
with open(file_path, "wb") as f:
f.write(img_data)
if "," in b64_str:
header, encoded = b64_str.split(",", 1)
mime_type = header.split(";")[0]
img_data = base64.b64decode(encoded)
image_format = mimetypes.guess_extension(mime_type)
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
with open(file_path, "wb") as f:
f.write(img_data)
return image_filename
else:
image_filename = f"{image_id}.png"
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
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_filename
return image_id
except Exception as e:
log.error(f"Error saving image: {e}")
log.exception(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()
if r.headers["content-type"].split("/")[0] == "image":
with open(file_path, "wb") as image_file:
image_file.write(r.content)
mime_type = r.headers["content-type"]
image_format = mimetypes.guess_extension(mime_type)
if not image_format:
raise ValueError("Could not determine image type from MIME type")
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
with open(file_path, "wb") as image_file:
for chunk in r.iter_content(chunk_size=8192):
image_file.write(chunk)
return image_filename
else:
log.error(f"Url does not point to an image.")
return None
return image_id
except Exception as e:
log.exception(f"Error saving image: {e}")
return None
@@ -355,26 +397,32 @@ def generate_image(
user=Depends(get_current_user),
):
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
r = None
try:
if app.state.ENGINE == "openai":
if app.state.config.ENGINE == "openai":
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
data = {
"model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
"model": (
app.state.config.MODEL
if app.state.config.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,
"size": (
form_data.size if form_data.size else app.state.config.IMAGE_SIZE
),
"response_format": "b64_json",
}
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
json=data,
headers=headers,
)
@@ -385,16 +433,16 @@ def generate_image(
images = []
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")
image_filename = save_b64_image(image["b64_json"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data, f)
return images
elif app.state.ENGINE == "comfyui":
elif app.state.config.ENGINE == "comfyui":
data = {
"prompt": form_data.prompt,
@@ -403,28 +451,28 @@ def generate_image(
"n": form_data.n,
}
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
if app.state.config.IMAGE_STEPS is not None:
data["steps"] = app.state.config.IMAGE_STEPS
if form_data.negative_prompt != None:
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
data = ImageGenerationPayload(**data)
res = comfyui_generate_image(
app.state.MODEL,
app.state.config.MODEL,
data,
user.id,
app.state.COMFYUI_BASE_URL,
app.state.config.COMFYUI_BASE_URL,
)
log.debug(f"res: {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")
image_filename = save_url_image(image["url"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f)
@@ -442,14 +490,14 @@ def generate_image(
"height": height,
}
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
if app.state.config.IMAGE_STEPS is not None:
data["steps"] = app.state.config.IMAGE_STEPS
if form_data.negative_prompt != None:
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
)
@@ -460,9 +508,9 @@ def generate_image(
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")
image_filename = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump({**data, "info": res["info"]}, f)
+19 -12
View File
@@ -1,4 +1,5 @@
import sys
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from fastapi.routing import APIRoute
@@ -36,13 +37,26 @@ from config import (
LITELLM_PROXY_HOST,
)
import warnings
warnings.simplefilter("ignore")
from litellm.utils import get_llm_provider
import asyncio
import subprocess
import yaml
app = FastAPI()
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("startup_event")
# TODO: Check config.yaml file and create one
asyncio.create_task(start_litellm_background())
yield
app = FastAPI(lifespan=lifespan)
origins = ["*"]
@@ -61,6 +75,10 @@ with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config
@@ -137,17 +155,6 @@ async def shutdown_litellm_background():
background_process = None
@app.on_event("startup")
async def startup_event():
log.info("startup_event")
# TODO: Check config.yaml file and create one
asyncio.create_task(start_litellm_background())
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@app.get("/")
async def get_status():
return {"status": True}
+121 -37
View File
@@ -25,13 +25,19 @@ import uuid
import aiohttp
import asyncio
import logging
import time
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 utils.utils import (
decode_token,
get_current_user,
get_verified_user,
get_admin_user,
)
from config import (
@@ -40,6 +46,7 @@ from config import (
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
UPLOAD_DIR,
AppConfig,
)
from utils.misc import calculate_sha256
@@ -55,11 +62,12 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.config = AppConfig()
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
app.state.MODELS = {}
@@ -90,7 +98,7 @@ async def get_status():
@app.get("/urls")
async def get_ollama_api_urls(user=Depends(get_admin_user)):
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
class UrlUpdateForm(BaseModel):
@@ -99,10 +107,10 @@ class UrlUpdateForm(BaseModel):
@app.post("/urls/update")
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
app.state.OLLAMA_BASE_URLS = form_data.urls
app.state.config.OLLAMA_BASE_URLS = form_data.urls
log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}")
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}")
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
@app.get("/cancel/{request_id}")
@@ -116,8 +124,9 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user))
async def fetch_url(url):
timeout = aiohttp.ClientTimeout(total=5)
try:
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
return await response.json()
except Exception as e:
@@ -147,7 +156,7 @@ def merge_models_lists(model_lists):
async def get_all_models():
log.info("get_all_models()")
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS]
responses = await asyncio.gather(*tasks)
models = {
@@ -164,23 +173,24 @@ async def get_all_models():
@app.get("/api/tags")
@app.get("/api/tags/{url_idx}")
async def get_ollama_tags(
url_idx: Optional[int] = None, user=Depends(get_current_user)
url_idx: Optional[int] = None, user=Depends(get_verified_user)
):
if url_idx == None:
models = await get_all_models()
if app.state.ENABLE_MODEL_FILTER:
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models["models"] = list(
filter(
lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
lambda model: model["name"]
in app.state.config.MODEL_FILTER_LIST,
models["models"],
)
)
return models
return models
else:
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
try:
r = requests.request(method="GET", url=f"{url}/api/tags")
r.raise_for_status()
@@ -210,7 +220,9 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
if url_idx == None:
# returns lowest version
tasks = [fetch_url(f"{url}/api/version") for url in app.state.OLLAMA_BASE_URLS]
tasks = [
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
]
responses = await asyncio.gather(*tasks)
responses = list(filter(lambda x: x is not None, responses))
@@ -229,7 +241,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
)
else:
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
try:
r = requests.request(method="GET", url=f"{url}/api/version")
r.raise_for_status()
@@ -261,7 +273,7 @@ class ModelNameForm(BaseModel):
async def pull_model(
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
):
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
@@ -349,7 +361,7 @@ async def push_model(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.debug(f"url: {url}")
r = None
@@ -411,7 +423,7 @@ async def create_model(
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
):
log.debug(f"form_data: {form_data}")
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
@@ -484,7 +496,7 @@ async def copy_model(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
@@ -531,7 +543,7 @@ async def delete_model(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
@@ -563,7 +575,7 @@ async def delete_model(
@app.post("/api/show")
async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_user)):
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
if form_data.name not in app.state.MODELS:
raise HTTPException(
status_code=400,
@@ -571,7 +583,7 @@ 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]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
@@ -612,7 +624,7 @@ class GenerateEmbeddingsForm(BaseModel):
async def generate_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
model = form_data.model
@@ -628,7 +640,7 @@ async def generate_embeddings(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
@@ -678,7 +690,7 @@ def generate_ollama_embeddings(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
@@ -730,7 +742,7 @@ class GenerateCompletionForm(BaseModel):
async def generate_completion(
form_data: GenerateCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -747,7 +759,7 @@ async def generate_completion(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
@@ -833,7 +845,7 @@ class GenerateChatCompletionForm(BaseModel):
async def generate_chat_completion(
form_data: GenerateChatCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -850,7 +862,7 @@ async def generate_chat_completion(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
@@ -942,7 +954,7 @@ class OpenAIChatCompletionForm(BaseModel):
async def generate_openai_chat_completion(
form_data: OpenAIChatCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -959,7 +971,7 @@ async def generate_openai_chat_completion(
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
@@ -1026,6 +1038,76 @@ async def generate_openai_chat_completion(
)
@app.get("/v1/models")
@app.get("/v1/models/{url_idx}")
async def get_openai_models(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx == None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models["models"] = list(
filter(
lambda model: model["name"]
in app.state.config.MODEL_FILTER_LIST,
models["models"],
)
)
return {
"data": [
{
"id": model["model"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in models["models"]
],
"object": "list",
}
else:
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
try:
r = requests.request(method="GET", url=f"{url}/api/tags")
r.raise_for_status()
models = r.json()
return {
"data": [
{
"id": model["model"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in models["models"]
],
"object": "list",
}
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
error_detail = f"Ollama: {e}"
raise HTTPException(
status_code=r.status_code if r else 500,
detail=error_detail,
)
class UrlForm(BaseModel):
url: str
@@ -1123,7 +1205,7 @@ async def download_model(
if url_idx == None:
url_idx = 0
url = app.state.OLLAMA_BASE_URLS[url_idx]
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
file_name = parse_huggingface_url(form_data.url)
@@ -1142,7 +1224,7 @@ async def download_model(
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]
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
file_path = f"{UPLOAD_DIR}/{file.filename}"
@@ -1207,7 +1289,7 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
# 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]
# url = app.state.config.OLLAMA_BASE_URLS[url_idx]
# file_location = os.path.join(UPLOAD_DIR, file.filename)
# total_size = file.size
@@ -1241,8 +1323,10 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
@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]
async def deprecated_proxy(
path: str, request: Request, user=Depends(get_verified_user)
):
url = app.state.config.OLLAMA_BASE_URLS[0]
target_url = f"{url}/{path}"
body = await request.body()
+64 -29
View File
@@ -21,11 +21,13 @@ from utils.utils import (
)
from config import (
SRC_LOG_LEVELS,
ENABLE_OPENAI_API,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
AppConfig,
)
from typing import List, Optional
@@ -45,11 +47,16 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
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.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
app.state.MODELS = {}
@@ -65,6 +72,21 @@ async def check_url(request: Request, call_next):
return response
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
class OpenAIConfigForm(BaseModel):
enable_openai_api: Optional[bool] = None
@app.post("/config/update")
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
class UrlsUpdateForm(BaseModel):
urls: List[str]
@@ -75,32 +97,32 @@ class KeysUpdateForm(BaseModel):
@app.get("/urls")
async def get_openai_urls(user=Depends(get_admin_user)):
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.post("/urls/update")
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
await get_all_models()
app.state.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.get("/keys")
async def get_openai_keys(user=Depends(get_admin_user)):
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
return {"OPENAI_API_KEYS": app.state.config.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.state.config.OPENAI_API_KEYS = form_data.keys
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
@app.post("/audio/speech")
async def speech(request: Request, user=Depends(get_verified_user)):
idx = None
try:
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
body = await request.body()
name = hashlib.sha256(body).hexdigest()
@@ -114,13 +136,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
headers["Content-Type"] = "application/json"
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
headers["HTTP-Referer"] = "https://openwebui.com/"
headers["X-Title"] = "Open WebUI"
r = None
try:
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
data=body,
headers=headers,
stream=True,
@@ -159,11 +183,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async def fetch_url(url, key):
timeout = aiohttp.ClientTimeout(total=5)
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()
if key != "":
headers = {"Authorization": f"Bearer {key}"}
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
return await response.json()
else:
return None
except Exception as e:
# Handle connection error here
log.error(f"Connection error: {e}")
@@ -171,6 +199,7 @@ async def fetch_url(url, key):
def merge_models_lists(model_lists):
log.info(f"merge_models_lists {model_lists}")
merged_list = []
for idx, models in enumerate(model_lists):
@@ -179,7 +208,8 @@ def merge_models_lists(model_lists):
[
{**model, "urlIdx": idx}
for model in models
if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
if "api.openai.com"
not in app.state.config.OPENAI_API_BASE_URLS[idx]
or "gpt" in model["id"]
]
)
@@ -190,23 +220,28 @@ def merge_models_lists(model_lists):
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] == "":
if (
len(app.state.config.OPENAI_API_KEYS) == 1
and app.state.config.OPENAI_API_KEYS[0] == ""
) or not app.state.config.ENABLE_OPENAI_API:
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)
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
responses = await asyncio.gather(*tasks)
log.info(f"get_all_models:responses() {responses}")
models = {
"data": merge_models_lists(
list(
map(
lambda response: (
response["data"]
if response and "data" in response
else None
if (response and "data" in response)
else (response if isinstance(response, list) else None)
),
responses,
)
@@ -225,18 +260,18 @@ async def get_all_models():
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.ENABLE_MODEL_FILTER:
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models["data"] = list(
filter(
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
models["data"],
)
)
return models
return models
else:
url = app.state.OPENAI_API_BASE_URLS[url_idx]
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
r = None
@@ -300,8 +335,8 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
except json.JSONDecodeError as 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]
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
+257 -100
View File
@@ -28,9 +28,15 @@ from langchain_community.document_loaders import (
UnstructuredXMLLoader,
UnstructuredRSTLoader,
UnstructuredExcelLoader,
YoutubeLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
import validators
import urllib.parse
import socket
from pydantic import BaseModel
from typing import Optional
import mimetypes
@@ -63,6 +69,7 @@ from utils.misc import (
from utils.utils import get_current_user, get_admin_user
from config import (
ENV,
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
@@ -73,6 +80,7 @@ from config import (
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
RAG_RERANKING_MODEL,
PDF_EXTRACT_IMAGES,
RAG_RERANKING_MODEL_AUTO_UPDATE,
@@ -84,6 +92,9 @@ from config import (
CHUNK_SIZE,
CHUNK_OVERLAP,
RAG_TEMPLATE,
ENABLE_RAG_LOCAL_WEB_FETCH,
YOUTUBE_LOADER_LANGUAGE,
AppConfig,
)
from constants import ERROR_MESSAGES
@@ -93,30 +104,40 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
app = FastAPI()
app.state.TOP_K = RAG_TOP_K
app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config = AppConfig()
app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.TOP_K = RAG_TOP_K
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.config.CHUNK_SIZE = CHUNK_SIZE
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.YOUTUBE_LOADER_TRANSLATION = None
def update_embedding_model(
embedding_model: str,
update_model: bool = False,
):
if embedding_model and app.state.RAG_EMBEDDING_ENGINE == "":
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
get_model_path(embedding_model, update_model),
device=DEVICE_TYPE,
@@ -141,22 +162,22 @@ def update_reranking_model(
update_embedding_model(
app.state.RAG_EMBEDDING_MODEL,
app.state.config.RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
)
update_reranking_model(
app.state.RAG_RERANKING_MODEL,
app.state.config.RAG_RERANKING_MODEL,
RAG_RERANKING_MODEL_AUTO_UPDATE,
)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
origins = ["*"]
@@ -175,7 +196,7 @@ class CollectionNameForm(BaseModel):
collection_name: Optional[str] = "test"
class StoreWebForm(CollectionNameForm):
class UrlForm(CollectionNameForm):
url: str
@@ -183,12 +204,12 @@ class StoreWebForm(CollectionNameForm):
async def get_status():
return {
"status": True,
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"template": app.state.RAG_TEMPLATE,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"reranking_model": app.state.RAG_RERANKING_MODEL,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
"template": app.state.config.RAG_TEMPLATE,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
@@ -196,18 +217,21 @@ async def get_status():
async def get_embedding_config(user=Depends(get_admin_user)):
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
},
}
@app.get("/reranking")
async def get_reraanking_config(user=Depends(get_admin_user)):
return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
return {
"status": True,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
class OpenAIConfigForm(BaseModel):
@@ -226,34 +250,34 @@ async def update_embedding_config(
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
)
try:
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
if form_data.openai_config != None:
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.OPENAI_API_KEY = form_data.openai_config.key
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
},
}
except Exception as e:
@@ -273,16 +297,16 @@ async def update_reranking_config(
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating reranking model: {app.state.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
)
try:
app.state.RAG_RERANKING_MODEL = form_data.reranking_model
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
return {
"status": True,
"reranking_model": app.state.RAG_RERANKING_MODEL,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
except Exception as e:
log.exception(f"Problem updating reranking model: {e}")
@@ -296,10 +320,15 @@ async def update_reranking_config(
async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
}
@@ -309,23 +338,67 @@ class ChunkParamUpdateForm(BaseModel):
chunk_overlap: int
class YoutubeLoaderConfig(BaseModel):
language: List[str]
translation: Optional[str] = None
class ConfigUpdateForm(BaseModel):
pdf_extract_images: bool
chunk: ChunkParamUpdateForm
pdf_extract_images: Optional[bool] = None
chunk: Optional[ChunkParamUpdateForm] = None
web_loader_ssl_verification: Optional[bool] = None
youtube: Optional[YoutubeLoaderConfig] = None
@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
app.state.config.PDF_EXTRACT_IMAGES = (
form_data.pdf_extract_images
if form_data.pdf_extract_images is not None
else app.state.config.PDF_EXTRACT_IMAGES
)
app.state.config.CHUNK_SIZE = (
form_data.chunk.chunk_size
if form_data.chunk is not None
else app.state.config.CHUNK_SIZE
)
app.state.config.CHUNK_OVERLAP = (
form_data.chunk.chunk_overlap
if form_data.chunk is not None
else app.state.config.CHUNK_OVERLAP
)
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web_loader_ssl_verification
if form_data.web_loader_ssl_verification != None
else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.YOUTUBE_LOADER_LANGUAGE = (
form_data.youtube.language
if form_data.youtube is not None
else app.state.config.YOUTUBE_LOADER_LANGUAGE
)
app.state.YOUTUBE_LOADER_TRANSLATION = (
form_data.youtube.translation
if form_data.youtube is not None
else app.state.YOUTUBE_LOADER_TRANSLATION
)
return {
"status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
}
@@ -334,7 +407,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
async def get_rag_template(user=Depends(get_current_user)):
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"template": app.state.config.RAG_TEMPLATE,
}
@@ -342,10 +415,10 @@ async def get_rag_template(user=Depends(get_current_user)):
async def get_query_settings(user=Depends(get_admin_user)):
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
"r": app.state.RELEVANCE_THRESHOLD,
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
"template": app.state.config.RAG_TEMPLATE,
"k": app.state.config.TOP_K,
"r": app.state.config.RELEVANCE_THRESHOLD,
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
}
@@ -360,16 +433,20 @@ class QuerySettingsForm(BaseModel):
async def update_query_settings(
form_data: QuerySettingsForm, user=Depends(get_admin_user)
):
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
app.state.TOP_K = form_data.k if form_data.k else 4
app.state.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
app.state.config.RAG_TEMPLATE = (
form_data.template if form_data.template else RAG_TEMPLATE
)
app.state.config.TOP_K = form_data.k if form_data.k else 4
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
form_data.hybrid if form_data.hybrid else False
)
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
"r": app.state.RELEVANCE_THRESHOLD,
"hybrid": app.state.ENABLE_RAG_HYBRID_SEARCH,
"template": app.state.config.RAG_TEMPLATE,
"k": app.state.config.TOP_K,
"r": app.state.config.RELEVANCE_THRESHOLD,
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
}
@@ -387,21 +464,23 @@ def query_doc_handler(
user=Depends(get_current_user),
):
try:
if app.state.ENABLE_RAG_HYBRID_SEARCH:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
return query_doc_with_hybrid_search(
collection_name=form_data.collection_name,
query=form_data.query,
embeddings_function=app.state.EMBEDDING_FUNCTION,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
k=form_data.k if form_data.k else app.state.TOP_K,
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
r=(
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
),
)
else:
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
embeddings_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
)
except Exception as e:
log.exception(e)
@@ -425,21 +504,23 @@ def query_collection_handler(
user=Depends(get_current_user),
):
try:
if app.state.ENABLE_RAG_HYBRID_SEARCH:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
return query_collection_with_hybrid_search(
collection_names=form_data.collection_names,
query=form_data.query,
embeddings_function=app.state.EMBEDDING_FUNCTION,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
k=form_data.k if form_data.k else app.state.TOP_K,
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
r=(
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
),
)
else:
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
embeddings_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
)
except Exception as e:
@@ -450,11 +531,15 @@ def query_collection_handler(
)
@app.post("/web")
def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
try:
loader = WebBaseLoader(form_data.url)
loader = YoutubeLoader.from_youtube_url(
form_data.url,
add_video_info=True,
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
)
data = loader.load()
collection_name = form_data.collection_name
@@ -475,11 +560,70 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
)
@app.post("/web")
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try:
loader = get_web_loader(
form_data.url,
verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
)
data = loader.load()
collection_name = form_data.collection_name
if collection_name == "":
collection_name = calculate_sha256_string(form_data.url)[:63]
store_data_in_vector_db(data, collection_name, overwrite=True)
return {
"status": True,
"collection_name": collection_name,
"filename": form_data.url,
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def get_web_loader(url: str, verify_ssl: bool = True):
# Check if the URL is valid
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(url, verify_ssl=verify_ssl)
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
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,
chunk_size=app.state.config.CHUNK_SIZE,
chunk_overlap=app.state.config.CHUNK_OVERLAP,
add_start_index=True,
)
@@ -496,8 +640,8 @@ 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,
chunk_size=app.state.config.CHUNK_SIZE,
chunk_overlap=app.state.config.CHUNK_OVERLAP,
add_start_index=True,
)
docs = text_splitter.create_documents([text], metadatas=[metadata])
@@ -520,11 +664,11 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
collection = CHROMA_CLIENT.create_collection(name=collection_name)
embedding_func = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
@@ -532,7 +676,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
ids=[str(uuid.uuid4()) for _ in texts],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
@@ -598,7 +742,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
]
if file_ext == "pdf":
loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
@@ -806,3 +952,14 @@ def reset(user=Depends(get_admin_user)) -> bool:
log.exception(e)
return True
if ENV == "dev":
@app.get("/ef")
async def get_embeddings():
return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
@app.get("/ef/{text}")
async def get_embeddings_text(text: str):
return {"result": app.state.EMBEDDING_FUNCTION(text)}
+48 -37
View File
@@ -35,6 +35,7 @@ def query_doc(
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
query_embeddings = embedding_function(query)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
@@ -52,7 +53,7 @@ def query_doc_with_hybrid_search(
embedding_function,
k: int,
reranking_function,
r: int,
r: float,
):
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
@@ -76,9 +77,9 @@ def query_doc_with_hybrid_search(
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k,
reranking_function=reranking_function,
r_score=r,
top_n=k,
)
compression_retriever = ContextualCompressionRetriever(
@@ -91,6 +92,7 @@ def query_doc_with_hybrid_search(
"documents": [[d.page_content for d in result]],
"metadatas": [[d.metadata for d in result]],
}
log.info(f"query_doc_with_hybrid_search:result {result}")
return result
except Exception as e:
@@ -167,7 +169,6 @@ def query_collection_with_hybrid_search(
reranking_function,
r: float,
):
results = []
for collection_name in collection_names:
try:
@@ -182,7 +183,6 @@ def query_collection_with_hybrid_search(
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k=k, reverse=True)
@@ -271,14 +271,14 @@ def rag_messages(
for doc in docs:
context = None
collection = doc.get("collection_name")
if collection:
collection = [collection]
else:
collection = doc.get("collection_names", [])
collection_names = (
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
)
collection = set(collection).difference(extracted_collections)
if not collection:
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted")
continue
@@ -288,11 +288,7 @@ def rag_messages(
else:
if hybrid_search:
context = query_collection_with_hybrid_search(
collection_names=(
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
collection_names=collection_names,
query=query,
embedding_function=embedding_function,
k=k,
@@ -301,11 +297,7 @@ def rag_messages(
)
else:
context = query_collection(
collection_names=(
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
collection_names=collection_names,
query=query,
embedding_function=embedding_function,
k=k,
@@ -315,14 +307,31 @@ def rag_messages(
context = None
if context:
relevant_contexts.append(context)
relevant_contexts.append({**context, "source": doc})
extracted_collections.extend(collection)
extracted_collections.extend(collection_names)
context_string = ""
citations = []
for context in relevant_contexts:
items = context["documents"][0]
context_string += "\n\n".join(items)
try:
if "documents" in context:
context_string += "\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
if "metadatas" in context:
citations.append(
{
"source": context["source"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
)
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
@@ -351,7 +360,7 @@ def rag_messages(
messages[last_user_message_idx] = new_user_message
return messages
return messages, citations
def get_model_path(model: str, update_model: bool = False):
@@ -443,13 +452,15 @@ class ChromaRetriever(BaseRetriever):
metadatas = results["metadatas"][0]
documents = results["documents"][0]
return [
Document(
metadata=metadatas[idx],
page_content=documents[idx],
results = []
for idx in range(len(ids)):
results.append(
Document(
metadata=metadatas[idx],
page_content=documents[idx],
)
)
for idx in range(len(ids))
]
return results
import operator
@@ -465,9 +476,9 @@ from sentence_transformers import util
class RerankCompressor(BaseDocumentCompressor):
embedding_function: Any
top_n: int
reranking_function: Any
r_score: float
top_n: int
class Config:
extra = Extra.forbid
@@ -479,7 +490,9 @@ class RerankCompressor(BaseDocumentCompressor):
query: str,
callbacks: Optional[Callbacks] = None,
) -> Sequence[Document]:
if self.reranking_function:
reranking = self.reranking_function is not None
if reranking:
scores = self.reranking_function.predict(
[(query, doc.page_content) for doc in documents]
)
@@ -496,9 +509,7 @@ class RerankCompressor(BaseDocumentCompressor):
(d, s) for d, s in docs_with_scores if s >= self.r_score
]
reverse = self.reranking_function is not None
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=reverse)
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
final_results = []
for doc, doc_score in result[: self.top_n]:
metadata = doc.metadata
@@ -65,7 +65,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chat"
@@ -76,7 +76,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chatidtag"
@@ -90,7 +90,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "document"
@@ -101,7 +101,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "modelfile"
@@ -113,7 +113,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "prompt"
@@ -135,7 +135,7 @@ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "user"
@@ -47,7 +47,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
# Populate the new fields from an existing 'timestamp' field
migrator.sql(
"UPDATE user SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL"
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
)
# Now that the data has been copied, remove the original 'timestamp' field
@@ -70,7 +70,7 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql("UPDATE user SET timestamp = created_at")
migrator.sql('UPDATE "user" SET timestamp = created_at')
# Remove the created_at and updated_at fields
migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
@@ -0,0 +1,53 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Memory(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
content = pw.TextField(null=False)
updated_at = pw.BigIntegerField(null=False)
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "memory"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("memory")
+18 -9
View File
@@ -9,6 +9,7 @@ from apps.web.routers import (
modelfiles,
prompts,
configs,
memories,
utils,
)
from config import (
@@ -21,22 +22,27 @@ from config import (
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
JWT_EXPIRES_IN,
AppConfig,
)
app = FastAPI()
origins = ["*"]
app.state.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.JWT_EXPIRES_IN = "-1"
app.state.config = AppConfig()
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.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -48,9 +54,12 @@ app.add_middleware(
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@@ -61,6 +70,6 @@ async def get_status():
return {
"status": True,
"auth": WEBUI_AUTH,
"default_models": app.state.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}
+4
View File
@@ -89,6 +89,10 @@ class SignupForm(BaseModel):
profile_image_url: Optional[str] = "/user.png"
class AddUserForm(SignupForm):
role: Optional[str] = "pending"
class AuthsTable:
def __init__(self, db):
self.db = db
+118
View File
@@ -0,0 +1,118 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from apps.web.internal.db import DB
from apps.web.models.chats import Chats
import time
import uuid
####################
# Memory DB Schema
####################
class Memory(Model):
id = CharField(unique=True)
user_id = CharField()
content = TextField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta:
database = DB
class MemoryModel(BaseModel):
id: str
user_id: str
content: str
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
####################
# Forms
####################
class MemoriesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Memory])
def insert_new_memory(
self,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
id = str(uuid.uuid4())
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory.create(**memory.model_dump())
if result:
return memory
else:
return None
def get_memories(self) -> List[MemoryModel]:
try:
memories = Memory.select()
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
try:
memories = Memory.select().where(Memory.user_id == user_id)
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
try:
memory = Memory.get(Memory.id == id)
return MemoryModel(**model_to_dict(memory))
except:
return None
def delete_memory_by_id(self, id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.user_id == user_id)
query.execute()
return True
except:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
query.execute()
return True
except:
return False
Memories = MemoriesTable(DB)
+7
View File
@@ -123,6 +123,13 @@ class UsersTable:
def get_num_users(self) -> Optional[int]:
return User.select().count()
def get_first_user(self) -> UserModel:
try:
user = User.select().order_by(User.created_at).first()
return UserModel(**model_to_dict(user))
except:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
query = User.update(role=role).where(User.id == id)
+82 -18
View File
@@ -1,16 +1,19 @@
import logging
from fastapi import Request
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi import APIRouter
from pydantic import BaseModel
import re
import uuid
import csv
from apps.web.models.auths import (
SigninForm,
SignupForm,
AddUserForm,
UpdateProfileForm,
UpdatePasswordForm,
UserResponse,
@@ -30,7 +33,7 @@ from utils.utils import (
from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
router = APIRouter()
@@ -115,13 +118,29 @@ async def signin(request: Request, form_data: SigninForm):
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
elif WEBUI_AUTH == False:
admin_email = "admin@localhost"
admin_password = "admin"
if Users.get_user_by_email(admin_email.lower()):
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
if Users.get_num_users() != 0:
raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
await signup(
request,
SignupForm(email=admin_email, password=admin_password, name="User"),
)
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user:
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
return {
@@ -144,7 +163,7 @@ async def signin(request: Request, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, form_data: SignupForm):
if not request.app.state.ENABLE_SIGNUP:
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
@@ -161,7 +180,7 @@ async def signup(request: Request, form_data: SignupForm):
role = (
"admin"
if Users.get_num_users() == 0
else request.app.state.DEFAULT_USER_ROLE
else request.app.state.config.DEFAULT_USER_ROLE
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
@@ -175,13 +194,13 @@ async def signup(request: Request, form_data: SignupForm):
if user:
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# response.set_cookie(key='token', value=token, httponly=True)
if request.app.state.WEBHOOK_URL:
if request.app.state.config.WEBHOOK_URL:
post_webhook(
request.app.state.WEBHOOK_URL,
request.app.state.config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
"action": "signup",
@@ -205,6 +224,51 @@ async def signup(request: Request, form_data: SignupForm):
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################
# AddUser
############################
@router.post("/add", response_model=SigninResponse)
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
if not validate_email_format(form_data.email.lower()):
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
)
if Users.get_user_by_email(form_data.email.lower()):
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try:
print(form_data)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
hashed,
form_data.name,
form_data.profile_image_url,
form_data.role,
)
if user:
token = create_token(data={"id": user.id})
return {
"token": token,
"token_type": "Bearer",
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################
# ToggleSignUp
############################
@@ -212,13 +276,13 @@ async def signup(request: Request, form_data: SignupForm):
@router.get("/signup/enabled", response_model=bool)
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
return request.app.state.ENABLE_SIGNUP
return request.app.state.config.ENABLE_SIGNUP
@router.get("/signup/enabled/toggle", response_model=bool)
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP
return request.app.state.ENABLE_SIGNUP
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
return request.app.state.config.ENABLE_SIGNUP
############################
@@ -228,7 +292,7 @@ async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
@router.get("/signup/user/role")
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
return request.app.state.DEFAULT_USER_ROLE
return request.app.state.config.DEFAULT_USER_ROLE
class UpdateRoleForm(BaseModel):
@@ -240,8 +304,8 @@ async def update_default_user_role(
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
):
if form_data.role in ["pending", "user", "admin"]:
request.app.state.DEFAULT_USER_ROLE = form_data.role
return request.app.state.DEFAULT_USER_ROLE
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
return request.app.state.config.DEFAULT_USER_ROLE
############################
@@ -251,7 +315,7 @@ async def update_default_user_role(
@router.get("/token/expires")
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
return request.app.state.JWT_EXPIRES_IN
return request.app.state.config.JWT_EXPIRES_IN
class UpdateJWTExpiresDurationForm(BaseModel):
@@ -268,10 +332,10 @@ async def update_token_expires_duration(
# Check if the input string matches the pattern
if re.match(pattern, form_data.duration):
request.app.state.JWT_EXPIRES_IN = form_data.duration
return request.app.state.JWT_EXPIRES_IN
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
return request.app.state.config.JWT_EXPIRES_IN
else:
return request.app.state.JWT_EXPIRES_IN
return request.app.state.config.JWT_EXPIRES_IN
############################
+76 -66
View File
@@ -58,7 +58,7 @@ async def delete_all_user_chats(request: Request, user=Depends(get_current_user)
if (
user.role == "user"
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -93,6 +93,31 @@ async def get_archived_session_user_chat_list(
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# GetChats
############################
@@ -141,6 +166,55 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
)
############################
# GetChatsByTags
############################
class TagNameForm(BaseModel):
name: str
skip: Optional[int] = 0
limit: Optional[int] = 50
@router.post("/tags", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_current_user)
):
print(form_data)
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
form_data.name, user.id
)
]
chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
return chats
############################
# GetAllTags
############################
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChatById
############################
@@ -192,7 +266,7 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
result = Chats.delete_chat_by_id(id)
return result
else:
if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
@@ -274,70 +348,6 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# GetAllTags
############################
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChatsByTags
############################
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
]
chats = Chats.get_chat_list_by_chat_ids(chat_ids, skip, limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
return chats
############################
# GetChatTagsById
############################
+4 -4
View File
@@ -44,8 +44,8 @@ class SetDefaultSuggestionsForm(BaseModel):
async def set_global_default_models(
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
):
request.app.state.DEFAULT_MODELS = form_data.models
return request.app.state.DEFAULT_MODELS
request.app.state.config.DEFAULT_MODELS = form_data.models
return request.app.state.config.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
@@ -55,5 +55,5 @@ async def set_global_default_suggestions(
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
+145
View File
@@ -0,0 +1,145 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import logging
from apps.web.models.memories import Memories, MemoryModel
from utils.utils import get_verified_user
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
@router.get("/ef")
async def get_embeddings(request: Request):
return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")}
############################
# GetMemories
############################
@router.get("/", response_model=List[MemoryModel])
async def get_memories(user=Depends(get_verified_user)):
return Memories.get_memories_by_user_id(user.id)
############################
# AddMemory
############################
class AddMemoryForm(BaseModel):
content: str
@router.post("/add", response_model=Optional[MemoryModel])
async def add_memory(
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
):
memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[{"created_at": memory.created_at}],
)
return memory
############################
# QueryMemory
############################
class QueryMemoryForm(BaseModel):
content: str
@router.post("/query")
async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
):
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
results = collection.query(
query_embeddings=[query_embedding],
n_results=1, # how many results to return
)
return results
############################
# ResetMemoryFromVectorDB
############################
@router.get("/reset", response_model=bool)
async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user)
):
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id)
for memory in memories:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
)
return True
############################
# DeleteMemoriesByUserId
############################
@router.delete("/user", response_model=bool)
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
result = Memories.delete_memories_by_user_id(user.id)
if result:
try:
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
except Exception as e:
log.error(e)
return True
return False
############################
# DeleteMemoryById
############################
@router.delete("/{memory_id}", response_model=bool)
async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
if result:
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
)
collection.delete(ids=[memory_id])
return True
return False
+41 -5
View File
@@ -11,8 +11,9 @@ import logging
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
from apps.web.models.auths import Auths
from apps.web.models.chats import Chats
from utils.utils import get_current_user, get_password_hash, get_admin_user
from utils.utils import get_verified_user, get_password_hash, get_admin_user
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
@@ -39,15 +40,15 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
@router.get("/permissions/user")
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
return request.app.state.USER_PERMISSIONS
return request.app.state.config.USER_PERMISSIONS
@router.post("/permissions/user")
async def update_user_permissions(
request: Request, form_data: dict, user=Depends(get_admin_user)
):
request.app.state.USER_PERMISSIONS = form_data
return request.app.state.USER_PERMISSIONS
request.app.state.config.USER_PERMISSIONS = form_data
return request.app.state.config.USER_PERMISSIONS
############################
@@ -58,7 +59,7 @@ async def update_user_permissions(
@router.post("/update/role", response_model=Optional[UserModel])
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
if user.id != form_data.id:
if user.id != form_data.id and form_data.id != Users.get_first_user().id:
return Users.update_user_role_by_id(form_data.id, form_data.role)
raise HTTPException(
@@ -67,6 +68,41 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
)
############################
# GetUserById
############################
class UserResponse(BaseModel):
name: str
profile_image_url: str
@router.get("/{user_id}", response_model=UserResponse)
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
if user_id.startswith("shared-"):
chat_id = user_id.replace("shared-", "")
chat = Chats.get_chat_by_id(chat_id)
if chat:
user_id = chat.user_id
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
user = Users.get_user_by_id(user_id)
if user:
return UserResponse(name=user.name, profile_image_url=user.profile_image_url)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserById
############################
+345 -90
View File
@@ -5,6 +5,7 @@ import chromadb
from chromadb import Settings
from base64 import b64encode
from bs4 import BeautifulSoup
from typing import TypeVar, Generic, Union
from pathlib import Path
import json
@@ -17,6 +18,17 @@ import shutil
from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
print("dotenv not installed, skipping...")
####################################
# LOGGING
@@ -59,23 +71,15 @@ for source in log_sources:
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
####################################
# ENV (dev,test,prod)
####################################
@@ -149,6 +153,13 @@ for version in soup.find_all("h2"):
CHANGELOG = changelog_json
####################################
# WEBUI_VERSION
####################################
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
####################################
# DATA/FRONTEND BUILD DIR
####################################
@@ -162,13 +173,119 @@ try:
except:
CONFIG_DATA = {}
####################################
# Config helpers
####################################
def save_config():
try:
with open(f"{DATA_DIR}/config.json", "w") as f:
json.dump(CONFIG_DATA, f, indent="\t")
except Exception as e:
log.exception(e)
def get_config_value(config_path: str):
path_parts = config_path.split(".")
cur_config = CONFIG_DATA
for key in path_parts:
if key in cur_config:
cur_config = cur_config[key]
else:
return None
return cur_config
T = TypeVar("T")
class PersistentConfig(Generic[T]):
def __init__(self, env_name: str, config_path: str, env_value: T):
self.env_name = env_name
self.config_path = config_path
self.env_value = env_value
self.config_value = get_config_value(config_path)
if self.config_value is not None:
log.info(f"'{env_name}' loaded from config.json")
self.value = self.config_value
else:
self.value = env_value
def __str__(self):
return str(self.value)
@property
def __dict__(self):
raise TypeError(
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
)
def __getattribute__(self, item):
if item == "__dict__":
raise TypeError(
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
)
return super().__getattribute__(item)
def save(self):
# Don't save if the value is the same as the env value and the config value
if self.env_value == self.value:
if self.config_value == self.value:
return
log.info(f"Saving '{self.env_name}' to config.json")
path_parts = self.config_path.split(".")
config = CONFIG_DATA
for key in path_parts[:-1]:
if key not in config:
config[key] = {}
config = config[key]
config[path_parts[-1]] = self.value
save_config()
self.config_value = self.value
class AppConfig:
_state: dict[str, PersistentConfig]
def __init__(self):
super().__setattr__("_state", {})
def __setattr__(self, key, value):
if isinstance(value, PersistentConfig):
self._state[key] = value
else:
self._state[key].value = value
self._state[key].save()
def __getattr__(self, key):
return self._state[key].value
####################################
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
####################################
# Static DIR
####################################
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
if os.path.exists(frontend_favicon):
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
####################################
# CUSTOM_NAME
@@ -292,12 +409,22 @@ 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 = PersistentConfig(
"OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
)
####################################
# OPENAI_API
####################################
ENABLE_OPENAI_API = PersistentConfig(
"ENABLE_OPENAI_API",
"openai.enable",
os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
@@ -309,7 +436,9 @@ 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_KEYS = PersistentConfig(
"OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
)
OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
OPENAI_API_BASE_URLS = (
@@ -320,33 +449,42 @@ OPENAI_API_BASE_URLS = [
url.strip() if url != "" else "https://api.openai.com/v1"
for url in OPENAI_API_BASE_URLS.split(";")
]
OPENAI_API_BASE_URLS = PersistentConfig(
"OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
)
OPENAI_API_KEY = ""
try:
OPENAI_API_KEY = OPENAI_API_KEYS[
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
OPENAI_API_KEY = OPENAI_API_KEYS.value[
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
]
except:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
####################################
# WEBUI
####################################
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
ENABLE_SIGNUP = PersistentConfig(
"ENABLE_SIGNUP",
"ui.enable_signup",
(
False
if not WEBUI_AUTH
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
),
)
DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
DEFAULT_PROMPT_SUGGESTIONS = (
CONFIG_DATA["ui"]["prompt_suggestions"]
if "ui" in CONFIG_DATA
and "prompt_suggestions" in CONFIG_DATA["ui"]
and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list
else [
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
"DEFAULT_PROMPT_SUGGESTIONS",
"ui.prompt_suggestions",
[
{
"title": ["Help me study", "vocabulary for a college entrance exam"],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
@@ -363,41 +501,54 @@ DEFAULT_PROMPT_SUGGESTIONS = (
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
},
]
{
"title": [
"Explain options trading",
"if I'm familiar with buying and selling stocks",
],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
},
],
)
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
DEFAULT_USER_ROLE = PersistentConfig(
"DEFAULT_USER_ROLE",
"ui.default_user_role",
os.getenv("DEFAULT_USER_ROLE", "pending"),
)
USER_PERMISSIONS_CHAT_DELETION = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
)
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"ui.user_permissions",
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
)
ENABLE_MODEL_FILTER = os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true"
ENABLE_MODEL_FILTER = PersistentConfig(
"ENABLE_MODEL_FILTER",
"model_filter.enable",
os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
)
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
MODEL_FILTER_LIST = PersistentConfig(
"MODEL_FILTER_LIST",
"model_filter.list",
[model.strip() for model in MODEL_FILTER_LIST.split(";")],
)
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
WEBHOOK_URL = PersistentConfig(
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
)
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
####################################
# WEBUI_VERSION
####################################
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
####################################
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = True
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
####################################
# WEBUI_SECRET_KEY
####################################
@@ -432,21 +583,45 @@ else:
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
ENABLE_RAG_HYBRID_SEARCH = (
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
RAG_TOP_K = PersistentConfig(
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
)
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
"RAG_RELEVANCE_THRESHOLD",
"rag.relevance_threshold",
float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
)
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
RAG_EMBEDDING_MODEL = os.environ.get(
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
"ENABLE_RAG_HYBRID_SEARCH",
"rag.enable_hybrid_search",
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
"rag.enable_web_loader_ssl_verification",
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
)
RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE",
"rag.embedding_engine",
os.environ.get("RAG_EMBEDDING_ENGINE", ""),
)
PDF_EXTRACT_IMAGES = PersistentConfig(
"PDF_EXTRACT_IMAGES",
"rag.pdf_extract_images",
os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
)
RAG_EMBEDDING_MODEL = PersistentConfig(
"RAG_EMBEDDING_MODEL",
"rag.embedding_model",
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -456,9 +631,13 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
if not RAG_RERANKING_MODEL == "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
RAG_RERANKING_MODEL = PersistentConfig(
"RAG_RERANKING_MODEL",
"rag.reranking_model",
os.environ.get("RAG_RERANKING_MODEL", ""),
)
if RAG_RERANKING_MODEL.value != "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
RAG_RERANKING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -468,13 +647,6 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
if USE_CUDA.lower() == "true":
DEVICE_TYPE = "cuda"
else:
DEVICE_TYPE = "cpu"
if CHROMA_HTTP_HOST != "":
CHROMA_CLIENT = chromadb.HttpClient(
@@ -494,8 +666,23 @@ else:
database=CHROMA_DATABASE,
)
CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
if USE_CUDA.lower() == "true":
DEVICE_TYPE = "cuda"
else:
DEVICE_TYPE = "cpu"
CHUNK_SIZE = PersistentConfig(
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
)
CHUNK_OVERLAP = PersistentConfig(
"CHUNK_OVERLAP",
"rag.chunk_overlap",
int(os.environ.get("CHUNK_OVERLAP", "100")),
)
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
@@ -511,10 +698,32 @@ And answer according to the language of the user's question.
Given the context information, answer the query.
Query: [query]"""
RAG_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
RAG_TEMPLATE = PersistentConfig(
"RAG_TEMPLATE",
"rag.template",
os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
)
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
RAG_OPENAI_API_BASE_URL = PersistentConfig(
"RAG_OPENAI_API_BASE_URL",
"rag.openai_api_base_url",
os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
RAG_OPENAI_API_KEY = PersistentConfig(
"RAG_OPENAI_API_KEY",
"rag.openai_api_key",
os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
"YOUTUBE_LOADER_LANGUAGE",
"rag.youtube_loader_language",
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
)
####################################
# Transcribe
@@ -531,32 +740,78 @@ WHISPER_MODEL_AUTO_UPDATE = (
# Images
####################################
IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
ENABLE_IMAGE_GENERATION = (
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
IMAGE_GENERATION_ENGINE = PersistentConfig(
"IMAGE_GENERATION_ENGINE",
"image_generation.engine",
os.getenv("IMAGE_GENERATION_ENGINE", ""),
)
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
IMAGES_OPENAI_API_BASE_URL = os.getenv(
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
ENABLE_IMAGE_GENERATION = PersistentConfig(
"ENABLE_IMAGE_GENERATION",
"image_generation.enable",
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
)
AUTOMATIC1111_BASE_URL = PersistentConfig(
"AUTOMATIC1111_BASE_URL",
"image_generation.automatic1111.base_url",
os.getenv("AUTOMATIC1111_BASE_URL", ""),
)
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
COMFYUI_BASE_URL = PersistentConfig(
"COMFYUI_BASE_URL",
"image_generation.comfyui.base_url",
os.getenv("COMFYUI_BASE_URL", ""),
)
IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_OPENAI_API_BASE_URL",
"image_generation.openai.api_base_url",
os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
IMAGES_OPENAI_API_KEY = PersistentConfig(
"IMAGES_OPENAI_API_KEY",
"image_generation.openai.api_key",
os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
)
IMAGE_GENERATION_MODEL = os.getenv("IMAGE_GENERATION_MODEL", "")
IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
)
IMAGE_STEPS = PersistentConfig(
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
)
IMAGE_GENERATION_MODEL = PersistentConfig(
"IMAGE_GENERATION_MODEL",
"image_generation.model",
os.getenv("IMAGE_GENERATION_MODEL", ""),
)
####################################
# Audio
####################################
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_OPENAI_API_BASE_URL",
"audio.openai.api_base_url",
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_OPENAI_API_KEY = PersistentConfig(
"AUDIO_OPENAI_API_KEY",
"audio.openai.api_key",
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_OPENAI_API_MODEL = PersistentConfig(
"AUDIO_OPENAI_API_MODEL",
"audio.openai.api_model",
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
)
AUDIO_OPENAI_API_VOICE = PersistentConfig(
"AUDIO_OPENAI_API_VOICE",
"audio.openai.api_voice",
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
)
####################################
# LiteLLM
+7
View File
@@ -42,6 +42,9 @@ class ERROR_MESSAGES(str, Enum):
"The password provided is incorrect. Please check for typos and try again."
)
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = (
@@ -71,3 +74,7 @@ class ERROR_MESSAGES(str, Enum):
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
+12
View File
@@ -18,6 +18,18 @@
{
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
},
{
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
},
{
"title": ["Grammar check", "rewrite it for better readability "],
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
}
]
}
Regular → Executable
View File
+137 -49
View File
@@ -1,3 +1,4 @@
from contextlib import asynccontextmanager
from bs4 import BeautifulSoup
import json
import markdown
@@ -15,7 +16,7 @@ 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 starlette.responses import StreamingResponse, Response
from apps.ollama.main import app as ollama_app
from apps.openai.main import app as openai_app
@@ -25,6 +26,8 @@ from apps.litellm.main import (
start_litellm_background,
shutdown_litellm_background,
)
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
@@ -41,6 +44,8 @@ from apps.rag.utils import rag_messages
from config import (
CONFIG_DATA,
WEBUI_NAME,
WEBUI_URL,
WEBUI_AUTH,
ENV,
VERSION,
CHANGELOG,
@@ -54,6 +59,7 @@ from config import (
SRC_LOG_LEVELS,
WEBHOOK_URL,
ENABLE_ADMIN_EXPORT,
AppConfig,
)
from constants import ERROR_MESSAGES
@@ -74,7 +80,7 @@ class SPAStaticFiles(StaticFiles):
print(
f"""
rf"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
@@ -88,18 +94,45 @@ https://github.com/open-webui/open-webui
"""
)
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@asynccontextmanager
async def lifespan(app: FastAPI):
if ENABLE_LITELLM:
asyncio.create_task(start_litellm_background())
yield
if ENABLE_LITELLM:
await shutdown_litellm_background()
app.state.WEBHOOK_URL = WEBHOOK_URL
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
)
app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.WEBHOOK_URL = WEBHOOK_URL
origins = ["*"]
# Custom middleware to add security headers
# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
# async def dispatch(self, request: Request, call_next):
# response: Response = await call_next(request)
# response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
# return response
# app.add_middleware(SecurityHeadersMiddleware)
class RAGMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
return_citations = False
if request.method == "POST" and (
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
):
@@ -112,23 +145,29 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Parse string to JSON
data = json.loads(body_str) if body_str else {}
return_citations = data.get("citations", False)
if "citations" in data:
del data["citations"]
# 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["messages"], citations = rag_messages(
docs=data["docs"],
messages=data["messages"],
template=rag_app.state.RAG_TEMPLATE,
template=rag_app.state.config.RAG_TEMPLATE,
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
k=rag_app.state.TOP_K,
k=rag_app.state.config.TOP_K,
reranking_function=rag_app.state.sentence_transformer_rf,
r=rag_app.state.RELEVANCE_THRESHOLD,
hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
r=rag_app.state.config.RELEVANCE_THRESHOLD,
hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
)
del data["docs"]
log.debug(f"data['messages']: {data['messages']}")
log.debug(
f"data['messages']: {data['messages']}, citations: {citations}"
)
modified_body_bytes = json.dumps(data).encode("utf-8")
@@ -146,11 +185,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
]
response = await call_next(request)
if return_citations:
# Inject the citations into the response
if isinstance(response, StreamingResponse):
# If it's a streaming response, inject it as SSE event or NDJSON line
content_type = response.headers.get("Content-Type")
if "text/event-stream" in content_type:
return StreamingResponse(
self.openai_stream_wrapper(response.body_iterator, citations),
)
if "application/x-ndjson" in content_type:
return StreamingResponse(
self.ollama_stream_wrapper(response.body_iterator, citations),
)
return response
async def _receive(self, body: bytes):
return {"type": "http.request", "body": body, "more_body": False}
async def openai_stream_wrapper(self, original_generator, citations):
yield f"data: {json.dumps({'citations': citations})}\n\n"
async for data in original_generator:
yield data
async def ollama_stream_wrapper(self, original_generator, citations):
yield f"{json.dumps({'citations': citations})}\n"
async for data in original_generator:
yield data
app.add_middleware(RAGMiddleware)
@@ -174,15 +238,15 @@ async def check_url(request: Request, call_next):
return response
@app.on_event("startup")
async def on_startup():
if ENABLE_LITELLM:
asyncio.create_task(start_litellm_background())
@app.middleware("http")
async def update_embedding_function(request: Request, call_next):
response = await call_next(request)
if "/embedding/update" in request.url.path:
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
return response
app.mount("/api/v1", webui_app)
app.mount("/litellm/api", litellm_app)
app.mount("/ollama", ollama_app)
app.mount("/openai/api", openai_app)
@@ -190,6 +254,10 @@ app.mount("/images/api/v1", images_app)
app.mount("/audio/api/v1", audio_app)
app.mount("/rag/api/v1", rag_app)
app.mount("/api/v1", webui_app)
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
@app.get("/api/config")
async def get_app_config():
@@ -204,10 +272,11 @@ async def get_app_config():
"status": True,
"name": WEBUI_NAME,
"version": VERSION,
"auth": WEBUI_AUTH,
"default_locale": default_locale,
"images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
"images": images_app.state.config.ENABLED,
"default_models": webui_app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"admin_export_enabled": ENABLE_ADMIN_EXPORT,
}
@@ -216,8 +285,8 @@ 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.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
@@ -230,28 +299,28 @@ class ModelFilterConfigForm(BaseModel):
async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
):
app.state.ENABLE_MODEL_FILTER = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models
app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
app.state.config.MODEL_FILTER_LIST = form_data.models
ollama_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
ollama_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
ollama_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
openai_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
openai_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
openai_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
litellm_app.state.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
litellm_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER
litellm_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST
return {
"enabled": app.state.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
@app.get("/api/webhook")
async def get_webhook_url(user=Depends(get_admin_user)):
return {
"url": app.state.WEBHOOK_URL,
"url": app.state.config.WEBHOOK_URL,
}
@@ -261,12 +330,12 @@ class UrlForm(BaseModel):
@app.post("/api/webhook")
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
app.state.WEBHOOK_URL = form_data.url
app.state.config.WEBHOOK_URL = form_data.url
webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
return {
"url": app.state.WEBHOOK_URL,
"url": app.state.config.WEBHOOK_URL,
}
@@ -311,21 +380,40 @@ async def get_manifest_json():
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
}
@app.get("/opensearch.xml")
async def get_opensearch_xml():
xml_content = rf"""
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>{WEBUI_NAME}</ShortName>
<Description>Search {WEBUI_NAME}</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image>
<Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
<moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
</OpenSearchDescription>
"""
return Response(content=xml_content, media_type="application/xml")
@app.get("/health")
async def healthcheck():
return {"status": True}
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
@app.on_event("shutdown")
async def shutdown_event():
if ENABLE_LITELLM:
await shutdown_litellm_background()
if os.path.exists(FRONTEND_BUILD_DIR):
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
else:
log.warning(
f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
)
+48 -47
View File
@@ -1,59 +1,60 @@
fastapi
uvicorn[standard]
pydantic
python-multipart
fastapi==0.109.2
uvicorn[standard]==0.22.0
pydantic==2.7.1
python-multipart==0.0.9
flask
flask_cors
Flask==3.0.3
Flask-Cors==4.0.0
python-socketio
python-jose
passlib[bcrypt]
uuid
python-socketio==5.11.2
python-jose==3.3.0
passlib[bcrypt]==1.7.4
requests
aiohttp
peewee
peewee-migrate
psycopg2-binary
pymysql
bcrypt
requests==2.31.0
aiohttp==3.9.5
peewee==3.17.3
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.0
bcrypt==4.1.2
litellm==1.35.17
litellm[proxy]==1.35.17
litellm[proxy]==1.35.28
boto3
boto3==1.34.95
argon2-cffi
apscheduler
google-generativeai
argon2-cffi==23.1.0
APScheduler==3.10.4
google-generativeai==0.5.2
langchain
langchain-chroma
langchain-community
fake_useragent
chromadb
sentence_transformers
pypdf
docx2txt
unstructured
markdown
pypandoc
pandas
openpyxl
pyxlsb
xlrd
langchain==0.1.16
langchain-community==0.0.34
langchain-chroma==0.1.0
opencv-python-headless
rapidocr-onnxruntime
fake-useragent==1.5.1
chromadb==0.4.24
sentence-transformers==2.7.0
pypdf==4.2.0
docx2txt==0.8
unstructured==0.11.8
Markdown==3.6
pypandoc==1.13
pandas==2.2.2
openpyxl==3.1.2
pyxlsb==1.0.10
xlrd==2.0.1
validators==0.28.1
fpdf2
rank_bm25
opencv-python-headless==4.9.0.80
rapidocr-onnxruntime==1.2.3
faster-whisper
fpdf2==2.7.8
rank-bm25==0.2.2
PyJWT
pyjwt[crypto]
faster-whisper==1.0.1
black
langfuse
PyJWT[crypto]==2.8.0
black==24.4.2
langfuse==2.27.3
youtube-transcript-api==0.6.2
pytube
+1 -1
View File
@@ -8,7 +8,7 @@ KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}"
HOST="${HOST:-0.0.0.0}"
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
echo "No WEBUI_SECRET_KEY provided"
echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
if ! [ -e "$KEY_FILE" ]; then
echo "Generating WEBUI_SECRET_KEY"
+1 -1
View File
@@ -13,7 +13,7 @@ SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
echo No WEBUI_SECRET_KEY provided
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
IF NOT EXIST "%KEY_FILE%" (
echo Generating WEBUI_SECRET_KEY
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+1
View File
@@ -0,0 +1 @@
Name,Email,Password,Role
1 Name Email Password Role
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+4 -3
View File
@@ -38,9 +38,10 @@ def calculate_sha256_string(string):
def validate_email_format(email: str) -> bool:
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return False
return True
if email.endswith("@localhost"):
return True
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
def sanitize_filename(file_name):
+34 -2
View File
@@ -21,14 +21,14 @@ describe('Settings', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('div[role="option"][data-value]').first().click();
cy.get('button[aria-label="model-item"]').first().click();
});
it('user can perform text chat', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('div[role="option"][data-value]').first().click();
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
@@ -42,5 +42,37 @@ describe('Settings', () => {
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
});
it('user can share chat', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
});
// Send the message
cy.get('button[type="submit"]').click();
// User's message should be visible
cy.get('.chat-user').should('exist');
// Wait for the response
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
// spy on requests
const spy = cy.spy();
cy.intercept('GET', '/api/v1/chats/*', spy);
// Open context menu
cy.get('#chat-context-menu-button').click();
// Click share button
cy.get('#chat-share-button').click();
// Check if the share dialog is visible
cy.get('#copy-and-share-chat-button').should('exist');
cy.wrap({}, { timeout: 5000 }).should(() => {
// Check if the request was made twice (once for to replace chat object and once more due to change event)
expect(spy).to.be.callCount(2);
});
});
});
});
+2 -6
View File
@@ -15,12 +15,8 @@ describe('Settings', () => {
cy.loginAdmin();
// Visit the home page
cy.visit('/');
// Open the sidebar if it is not already open
cy.get('[aria-label="Open sidebar"]').then(() => {
cy.get('button[id="sidebar-toggle-button"]').click();
});
// Click on the profile link
cy.get('button').contains(adminUser.name).click();
// Click on the user menu
cy.get('button[aria-label="User Menu"]').click();
// Click on the settings link
cy.get('button').contains('Settings').click();
});
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
# Expose Ollama API outside the container stack
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
volumes:
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
# GPU support
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
volumes:
+2 -2
View File
@@ -17,7 +17,7 @@ If your issue or contribution pertains directly to the core Ollama technology, p
### 🚨 Reporting Issues
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/oopen-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/open-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
> [!IMPORTANT]
>
@@ -54,7 +54,7 @@ Help us make Open WebUI more accessible by improving documentation, writing tuto
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.
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:
-1
View File
@@ -1 +0,0 @@
values-minikube.yaml
-21
View File
@@ -1,21 +0,0 @@
apiVersion: v2
name: open-webui
version: 1.0.0
appVersion: "latest"
home: https://www.openwebui.com/
icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
keywords:
- llm
- chat
- web-ui
sources:
- https://github.com/open-webui/open-webui/tree/main/kubernetes/helm
- https://hub.docker.com/r/ollama/ollama
- https://github.com/open-webui/open-webui/pkgs/container/open-webui
annotations:
licenses: MIT
+4
View File
@@ -0,0 +1,4 @@
# Helm Charts
Open WebUI Helm Charts are now hosted in a separate repo, which can be found here: https://github.com/open-webui/helm-charts
The charts are released at https://helm.openwebui.com.
-51
View File
@@ -1,51 +0,0 @@
{{- define "open-webui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{- define "ollama.name" -}}
ollama
{{- end -}}
{{- define "ollama.url" -}}
{{- if .Values.ollama.externalHost }}
{{- printf .Values.ollama.externalHost }}
{{- else }}
{{- printf "http://%s.%s.svc.cluster.local:%d" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }}
{{- end }}
{{- define "chart.name" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "base.labels" -}}
helm.sh/chart: {{ include "chart.name" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "base.selectorLabels" -}}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "open-webui.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ .Chart.Name }}
{{- end }}
{{- define "open-webui.labels" -}}
{{ include "base.labels" . }}
{{ include "open-webui.selectorLabels" . }}
{{- end }}
{{- define "ollama.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ include "ollama.name" . }}
{{- end }}
{{- define "ollama.labels" -}}
{{ include "base.labels" . }}
{{ include "ollama.selectorLabels" . }}
{{- end }}
@@ -1,23 +0,0 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "ollama.name" . }}
labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
{{- include "ollama.selectorLabels" . | nindent 4 }}
{{- with .Values.ollama.service }}
type: {{ .type }}
ports:
- protocol: TCP
name: http
port: {{ .port }}
targetPort: http
{{- end }}
{{- end }}
@@ -1,98 +0,0 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "ollama.name" . }}
labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
serviceName: {{ include "ollama.name" . }}
replicas: {{ .Values.ollama.replicaCount }}
selector:
matchLabels:
{{- include "ollama.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "ollama.labels" . | nindent 8 }}
{{- with .Values.ollama.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
enableServiceLinks: false
automountServiceAccountToken: false
{{- with .Values.ollama.runtimeClassName }}
runtimeClassName: {{ . }}
{{- end }}
containers:
- name: {{ include "ollama.name" . }}
{{- with .Values.ollama.image }}
image: {{ .repository }}:{{ .tag }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
tty: true
ports:
- name: http
containerPort: {{ .Values.ollama.service.containerPort }}
env:
{{- if .Values.ollama.gpu.enabled }}
- name: PATH
value: /usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
- name: LD_LIBRARY_PATH
value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64
- name: NVIDIA_DRIVER_CAPABILITIES
value: compute,utility
{{- end }}
{{- with .Values.ollama.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /root/.ollama
{{- with .Values.ollama.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ollama.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if and .Values.ollama.persistence.enabled .Values.ollama.persistence.existingClaim }}
- name: data
persistentVolumeClaim:
claimName: {{ .Values.ollama.persistence.existingClaim }}
{{- else if not .Values.ollama.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.ollama.persistence.enabled (not .Values.ollama.persistence.existingClaim) }}
[]
volumeClaimTemplates:
- metadata:
name: data
labels:
{{- include "ollama.selectorLabels" . | nindent 8 }}
{{- with .Values.ollama.persistence.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
accessModes:
{{- range .Values.ollama.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.ollama.persistence.size | quote }}
storageClassName: {{ .Values.ollama.persistence.storageClass }}
{{- with .Values.ollama.persistence.selector }}
selector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,62 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.webui.replicaCount }}
selector:
matchLabels:
{{- include "open-webui.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "open-webui.labels" . | nindent 8 }}
{{- with .Values.webui.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
enableServiceLinks: false
automountServiceAccountToken: false
containers:
- name: {{ .Chart.Name }}
{{- with .Values.webui.image }}
image: {{ .repository }}:{{ .tag | default $.Chart.AppVersion }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.webui.service.containerPort }}
{{- with .Values.webui.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /app/backend/data
env:
- name: OLLAMA_BASE_URL
value: {{ include "ollama.url" . | quote }}
tty: true
{{- with .Values.webui.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if and .Values.webui.persistence.enabled .Values.webui.persistence.existingClaim }}
- name: data
persistentVolumeClaim:
claimName: {{ .Values.webui.persistence.existingClaim }}
{{- else if not .Values.webui.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
- name: data
persistentVolumeClaim:
claimName: {{ include "open-webui.name" . }}
{{- end }}
@@ -1,33 +0,0 @@
{{- if .Values.webui.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.webui.ingress.class }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.webui.ingress.tls }}
tls:
- hosts:
- {{ .Values.webui.ingress.host | quote }}
secretName: {{ default (printf "%s-tls" .Release.Name) .Values.webui.ingress.existingSecret }}
{{- end }}
rules:
- host: {{ .Values.webui.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "open-webui.name" . }}
port:
name: http
{{- end }}
-27
View File
@@ -1,27 +0,0 @@
{{- if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.selectorLabels" . | nindent 4 }}
{{- with .Values.webui.persistence.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
accessModes:
{{- range .Values.webui.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.webui.persistence.size }}
{{- if .Values.webui.persistence.storageClass }}
storageClassName: {{ .Values.webui.persistence.storageClass }}
{{- end }}
{{- with .Values.webui.persistence.selector }}
selector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -1,29 +0,0 @@
apiVersion: v1
kind: Service
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 }}
{{- end }}
spec:
selector:
{{- include "open-webui.selectorLabels" . | nindent 4 }}
type: {{ .Values.webui.service.type | default "ClusterIP" }}
ports:
- protocol: TCP
name: http
port: {{ .Values.webui.service.port }}
targetPort: http
{{- if .Values.webui.service.nodePort }}
nodePort: {{ .Values.webui.service.nodePort | int }}
{{- end }}
{{- if .Values.webui.service.loadBalancerClass }}
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
{{- end }}
-27
View File
@@ -1,27 +0,0 @@
ollama:
resources:
requests:
cpu: "2000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
nvidia.com/gpu: "0"
service:
type: ClusterIP
gpu:
enabled: false
webui:
resources:
requests:
cpu: "500m"
memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ingress:
enabled: true
host: open-webui.minikube.local
service:
type: NodePort
-75
View File
@@ -1,75 +0,0 @@
nameOverride: ""
ollama:
externalHost: ""
annotations: {}
podAnnotations: {}
replicaCount: 1
image:
repository: ollama/ollama
tag: latest
pullPolicy: Always
resources: {}
persistence:
enabled: true
size: 30Gi
existingClaim: ""
accessModes:
- ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {}
# -- If using a special runtime container such as nvidia, set it here.
runtimeClassName: ""
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
service:
type: ClusterIP
annotations: {}
port: 80
containerPort: 11434
gpu:
# -- Enable additional ENV values to help Ollama discover GPU usage
enabled: false
webui:
annotations: {}
podAnnotations: {}
replicaCount: 1
image:
repository: ghcr.io/open-webui/open-webui
tag: ""
pullPolicy: Always
resources: {}
ingress:
enabled: false
class: ""
# -- Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: /
annotations: {}
host: ""
tls: false
existingSecret: ""
persistence:
enabled: true
size: 2Gi
existingClaim: ""
# -- If using multiple replicas, you must update accessModes to ReadWriteMany
accessModes:
- ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {}
tolerations: []
service:
type: ClusterIP
annotations: {}
port: 80
containerPort: 8080
nodePort: ""
labels: {}
loadBalancerClass: ""
+1155 -5
View File
File diff suppressed because it is too large Load Diff
+13 -7
View File
@@ -1,10 +1,10 @@
{
"name": "open-webui",
"version": "0.1.122",
"version": "0.1.125",
"private": true,
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"dev": "npm run pyodide:fetch && vite dev --host",
"build": "npm run pyodide:fetch && vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -12,10 +12,12 @@
"lint:frontend": "eslint . --fix",
"lint:types": "npm run check",
"lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
"format:backend": "black . --exclude \"/venv/\"",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
"cy:open": "cypress open"
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"",
"cy:open": "cypress open",
"test:frontend": "vitest",
"pyodide:fetch": "node scripts/prepare-pyodide.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
@@ -41,14 +43,17 @@
"tailwindcss": "^3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2"
"vite": "^4.4.2",
"vitest": "^1.6.0"
},
"type": "module",
"dependencies": {
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"i18next": "^23.10.0",
@@ -58,6 +63,7 @@
"js-sha256": "^0.10.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"pyodide": "^0.26.0-alpha.4",
"svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1"
+39
View File
@@ -0,0 +1,39 @@
const packages = [
'requests',
'beautifulsoup4',
'numpy',
'pandas',
'matplotlib',
'scikit-learn',
'scipy',
'regex',
'seaborn'
];
import { loadPyodide } from 'pyodide';
import { writeFile, copyFile, readdir } from 'fs/promises';
async function downloadPackages() {
console.log('Setting up pyodide + micropip');
const pyodide = await loadPyodide({
packageCacheDir: 'static/pyodide'
});
await pyodide.loadPackage('micropip');
const micropip = pyodide.pyimport('micropip');
console.log('Downloading Pyodide packages:', packages);
await micropip.install(packages);
console.log('Pyodide packages downloaded, freezing into lock file');
const lockFile = await micropip.freeze();
await writeFile('static/pyodide/pyodide-lock.json', lockFile);
}
async function copyPyodide() {
console.log('Copying Pyodide files into static directory');
// Copy all files from node_modules/pyodide to static/pyodide
for await (const entry of await readdir('node_modules/pyodide')) {
await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`);
}
}
await downloadPackages();
await copyPyodide();
+29
View File
@@ -82,3 +82,32 @@ select {
.katex-mathml {
display: none;
}
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-hidden::-webkit-scrollbar-thumb {
visibility: hidden;
}
.scrollbar-none::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.scrollbar-none {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
+7
View File
@@ -6,6 +6,13 @@
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<link
rel="search"
type="application/opensearchdescription+xml"
title="Open WebUI"
href="/opensearch.xml"
/>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => {
+5 -2
View File
@@ -30,6 +30,8 @@ export const getAudioConfig = async (token: string) => {
type OpenAIConfigForm = {
url: string;
key: string;
model: string;
speaker: string;
};
export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => {
@@ -95,7 +97,8 @@ export const transcribeAudio = async (token: string, file: File) => {
export const synthesizeOpenAISpeech = async (
token: string = '',
speaker: string = 'alloy',
text: string = ''
text: string = '',
model: string = 'tts-1'
) => {
let error = null;
@@ -106,7 +109,7 @@ export const synthesizeOpenAISpeech = async (
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
model: model,
input: text,
voice: speaker
})
+39
View File
@@ -95,6 +95,45 @@ export const userSignUp = async (
return res;
};
export const addUser = async (
token: string,
name: string,
email: string,
password: string,
role: string = 'pending'
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
name: name,
email: email,
password: password,
role: role
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
let error = null;
+19 -6
View File
@@ -1,4 +1,5 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { getTimeRange } from '$lib/utils';
export const createNewChat = async (token: string, chat: object) => {
let error = null;
@@ -59,7 +60,10 @@ export const getChatList = async (token: string = '') => {
throw error;
}
return res;
return res.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
export const getChatListByUserId = async (token: string = '', userId: string) => {
@@ -90,7 +94,10 @@ export const getChatListByUserId = async (token: string = '', userId: string) =>
throw error;
}
return res;
return res.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
export const getArchivedChatList = async (token: string = '') => {
@@ -220,13 +227,16 @@ export const getAllChatTags = async (token: string) => {
export const getChatListByTagName = async (token: string = '', tagName: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags/tag/${tagName}`, {
method: 'GET',
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
},
body: JSON.stringify({
name: tagName
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
@@ -245,7 +255,10 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
throw error;
}
return res;
return res.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
export const getChatById = async (token: string, id: string) => {
+155
View File
@@ -0,0 +1,155 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getMemories = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const addNewMemory = async (token: string, content: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/add`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const queryMemory = async (token: string, content: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/query`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteMemoryById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteMemoriesByUserId = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/user`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
+6 -2
View File
@@ -159,7 +159,11 @@ export const generateTitle = async (
body: JSON.stringify({
model: model,
prompt: template,
stream: false
stream: false,
options: {
// Restrict the number of tokens generated to 50
num_predict: 50
}
})
})
.then(async (res) => {
@@ -178,7 +182,7 @@ export const generateTitle = async (
throw error;
}
return res?.response ?? 'New Chat';
return res?.response.replace(/["']/g, '') ?? 'New Chat';
};
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
+74 -4
View File
@@ -1,6 +1,73 @@
import { OPENAI_API_BASE_URL } from '$lib/constants';
import { promptTemplate } from '$lib/utils';
export const getOpenAIConfig = async (token: string = '') => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/config`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.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 updateOpenAIConfig = async (token: string = '', enable_openai_api: boolean) => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/config/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
enable_openai_api: enable_openai_api
})
})
.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 getOpenAIUrls = async (token: string = '') => {
let error = null;
@@ -239,7 +306,8 @@ export const generateOpenAIChatCompletion = async (
export const synthesizeOpenAISpeech = async (
token: string = '',
speaker: string = 'alloy',
text: string = ''
text: string = '',
model: string = 'tts-1'
) => {
let error = null;
@@ -250,7 +318,7 @@ export const synthesizeOpenAISpeech = async (
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
model: model,
input: text,
voice: speaker
})
@@ -295,7 +363,9 @@ export const generateTitle = async (
content: template
}
],
stream: false
stream: false,
// Restricting the max tokens to 50 to avoid long titles
max_tokens: 50
})
})
.then(async (res) => {
@@ -314,5 +384,5 @@ export const generateTitle = async (
throw error;
}
return res?.choices[0]?.message?.content ?? 'New Chat';
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
};
+40 -2
View File
@@ -32,9 +32,16 @@ type ChunkConfigForm = {
chunk_overlap: number;
};
type YoutubeConfigForm = {
language: string[];
translation?: string | null;
};
type RAGConfigForm = {
pdf_extract_images: boolean;
chunk: ChunkConfigForm;
pdf_extract_images?: boolean;
chunk?: ChunkConfigForm;
web_loader_ssl_verification?: boolean;
youtube?: YoutubeConfigForm;
};
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
@@ -221,6 +228,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
return res;
};
export const uploadYoutubeTranscriptionToVectorDB = async (token: string, url: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/youtube`, {
method: 'POST',
headers: {
Accept: 'application/json',
'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) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const queryDoc = async (
token: string,
collection_name: string,
+48 -28
View File
@@ -1,15 +1,26 @@
import { EventSourceParserStream } from 'eventsource-parser/stream';
import type { ParsedEvent } from 'eventsource-parser';
type TextStreamUpdate = {
done: boolean;
value: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
citations?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error?: any;
};
// createOpenAITextStream takes a ReadableStreamDefaultReader from an SSE response,
// createOpenAITextStream takes a responseBody with a SSE response,
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
export async function createOpenAITextStream(
messageStream: ReadableStreamDefaultReader,
responseBody: ReadableStream<Uint8Array>,
splitLargeDeltas: boolean
): Promise<AsyncGenerator<TextStreamUpdate>> {
let iterator = openAIStreamToIterator(messageStream);
const eventStream = responseBody
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
.getReader();
let iterator = openAIStreamToIterator(eventStream);
if (splitLargeDeltas) {
iterator = streamLargeDeltasAsRandomChunks(iterator);
}
@@ -17,7 +28,7 @@ export async function createOpenAITextStream(
}
async function* openAIStreamToIterator(
reader: ReadableStreamDefaultReader
reader: ReadableStreamDefaultReader<ParsedEvent>
): AsyncGenerator<TextStreamUpdate> {
while (true) {
const { value, done } = await reader.read();
@@ -25,31 +36,32 @@ async function* openAIStreamToIterator(
yield { done: true, value: '' };
break;
}
const lines = value.split('\n');
for (let line of lines) {
if (line.endsWith('\r')) {
// Remove trailing \r
line = line.slice(0, -1);
}
if (line !== '') {
console.log(line);
if (line === 'data: [DONE]') {
yield { done: true, value: '' };
} else if (line.startsWith(':')) {
// Events starting with : are comments https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
// OpenRouter sends heartbeats like ": OPENROUTER PROCESSING"
continue;
} else {
try {
const data = JSON.parse(line.replace(/^data: /, ''));
console.log(data);
if (!value) {
continue;
}
const data = value.data;
if (data.startsWith('[DONE]')) {
yield { done: true, value: '' };
break;
}
yield { done: false, value: data.choices?.[0]?.delta?.content ?? '' };
} catch (e) {
console.error('Error extracting delta from SSE event:', e);
}
}
try {
const parsedData = JSON.parse(data);
console.log(parsedData);
if (parsedData.error) {
yield { done: true, value: '', error: parsedData.error };
break;
}
if (parsedData.citations) {
yield { done: false, value: '', citations: parsedData.citations };
continue;
}
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
} catch (e) {
console.error('Error extracting delta from SSE event:', e);
}
}
}
@@ -64,6 +76,10 @@ async function* streamLargeDeltasAsRandomChunks(
yield textStreamUpdate;
return;
}
if (textStreamUpdate.citations) {
yield textStreamUpdate;
continue;
}
let content = textStreamUpdate.value;
if (content.length < 5) {
yield { done: false, value: content };
@@ -73,7 +89,11 @@ async function* streamLargeDeltasAsRandomChunks(
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
const chunk = content.slice(0, chunkSize);
yield { done: false, value: chunk };
await sleep(5);
// Do not sleep if the tab is hidden
// Timers are throttled to 1s in hidden tabs
if (document?.visibilityState !== 'hidden') {
await sleep(5);
}
content = content.slice(chunkSize);
}
}
+27
View File
@@ -115,6 +115,33 @@ export const getUsers = async (token: string) => {
return res ? res : [];
};
export const getUserById = async (token: string, userId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteUserById = async (token: string, userId: string) => {
let error = null;
+3 -5
View File
@@ -22,7 +22,7 @@
</script>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
<div class="flex justify-between items-start">
<div class="text-xl font-bold">
{$i18n.t('Whats New in')}
@@ -57,10 +57,8 @@
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" overflow-y-scroll max-h-80">
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
<div class=" overflow-y-scroll max-h-80 scrollbar-hidden">
<div class="mb-3">
{#if changelog}
{#each Object.keys(changelog) as version}
@@ -0,0 +1,337 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import { addUser } from '$lib/apis/auths';
import Modal from '../common/Modal.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
let loading = false;
let tab = '';
let inputFiles;
let _user = {
name: '',
email: '',
password: '',
role: 'user'
};
$: if (show) {
_user = {
name: '',
email: '',
password: '',
role: 'user'
};
}
const submitHandler = async () => {
const stopLoading = () => {
dispatch('save');
loading = false;
};
if (tab === '') {
loading = true;
const res = await addUser(
localStorage.token,
_user.name,
_user.email,
_user.password,
_user.role
).catch((error) => {
toast.error(error);
});
if (res) {
stopLoading();
show = false;
}
} else {
if (inputFiles) {
loading = true;
const file = inputFiles[0];
const reader = new FileReader();
reader.onload = async (e) => {
const csv = e.target.result;
const rows = csv.split('\n');
let userCount = 0;
for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim());
console.log(idx, columns);
if (idx > 0) {
if (
columns.length === 4 &&
['admin', 'user', 'pending'].includes(columns[3].toLowerCase())
) {
const res = await addUser(
localStorage.token,
columns[0],
columns[1],
columns[2],
columns[3].toLowerCase()
).catch((error) => {
toast.error(`Row ${idx + 1}: ${error}`);
return null;
});
if (res) {
userCount = userCount + 1;
}
} else {
toast.error(`Row ${idx + 1}: invalid format.`);
}
}
}
toast.success(`Successfully imported ${userCount} users.`);
inputFiles = null;
const uploadInputElement = document.getElementById('upload-user-csv-input');
if (uploadInputElement) {
uploadInputElement.value = null;
}
stopLoading();
};
reader.readAsText(file);
} else {
toast.error($i18n.t('File not found.'));
}
}
};
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Add User')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
<button
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = '';
}}>Form</button
>
<button
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = 'import';
}}>CSV Import</button
>
</div>
<div class="px-1">
{#if tab === ''}
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
<div class="flex-1">
<select
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
bind:value={_user.role}
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
</select>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Full Name')}
autocomplete="off"
required
/>
</div>
</div>
<hr class=" dark:border-gray-800 my-3 w-full" />
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="password"
bind:value={_user.password}
placeholder={$i18n.t('Enter Your Password')}
autocomplete="off"
/>
</div>
</div>
{:else if tab === 'import'}
<div>
<div class="mb-3 w-full">
<input
id="upload-user-csv-input"
hidden
bind:files={inputFiles}
type="file"
accept=".csv"
/>
<button
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
type="button"
on:click={() => {
document.getElementById('upload-user-csv-input')?.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
{$i18n.t('Click here to select a csv file.')}
{/if}
</button>
</div>
<div class=" text-xs text-gray-500">
{$i18n.t(
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
)}
<a
class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv"
>
Click here to download user import template file.
</a>
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
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}
>
{$i18n.t('Submit')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
@@ -123,7 +123,7 @@
<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"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={`https://example.com/webhook`}
bind:value={webhookUrl}
@@ -140,7 +140,7 @@
<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"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={JWTExpiresIn}
@@ -15,7 +15,7 @@
<Modal bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button
class="self-center"
@@ -35,7 +35,6 @@
</svg>
</button>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div
@@ -70,7 +70,7 @@
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
@@ -96,7 +96,7 @@
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
<Tooltip content="Delete Chat">
<Tooltip content={$i18n.t('Delete Chat')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
@@ -133,7 +133,10 @@
{/each} -->
</div>
{:else}
<div class="text-left text-sm w-full mb-8">You have no archived conversations.</div>
<div class="text-left text-sm w-full mb-8">
{user.name}
{$i18n.t('has no conversations.')}
</div>
{/if}
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@
{
name: 'All Documents',
type: 'collection',
title: 'All Documents',
title: $i18n.t('All Documents'),
collection_names: $documents.map((doc) => doc.collection_name)
}
]
@@ -87,6 +87,17 @@
chatInputElement?.focus();
await tick();
};
const confirmSelectYoutube = async (url) => {
dispatch('youtube', url);
prompt = removeFirstHashWord(prompt);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement?.focus();
await tick();
};
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
@@ -132,7 +143,34 @@
</button>
{/each}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if prompt
.split(' ')
.some((s) => s.substring(1).startsWith('https://www.youtube.com') || s
.substring(1)
.startsWith('https://youtu.be'))}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
on:click={() => {
const url = prompt.split(' ')?.at(0)?.substring(1);
if (isValidHttpUrl(url)) {
confirmSelectYoutube(url);
} else {
toast.error(
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
}
}}
>
<div class=" font-medium text-black line-clamp-1">
{prompt.split(' ')?.at(0)?.substring(1)}
</div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
</button>
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
@@ -1,4 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
@@ -7,6 +9,8 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let prompt = '';
export let user = null;
@@ -17,12 +21,7 @@
let filteredModels = [];
$: filteredModels = $models
.filter(
(p) =>
p.name !== 'hr' &&
!p.external &&
p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
)
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
.sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) {
@@ -38,6 +37,11 @@
};
const confirmSelect = async (model) => {
prompt = '';
dispatch('select', model);
};
const confirmSelectCollaborativeChat = async (model) => {
// dispatch('select', model);
prompt = '';
user = JSON.parse(JSON.stringify(model.name));
@@ -127,40 +131,42 @@
};
</script>
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
{#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black line-clamp-1">
{model.name}
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black line-clamp-1">
{model.name}
</div>
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
{doc.title}
</div> -->
</button>
{/each}
</button>
{/each}
</div>
</div>
</div>
</div>
</div>
{/if}
{/if}
@@ -1,56 +1,118 @@
<script lang="ts">
import Bolt from '$lib/components/icons/Bolt.svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let suggestionPrompts = [];
let prompts = [];
$: prompts =
suggestionPrompts.length <= 4
? suggestionPrompts
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
$: prompts = suggestionPrompts
.reduce((acc, current) => [...acc, ...[current]], [])
.sort(() => Math.random() - 0.5);
// suggestionPrompts.length <= 4
// ? suggestionPrompts
// : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
onMount(() => {
const containerElement = document.getElementById('suggestions-container');
if (containerElement) {
containerElement.addEventListener('wheel', function (event) {
if (event.deltaY !== 0) {
// If scrolling vertically, prevent default behavior
event.preventDefault();
// Adjust horizontal scroll position based on vertical scroll
containerElement.scrollLeft += event.deltaY;
}
});
}
});
</script>
<div class=" mb-3 md:p-1 text-left w-full">
<div class=" flex flex-wrap-reverse px-2 text-left">
{#if prompts.length > 0}
<div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
<Bolt />
{$i18n.t('Suggested')}
</div>
{/if}
<div class="w-full">
<div
class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs"
id="suggestions-container"
>
{#each prompts as prompt, promptIdx}
<div
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
>
<div class="snap-center shrink-0">
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
class="flex flex-col flex-1 shrink-0 w-64 justify-between h-36 p-5 px-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-3xl transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
>
<div class="flex flex-col text-left self-center">
<div class="flex flex-col text-left">
{#if prompt.title && prompt.title[0] !== ''}
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
<div
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition"
>
{prompt.title[0]}
</div>
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
<div
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
>
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
<div class="w-full flex justify-between">
<div
class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
{$i18n.t('Prompt')}
</div>
<div
class="self-end p-1 rounded-lg text-gray-300 group-hover:text-gray-800 dark:text-gray-700 dark:group-hover:text-gray-100 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</button>
</div>
{/each}
<!-- <div class="snap-center shrink-0">
<img
class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white"
src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=320&amp;h=160&amp;q=80"
/>
</div> -->
</div>
</div>
<style>
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
+148 -105
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { chats, config, modelfiles, settings, user as _user, mobile } from '$lib/stores';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
@@ -12,7 +12,9 @@
import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
import { copyToClipboard } from '$lib/utils';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import CompareMessages from './Messages/CompareMessages.svelte';
import { stringify } from 'postcss';
const i18n = getContext('i18n');
@@ -22,13 +24,16 @@
export let continueGeneration: Function;
export let regenerateResponse: Function;
export let user = $_user;
export let prompt;
export let suggestionPrompts = [];
export let processing = '';
export let bottomPadding = false;
export let autoScroll;
export let selectedModels;
export let history = {};
export let messages = [];
export let selectedModels;
export let selectedModelfiles = [];
$: if (autoScroll && bottomPadding) {
@@ -60,7 +65,8 @@
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files })
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
};
let messageParentId = history.messages[messageId].parentId;
@@ -76,7 +82,7 @@
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId, chatId);
await sendPrompt(userPrompt, userMessageId);
};
const updateChatMessages = async () => {
@@ -236,108 +242,145 @@
history: history
});
};
// const messageDeleteHandler = async (messageId) => {
// const message = history.messages[messageId];
// const parentId = message.parentId;
// const childrenIds = message.childrenIds ?? [];
// const grandchildrenIds = [];
// // Iterate through childrenIds to find grandchildrenIds
// for (const childId of childrenIds) {
// const childMessage = history.messages[childId];
// const grandChildrenIds = childMessage.childrenIds ?? [];
// for (const grandchildId of grandchildrenIds) {
// const childMessage = history.messages[grandchildId];
// childMessage.parentId = parentId;
// }
// grandchildrenIds.push(...grandChildrenIds);
// }
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
// (id) => id !== messageId
// );
// // Select latest message
// let currentMessageId = grandchildrenIds.at(-1);
// if (currentMessageId) {
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
// while (messageChildrenIds.length !== 0) {
// currentMessageId = messageChildrenIds.at(-1);
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
// }
// history.currentId = currentMessageId;
// }
// await updateChatById(localStorage.token, chatId, { messages, history });
// };
</script>
{#if messages.length == 0}
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
{:else}
<div class=" pb-10">
{#key chatId}
{#each messages as message, messageIdx}
<div class=" w-full">
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-3xl'} mx-auto rounded-lg group"
>
{#if message.role === 'user'}
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
user={$user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? []}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else}
<ResponseMessage
{message}
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{updateChatMessages}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:save={async (e) => {
console.log('save', e);
<div class="h-full flex mb-16">
{#if messages.length == 0}
<Placeholder
models={selectedModels}
modelfiles={selectedModelfiles}
{suggestionPrompts}
submitPrompt={async (p) => {
let text = p;
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/if}
if (p.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}';
});
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
}
prompt = text;
await tick();
const chatInputElement = document.getElementById('chat-textarea');
if (chatInputElement) {
prompt = p;
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
chatInputElement.focus();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
await tick();
}}
/>
{:else}
<div class="w-full pt-2">
{#key chatId}
{#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? 'pb-28' : ''}">
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group"
>
{#if message.role === 'user'}
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
{user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? []}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{#key message.id}
<ResponseMessage
{message}
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{updateChatMessages}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/key}
{:else}
{#key message.parentId}
<CompareMessages
bind:history
{messages}
{chatId}
parentMessage={history.messages[message.parentId]}
{messageIdx}
{selectedModelfiles}
{updateChatMessages}
{confirmEditResponseMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:change={async () => {
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
if (autoScroll) {
const element = document.getElementById('messages-container');
autoScroll =
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
}}
/>
{/key}
{/if}
</div>
</div>
</div>
{/each}
{/each}
{#if bottomPadding}
<div class=" mb-10" />
{/if}
{/key}
</div>
{/if}
{#if bottomPadding}
<div class=" pb-20" />
{/if}
{/key}
</div>
{/if}
</div>
@@ -0,0 +1,77 @@
<script lang="ts">
import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
export let citation;
let mergedDocuments = [];
$: if (citation) {
mergedDocuments = citation.document?.map((c, i) => {
return {
source: citation.source,
document: c,
metadata: citation.metadata?.[i]
};
});
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center capitalize">
{$i18n.t('Citation')}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden"
>
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full">
<div class="text-sm font-medium dark:text-gray-300">
{$i18n.t('Source')}
</div>
<div class="text-sm dark:text-gray-400">
{document.source?.name ?? $i18n.t('No source available')}
</div>
</div>
<div class="flex flex-col w-full">
<div class=" text-sm font-medium dark:text-gray-300">
{$i18n.t('Content')}
</div>
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
{document.document}
</pre>
</div>
{#if documentIdx !== mergedDocuments.length - 1}
<hr class=" dark:border-gray-850 my-3" />
{/if}
{/each}
</div>
</div>
</div>
</Modal>
@@ -1,11 +1,23 @@
<script lang="ts">
import Spinner from '$lib/components/common/Spinner.svelte';
import { copyToClipboard } from '$lib/utils';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.min.css';
import { loadPyodide } from 'pyodide';
import { tick } from 'svelte';
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
export let id = '';
export let lang = '';
export let code = '';
let executing = false;
let stdout = null;
let stderr = null;
let result = null;
let copied = false;
const copyCode = async () => {
@@ -17,22 +29,233 @@
}, 1000);
};
const checkPythonCode = (str) => {
// Check if the string contains typical Python syntax characters
const pythonSyntax = [
'def ',
'else:',
'elif ',
'try:',
'except:',
'finally:',
'yield ',
'lambda ',
'assert ',
'nonlocal ',
'del ',
'True',
'False',
'None',
' and ',
' or ',
' not ',
' in ',
' is ',
' with '
];
for (let syntax of pythonSyntax) {
if (str.includes(syntax)) {
return true;
}
}
// If none of the above conditions met, it's probably not Python code
return false;
};
const executePython = async (code) => {
if (!code.includes('input') && !code.includes('matplotlib')) {
executePythonAsWorker(code);
} else {
result = null;
stdout = null;
stderr = null;
executing = true;
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
let pyodide = await loadPyodide({
indexURL: '/pyodide/',
stdout: (text) => {
console.log('Python output:', text);
if (stdout) {
stdout += `${text}\n`;
} else {
stdout = `${text}\n`;
}
},
stderr: (text) => {
console.log('An error occured:', text);
if (stderr) {
stderr += `${text}\n`;
} else {
stderr = `${text}\n`;
}
},
packages: ['micropip']
});
try {
const micropip = pyodide.pyimport('micropip');
await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
let packages = [
code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null,
code.includes('numpy') ? 'numpy' : null,
code.includes('pandas') ? 'pandas' : null,
code.includes('matplotlib') ? 'matplotlib' : null,
code.includes('sklearn') ? 'scikit-learn' : null,
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
].filter(Boolean);
console.log(packages);
await micropip.install(packages);
result = await pyodide.runPythonAsync(`from js import prompt
def input(p):
return prompt(p)
__builtins__.input = input`);
result = await pyodide.runPython(code);
if (!result) {
result = '[NO OUTPUT]';
}
console.log(result);
console.log(stdout);
console.log(stderr);
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
if (pltCanvasElement?.innerHTML !== '') {
pltCanvasElement.classList.add('pt-4');
}
} catch (error) {
console.error('Error:', error);
stderr = error;
}
executing = false;
}
};
const executePythonAsWorker = async (code) => {
result = null;
stdout = null;
stderr = null;
executing = true;
let packages = [
code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null,
code.includes('numpy') ? 'numpy' : null,
code.includes('pandas') ? 'pandas' : null,
code.includes('sklearn') ? 'scikit-learn' : null,
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
].filter(Boolean);
console.log(packages);
const pyodideWorker = new PyodideWorker();
pyodideWorker.postMessage({
id: id,
code: code,
packages: packages
});
setTimeout(() => {
if (executing) {
executing = false;
stderr = 'Execution Time Limit Exceeded';
pyodideWorker.terminate();
}
}, 60000);
pyodideWorker.onmessage = (event) => {
console.log('pyodideWorker.onmessage', event);
const { id, ...data } = event.data;
console.log(id, data);
data['stdout'] && (stdout = data['stdout']);
data['stderr'] && (stderr = data['stderr']);
data['result'] && (result = data['result']);
executing = false;
};
pyodideWorker.onerror = (event) => {
console.log('pyodideWorker.onerror', event);
executing = false;
};
};
$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : '';
</script>
{#if code}
<div class="mb-4">
<div class="mb-4" dir="ltr">
<div
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
>
<div class="p-1">{@html lang}</div>
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
>{copied ? 'Copied' : 'Copy Code'}</button
>
<div class="flex items-center">
{#if ['', 'python'].includes(lang) && (lang === 'python' || checkPythonCode(code))}
{#if executing}
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
{:else}
<button
class="copy-code-button bg-none border-none p-1"
on:click={() => {
executePython(code);
}}>Run</button
>
{/if}
{/if}
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
>{copied ? 'Copied' : 'Copy Code'}</button
>
</div>
</div>
<pre class=" rounded-b-lg hljs p-4 px-5 overflow-x-auto rounded-t-none"><code
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
</div>
{/if}
@@ -0,0 +1,163 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { updateChatById } from '$lib/apis/chats';
import { onMount, tick } from 'svelte';
import ResponseMessage from './ResponseMessage.svelte';
export let chatId;
export let history;
export let messages = [];
export let messageIdx;
export let parentMessage;
export let selectedModelfiles;
export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function;
export let rateMessage: Function;
export let copyToClipboard: Function;
export let continueGeneration: Function;
export let regenerateResponse: Function;
const dispatch = createEventDispatcher();
let currentMessageId;
let groupedMessagesIdx = {};
let groupedMessages = {};
$: groupedMessages = parentMessage?.models.reduce((a, model) => {
const modelMessages = parentMessage?.childrenIds
.map((id) => history.messages[id])
.filter((m) => m.model === model);
return {
...a,
[model]: { messages: modelMessages }
};
}, {});
const showPreviousMessage = (model) => {
groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
console.log(messageId);
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
dispatch('change');
};
const showNextMessage = (model) => {
groupedMessagesIdx[model] = Math.min(
groupedMessages[model].messages.length - 1,
groupedMessagesIdx[model] + 1
);
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
console.log(messageId);
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
dispatch('change');
};
onMount(async () => {
await tick();
currentMessageId = messages[messageIdx].id;
for (const model of parentMessage?.models) {
const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
if (idx !== -1) {
groupedMessagesIdx[model] = idx;
} else {
groupedMessagesIdx[model] = 0;
}
}
});
</script>
<div>
<div
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
id="responses-container-{parentMessage.id}"
>
{#each Object.keys(groupedMessages) as model}
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
currentMessageId
].model === model
? 'border-gray-100 dark:border-gray-700 border-[1.5px]'
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
on:click={() => {
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
console.log(messageId);
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
dispatch('change');
}}
>
<ResponseMessage
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
modelfiles={selectedModelfiles}
siblings={groupedMessages[model].messages.map((m) => m.id)}
isLastMessage={true}
{updateChatMessages}
{confirmEditResponseMessage}
showPreviousMessage={() => showPreviousMessage(model)}
showNextMessage={() => showNextMessage(model)}
{rateMessage}
{copyToClipboard}
{continueGeneration}
regenerateResponse={async (message) => {
regenerateResponse(message);
await tick();
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
</div>
{/if}
{/each}
</div>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
<div class=" self-center font-bold mb-0.5 capitalize line-clamp-1">
<div class=" self-center font-bold mb-0.5 line-clamp-1 contents">
<slot />
</div>
@@ -3,11 +3,19 @@
import { user } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { blur, fade } from 'svelte/transition';
import Suggestions from '../MessageInput/Suggestions.svelte';
const i18n = getContext('i18n');
export let models = [];
export let modelfiles = [];
export let submitPrompt;
export let suggestionPrompts;
let mounted = false;
let modelfile = null;
let selectedModelIdx = 0;
@@ -17,12 +25,16 @@
$: if (models.length > 0) {
selectedModelIdx = models.length - 1;
}
onMount(() => {
mounted = true;
});
</script>
{#if models.length > 0}
<div class="m-auto text-center max-w-md px-2">
<div class="flex justify-center mt-8">
<div class="flex -space-x-4 mb-1">
{#key mounted}
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16">
<div class="flex justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx}
<button
on:click={() => {
@@ -31,17 +43,19 @@
>
{#if model in modelfiles}
<img
crossorigin="anonymous"
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt="modelfile"
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false"
/>
{:else}
<img
src={models.length === 1
? `${WEBUI_BASE_URL}/static/favicon.png`
crossorigin="anonymous"
src={$i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
@@ -50,26 +64,42 @@
{/each}
</div>
</div>
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
{#if modelfile}
<span class=" capitalize">
{modelfile.title}
</span>
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
{modelfile.desc}
</div>
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
</div>
{/if}
{:else}
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
<div>{$i18n.t('How can I help you today?')}</div>
{/if}
<div
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
>
<div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
{#if modelfile}
{modelfile.title}
{:else}
{$i18n.t('Hello, {{name}}', { name: $user.name })}
{/if}
</div>
<div in:fade={{ duration: 200, delay: 200 }}>
{#if modelfile}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
{modelfile.desc}
</div>
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
</div>
{/if}
{:else}
<div class=" font-medium text-gray-400 dark:text-gray-500">
{$i18n.t('How can I help you today?')}
</div>
{/if}
</div>
</div>
</div>
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions {suggestionPrompts} {submitPrompt} />
</div>
</div>
{/if}
{/key}
@@ -1,7 +1,20 @@
<script lang="ts">
import { settings } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
export let src = '/user.png';
</script>
<div class=" mr-4">
<img {src} class=" max-w-[28px] object-cover rounded-full" alt="profile" draggable="false" />
<div class={($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}>
<img
crossorigin="anonymous"
src={src.startsWith(WEBUI_BASE_URL) ||
src.startsWith('https://www.gravatar.com/avatar/') ||
src.startsWith('data:')
? src
: `/user.png`}
class=" w-8 object-cover rounded-full"
alt="profile"
draggable="false"
/>
</div>
@@ -39,9 +39,9 @@
let selectedReason = null;
let comment = '';
$: if (message.annotation.rating === 1) {
$: if (message?.annotation?.rating === 1) {
reasons = LIKE_REASONS;
} else if (message.annotation.rating === -1) {
} else if (message?.annotation?.rating === -1) {
reasons = DISLIKE_REASONS;
}
@@ -123,7 +123,7 @@
submitHandler();
}}
>
Submit
{$i18n.t('Submit')}
</button>
</div>
</div>
@@ -23,15 +23,16 @@
revertSanitizedResponseContent,
sanitizeResponseContent
} from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import Skeleton from './Skeleton.svelte';
import CodeBlock from './CodeBlock.svelte';
import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte';
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
export let modelfiles = [];
export let message;
@@ -64,8 +65,11 @@
let generatingImage = false;
let showRateComment = false;
let showCitationModal = false;
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
let selectedCitation = null;
$: tokens = marked.lexer(sanitizeResponseContent(message?.content));
const renderer = new marked.Renderer();
@@ -137,20 +141,22 @@
.getElementById(`message-${message.id}`)
?.getElementsByClassName('chat-assistant');
for (const element of chatMessageElements) {
auto_render(element, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{ left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
// • rendering keys, e.g.:
throwOnError: false
});
if (chatMessageElements) {
for (const element of chatMessageElements) {
auto_render(element, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{ left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
// • rendering keys, e.g.:
throwOnError: false
});
}
}
};
@@ -221,7 +227,8 @@
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.speaker,
sentence
sentence,
$settings?.audio?.model
).catch((error) => {
toast.error(error);
@@ -322,13 +329,20 @@
});
</script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
{#key message.id}
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<div
class=" flex w-full message-{message.id}"
id="message-{message.id}"
dir={$settings.chatDirection}
>
<ProfileImage
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
src={modelfiles[message.model]?.imageUrl ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div class="w-full overflow-hidden">
<div class="w-full overflow-hidden pl-1">
<Name>
{#if message.model in modelfiles}
{modelfiles[message.model]?.title}
@@ -337,166 +351,214 @@
{/if}
{#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
<span
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
>
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
</span>
{/if}
</Name>
{#if message.content === ''}
<Skeleton />
{:else}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
{/if}
</div>
{/each}
</div>
{/if}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
{/if}
</div>
{/each}
</div>
{/if}
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
<div>
{#if edit === true}
<div class=" w-full">
<textarea
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
<div>
{#if edit === true}
<div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
<textarea
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button
id="close-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
/>
>
{$i18n.t('Cancel')}
</button>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg-lg"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save')}
</button>
<button
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
</div>
<button
id="save-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save')}
</button>
</div>
{:else}
<div class="w-full">
{#if message?.error === true}
<div
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
</div>
{:else}
<div class="w-full">
{#if message?.error === true}
<div
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 self-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 self-center"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<div class=" self-center">
{message.content}
</div>
<div class=" self-center">
{message.content}
</div>
{:else}
{#each tokens as token}
{#if token.type === 'code'}
<CodeBlock
lang={token.lang}
code={revertSanitizedResponseContent(token.text)}
/>
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
{/if}
</div>
{:else if message.content === ''}
<Skeleton />
{:else}
{#each tokens as token, tokenIdx}
{#if token.type === 'code'}
<CodeBlock
id={`${message.id}-${tokenIdx}`}
lang={token.lang}
code={revertSanitizedResponseContent(token.text)}
/>
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
{/if}
{#if message.done}
<div
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">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<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="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
{#if message.citations}
<div class="mt-1 mb-2 w-full flex gap-1 items-center">
{#each message.citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
{siblings.indexOf(message.id) + 1} / {siblings.length}
const existingSource = acc.find((item) => item.id === id);
if (existingSource) {
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: citation?.source, document: [document], metadata: metadata ? [metadata] : [] } );
}
});
return acc;
}, []) as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<button
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
{idx + 1}
</div>
<div class="flex-1 mx-2 line-clamp-1">
{citation.source.name}
</div>
</button>
</div>
{/each}
</div>
{/if}
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showNextMessage(message);
}}
{#if message.done || siblings.length > 1}
<div
class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
>
{#if siblings.length > 1}
<div class="flex self-center min-w-fit" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<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.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div
class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
>
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
{#if message.done}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
editMessageHandler();
}}
@@ -505,7 +567,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -519,11 +581,11 @@
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
on:click={() => {
copyToClipboard(message.content);
}}
@@ -532,7 +594,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -545,83 +607,12 @@
</button>
</Tooltip>
{#if !readOnly}
<Tooltip content="Good Response" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, 1);
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
</Tooltip>
<Tooltip content="Bad Response" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === -1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
</button>
</Tooltip>
{/if}
<Tooltip content="Read Aloud" placement="bottom">
<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
<button
id="speak-button-{message.id}"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!loadingSpeech) {
toggleSpeakMessage(message);
@@ -668,7 +659,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -683,7 +674,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -702,7 +693,7 @@
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!generatingImage) {
generateImage(message);
@@ -749,7 +740,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -765,11 +756,11 @@
{/if}
{#if message.info}
<Tooltip content="Generation Info" placement="bottom">
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
<button
class=" {isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
on:click={() => {
console.log(message);
}}
@@ -779,7 +770,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -793,13 +784,84 @@
</Tooltip>
{/if}
{#if !readOnly}
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
?.annotation?.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, 1);
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2.3"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
</Tooltip>
<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
?.annotation?.rating === -1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2.3"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
</button>
</Tooltip>
{/if}
{#if isLastMessage && !readOnly}
<Tooltip content="Continue Response" placement="bottom">
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
<button
type="button"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
continueGeneration();
}}
@@ -808,7 +870,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -826,19 +888,21 @@
</button>
</Tooltip>
<Tooltip content="Regenerate" placement="bottom">
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
<button
type="button"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={regenerateResponse}
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
regenerateResponse(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -851,24 +915,24 @@
</button>
</Tooltip>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
{#if showRateComment}
<RateComment
messageId={message.id}
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div>
{/if}
</div>
{#if message.done && showRateComment}
<RateComment
messageId={message.id}
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
{/key}
@@ -1,4 +1,4 @@
<div class="w-full mt-3">
<div class="w-full mt-3 mb-4">
<div class="animate-pulse flex w-full">
<div class="space-y-2 w-full">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
@@ -7,6 +7,8 @@
import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { user as _user } from '$lib/stores';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
@@ -54,49 +56,55 @@
};
</script>
<div class=" flex w-full">
<ProfileImage
src={message.user
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? '/user.png'
: user?.profile_image_url ?? '/user.png'}
/>
<div class="w-full overflow-hidden">
<div class="user-message">
<Name>
{#if message.user}
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
<div class=" flex w-full user-message" dir={$settings.chatDirection}>
{#if !($settings?.chatBubble ?? true)}
<ProfileImage
src={message.user
? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ??
'/user.png'
: user?.profile_image_url ?? '/user.png'}
/>
{/if}
<div class="w-full overflow-hidden pl-1">
{#if !($settings?.chatBubble ?? true)}
<div>
<Name>
{#if message.user}
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{:else}
{$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if}
{:else if $settings.showUsername || $_user.name !== user.name}
{user.name}
{:else}
{$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if}
{:else if $settings.showUsername}
{user.name}
{:else}
{$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($i18n.t('DD/MM/YYYY HH:mm'))}
</span>
{/if}
</Name>
</div>
{#if message.timestamp}
<span
class=" invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
>
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
</span>
{/if}
</Name>
</div>
{/if}
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
class="prose chat-{message.role} w-full max-w-full flex flex-col justify-end dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
>
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
{#each message.files as file}
<div>
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else if file.type === 'doc'}
<button
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={() => {
if (file?.url) {
@@ -132,7 +140,7 @@
</button>
{:else if file.type === 'collection'}
<button
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
@@ -166,7 +174,7 @@
{/if}
{#if edit === true}
<div class=" w-full">
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
<textarea
id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
@@ -190,85 +198,104 @@
}}
/>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
id="save-edit-message-button"
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg-lg"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save & Submit')}
</button>
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button
id="close-edit-message-button"
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
<button
id="save-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Send')}
</button>
</div>
</div>
{:else}
<div class="w-full">
<pre id="user-message">{message.content}</pre>
<div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2">
<div
class="rounded-3xl {$settings?.chatBubble ?? true
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : ''
}`
: ''} "
>
<pre id="user-message">{message.content}</pre>
</div>
</div>
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
{#if siblings.length > 1}
<div class="flex self-center">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
<div
class=" flex {$settings?.chatBubble ?? true
? 'justify-end'
: ''} text-gray-600 dark:text-gray-500"
>
{#if !($settings?.chatBubble ?? true)}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<path
fill-rule="evenodd"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-xs font-bold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1} / {siblings.length}
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showNextMessage(message);
}}
>
<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.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
{/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button"
on:click={() => {
editMessageHandler();
}}
@@ -277,7 +304,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -291,9 +318,9 @@
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
on:click={() => {
copyToClipboard(message.content);
}}
@@ -302,7 +329,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
>
@@ -316,7 +343,7 @@
</Tooltip>
{#if !isFirstMessage && !readOnly}
<Tooltip content="Delete" placement="bottom">
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
@@ -340,6 +367,60 @@
</button>
</Tooltip>
{/if}
{#if $settings?.chatBubble ?? true}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5"
/>
</svg>
</button>
<div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100">
{siblings.indexOf(message.id) + 1}/{siblings.length}
</div>
<button
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
</button>
</div>
{/if}
{/if}
</div>
</div>
{/if}
+17 -13
View File
@@ -2,7 +2,7 @@
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { models, showSettings, settings, user, mobile } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Selector from './ModelSelector/Selector.svelte';
@@ -13,6 +13,8 @@
export let selectedModels = [''];
export let disabled = false;
export let showSetDefault = true;
const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) {
@@ -36,11 +38,11 @@
}
</script>
<div class="flex flex-col mt-0.5 w-full">
<div class="flex flex-col w-full items-center md:items-start">
{#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full">
<div class="flex w-full max-w-fit">
<div class="overflow-hidden w-full">
<div class="mr-0.5 max-w-full">
<div class="mr-1 max-w-full">
<Selector
placeholder={$i18n.t('Select a model')}
items={$models
@@ -57,7 +59,7 @@
{#if selectedModelIdx === 0}
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
<Tooltip content="Add Model">
<Tooltip content={$i18n.t('Add Model')}>
<button
class=" "
{disabled}
@@ -69,9 +71,9 @@
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"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
@@ -80,7 +82,7 @@
</div>
{:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content="Remove Model">
<Tooltip content={$i18n.t('Remove Model')}>
<button
{disabled}
on:click={() => {
@@ -92,9 +94,9 @@
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"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
@@ -106,6 +108,8 @@
{/each}
</div>
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
{#if showSetDefault && !$mobile}
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
{/if}
@@ -1,5 +1,5 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
@@ -10,7 +10,7 @@
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -21,15 +21,22 @@
export let value = '';
export let placeholder = 'Select a model';
export let searchEnabled = true;
export let searchPlaceholder = 'Search a model';
export let searchPlaceholder = $i18n.t('Search a model');
export let items = [{ value: 'mango', label: 'Mango' }];
export let className = 'w-[30rem]';
let show = false;
let selectedModel = '';
$: selectedModel = items.find((item) => item.value === value) ?? '';
let searchValue = '';
let ollamaVersion = null;
$: filteredItems = searchValue
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase()))
: items;
const pullModelHandler = async () => {
@@ -144,7 +151,7 @@
models.set(await getModels(localStorage.token));
} else {
toast.error('Download canceled');
toast.error($i18n.t('Download canceled'));
}
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
@@ -175,27 +182,32 @@
};
</script>
<Select.Root
{items}
<DropdownMenu.Root
bind:open={show}
onOpenChange={async () => {
searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
selected={items.find((item) => item.value === value) ?? ''}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
}}
>
<Select.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
{placeholder}
/>
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
<div
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
>
{#if selectedModel}
{selectedModel.label}
{:else}
{placeholder}
{/if}
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class=" z-40 {$mobile
? `w-full`
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none "
transition={flyAndScale}
side={$mobile ? 'bottom' : 'bottom-start'}
sideOffset={4}
>
<slot>
@@ -208,18 +220,23 @@
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none"
placeholder={searchPlaceholder}
autocomplete="off"
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-72 overflow-y-auto">
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden">
{#each filteredItems as item}
<Select.Item
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
value={item.value}
label={item.label}
<button
aria-label="model-item"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
on:click={() => {
value = item.value;
show = false;
}}
>
<div class="flex items-center gap-2">
<div class="line-clamp-1">
@@ -287,23 +304,23 @@
<Check />
</div>
{/if}
</Select.Item>
</button>
{:else}
<div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
{$i18n.t('No results found')}
</div>
</div>
{/each}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
<button
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
on:click={() => {
pullModelHandler();
}}
>
Pull "{searchValue}" from Ollama.com
{$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })}
</button>
{/if}
@@ -354,7 +371,7 @@
</div>
<div class="mr-2 translate-y-0.5">
<Tooltip content="Cancel">
<Tooltip content={$i18n.t('Cancel')}>
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
@@ -384,6 +401,20 @@
</div>
{/each}
</div>
<div class="hidden w-[42rem]" />
<div class="hidden w-[32rem]" />
</slot>
</Select.Content>
</Select.Root>
</DropdownMenu.Content>
</DropdownMenu.Root>
<style>
.scrollbar-hidden:active::-webkit-scrollbar-thumb,
.scrollbar-hidden:focus::-webkit-scrollbar-thumb,
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-hidden::-webkit-scrollbar-thumb {
visibility: hidden;
}
</style>
@@ -71,7 +71,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-[22rem]">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
<input
id="profile-image-input"
bind:this={profileImageInputElement}
@@ -127,7 +127,7 @@
if (
files.length > 0 &&
['image/gif', 'image/jpeg', 'image/png'].includes(files[0]['type'])
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type'])
) {
reader.readAsDataURL(files[0]);
}
@@ -447,7 +447,7 @@
{/if}
</button>
<Tooltip content="Create new key">
<Tooltip content={$i18n.t('Create new key')}>
<button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => {
@@ -479,7 +479,7 @@
>
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button
{$i18n.t('Create new secret key')}</button
>
{/if}
</div>
@@ -84,7 +84,7 @@
{#if keepAlive !== null}
<div class="flex mt-1 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"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={keepAlive}
@@ -27,7 +27,7 @@
<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"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder="Enter Seed"
bind:value={options.seed}
@@ -43,7 +43,7 @@
<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"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
placeholder={$i18n.t('Enter stop sequence')}
bind:value={options.stop}
@@ -492,8 +492,8 @@
<input
id="steps-range"
type="range"
min="1"
max="16000"
min="-1"
max="10240000"
step="1"
bind:value={options.num_ctx}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
@@ -504,9 +504,8 @@
bind:value={options.num_ctx}
type="number"
class=" bg-transparent text-center w-14"
min="1"
max="16000"
step="1"
min="-1"
step="10"
/>
</div>
</div>
+39 -4
View File
@@ -26,6 +26,8 @@
let voices = [];
let speaker = '';
let models = [];
let model = '';
const getOpenAIVoices = () => {
voices = [
@@ -38,6 +40,10 @@
];
};
const getOpenAIVoicesModel = () => {
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
};
const getWebAPIVoices = () => {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
@@ -78,12 +84,16 @@
if (TTSEngine === 'openai') {
const res = await updateAudioConfig(localStorage.token, {
url: OpenAIUrl,
key: OpenAIKey
key: OpenAIKey,
model: model,
speaker: speaker
});
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL;
speaker = res.OPENAI_API_VOICE;
}
}
};
@@ -98,9 +108,11 @@
STTEngine = settings?.audio?.STTEngine ?? '';
TTSEngine = settings?.audio?.TTSEngine ?? '';
speaker = settings?.audio?.speaker ?? '';
model = settings?.audio?.model ?? '';
if (TTSEngine === 'openai') {
getOpenAIVoices();
getOpenAIVoicesModel();
} else {
getWebAPIVoices();
}
@@ -111,6 +123,8 @@
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
model = res.OPENAI_API_MODEL;
speaker = res.OPENAI_API_VOICE;
}
}
});
@@ -126,13 +140,14 @@
audio: {
STTEngine: STTEngine !== '' ? STTEngine : undefined,
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
speaker: speaker !== '' ? speaker : undefined
speaker: speaker !== '' ? speaker : undefined,
model: model !== '' ? model : undefined
}
});
dispatch('save');
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
@@ -215,6 +230,7 @@
if (e.target.value === 'openai') {
getOpenAIVoices();
speaker = 'alloy';
model = 'tts-1';
} else {
getWebAPIVoices();
speaker = '';
@@ -307,10 +323,29 @@
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={model}
placeholder="Select a model"
/>
<datalist id="model-list">
{#each models as model}
<option value={model.name} />
{/each}
</datalist>
</div>
</div>
</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-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"

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