Compare commits

..
179 Commits
Author SHA1 Message Date
Tim Jaeryang Baek e6afa69f59 Merge pull request #13887 from open-webui/dev
0.6.10
2025-05-19 05:34:23 +04:00
Timothy Jaeryang Baek 2b1c3b5208 Update CHANGELOG.md 2025-05-19 05:27:29 +04:00
Timothy Jaeryang Baek b905b73935 Update CHANGELOG.md 2025-05-19 05:24:03 +04:00
Timothy Jaeryang Baek fa5cd59ef8 doc: changelog 2025-05-19 05:20:57 +04:00
Timothy Jaeryang Baek 030c20feff refac 2025-05-19 05:12:53 +04:00
Tim Jaeryang Baek 8fb7b980a2 Merge pull request #14019 from silentoplayz/clear
feat: Improve remaining export buttons with total item count
2025-05-19 04:33:34 +04:00
Timothy Jaeryang Baek eafb8e014a chore: format 2025-05-19 04:32:00 +04:00
Timothy Jaeryang Baek 2ab5aa4d34 refac: azure openai 2025-05-19 04:31:04 +04:00
Silentoplayz 5cb1475e29 fix: apply change to functions and models pages 2025-05-18 20:12:53 -04:00
Timothy Jaeryang Baek 2f2a7ee0b0 chore: format 2025-05-19 03:58:42 +04:00
Timothy Jaeryang Baek 2e56b1f13d refac 2025-05-19 03:55:56 +04:00
Timothy Jaeryang Baek caeb822cdc feat: azure openai support 2025-05-19 03:40:32 +04:00
Timothy Jaeryang Baek 47f8b3500b feat: azure openai connections ui 2025-05-19 03:29:14 +04:00
Timothy Jaeryang Baek 73e64fe7fb refac: audio upload handling 2025-05-19 02:52:48 +04:00
Timothy Jaeryang Baekandsilentoplayz aee9d05916 enh: workspace search clear button
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-05-19 02:36:15 +04:00
Timothy Jaeryang Baek 3af289d196 refac 2025-05-19 02:32:06 +04:00
Timothy Jaeryang Baek 9f9fb3403d refac 2025-05-19 02:07:41 +04:00
Timothy Jaeryang Baek c6580a2a02 refac 2025-05-19 02:01:07 +04:00
Timothy Jaeryang Baek 417ba4980a chore: format 2025-05-19 01:55:02 +04:00
Timothy Jaeryang Baek 0c6f22b20b refac 2025-05-19 01:54:26 +04:00
Timothy Jaeryang Baek 96c7a0ac3c refac 2025-05-19 01:52:40 +04:00
Timothy Jaeryang Baek e895c33baa refac 2025-05-19 01:47:12 +04:00
Timothy Jaeryang Baek fbfd3ff134 refac 2025-05-19 01:44:27 +04:00
Timothy Jaeryang Baek 5a40feea1c enh: search shortcut 2025-05-19 01:40:51 +04:00
Timothy Jaeryang Baek 1f5860c4ab refac 2025-05-19 01:40:10 +04:00
Timothy Jaeryang Baek d069d1264d feat: search modal 2025-05-19 01:39:33 +04:00
Tim Jaeryang Baek 4f4811adb1 Merge pull request #14017 from athoik/title_tags
feat: read max_tokens from model config with fallback to 1000 for title and tag generation
2025-05-19 00:43:05 +04:00
Timothy Jaeryang Baek 05b79ce221 refac: model selection 2025-05-19 00:41:49 +04:00
Timothy Jaeryang Baekandsilentoplayz 18d6311951 feat: export count
Co-Authored-By: silentoplayz <50341825+silentoplayz@users.noreply.github.com>
2025-05-19 00:15:07 +04:00
Timothy Jaeryang Baek 8f4104fb7a refac 2025-05-19 00:13:03 +04:00
Tim Jaeryang Baek 349b0ca0b7 Merge pull request #14002 from silentoplayz/search-by-user-or-email
feat: enhance search with username/email search across workspace pages
2025-05-19 00:12:11 +04:00
Tim Jaeryang Baek 2dde4102ad Merge pull request #14009 from TiancongLx/dev
i18n: update and improve zh-TW
2025-05-19 00:11:32 +04:00
Athanasios Oikonomou eabdd4a140 feat: read max_tokens from model config with fallback to 1000 for title and tag generation
Improves title and tag generation by using the max_tokens value from the model configuration when available, with a fallback to the previous default of 1000.

This change is necessary for models like Gemini Pro that generate longer responses and require a higher token limit to successfully generate titles or tags.
2025-05-18 22:45:33 +03:00
Tiancong Li 4667328d40 i18n: update zh-TW 2025-05-18 21:16:16 +08:00
Tim Jaeryang Baek 56740ab8d6 Merge pull request #13996 from Davixk/update-IT-translation
i18n: add and improve Italian translations
2025-05-18 15:04:54 +04:00
Silentoplayz 95badda1c2 feat: username and email search in workspace pages 2025-05-18 03:23:38 -04:00
Dave ebf2e4141e Merge remote-tracking branch 'upstream/dev' into update-IT-translation 2025-05-18 04:46:41 +02:00
Dave a8813c1a69 i18n: Update and Improve Italian translations 2025-05-18 04:36:12 +02:00
Tim Jaeryang Baek fbcc80485b Merge pull request #13972 from jarrod-lowe/otel-attrs
feat: Add user details to opentelemetry spans
2025-05-17 21:54:56 +04:00
Timothy Jaeryang Baek 765acc4cad chore: dep bump 2025-05-17 21:53:31 +04:00
Timothy Jaeryang Baek 9801a2d23f refac 2025-05-17 03:14:26 +04:00
Timothy Jaeryang Baek 424f6f71cf chore: duckduckgo-search bump 2025-05-17 03:03:07 +04:00
Timothy Jaeryang Baek b280f828b0 enh: very long audio transcription 2025-05-17 02:51:28 +04:00
Timothy Jaeryang Baek d54e588ec3 refac 2025-05-17 02:26:28 +04:00
Timothy Jaeryang Baek 08e4c163ea feat: local/external connections 2025-05-17 01:47:48 +04:00
Timothy Jaeryang Baek 000906cd99 chore: format 2025-05-17 01:13:36 +04:00
Timothy Jaeryang Baek c3acb30bb1 feat: model file upload capability 2025-05-17 01:13:13 +04:00
Jarrod Lowe df853246f3 Add user details to otel span 2025-05-17 09:11:26 +12:00
Timothy Jaeryang Baek 6692fb2181 chore: format 2025-05-17 01:00:37 +04:00
Timothy Jaeryang Baek b14398481d enh: model capabilities 2025-05-17 00:59:00 +04:00
Timothy Jaeryang Baek 7df6d7f325 refac/fix: signout redirect flow 2025-05-17 00:38:39 +04:00
Timothy Jaeryang Baek fcb93f3155 feat: artifacts download button 2025-05-17 00:27:20 +04:00
Timothy Jaeryang Baek 02e5bbc903 refac 2025-05-17 00:19:08 +04:00
Timothy Jaeryang Baek 359bcb837d refac 2025-05-17 00:08:03 +04:00
Timothy Jaeryang Baek ecae944fd0 refac 2025-05-16 23:59:24 +04:00
Timothy Jaeryang Baek 05f284d4a6 refac 2025-05-16 23:41:51 +04:00
Timothy Jaeryang Baek ea723374c7 refac 2025-05-16 23:36:33 +04:00
Timothy Jaeryang Baek 1f38350128 feat: toggle filter middleware 2025-05-16 23:33:02 +04:00
Timothy Jaeryang Baek e37433f2b1 refac 2025-05-16 23:21:08 +04:00
Timothy Jaeryang Baek b61efcf54a feat: toggle filter 2025-05-16 22:59:02 +04:00
Timothy Jaeryang Baek 32b386e8d0 refac 2025-05-16 22:44:47 +04:00
Timothy Jaeryang Baek 7bbeaec93b revert 2025-05-16 22:43:42 +04:00
Timothy Jaeryang Baek 34ef249a10 revert: remove reasoning capability 2025-05-16 22:24:23 +04:00
Timothy Jaeryang Baek 363ac35c09 refac 2025-05-16 22:17:47 +04:00
Tim Jaeryang Baek 50bc6d9b12 Merge pull request #13919 from dongfangzan/main
feat: add switching thinking/non-thinking modes to models
2025-05-16 22:08:04 +04:00
Timothy Jaeryang Baek 27bb5f5369 fix/refac: tts remove formattings 2025-05-16 21:51:29 +04:00
Timothy Jaeryang Baek 54dc24986f feat: preview html 2025-05-16 21:47:43 +04:00
Timothy Jaeryang Baek 07b5e84221 refac 2025-05-16 21:29:50 +04:00
Timothy Jaeryang Baek 2bd7db12a2 enh: ALLOWED_FILE_EXTENSIONS ui 2025-05-16 21:05:52 +04:00
Timothy Jaeryang Baek 528a2cf96a refac 2025-05-16 18:19:28 +04:00
Timothy Jaeryang Baek 2aa5e73728 refac: filter details 2025-05-16 18:01:55 +04:00
Timothy Jaeryang Baek dec9dd1ac0 refac: web search queries 2025-05-16 17:11:42 +04:00
Timothy Jaeryang Baek bb06df80a2 refac 2025-05-16 17:07:52 +04:00
Timothy Jaeryang Baek 0b761776c4 enh: high contrast mode 2025-05-16 17:06:21 +04:00
Timothy Jaeryang Baek 2450f2c5d8 chore: format 2025-05-16 16:57:30 +04:00
Timothy Jaeryang Baek 62180b15e1 refac: command arrow up/down behaviour 2025-05-16 15:39:26 +04:00
Tim Jaeryang Baek c201b4d689 Merge pull request #13931 from itk-dev/feature/better-frontend-error
WIP: Minor clean up of console log usage in the front end
2025-05-16 13:33:16 +04:00
Tim Jaeryang Baek bc66755fb0 Merge pull request #13950 from uryukikutani/feature/screen-reader
fix: made the chat response messages readable with a screen reader
2025-05-16 13:32:40 +04:00
Tim Jaeryang Baek e74c06d2f5 Merge pull request #13929 from mini-777/feat--Korean-translations-for-clarity-and-accuracy
i18n : korean translations for clarity and accuracy
2025-05-16 13:32:10 +04:00
uryukikutani 53dcaa63d4 made the chat response messages readable with a screen reader 2025-05-16 17:41:56 +09:00
Tim Jaeryang Baek 186a89bcba Merge pull request #13937 from tanerim/patch-1
Update translation.json
2025-05-16 10:23:47 +04:00
dongfangzan 351f0186b9 fix: typo 2025-05-16 08:05:49 +08:00
Taner Sezer 21e96247a9 Update translation.json
Added new translations
2025-05-16 01:19:53 +03:00
Jesper Kristensen 84e0605835 Cleaning up usage of console log in front end 2025-05-15 21:53:07 +02:00
Jesper Kristensen 20c1c3f66b Changed console logging of errors to use console error logging 2025-05-15 21:35:07 +02:00
mini-777 2a4fe1335e i18n: update Korean translations with new entries and corrections 2025-05-16 02:45:19 +09:00
mini-777 e24e22debe Merge commit 'e4179aa114fecc8d35ba48f091ce7c9abcc96065' into HEAD 2025-05-16 02:10:01 +09:00
mini-777 208b86ae53 feat: add new notes feature with editor and list components, and update routing for notes management 2025-05-16 02:06:36 +09:00
Tim Jaeryang Baek e4179aa114 Merge pull request #13922 from TiancongLx/dev
i18n: update zh-TW
2025-05-15 19:02:37 +04:00
Tiancong Li df5ef8e553 i18n: update zh-TW 2025-05-15 21:15:22 +08:00
dongfangzan 9b93b81f84 feat: add switching thinking/non-thinking modes to models 2025-05-15 19:56:10 +08:00
dongfangzan a7c90925ef test contribute 2025-05-15 19:31:40 +08:00
Tim Jaeryang Baek ca2cfb5bad Merge pull request #13899 from loitragg/apply-multitenant-for-qdrant
perf: implement multi-tenancy for Qdrant vector store client
2025-05-15 13:37:36 +04:00
Kiet Trinh 418ac1a8da refac: Rename Qdrant multi-tenancy variable for improved clarity and consistency 2025-05-15 09:09:24 +00:00
Tim Jaeryang Baek db24af6356 Merge pull request #13902 from yo-nagase/dev
i18n: Update Japanese translation
2025-05-15 12:59:17 +04:00
Timothy Jaeryang Baek 72b2555953 refac 2025-05-15 12:58:44 +04:00
Kiet Trinh 485bd7666c fix: Update Qdrant multi-tenancy variable name for consistency in configuration 2025-05-15 08:02:58 +00:00
Nagase Yoshimasa 6eb772c741 i18n: Update Japanese translation 2025-05-15 15:41:04 +09:00
LoiTra 184d8dfd7e feat: Implement Qdrant multi-tenancy support with collection management and tenant isolation 2025-05-15 11:28:06 +07:00
Timothy Jaeryang Baek 0a8cecfbfa feat: response watermark 2025-05-14 23:53:28 +04:00
Timothy Jaeryang Baek b143c71da2 refac: AIOHTTP_CLIENT_SESSION_SSL 2025-05-14 23:33:52 +04:00
Timothy Jaeryang Baek 04287eb6d6 refac 2025-05-14 23:27:34 +04:00
Timothy Jaeryang Baek 39b06dc3b6 refac 2025-05-14 23:13:06 +04:00
Tim Jaeryang Baek e358a66fb1 Merge pull request #13852 from fizyr-forks/self-signed-cert
feat: Add option to disable certificate validation for LDAP certificates.
2025-05-14 23:12:24 +04:00
Timothy Jaeryang Baek a6624a4b16 feat: RAG_ALLOWED_FILE_EXTENSIONS 2025-05-14 23:06:33 +04:00
Timothy Jaeryang Baek 7e241a3904 refac 2025-05-14 22:48:23 +04:00
Timothy Jaeryang Baek 42382b5167 fix 2025-05-14 22:46:01 +04:00
Timothy Jaeryang Baek 8732b64b6b feat: external document loader support 2025-05-14 22:28:40 +04:00
Timothy Jaeryang Baek 32ea31144e refac: file upload handling behaviour 2025-05-14 22:00:42 +04:00
Timothy Jaeryang Baek b80f0751f6 refac 2025-05-14 21:59:17 +04:00
Timothy Jaeryang Baek 0280ee30c7 chore: format 2025-05-14 21:56:22 +04:00
Timothy Jaeryang Baek 8fe4f789d4 fix 2025-05-14 21:53:46 +04:00
Timothy Jaeryang Baek dacf7e015d i18n: revert 2025-05-14 21:52:42 +04:00
Timothy Jaeryang Baek ac83568194 chore: format 2025-05-14 21:40:51 +04:00
Timothy Jaeryang Baek 10108fcd04 chore: format 2025-05-14 21:39:50 +04:00
Timothy Jaeryang Baek 74ccec2812 refac 2025-05-14 21:39:17 +04:00
Tim Jaeryang Baek 29ae231ee0 Merge pull request #13889 from Classic298/pendingtext
feat: Custom Title and Text for Account Activation Screen for pending users
2025-05-14 21:28:16 +04:00
Timothy Jaeryang Baek de70d0cb64 feat: docling do picture description support 2025-05-14 21:26:49 +04:00
DrMelone 3974e53cc3 i18n 2025-05-14 19:11:53 +02:00
DrMelone 92140118f2 revert i18n 2025-05-14 19:07:45 +02:00
DrMelone e9eb661b83 fix + i18n 2025-05-14 18:58:54 +02:00
DrMelone bdb3778f8b fix 2025-05-14 18:10:23 +02:00
Timothy Jaeryang Baek 6e8481e157 fix: evaluation feedback endpoints 2025-05-14 20:08:49 +04:00
DrMelone 143cb4cc34 feat custom text and title 2025-05-14 17:58:20 +02:00
Tim Jaeryang Baek 2217fc4fd9 Merge pull request #13883 from alpha-pet/fix-workspace-model-unintended-toggle
Fix: Unintentional toggling of model status on the workspace page.
2025-05-14 18:09:16 +04:00
Tim Jaeryang Baek 8029bf0dca Merge pull request #13881 from hwzhuhao/feat/vector-db
feat: Add vector type and vector factory class for vector database integration
2025-05-14 17:41:39 +04:00
Thomas Rehn 6b6ce36c96 fix: add unique key to model list to avoid state confusion when filtering 2025-05-14 15:33:32 +02:00
hwzhuhao 6f869ded43 feat:Add vector type and vector factory class for vector database integration 2025-05-14 21:30:50 +08:00
Tim Jaeryang Baek 4959e91fa7 Merge pull request #13832 from Ithanil/svelte_versioning
feat: Handle frontend updates according to Svelte docs
2025-05-14 17:19:32 +04:00
Tim Jaeryang Baek f77a076c16 Merge pull request #13882 from itk-dev/feature/enable_direct_connections
chore: Added missing "enable_direct_connections" to config type
2025-05-14 17:18:04 +04:00
Jesper Kristensen 8b194bc781 Added missing "enable_direct_connections" to config type 2025-05-14 14:53:36 +02:00
Tim Jaeryang Baek bd7d2978f1 Merge pull request #13846 from hary777/patch-2
i18n update translation.json fix typo "amin" in cs-CZ
2025-05-14 15:27:40 +04:00
Tim Jaeryang Baek c7e3ae9f39 Merge pull request #13812 from hwzhuhao/refactor/log
refactor: replace print statements with logging
2025-05-14 15:27:13 +04:00
Timothy Jaeryang Baek 92a8ea743a refac: form data edge case 2025-05-14 15:22:38 +04:00
Timothy Jaeryang Baek 3c4accaeb3 refac/security: ollama model upload path traversal 2025-05-14 15:17:07 +04:00
Timothy Jaeryang Baek a9c22bffc9 refac/security: pipelines upload path traversal 2025-05-14 15:15:05 +04:00
Tim Jaeryang Baek d89e37cc7c Merge pull request #13866 from itk-dev/feature/contrast-chat-input
feat: Contrast enhancements
2025-05-14 15:12:00 +04:00
Sine Jespersen 8eacd26573 add placeholder dark mode 2025-05-14 12:50:47 +02:00
Sine Jespersen 90bcf45b7b placeholder contrast changed 2025-05-14 10:24:09 +02:00
Yeyeong Cho bbb4e75df8 Merge pull request #2 from mini-777/i18n-ko-rewriting
feat(openwebui) : improve Korean translations for better readability Fixes mini-777/8dobibim#2
2025-05-14 17:23:48 +09:00
Sine Jespersen b052b5cced remove whitespace 2025-05-14 10:15:31 +02:00
Sine Jespersen 5db783d67b improve suggestions contrast 2025-05-14 09:52:44 +02:00
Sine Jespersen 43d0db5eea model selector contrast 2025-05-14 09:52:10 +02:00
Sine Jespersen 41f64da0d6 update message input contrast 2025-05-14 09:51:38 +02:00
Sine Jespersen 5cd0b99ae0 update toast contrast 2025-05-14 09:51:07 +02:00
mini-777 571d078a82 fix: improve Korean translations for better readability (fixes mini-777/8dobibim#2) 2025-05-14 15:37:59 +09:00
mini-777 f72fa9b269 fix: update Korean translations for clarity and accuracy 2025-05-14 15:28:46 +09:00
Hans Gaiser f63e76e653 Add option to disable certificate validation. 2025-05-13 17:38:06 +02:00
hary777 66d73cb1de Update translation.json fix typo
Fix typo "amin". I can not look at this anymore.
2025-05-13 15:24:48 +02:00
Tim Jaeryang Baek 950f3cdcc0 Merge pull request #13838 from TiancongLx/dev
i18n: update, complete, and optimize zh-TW
2025-05-13 16:53:52 +04:00
Tim Jaeryang Baek f907e7c979 Merge pull request #13836 from itk-dev/feature/build-watch
build: Added build:watch command to package.json
2025-05-13 16:53:24 +04:00
Jesper Kristensen 24c1b8911c Added build:watch script/command to package.json
Should make it eaiser to automatically build the front-end when
doing local development in docker node containers.
2025-05-13 10:36:48 +02:00
Tiancong Li b41fc1d2f4 i18n: update zh-TW 2025-05-13 16:30:59 +08:00
Jan Kessler dfd4037132 better handling of frontend updates, following svelte docs 2025-05-13 09:51:51 +02:00
Timothy Jaeryang Baek a904f874ee fix: admin signup logic 2025-05-12 20:03:40 +04:00
hwzhuhao fb2950201a refactor: replace print statements with logging 2025-05-12 22:18:47 +08:00
Timothy Jaeryang Baek e9b799676b chore: format 2025-05-12 17:34:33 +04:00
Tim Jaeryang Baek c7f743ddb7 Merge pull request #13810 from phpia/phpia-patch-1
i18n es
2025-05-12 17:31:18 +04:00
phpia 16a9210321 Update translation for mantain prompt
This pull request updates the Spanish localization by replacing all instances of "indicador"/"indicadores" with the more accurate terms "prompt"/"prompts" throughout the UI, documentation, and messages. 

Reason and Rationale: 

In the context of AI and Large Language Models (LLMs), the words "prompt" and "prompts" have become standardized terms worldwide, including within the Spanish-speaking technical community. While "indicador" is a valid literal translation of "prompt" in general Spanish, in generative AI contexts it is potentially confusing, since "indicador" more commonly refers to a gauge, dashboard indicator, or measuring device. This can cause misunderstanding for users, especially those familiar with AI concepts, who expect the term "prompt" to refer to the input text given to a model.
2025-05-12 14:59:15 +02:00
Sung Min c049837a11 Merge pull request #1 from mini-777/i18n-ko-missing
feat(openwebui) : add missing Korean translation strings Fixes mini-777/8dobibim#1
2025-05-12 20:55:14 +09:00
Tim Jaeryang Baek ca34ba318e Merge pull request #13801 from aleixdorca/dev
i18n: Update catalan translation.json
2025-05-12 14:56:19 +04:00
Tim Jaeryang Baek 0d8e24320e Merge pull request #13794 from Lutherwaves/dev
i18n: Bulgarian translations
2025-05-12 14:55:10 +04:00
Aleix Dorca b7ef9a03ac Update catalan translation.json 2025-05-12 08:56:52 +02:00
Tim Jaeryang Baek 4ce1e88750 Merge pull request #13795 from open-webui/dev
refac
2025-05-12 01:10:34 +04:00
Timothy Jaeryang Baek f32b042a00 refac 2025-05-12 01:08:49 +04:00
lutherwaves 792beb095e i18n: Bulgarian translations 2025-05-11 23:58:31 +03:00
Timothy Jaeryang Baek 4c06683e95 refac: Vazirmatn 2025-05-12 00:05:04 +04:00
Tim Jaeryang Baek 0cef844168 Merge pull request #13758 from open-webui/dev
0.6.9
2025-05-10 23:04:48 +04:00
Timothy Jaeryang Baek c56d6963b0 doc: changelog 2025-05-10 23:01:48 +04:00
Timothy Jaeryang Baek 92e2cfdd2f refac 2025-05-10 23:00:53 +04:00
Timothy Jaeryang Baek 3ad5a7d97c chore: bump 2025-05-10 22:48:26 +04:00
Timothy Jaeryang Baek 867fddb821 chore: format 2025-05-10 22:45:58 +04:00
Timothy Jaeryang Baek de69c08a1d enh: alert private task model 2025-05-10 22:43:53 +04:00
Timothy Jaeryang Baek 30cc51ec82 feat: edit message files 2025-05-10 22:27:39 +04:00
Timothy Jaeryang Baek ba69c751ca refac: temporary chat behaviour 2025-05-10 22:12:36 +04:00
Timothy Jaeryang Baek be85f7cc35 fix: confirm dialog focus 2025-05-10 22:09:05 +04:00
Tim Jaeryang Baek 4191ec692e Merge pull request #13751 from open-webui/dev
fix: external reranker
2025-05-10 19:33:59 +04:00
Timothy Jaeryang Baek 6b5f99bf66 fix: external reranker 2025-05-10 19:33:34 +04:00
Yeyeong Cho 13e7a3dee9 fix:add missing Korean translation strings Fixes mini-777/8dobibim#1 2025-05-09 17:54:50 +09:00
175 changed files with 6718 additions and 2689 deletions
+43
View File
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.10] - 2025-05-19
### Added
- 🧩 **Experimental Azure OpenAI Support**: Instantly connect to Azure OpenAI endpoints by simply pasting your Azure OpenAI URL into the model connections—bringing flexible, enterprise-grade AI integration directly to your workflow.
- 💧 **Watermark AI Responses**: Easily add a visible watermark to AI-generated responses for clear content provenance and compliance with EU AI regulations—perfect for regulated environments and transparent communication.
- 🔍 **Enhanced Search Experience with Dedicated Modal**: Enjoy a modern, powerful search UI in a dedicated modal (open with Ctrl/Cmd + K) accessible from anywhere—quickly find chats, models, or content and boost your productivity.
- 🔲 **"Toggle" Filter Type for Chat Input**: Add interactive toggle filters (e.g. Web Search, Image, Code Interpreter) right into the chat input—giving you one-click control to activate features and simplifying the chat configuration process.
- 🧰 **Granular Model Capabilities Editor**: Define detailed capabilities and feature access for each AI model directly from the model editor—enabling tailored behavior, modular control, and a more customized AI environment for every team or use case.
- 🌐 **Flexible Local and External Connection Support**: Now you can designate any AI connection—whether OpenAI, Ollama, or others—as local or external, enabling seamless setup for on-premise, self-hosted, or cloud configurations and giving you maximum control and flexibility.
- 🗂️ **Allowed File Extensions for RAG**: Gain full control over your Retrieval-Augmented Generation (RAG) workflows by specifying exactly which file extensions are permitted for upload, improving security and relevance of indexed documents.
- 🔊 **Enhanced Audio Transcription Logic**: Experience smoother, more reliable audio transcription—very long audio files are now automatically split and processed in segments, preventing errors and ensuring even challenging files are transcribed seamlessly, all part of a broader stability upgrade for robust media workflows.
- 🦾 **External Document Loader Support**: Enhance knowledge base building by integrating documents using external loaders from a wide range of data sources, expanding what your AI can read and process.
- 📝 **Preview Button for Code Artifacts**: Instantly jump from an HTML code block to its associated artifacts page with the click of a new preview button—speeding up review and streamlining analysis.
- 🦻 **Screen Reader Support for Response Messages**: All chat responses are now fully compatible with screen readers, making the platform more inclusive and accessible for everyone.
- 🧑‍💼 **Customizable Pending User Overlay**: You can now tailor the overlay title and content shown to pending users, ensuring onboarding messaging is perfectly aligned with your organization’s tone and requirements.
- 🔐 **Option to Disable LDAP Certificate Validation**: You can now disable LDAP certificate validation for maximum flexibility in diverse IT environments—making integrations and troubleshooting much easier.
- 🎯 **Workspace Search by Username or Email**: Easily search across workspace pages using any username or email address, streamlining user and resource management.
- 🎨 **High Contrast & Dark Mode Enhancements**: Further improved placeholder, input, suggestion, toast, and model selector contrasts—including a dedicated placeholder dark mode—for more comfortable viewing in all lighting conditions.
- 🛡️ **Refined Security for Pipelines & Model Uploads**: Strengthened safeguards against path traversal vulnerabilities during uploads—ensuring your platform’s document and model management remains secure.
- 🌏 **Major Localization Upgrades**: Comprehensive translation updates and improvements across Korean, Bulgarian, Catalan, Japanese, Italian, Traditional Chinese, and Spanish—including more accurate AI terminology for clarity; your experience is now more natural, inclusive, and professional everywhere.
- 🦾 **General Backend Stability & Security**: Multiple backend improvements (including file upload, command navigation, and logging refactorings) deliver increased resilience, better error handling, and a more robust platform for all users.
### Fixed
- ✅ **Evaluation Feedback Endpoint Reliability**: Addressed issues with feedback submission endpoints to ensure seamless user feedback collection on model responses.
- 🫰 **Model List State Fixes**: Resolved issues where model status toggles in the workspace page might inadvertently switch or confuse state, making the management of active/inactive models more dependable.
- ✍️ **Admin Signup Logic**: Admin self-signup experience and validation flow is smoother and more robust.
- 🔁 **Signout Redirect Flow Improved**: Logging out now redirects more reliably, reducing confusion and making session management seamless.
## [0.6.9] - 2025-05-10
### Added
- 📝 **Edit Attached Images/Files in Messages**: You can now easily edit your sent messages by removing attached files—streamlining document management, correcting mistakes on the fly, and keeping your chats clutter-free.
- 🚨 **Clear Alerts for Private Task Models**: When interacting with private task models, the UI now clearly alerts you—making it easier to understand resource availability and access, reducing confusion during workflow setup.
### Fixed
- 🛡️ **Confirm Dialog Focus Trap Reliability**: The focus now stays correctly within confirmation dialogs, ensuring keyboard navigation and accessibility is seamless and preventing accidental operations—especially helpful during critical or rapid workflows.
- 💬 **Temporary Chat Admin Controls & Session Cleanliness**: Admins are now able to properly enable temporary chat mode without errors, and previous session prompts or tool selections no longer carry over—delivering a fresh, predictable, and consistent temporary chat experience every time.
- 🤖 **External Reranker Integration Functionality Restored**: External reranker integrations now work correctly, allowing you to fully leverage advanced ranking services for sharper, more relevant search results in your RAG and knowledge base workflows.
## [0.6.8] - 2025-05-10 ## [0.6.8] - 2025-05-10
### Added ### Added
+3
View File
@@ -26,6 +26,9 @@ ARG BUILD_HASH
WORKDIR /app WORKDIR /app
# to store git revision in build
RUN apk add --no-cache git
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
+57
View File
@@ -989,6 +989,26 @@ DEFAULT_USER_ROLE = PersistentConfig(
os.getenv("DEFAULT_USER_ROLE", "pending"), os.getenv("DEFAULT_USER_ROLE", "pending"),
) )
PENDING_USER_OVERLAY_TITLE = PersistentConfig(
"PENDING_USER_OVERLAY_TITLE",
"ui.pending_user_overlay_title",
os.environ.get("PENDING_USER_OVERLAY_TITLE", ""),
)
PENDING_USER_OVERLAY_CONTENT = PersistentConfig(
"PENDING_USER_OVERLAY_CONTENT",
"ui.pending_user_overlay_content",
os.environ.get("PENDING_USER_OVERLAY_CONTENT", ""),
)
RESPONSE_WATERMARK = PersistentConfig(
"RESPONSE_WATERMARK",
"ui.watermark",
os.environ.get("RESPONSE_WATERMARK", ""),
)
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower() os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
== "true" == "true"
@@ -1731,6 +1751,9 @@ QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true" QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true"
QDRANT_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "False").lower() == "true" QDRANT_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "False").lower() == "true"
QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334")) QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
ENABLE_QDRANT_MULTITENANCY_MODE = (
os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "false").lower() == "true"
)
# OpenSearch # OpenSearch
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200") OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
@@ -1825,6 +1848,18 @@ CONTENT_EXTRACTION_ENGINE = PersistentConfig(
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(), os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
) )
EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig(
"EXTERNAL_DOCUMENT_LOADER_URL",
"rag.external_document_loader_url",
os.environ.get("EXTERNAL_DOCUMENT_LOADER_URL", ""),
)
EXTERNAL_DOCUMENT_LOADER_API_KEY = PersistentConfig(
"EXTERNAL_DOCUMENT_LOADER_API_KEY",
"rag.external_document_loader_api_key",
os.environ.get("EXTERNAL_DOCUMENT_LOADER_API_KEY", ""),
)
TIKA_SERVER_URL = PersistentConfig( TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL", "TIKA_SERVER_URL",
"rag.tika_server_url", "rag.tika_server_url",
@@ -1849,6 +1884,12 @@ DOCLING_OCR_LANG = PersistentConfig(
os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"), os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"),
) )
DOCLING_DO_PICTURE_DESCRIPTION = PersistentConfig(
"DOCLING_DO_PICTURE_DESCRIPTION",
"rag.docling_do_picture_description",
os.getenv("DOCLING_DO_PICTURE_DESCRIPTION", "False").lower() == "true",
)
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig( DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
"DOCUMENT_INTELLIGENCE_ENDPOINT", "DOCUMENT_INTELLIGENCE_ENDPOINT",
"rag.document_intelligence_endpoint", "rag.document_intelligence_endpoint",
@@ -1920,6 +1961,16 @@ RAG_FILE_MAX_SIZE = PersistentConfig(
), ),
) )
RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig(
"RAG_ALLOWED_FILE_EXTENSIONS",
"rag.file.allowed_extensions",
[
ext.strip()
for ext in os.environ.get("RAG_ALLOWED_FILE_EXTENSIONS", "").split(",")
if ext.strip()
],
)
RAG_EMBEDDING_ENGINE = PersistentConfig( RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE", "RAG_EMBEDDING_ENGINE",
"rag.embedding_engine", "rag.embedding_engine",
@@ -2839,6 +2890,12 @@ LDAP_CA_CERT_FILE = PersistentConfig(
os.environ.get("LDAP_CA_CERT_FILE", ""), os.environ.get("LDAP_CA_CERT_FILE", ""),
) )
LDAP_VALIDATE_CERT = PersistentConfig(
"LDAP_VALIDATE_CERT",
"ldap.server.validate_cert",
os.environ.get("LDAP_VALIDATE_CERT", "True").lower() == "true",
)
LDAP_CIPHERS = PersistentConfig( LDAP_CIPHERS = PersistentConfig(
"LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL") "LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL")
) )
+29 -3
View File
@@ -197,6 +197,7 @@ from open_webui.config import (
RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_BATCH_SIZE, RAG_EMBEDDING_BATCH_SIZE,
RAG_RELEVANCE_THRESHOLD, RAG_RELEVANCE_THRESHOLD,
RAG_ALLOWED_FILE_EXTENSIONS,
RAG_FILE_MAX_COUNT, RAG_FILE_MAX_COUNT,
RAG_FILE_MAX_SIZE, RAG_FILE_MAX_SIZE,
RAG_OPENAI_API_BASE_URL, RAG_OPENAI_API_BASE_URL,
@@ -206,10 +207,13 @@ from open_webui.config import (
CHUNK_OVERLAP, CHUNK_OVERLAP,
CHUNK_SIZE, CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE, CONTENT_EXTRACTION_ENGINE,
EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL, TIKA_SERVER_URL,
DOCLING_SERVER_URL, DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE, DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG, DOCLING_OCR_LANG,
DOCLING_DO_PICTURE_DESCRIPTION,
DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_KEY, MISTRAL_OCR_API_KEY,
@@ -291,6 +295,8 @@ from open_webui.config import (
ENABLE_EVALUATION_ARENA_MODELS, ENABLE_EVALUATION_ARENA_MODELS,
USER_PERMISSIONS, USER_PERMISSIONS,
DEFAULT_USER_ROLE, DEFAULT_USER_ROLE,
PENDING_USER_OVERLAY_CONTENT,
PENDING_USER_OVERLAY_TITLE,
DEFAULT_PROMPT_SUGGESTIONS, DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_MODELS, DEFAULT_MODELS,
DEFAULT_ARENA_MODEL, DEFAULT_ARENA_MODEL,
@@ -317,6 +323,7 @@ from open_webui.config import (
LDAP_APP_PASSWORD, LDAP_APP_PASSWORD,
LDAP_USE_TLS, LDAP_USE_TLS,
LDAP_CA_CERT_FILE, LDAP_CA_CERT_FILE,
LDAP_VALIDATE_CERT,
LDAP_CIPHERS, LDAP_CIPHERS,
# Misc # Misc
ENV, ENV,
@@ -327,6 +334,7 @@ from open_webui.config import (
DEFAULT_LOCALE, DEFAULT_LOCALE,
OAUTH_PROVIDERS, OAUTH_PROVIDERS,
WEBUI_URL, WEBUI_URL,
RESPONSE_WATERMARK,
# Admin # Admin
ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_CHAT_ACCESS,
ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_EXPORT,
@@ -373,6 +381,7 @@ from open_webui.env import (
OFFLINE_MODE, OFFLINE_MODE,
ENABLE_OTEL, ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL, EXTERNAL_PWA_MANIFEST_URL,
AIOHTTP_CLIENT_SESSION_SSL,
) )
@@ -573,6 +582,11 @@ app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE
app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS app.state.config.BANNERS = WEBUI_BANNERS
@@ -609,6 +623,7 @@ app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
app.state.config.LDAP_USE_TLS = LDAP_USE_TLS app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
@@ -631,6 +646,7 @@ app.state.FUNCTIONS = {}
app.state.config.TOP_K = RAG_TOP_K app.state.config.TOP_K = RAG_TOP_K
app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
@@ -641,10 +657,13 @@ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
@@ -1167,11 +1186,12 @@ async def chat_completion(
"chat_id": form_data.pop("chat_id", None), "chat_id": form_data.pop("chat_id", None),
"message_id": form_data.pop("id", None), "message_id": form_data.pop("id", None),
"session_id": form_data.pop("session_id", None), "session_id": form_data.pop("session_id", None),
"filter_ids": form_data.pop("filter_ids", []),
"tool_ids": form_data.get("tool_ids", None), "tool_ids": form_data.get("tool_ids", None),
"tool_servers": form_data.pop("tool_servers", None), "tool_servers": form_data.pop("tool_servers", None),
"files": form_data.get("files", None), "files": form_data.get("files", None),
"features": form_data.get("features", None), "features": form_data.get("features", {}),
"variables": form_data.get("variables", None), "variables": form_data.get("variables", {}),
"model": model, "model": model,
"direct": model_item.get("direct", False), "direct": model_item.get("direct", False),
**( **(
@@ -1395,6 +1415,11 @@ async def get_app_config(request: Request):
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value, "sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
"sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value, "sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value,
}, },
"ui": {
"pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE,
"pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT,
"response_watermark": app.state.config.RESPONSE_WATERMARK,
},
"license_metadata": app.state.LICENSE_METADATA, "license_metadata": app.state.LICENSE_METADATA,
**( **(
{ {
@@ -1446,7 +1471,8 @@ async def get_app_latest_release_version(user=Depends(get_verified_user)):
timeout = aiohttp.ClientTimeout(total=1) timeout = aiohttp.ClientTimeout(total=1)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get( async with session.get(
"https://api.github.com/repos/open-webui/open-webui/releases/latest" "https://api.github.com/repos/open-webui/open-webui/releases/latest",
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
data = await response.json() data = await response.json()
@@ -0,0 +1,58 @@
import requests
import logging
from typing import Iterator, List, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalDocumentLoader(BaseLoader):
def __init__(
self,
file_path,
url: str,
api_key: str,
mime_type=None,
**kwargs,
) -> None:
self.url = url
self.api_key = api_key
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
headers = {}
if self.mime_type is not None:
headers["Content-Type"] = self.mime_type
if self.api_key is not None:
headers["Authorization"] = f"Bearer {self.api_key}"
url = self.url
if url.endswith("/"):
url = url[:-1]
r = requests.put(f"{url}/process", data=data, headers=headers)
if r.ok:
res = r.json()
if res:
return [
Document(
page_content=res.get("page_content"),
metadata=res.get("metadata"),
)
]
else:
raise Exception("Error loading document: No content returned")
else:
raise Exception(f"Error loading document: {r.status_code} {r.text}")
@@ -10,7 +10,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalLoader(BaseLoader): class ExternalWebLoader(BaseLoader):
def __init__( def __init__(
self, self,
web_paths: Union[str, List[str]], web_paths: Union[str, List[str]],
@@ -32,7 +32,7 @@ class ExternalLoader(BaseLoader):
response = requests.post( response = requests.post(
self.external_url, self.external_url,
headers={ headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) External Web Loader",
"Authorization": f"Bearer {self.external_api_key}", "Authorization": f"Bearer {self.external_api_key}",
}, },
json={ json={
+45 -12
View File
@@ -21,6 +21,8 @@ from langchain_community.document_loaders import (
) )
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader
from open_webui.retrieval.loaders.mistral import MistralLoader from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@@ -126,14 +128,12 @@ class TikaLoader:
class DoclingLoader: class DoclingLoader:
def __init__( def __init__(self, url, file_path=None, mime_type=None, params=None):
self, url, file_path=None, mime_type=None, ocr_engine=None, ocr_lang=None
):
self.url = url.rstrip("/") self.url = url.rstrip("/")
self.file_path = file_path self.file_path = file_path
self.mime_type = mime_type self.mime_type = mime_type
self.ocr_engine = ocr_engine
self.ocr_lang = ocr_lang self.params = params or {}
def load(self) -> list[Document]: def load(self) -> list[Document]:
with open(self.file_path, "rb") as f: with open(self.file_path, "rb") as f:
@@ -150,11 +150,19 @@ class DoclingLoader:
"table_mode": "accurate", "table_mode": "accurate",
} }
if self.ocr_engine and self.ocr_lang: if self.params:
params["ocr_engine"] = self.ocr_engine if self.params.get("do_picture_classification"):
params["ocr_lang"] = [ params["do_picture_classification"] = self.params.get(
lang.strip() for lang in self.ocr_lang.split(",") if lang.strip() "do_picture_classification"
] )
if self.params.get("ocr_engine") and self.params.get("ocr_lang"):
params["ocr_engine"] = self.params.get("ocr_engine")
params["ocr_lang"] = [
lang.strip()
for lang in self.params.get("ocr_lang").split(",")
if lang.strip()
]
endpoint = f"{self.url}/v1alpha/convert/file" endpoint = f"{self.url}/v1alpha/convert/file"
r = requests.post(endpoint, files=files, data=params) r = requests.post(endpoint, files=files, data=params)
@@ -207,6 +215,17 @@ class Loader:
def _get_loader(self, filename: str, file_content_type: str, file_path: str): def _get_loader(self, filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower() file_ext = filename.split(".")[-1].lower()
if (
self.engine == "external"
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL")
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY")
):
loader = ExternalDocumentLoader(
file_path=file_path,
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
mime_type=file_content_type,
)
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"): if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type): if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True) loader = TextLoader(file_path, autodetect_encoding=True)
@@ -225,8 +244,13 @@ class Loader:
url=self.kwargs.get("DOCLING_SERVER_URL"), url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path, file_path=file_path,
mime_type=file_content_type, mime_type=file_content_type,
ocr_engine=self.kwargs.get("DOCLING_OCR_ENGINE"), params={
ocr_lang=self.kwargs.get("DOCLING_OCR_LANG"), "ocr_engine": self.kwargs.get("DOCLING_OCR_ENGINE"),
"ocr_lang": self.kwargs.get("DOCLING_OCR_LANG"),
"do_picture_classification": self.kwargs.get(
"DOCLING_DO_PICTURE_DESCRIPTION"
),
},
) )
elif ( elif (
self.engine == "document_intelligence" self.engine == "document_intelligence"
@@ -258,6 +282,15 @@ class Loader:
loader = MistralLoader( loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
) )
elif (
self.engine == "external"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
else: else:
if file_ext == "pdf": if file_ext == "pdf":
loader = PyPDFLoader( loader = PyPDFLoader(
+4 -2
View File
@@ -12,7 +12,7 @@ from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.config import VECTOR_DB from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.models.files import Files from open_webui.models.files import Files
@@ -818,7 +818,9 @@ class RerankCompressor(BaseDocumentCompressor):
) )
scores = util.cos_sim(query_embedding, document_embedding)[0] scores = util.cos_sim(query_embedding, document_embedding)[0]
docs_with_scores = list(zip(documents, scores.tolist())) docs_with_scores = list(
zip(documents, scores.tolist() if not isinstance(scores, list) else scores)
)
if self.r_score: if self.r_score:
docs_with_scores = [ docs_with_scores = [
(d, s) for d, s in docs_with_scores if s >= self.r_score (d, s) for d, s in docs_with_scores if s >= self.r_score
@@ -1,30 +0,0 @@
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
VECTOR_DB_CLIENT = MilvusClient()
elif VECTOR_DB == "qdrant":
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
VECTOR_DB_CLIENT = QdrantClient()
elif VECTOR_DB == "opensearch":
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
VECTOR_DB_CLIENT = OpenSearchClient()
elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
elif VECTOR_DB == "elasticsearch":
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
VECTOR_DB_CLIENT = ElasticsearchClient()
elif VECTOR_DB == "pinecone":
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
VECTOR_DB_CLIENT = PineconeClient()
else:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
VECTOR_DB_CLIENT = ChromaClient()
@@ -0,0 +1,712 @@
import logging
from typing import Optional, Tuple
from urllib.parse import urlparse
import grpc
from open_webui.config import (
QDRANT_API_KEY,
QDRANT_GRPC_PORT,
QDRANT_ON_DISK,
QDRANT_PREFER_GRPC,
QDRANT_URI,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.main import (
GetResult,
SearchResult,
VectorDBBase,
VectorItem,
)
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.exceptions import UnexpectedResponse
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
NO_LIMIT = 999999999
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
self.QDRANT_ON_DISK = QDRANT_ON_DISK
self.PREFER_GRPC = QDRANT_PREFER_GRPC
self.GRPC_PORT = QDRANT_GRPC_PORT
if not self.QDRANT_URI:
self.client = None
return
# Unified handling for either scheme
parsed = urlparse(self.QDRANT_URI)
host = parsed.hostname or self.QDRANT_URI
http_port = parsed.port or 6333 # default REST port
if self.PREFER_GRPC:
self.client = Qclient(
host=host,
port=http_port,
grpc_port=self.GRPC_PORT,
prefer_grpc=self.PREFER_GRPC,
api_key=self.QDRANT_API_KEY,
)
else:
self.client = Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
# Main collection types for multi-tenancy
self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories"
self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge"
self.FILE_COLLECTION = f"{self.collection_prefix}_files"
self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web-search"
self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash-based"
def _result_to_get_result(self, points) -> GetResult:
ids = []
documents = []
metadatas = []
for point in points:
payload = point.payload
ids.append(point.id)
documents.append(payload["text"])
metadatas.append(payload["metadata"])
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def _get_collection_and_tenant_id(self, collection_name: str) -> Tuple[str, str]:
"""
Maps the traditional collection name to multi-tenant collection and tenant ID.
Returns:
tuple: (collection_name, tenant_id)
"""
# Check for user memory collections
tenant_id = collection_name
if collection_name.startswith("user-memory-"):
return self.MEMORY_COLLECTION, tenant_id
# Check for file collections
elif collection_name.startswith("file-"):
return self.FILE_COLLECTION, tenant_id
# Check for web search collections
elif collection_name.startswith("web-search-"):
return self.WEB_SEARCH_COLLECTION, tenant_id
# Handle hash-based collections (YouTube and web URLs)
elif len(collection_name) == 63 and all(
c in "0123456789abcdef" for c in collection_name
):
return self.HASH_BASED_COLLECTION, tenant_id
else:
return self.KNOWLEDGE_COLLECTION, tenant_id
def _extract_error_message(self, exception):
"""
Extract error message from either HTTP or gRPC exceptions
Returns:
tuple: (status_code, error_message)
"""
# Check if it's an HTTP exception
if isinstance(exception, UnexpectedResponse):
try:
error_data = exception.structured()
error_msg = error_data.get("status", {}).get("error", "")
return exception.status_code, error_msg
except Exception as inner_e:
log.error(f"Failed to parse HTTP error: {inner_e}")
return exception.status_code, str(exception)
# Check if it's a gRPC exception
elif isinstance(exception, grpc.RpcError):
# Extract status code from gRPC error
status_code = None
if hasattr(exception, "code") and callable(exception.code):
status_code = exception.code().value[0]
# Extract error message
error_msg = str(exception)
if "details =" in error_msg:
# Parse the details line which contains the actual error message
try:
details_line = [
line.strip()
for line in error_msg.split("\n")
if "details =" in line
][0]
error_msg = details_line.split("details =")[1].strip(' "')
except (IndexError, AttributeError):
# Fall back to full message if parsing fails
pass
return status_code, error_msg
# For any other type of exception
return None, str(exception)
def _is_collection_not_found_error(self, exception):
"""
Check if the exception is due to collection not found, supporting both HTTP and gRPC
"""
status_code, error_msg = self._extract_error_message(exception)
# HTTP error (404)
if (
status_code == 404
and "Collection" in error_msg
and "doesn't exist" in error_msg
):
return True
# gRPC error (NOT_FOUND status)
if (
isinstance(exception, grpc.RpcError)
and exception.code() == grpc.StatusCode.NOT_FOUND
):
return True
return False
def _is_dimension_mismatch_error(self, exception):
"""
Check if the exception is due to dimension mismatch, supporting both HTTP and gRPC
"""
status_code, error_msg = self._extract_error_message(exception)
# Common patterns in both HTTP and gRPC
return (
"Vector dimension error" in error_msg
or "dimensions mismatch" in error_msg
or "invalid vector size" in error_msg
)
def _create_multi_tenant_collection_if_not_exists(
self, mt_collection_name: str, dimension: int = 384
):
"""
Creates a collection with multi-tenancy configuration if it doesn't exist.
Default dimension is set to 384 which corresponds to 'sentence-transformers/all-MiniLM-L6-v2'.
When creating collections dynamically (insert/upsert), the actual vector dimensions will be used.
"""
try:
# Try to create the collection directly - will fail if it already exists
self.client.create_collection(
collection_name=mt_collection_name,
vectors_config=models.VectorParams(
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
hnsw_config=models.HnswConfigDiff(
payload_m=16, # Enable per-tenant indexing
m=0,
on_disk=self.QDRANT_ON_DISK,
),
)
# Create tenant ID payload index
self.client.create_payload_index(
collection_name=mt_collection_name,
field_name="tenant_id",
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
is_tenant=True,
on_disk=self.QDRANT_ON_DISK,
),
wait=True,
)
log.info(
f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!"
)
except (UnexpectedResponse, grpc.RpcError) as e:
# Check for the specific error indicating collection already exists
status_code, error_msg = self._extract_error_message(e)
# HTTP status code 409 or gRPC ALREADY_EXISTS
if (isinstance(e, UnexpectedResponse) and status_code == 409) or (
isinstance(e, grpc.RpcError)
and e.code() == grpc.StatusCode.ALREADY_EXISTS
):
if "already exists" in error_msg:
log.debug(f"Collection {mt_collection_name} already exists")
return
# If it's not an already exists error, re-raise
raise e
except Exception as e:
raise e
def _create_points(self, items: list[VectorItem], tenant_id: str):
"""
Create point structs from vector items with tenant ID.
"""
return [
PointStruct(
id=item["id"],
vector=item["vector"],
payload={
"text": item["text"],
"metadata": item["metadata"],
"tenant_id": tenant_id,
},
)
for item in items
]
def has_collection(self, collection_name: str) -> bool:
"""
Check if a logical collection exists by checking for any points with the tenant ID.
"""
if not self.client:
return False
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
try:
# Try directly querying - most of the time collection should exist
response = self.client.query_points(
collection_name=mt_collection,
query_filter=models.Filter(must=[tenant_filter]),
limit=1,
)
# Collection exists with this tenant ID if there are points
return len(response.points) > 0
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(f"Collection {mt_collection} doesn't exist")
return False
else:
# For other API errors, log and return False
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error: {error_msg}")
return False
except Exception as e:
# For any other errors, log and return False
log.debug(f"Error checking collection {mt_collection}: {e}")
return False
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
"""
Delete vectors by ID or filter from a collection with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
must_conditions = [tenant_filter]
should_conditions = []
if ids:
for id_value in ids:
should_conditions.append(
models.FieldCondition(
key="metadata.id",
match=models.MatchValue(value=id_value),
),
)
elif filter:
for key, value in filter.items():
must_conditions.append(
models.FieldCondition(
key=f"metadata.{key}",
match=models.MatchValue(value=value),
),
)
try:
# Try to delete directly - most of the time collection should exist
update_result = self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=must_conditions, should=should_conditions)
),
)
return update_result
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, nothing to delete"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, re-raise
raise
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
"""
Search for the nearest neighbor items based on the vectors with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get the vector dimension from the query vector
dimension = len(vectors[0]) if vectors and len(vectors) > 0 else None
try:
# Try the search operation directly - most of the time collection should exist
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
# Ensure vector dimensions match the collection
collection_dim = self.client.get_collection(
mt_collection
).config.params.vectors.size
if collection_dim != dimension:
if collection_dim < dimension:
vectors = [vector[:collection_dim] for vector in vectors]
else:
vectors = [
vector + [0] * (collection_dim - dimension)
for vector in vectors
]
# Search with tenant filter
prefetch_query = models.Prefetch(
filter=models.Filter(must=[tenant_filter]),
limit=NO_LIMIT,
)
query_response = self.client.query_points(
collection_name=mt_collection,
query=vectors[0],
prefetch=prefetch_query,
limit=limit,
)
get_result = self._result_to_get_result(query_response.points)
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
# qdrant distance is [-1, 1], normalize to [0, 1]
distances=[
[(point.score + 1.0) / 2.0 for point in query_response.points]
],
)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, search returns None"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during search: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and return None
log.exception(f"Error searching collection '{collection_name}': {e}")
return None
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
"""
Query points with filters and tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Set default limit if not provided
if limit is None:
limit = NO_LIMIT
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
# Create metadata filters
field_conditions = []
for key, value in filter.items():
field_conditions.append(
models.FieldCondition(
key=f"metadata.{key}", match=models.MatchValue(value=value)
)
)
# Combine tenant filter with metadata filters
combined_filter = models.Filter(must=[tenant_filter, *field_conditions])
try:
# Try the query directly - most of the time collection should exist
points = self.client.query_points(
collection_name=mt_collection,
query_filter=combined_filter,
limit=limit,
)
return self._result_to_get_result(points.points)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, query returns None"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during query: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and re-raise
log.exception(f"Error querying collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
"""
Get all items in a collection with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
try:
# Try to get points directly - most of the time collection should exist
points = self.client.query_points(
collection_name=mt_collection,
query_filter=models.Filter(must=[tenant_filter]),
limit=NO_LIMIT,
)
return self._result_to_get_result(points.points)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(f"Collection {mt_collection} doesn't exist, get returns None")
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during get: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and return None
log.exception(f"Error getting collection '{collection_name}': {e}")
return None
def _handle_operation_with_error_retry(
self, operation_name, mt_collection, points, dimension
):
"""
Private helper to handle common error cases for insert and upsert operations.
Args:
operation_name: 'insert' or 'upsert'
mt_collection: The multi-tenant collection name
points: The vector points to insert/upsert
dimension: The dimension of the vectors
Returns:
The operation result (for upsert) or None (for insert)
"""
try:
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
except (UnexpectedResponse, grpc.RpcError) as e:
# Handle collection not found
if self._is_collection_not_found_error(e):
log.info(
f"Collection {mt_collection} doesn't exist. Creating it with dimension {dimension}."
)
# Create collection with correct dimensions from our vectors
self._create_multi_tenant_collection_if_not_exists(
mt_collection_name=mt_collection, dimension=dimension
)
# Try operation again - no need for dimension adjustment since we just created with correct dimensions
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
# Handle dimension mismatch
elif self._is_dimension_mismatch_error(e):
# For dimension errors, the collection must exist, so get its configuration
mt_collection_info = self.client.get_collection(mt_collection)
existing_size = mt_collection_info.config.params.vectors.size
log.info(
f"Dimension mismatch: Collection {mt_collection} expects {existing_size}, got {dimension}"
)
if existing_size < dimension:
# Truncate vectors to fit
log.info(
f"Truncating vectors from {dimension} to {existing_size} dimensions"
)
points = [
PointStruct(
id=point.id,
vector=point.vector[:existing_size],
payload=point.payload,
)
for point in points
]
elif existing_size > dimension:
# Pad vectors with zeros
log.info(
f"Padding vectors from {dimension} to {existing_size} dimensions with zeros"
)
points = [
PointStruct(
id=point.id,
vector=point.vector
+ [0] * (existing_size - len(point.vector)),
payload=point.payload,
)
for point in points
]
# Try operation again with adjusted dimensions
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
else:
# Not a known error we can handle, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unhandled Qdrant error: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, re-raise
raise
def insert(self, collection_name: str, items: list[VectorItem]):
"""
Insert items with tenant ID.
"""
if not self.client or not items:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get dimensions from the actual vectors
dimension = len(items[0]["vector"]) if items else None
# Create points with tenant ID
points = self._create_points(items, tenant_id)
# Handle the operation with error retry
return self._handle_operation_with_error_retry(
"insert", mt_collection, points, dimension
)
def upsert(self, collection_name: str, items: list[VectorItem]):
"""
Upsert items with tenant ID.
"""
if not self.client or not items:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get dimensions from the actual vectors
dimension = len(items[0]["vector"]) if items else None
# Create points with tenant ID
points = self._create_points(items, tenant_id)
# Handle the operation with error retry
return self._handle_operation_with_error_retry(
"upsert", mt_collection, points, dimension
)
def reset(self):
"""
Reset the database by deleting all collections.
"""
if not self.client:
return None
collection_names = self.client.get_collections().collections
for collection_name in collection_names:
if collection_name.name.startswith(self.collection_prefix):
self.client.delete_collection(collection_name=collection_name.name)
def delete_collection(self, collection_name: str):
"""
Delete a collection.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
field_conditions = [tenant_filter]
update_result = self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=field_conditions)
),
)
if self.client.get_collection(mt_collection).points_count == 0:
self.client.delete_collection(mt_collection)
return update_result
@@ -0,0 +1,55 @@
from open_webui.retrieval.vector.main import VectorDBBase
from open_webui.retrieval.vector.type import VectorType
from open_webui.config import VECTOR_DB, ENABLE_QDRANT_MULTITENANCY_MODE
class Vector:
@staticmethod
def get_vector(vector_type: str) -> VectorDBBase:
"""
get vector db instance by vector type
"""
match vector_type:
case VectorType.MILVUS:
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
return MilvusClient()
case VectorType.QDRANT:
if ENABLE_QDRANT_MULTITENANCY_MODE:
from open_webui.retrieval.vector.dbs.qdrant_multitenancy import (
QdrantClient,
)
return QdrantClient()
else:
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
return QdrantClient()
case VectorType.PINECONE:
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
return PineconeClient()
case VectorType.OPENSEARCH:
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
return OpenSearchClient()
case VectorType.PGVECTOR:
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
return PgvectorClient()
case VectorType.ELASTICSEARCH:
from open_webui.retrieval.vector.dbs.elasticsearch import (
ElasticsearchClient,
)
return ElasticsearchClient()
case VectorType.CHROMA:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
return ChromaClient()
case _:
raise ValueError(f"Unsupported vector type: {vector_type}")
VECTOR_DB_CLIENT = Vector.get_vector(VECTOR_DB)
@@ -0,0 +1,11 @@
from enum import StrEnum
class VectorType(StrEnum):
MILVUS = "milvus"
QDRANT = "qdrant"
CHROMA = "chroma"
PINECONE = "pinecone"
ELASTICSEARCH = "elasticsearch"
OPENSEARCH = "opensearch"
PGVECTOR = "pgvector"
+6 -4
View File
@@ -25,7 +25,7 @@ from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.retrieval.loaders.tavily import TavilyLoader from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.retrieval.loaders.external import ExternalLoader from open_webui.retrieval.loaders.external_web import ExternalWebLoader
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.config import ( from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_RAG_LOCAL_WEB_FETCH,
@@ -39,7 +39,7 @@ from open_webui.config import (
EXTERNAL_WEB_LOADER_URL, EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY, EXTERNAL_WEB_LOADER_API_KEY,
) )
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -515,7 +515,9 @@ class SafeWebBaseLoader(WebBaseLoader):
kwargs["ssl"] = False kwargs["ssl"] = False
async with session.get( async with session.get(
url, **(self.requests_kwargs | kwargs) url,
**(self.requests_kwargs | kwargs),
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
if self.raise_for_status: if self.raise_for_status:
response.raise_for_status() response.raise_for_status()
@@ -628,7 +630,7 @@ def get_web_loader(
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
if WEB_LOADER_ENGINE.value == "external": if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalLoader WebLoaderClass = ExternalWebLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value
web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value
+152 -58
View File
@@ -7,6 +7,7 @@ from functools import lru_cache
from pathlib import Path from pathlib import Path
from pydub import AudioSegment from pydub import AudioSegment
from pydub.silence import split_on_silence from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor
import aiohttp import aiohttp
import aiofiles import aiofiles
@@ -38,6 +39,7 @@ from open_webui.config import (
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ( from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT,
ENV, ENV,
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
@@ -49,7 +51,7 @@ from open_webui.env import (
router = APIRouter() router = APIRouter()
# Constants # Constants
MAX_FILE_SIZE_MB = 25 MAX_FILE_SIZE_MB = 20
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200 AZURE_MAX_FILE_SIZE_MB = 200
AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
@@ -71,35 +73,50 @@ from pydub import AudioSegment
from pydub.utils import mediainfo from pydub.utils import mediainfo
def get_audio_convert_format(file_path): def is_audio_conversion_required(file_path):
"""Check if the given file needs to be converted to a different format.""" """
Check if the given audio file needs conversion to mp3.
"""
SUPPORTED_FORMATS = {"flac", "m4a", "mp3", "mp4", "mpeg", "wav", "webm"}
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}") log.error(f"File not found: {file_path}")
return False return False
try: try:
info = mediainfo(file_path) info = mediainfo(file_path)
codec_name = info.get("codec_name", "").lower()
codec_type = info.get("codec_type", "").lower()
codec_tag_string = info.get("codec_tag_string", "").lower()
if codec_name == "aac" and codec_type == "audio" and codec_tag_string == "mp4a":
# File is AAC/mp4a audio, recommend mp3 conversion
return True
# If the codec name or file extension is in the supported formats
if ( if (
info.get("codec_name") == "aac" codec_name in SUPPORTED_FORMATS
and info.get("codec_type") == "audio" or os.path.splitext(file_path)[1][1:].lower() in SUPPORTED_FORMATS
and info.get("codec_tag_string") == "mp4a"
): ):
return "mp4" return False # Already supported
elif info.get("format_name") == "ogg":
return "ogg" return True
except Exception as e: except Exception as e:
log.error(f"Error getting audio format: {e}") log.error(f"Error getting audio format: {e}")
return False return False
return None
def convert_audio_to_mp3(file_path):
def convert_audio_to_wav(file_path, output_path, conversion_type): """Convert audio file to mp3 format."""
"""Convert MP4/OGG audio file to WAV format.""" try:
audio = AudioSegment.from_file(file_path, format=conversion_type) output_path = os.path.splitext(file_path)[0] + ".mp3"
audio.export(output_path, format="wav") audio = AudioSegment.from_file(file_path)
log.info(f"Converted {file_path} to {output_path}") audio.export(output_path, format="mp3")
log.info(f"Converted {file_path} to {output_path}")
return output_path
except Exception as e:
log.error(f"Error converting audio file: {e}")
return None
def set_faster_whisper_model(model: str, auto_update: bool = False): def set_faster_whisper_model(model: str, auto_update: bool = False):
@@ -326,6 +343,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
else {} else {}
), ),
}, },
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@@ -381,6 +399,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"Content-Type": "application/json", "Content-Type": "application/json",
"xi-api-key": request.app.state.config.TTS_API_KEY, "xi-api-key": request.app.state.config.TTS_API_KEY,
}, },
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@@ -439,6 +458,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"X-Microsoft-OutputFormat": output_format, "X-Microsoft-OutputFormat": output_format,
}, },
data=data, data=data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@@ -507,8 +527,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path) return FileResponse(file_path)
def transcribe(request: Request, file_path): def transcription_handler(request, file_path):
log.info(f"transcribe: {file_path}")
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
id = filename.split(".")[0] id = filename.split(".")[0]
@@ -542,19 +561,6 @@ def transcribe(request: Request, file_path):
log.debug(data) log.debug(data)
return data return data
elif request.app.state.config.STT_ENGINE == "openai": elif request.app.state.config.STT_ENGINE == "openai":
convert_format = get_audio_convert_format(file_path)
if convert_format:
ext = convert_format.split(".")[-1]
os.rename(file_path, file_path.replace(".{ext}", f".{convert_format}"))
# Convert unsupported audio file to WAV format
convert_audio_to_wav(
file_path.replace(".{ext}", f".{convert_format}"),
file_path,
convert_format,
)
r = None r = None
try: try:
r = requests.post( r = requests.post(
@@ -771,24 +777,122 @@ def transcribe(request: Request, file_path):
) )
def transcribe(request: Request, file_path):
log.info(f"transcribe: {file_path}")
if is_audio_conversion_required(file_path):
file_path = convert_audio_to_mp3(file_path)
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
# Always produce a list of chunk paths (could be one entry if small)
try:
chunk_paths = split_audio(file_path, MAX_FILE_SIZE)
print(f"Chunk paths: {chunk_paths}")
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
results = []
try:
with ThreadPoolExecutor() as executor:
# Submit tasks for each chunk_path
futures = [
executor.submit(transcription_handler, request, chunk_path)
for chunk_path in chunk_paths
]
# Gather results as they complete
for future in futures:
try:
results.append(future.result())
except Exception as transcribe_exc:
log.exception(f"Error transcribing chunk: {transcribe_exc}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error during transcription.",
)
finally:
# Clean up only the temporary chunks, never the original file
for chunk_path in chunk_paths:
if chunk_path != file_path and os.path.isfile(chunk_path):
try:
os.remove(chunk_path)
except Exception:
pass
return {
"text": " ".join([result["text"] for result in results]),
}
def compress_audio(file_path): def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE: if os.path.getsize(file_path) > MAX_FILE_SIZE:
id = os.path.splitext(os.path.basename(file_path))[
0
] # Handles names with multiple dots
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
audio = AudioSegment.from_file(file_path) audio = AudioSegment.from_file(file_path)
audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio
compressed_path = f"{file_dir}/{id}_compressed.opus"
audio.export(compressed_path, format="opus", bitrate="32k")
log.debug(f"Compressed audio to {compressed_path}")
if ( compressed_path = os.path.join(file_dir, f"{id}_compressed.mp3")
os.path.getsize(compressed_path) > MAX_FILE_SIZE audio.export(compressed_path, format="mp3", bitrate="32k")
): # Still larger than MAX_FILE_SIZE after compression # log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined
raise Exception(ERROR_MESSAGES.FILE_TOO_LARGE(size=f"{MAX_FILE_SIZE_MB}MB"))
return compressed_path return compressed_path
else: else:
return file_path return file_path
def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"):
"""
Splits audio into chunks not exceeding max_bytes.
Returns a list of chunk file paths. If audio fits, returns list with original path.
"""
file_size = os.path.getsize(file_path)
if file_size <= max_bytes:
return [file_path] # Nothing to split
audio = AudioSegment.from_file(file_path)
duration_ms = len(audio)
orig_size = file_size
approx_chunk_ms = max(int(duration_ms * (max_bytes / orig_size)) - 1000, 1000)
chunks = []
start = 0
i = 0
base, _ = os.path.splitext(file_path)
while start < duration_ms:
end = min(start + approx_chunk_ms, duration_ms)
chunk = audio[start:end]
chunk_path = f"{base}_chunk_{i}.{format}"
chunk.export(chunk_path, format=format, bitrate=bitrate)
# Reduce chunk duration if still too large
while os.path.getsize(chunk_path) > max_bytes and (end - start) > 5000:
end = start + ((end - start) // 2)
chunk = audio[start:end]
chunk.export(chunk_path, format=format, bitrate=bitrate)
if os.path.getsize(chunk_path) > max_bytes:
os.remove(chunk_path)
raise Exception("Audio chunk cannot be reduced below max file size.")
chunks.append(chunk_path)
start = end
i += 1
return chunks
@router.post("/transcriptions") @router.post("/transcriptions")
def transcription( def transcription(
request: Request, request: Request,
@@ -797,15 +901,11 @@ def transcription(
): ):
log.info(f"file.content_type: {file.content_type}") log.info(f"file.content_type: {file.content_type}")
supported_filetypes = ( SUPPORTED_CONTENT_TYPES = {"video/webm"} # Extend if you add more video types!
"audio/mpeg", if not (
"audio/wav", file.content_type.startswith("audio/")
"audio/ogg", or file.content_type in SUPPORTED_CONTENT_TYPES
"audio/x-m4a", ):
"audio/webm",
)
if not file.content_type.startswith(supported_filetypes):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
@@ -826,19 +926,13 @@ def transcription(
f.write(contents) f.write(contents)
try: try:
try: result = transcribe(request, file_path)
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
raise HTTPException( return {
status_code=status.HTTP_400_BAD_REQUEST, **result,
detail=ERROR_MESSAGES.DEFAULT(e), "filename": os.path.basename(file_path),
) }
data = transcribe(request, file_path)
file_path = file_path.split("/")[-1]
return {**data, "filename": file_path}
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
+48 -12
View File
@@ -31,7 +31,7 @@ from open_webui.env import (
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
) )
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response, JSONResponse
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel from pydantic import BaseModel
@@ -51,7 +51,7 @@ from open_webui.utils.access_control import get_permissions
from typing import Optional, List from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
if ENABLE_LDAP.value: if ENABLE_LDAP.value:
from ldap3 import Server, Connection, NONE, Tls from ldap3 import Server, Connection, NONE, Tls
@@ -186,6 +186,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_VALIDATE_CERT = (
CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE
)
LDAP_CIPHERS = ( LDAP_CIPHERS = (
request.app.state.config.LDAP_CIPHERS request.app.state.config.LDAP_CIPHERS
if request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS
@@ -197,7 +200,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
try: try:
tls = Tls( tls = Tls(
validate=CERT_REQUIRED, validate=LDAP_VALIDATE_CERT,
version=PROTOCOL_TLS, version=PROTOCOL_TLS,
ca_certs_file=LDAP_CA_CERT_FILE, ca_certs_file=LDAP_CA_CERT_FILE,
ciphers=LDAP_CIPHERS, ciphers=LDAP_CIPHERS,
@@ -478,10 +481,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
"admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE "admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
) )
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing. # The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72: if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException( raise HTTPException(
@@ -541,6 +540,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
user.id, request.app.state.config.USER_PERMISSIONS user.id, request.app.state.config.USER_PERMISSIONS
) )
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
return { return {
"token": token, "token": token,
"token_type": "Bearer", "token_type": "Bearer",
@@ -574,9 +577,14 @@ async def signout(request: Request, response: Response):
logout_url = openid_data.get("end_session_endpoint") logout_url = openid_data.get("end_session_endpoint")
if logout_url: if logout_url:
response.delete_cookie("oauth_id_token") response.delete_cookie("oauth_id_token")
return RedirectResponse(
return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}",
},
headers=response.headers, headers=response.headers,
url=f"{logout_url}?id_token_hint={oauth_id_token}",
) )
else: else:
raise HTTPException( raise HTTPException(
@@ -591,12 +599,18 @@ async def signout(request: Request, response: Response):
) )
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL: if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
return RedirectResponse( return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
},
headers=response.headers, headers=response.headers,
url=WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
) )
return {"status": True} return JSONResponse(
status_code=200, content={"status": True}, headers=response.headers
)
############################ ############################
@@ -696,6 +710,9 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
} }
@@ -713,6 +730,9 @@ class AdminConfig(BaseModel):
ENABLE_CHANNELS: bool ENABLE_CHANNELS: bool
ENABLE_NOTES: bool ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool ENABLE_USER_WEBHOOKS: bool
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
RESPONSE_WATERMARK: Optional[str] = None
@router.post("/admin/config") @router.post("/admin/config")
@@ -750,6 +770,15 @@ async def update_admin_config(
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
form_data.PENDING_USER_OVERLAY_TITLE
)
request.app.state.config.PENDING_USER_OVERLAY_CONTENT = (
form_data.PENDING_USER_OVERLAY_CONTENT
)
request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK
return { return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL, "WEBUI_URL": request.app.state.config.WEBUI_URL,
@@ -764,6 +793,9 @@ async def update_admin_config(
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
} }
@@ -779,6 +811,7 @@ class LdapServerConfig(BaseModel):
search_filters: str = "" search_filters: str = ""
use_tls: bool = True use_tls: bool = True
certificate_path: Optional[str] = None certificate_path: Optional[str] = None
validate_cert: bool = True
ciphers: Optional[str] = "ALL" ciphers: Optional[str] = "ALL"
@@ -796,6 +829,7 @@ async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS, "use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS, "ciphers": request.app.state.config.LDAP_CIPHERS,
} }
@@ -831,6 +865,7 @@ async def update_ldap_server(
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert
request.app.state.config.LDAP_CIPHERS = form_data.ciphers request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return { return {
@@ -845,6 +880,7 @@ async def update_ldap_server(
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS, "use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS, "ciphers": request.app.state.config.LDAP_CIPHERS,
} }
+11 -12
View File
@@ -74,13 +74,17 @@ class FeedbackUserResponse(FeedbackResponse):
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse]) @router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
async def get_all_feedbacks(user=Depends(get_admin_user)): async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks() feedbacks = Feedbacks.get_all_feedbacks()
return [
FeedbackUserResponse( feedback_list = []
**feedback.model_dump(), for feedback in feedbacks:
user=UserResponse(**Users.get_user_by_id(feedback.user_id).model_dump()), user = Users.get_user_by_id(feedback.user_id)
feedback_list.append(
FeedbackUserResponse(
**feedback.model_dump(),
user=UserResponse(**user.model_dump()) if user else None,
)
) )
for feedback in feedbacks return feedback_list
]
@router.delete("/feedbacks/all") @router.delete("/feedbacks/all")
@@ -92,12 +96,7 @@ async def delete_all_feedbacks(user=Depends(get_admin_user)):
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel]) @router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
async def get_all_feedbacks(user=Depends(get_admin_user)): async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks() feedbacks = Feedbacks.get_all_feedbacks()
return [ return feedbacks
FeedbackModel(
**feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
)
for feedback in feedbacks
]
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse]) @router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
+37 -25
View File
@@ -95,6 +95,20 @@ def upload_file(
unsanitized_filename = file.filename unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename) filename = os.path.basename(unsanitized_filename)
file_extension = os.path.splitext(filename)[1]
if request.app.state.config.ALLOWED_FILE_EXTENSIONS:
request.app.state.config.ALLOWED_FILE_EXTENSIONS = [
ext for ext in request.app.state.config.ALLOWED_FILE_EXTENSIONS if ext
]
if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(
f"File type {file_extension} is not allowed"
),
)
# replace filename with uuid # replace filename with uuid
id = str(uuid.uuid4()) id = str(uuid.uuid4())
name = filename name = filename
@@ -125,33 +139,31 @@ def upload_file(
) )
if process: if process:
try: try:
if file.content_type:
if file.content_type.startswith("audio/") or file.content_type in {
"video/webm"
}:
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
if file.content_type.startswith( process_file(
( request,
"audio/mpeg", ProcessFileForm(file_id=id, content=result.get("text", "")),
"audio/wav", user=user,
"audio/ogg", )
"audio/x-m4a", elif file.content_type not in [
"audio/webm", "image/png",
"video/webm", "image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
]:
process_file(request, ProcessFileForm(file_id=id), user=user)
else:
log.info(
f"File type {file.content_type} is not provided, but trying to process anyway"
) )
):
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif file.content_type not in [
"image/png",
"image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
]:
process_file(request, ProcessFileForm(file_id=id), user=user) process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id) file_item = Files.get_file_by_id(id=id)
+1 -1
View File
@@ -10,7 +10,7 @@ from open_webui.models.knowledge import (
KnowledgeUserResponse, KnowledgeUserResponse,
) )
from open_webui.models.files import Files, FileModel, FileMetadataResponse from open_webui.models.files import Files, FileModel, FileMetadataResponse
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.routers.retrieval import ( from open_webui.routers.retrieval import (
process_file, process_file,
ProcessFileForm, ProcessFileForm,
+1 -1
View File
@@ -4,7 +4,7 @@ import logging
from typing import Optional from typing import Optional
from open_webui.models.memories import Memories, MemoryModel from open_webui.models.memories import Memories, MemoryModel
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.utils.auth import get_verified_user from open_webui.utils.auth import get_verified_user
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS
+14 -8
View File
@@ -340,6 +340,8 @@ async def get_all_models(request: Request, user: UserModel = None):
), # Legacy support ), # Legacy support
) )
connection_type = api_config.get("connection_type", "local")
prefix_id = api_config.get("prefix_id", None) prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", []) tags = api_config.get("tags", [])
model_ids = api_config.get("model_ids", []) model_ids = api_config.get("model_ids", [])
@@ -352,14 +354,16 @@ async def get_all_models(request: Request, user: UserModel = None):
) )
) )
if prefix_id: for model in response.get("models", []):
for model in response.get("models", []): if prefix_id:
model["model"] = f"{prefix_id}.{model['model']}" model["model"] = f"{prefix_id}.{model['model']}"
if tags: if tags:
for model in response.get("models", []):
model["tags"] = tags model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
def merge_models_lists(model_lists): def merge_models_lists(model_lists):
merged_models = {} merged_models = {}
@@ -1585,7 +1589,9 @@ async def upload_model(
if url_idx is None: if url_idx is None:
url_idx = 0 url_idx = 0
ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
file_path = os.path.join(UPLOAD_DIR, file.filename)
filename = os.path.basename(file.filename)
file_path = os.path.join(UPLOAD_DIR, filename)
os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True)
# --- P1: save file locally --- # --- P1: save file locally ---
@@ -1630,13 +1636,13 @@ async def upload_model(
os.remove(file_path) os.remove(file_path)
# Create model in ollama # Create model in ollama
model_name, ext = os.path.splitext(file.filename) model_name, ext = os.path.splitext(filename)
log.info(f"Created Model: {model_name}") # DEBUG log.info(f"Created Model: {model_name}") # DEBUG
create_payload = { create_payload = {
"model": model_name, "model": model_name,
# Reference the file by its original name => the uploaded blob's digest # Reference the file by its original name => the uploaded blob's digest
"files": {file.filename: f"sha256:{file_hash}"}, "files": {filename: f"sha256:{file_hash}"},
} }
log.info(f"Model Payload: {create_payload}") # DEBUG log.info(f"Model Payload: {create_payload}") # DEBUG
@@ -1653,7 +1659,7 @@ async def upload_model(
done_msg = { done_msg = {
"done": True, "done": True,
"blob": f"sha256:{file_hash}", "blob": f"sha256:{file_hash}",
"name": file.filename, "name": filename,
"model_created": model_name, "model_created": model_name,
} }
yield f"data: {json.dumps(done_msg)}\n\n" yield f"data: {json.dumps(done_msg)}\n\n"
+251 -118
View File
@@ -353,21 +353,22 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
), # Legacy support ), # Legacy support
) )
connection_type = api_config.get("connection_type", "external")
prefix_id = api_config.get("prefix_id", None) prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", []) tags = api_config.get("tags", [])
if prefix_id: for model in (
for model in ( response if isinstance(response, list) else response.get("data", [])
response if isinstance(response, list) else response.get("data", []) ):
): if prefix_id:
model["id"] = f"{prefix_id}.{model['id']}" model["id"] = f"{prefix_id}.{model['id']}"
if tags: if tags:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
model["tags"] = tags model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
log.debug(f"get_all_models:responses() {responses}") log.debug(f"get_all_models:responses() {responses}")
return responses return responses
@@ -415,6 +416,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
"name": model.get("name", model["id"]), "name": model.get("name", model["id"]),
"owned_by": "openai", "owned_by": "openai",
"openai": model, "openai": model,
"connection_type": model.get("connection_type", "external"),
"urlIdx": idx, "urlIdx": idx,
} }
for model in models for model in models
@@ -461,60 +463,74 @@ async def get_models(
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx] url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = request.app.state.config.OPENAI_API_KEYS[url_idx] key = request.app.state.config.OPENAI_API_KEYS[url_idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(url_idx),
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
)
r = None r = None
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
trust_env=True, trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session: ) as session:
try: try:
async with session.get( headers = {
f"{url}/models", "Content-Type": "application/json",
headers={ **(
"Authorization": f"Bearer {key}", {
"Content-Type": "application/json", "X-OpenWebUI-User-Name": user.name,
**( "X-OpenWebUI-User-Id": user.id,
{ "X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Name": user.name, "X-OpenWebUI-User-Role": user.role,
"X-OpenWebUI-User-Id": user.id, }
"X-OpenWebUI-User-Email": user.email, if ENABLE_FORWARD_USER_INFO_HEADERS
"X-OpenWebUI-User-Role": user.role, else {}
} ),
if ENABLE_FORWARD_USER_INFO_HEADERS }
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json() if api_config.get("azure", False):
models = {
"data": api_config.get("model_ids", []) or [],
"object": "list",
}
else:
headers["Authorization"] = f"Bearer {key}"
# Check if we're calling OpenAI API based on the URL async with session.get(
if "api.openai.com" in url: f"{url}/models",
# Filter models according to the specified conditions headers=headers,
response_data["data"] = [ ssl=AIOHTTP_CLIENT_SESSION_SSL,
model ) as r:
for model in response_data.get("data", []) if r.status != 200:
if not any( # Extract response error details if available
name in model["id"] error_detail = f"HTTP Error: {r.status}"
for name in [ res = await r.json()
"babbage", if "error" in res:
"dall-e", error_detail = f"External Error: {res['error']}"
"davinci", raise Exception(error_detail)
"embedding",
"tts",
"whisper",
]
)
]
models = response_data response_data = await r.json()
# Check if we're calling OpenAI API based on the URL
if "api.openai.com" in url:
# Filter models according to the specified conditions
response_data["data"] = [
model
for model in response_data.get("data", [])
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
models = response_data
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues # ClientError covers all aiohttp requests issues
log.exception(f"Client error: {str(e)}") log.exception(f"Client error: {str(e)}")
@@ -536,6 +552,8 @@ class ConnectionVerificationForm(BaseModel):
url: str url: str
key: str key: str
config: Optional[dict] = None
@router.post("/verify") @router.post("/verify")
async def verify_connection( async def verify_connection(
@@ -544,39 +562,64 @@ async def verify_connection(
url = form_data.url url = form_data.url
key = form_data.key key = form_data.key
api_config = form_data.config or {}
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
trust_env=True, trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session: ) as session:
try: try:
async with session.get( headers = {
f"{url}/models", "Content-Type": "application/json",
headers={ **(
"Authorization": f"Bearer {key}", {
"Content-Type": "application/json", "X-OpenWebUI-User-Name": user.name,
**( "X-OpenWebUI-User-Id": user.id,
{ "X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Name": user.name, "X-OpenWebUI-User-Role": user.role,
"X-OpenWebUI-User-Id": user.id, }
"X-OpenWebUI-User-Email": user.email, if ENABLE_FORWARD_USER_INFO_HEADERS
"X-OpenWebUI-User-Role": user.role, else {}
} ),
if ENABLE_FORWARD_USER_INFO_HEADERS }
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json() if api_config.get("azure", False):
return response_data headers["api-key"] = key
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
async with session.get(
url=f"{url}/openai/models?api-version={api_version}",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json()
return response_data
else:
headers["Authorization"] = f"Bearer {key}"
async with session.get(
f"{url}/models",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
error_detail = f"HTTP Error: {r.status}"
res = await r.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
raise Exception(error_detail)
response_data = await r.json()
return response_data
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
# ClientError covers all aiohttp requests issues # ClientError covers all aiohttp requests issues
@@ -590,6 +633,63 @@ async def verify_connection(
raise HTTPException(status_code=500, detail=error_detail) raise HTTPException(status_code=500, detail=error_detail)
def convert_to_azure_payload(
url,
payload: dict,
):
model = payload.get("model", "")
# Filter allowed parameters based on Azure OpenAI API
allowed_params = {
"messages",
"temperature",
"role",
"content",
"contentPart",
"contentPartImage",
"enhancements",
"dataSources",
"n",
"stream",
"stop",
"max_tokens",
"presence_penalty",
"frequency_penalty",
"logit_bias",
"user",
"function_call",
"functions",
"tools",
"tool_choice",
"top_p",
"log_probs",
"top_logprobs",
"response_format",
"seed",
"max_completion_tokens",
}
# Special handling for o-series models
if model.startswith("o") and model.endswith("-mini"):
# Convert max_tokens to max_completion_tokens for o-series models
if "max_tokens" in payload:
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Remove temperature if not 1 for o-series models
if "temperature" in payload and payload["temperature"] != 1:
log.debug(
f"Removing temperature parameter for o-series model {model} as only default value (1) is supported"
)
del payload["temperature"]
# Filter out unsupported parameters
payload = {k: v for k, v in payload.items() if k in allowed_params}
url = f"{url}/openai/deployments/{model}"
return url, payload
@router.post("/chat/completions") @router.post("/chat/completions")
async def generate_chat_completion( async def generate_chat_completion(
request: Request, request: Request,
@@ -690,6 +790,38 @@ async def generate_chat_completion(
convert_logit_bias_input_to_json(payload["logit_bias"]) convert_logit_bias_input_to_json(payload["logit_bias"])
) )
headers = {
"Content-Type": "application/json",
**(
{
"HTTP-Referer": "https://openwebui.com/",
"X-Title": "Open WebUI",
}
if "openrouter.ai" in url
else {}
),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
}
if api_config.get("azure", False):
request_url, payload = convert_to_azure_payload(url, payload)
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
headers["api-key"] = key
headers["api-version"] = api_version
request_url = f"{request_url}/chat/completions?api-version={api_version}"
else:
request_url = f"{url}/chat/completions"
headers["Authorization"] = f"Bearer {key}"
payload = json.dumps(payload) payload = json.dumps(payload)
r = None r = None
@@ -704,30 +836,9 @@ async def generate_chat_completion(
r = await session.request( r = await session.request(
method="POST", method="POST",
url=f"{url}/chat/completions", url=request_url,
data=payload, data=payload,
headers={ headers=headers,
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"HTTP-Referer": "https://openwebui.com/",
"X-Title": "Open WebUI",
}
if "openrouter.ai" in url
else {}
),
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
) )
@@ -783,31 +894,53 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
idx = 0 idx = 0
url = request.app.state.config.OPENAI_API_BASE_URLS[idx] url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx]
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
str(idx),
request.app.state.config.OPENAI_API_CONFIGS.get(
request.app.state.config.OPENAI_API_BASE_URLS[idx], {}
), # Legacy support
)
r = None r = None
session = None session = None
streaming = False streaming = False
try: try:
headers = {
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
}
if api_config.get("azure", False):
headers["api-key"] = key
headers["api-version"] = (
api_config.get("api_version", "") or "2023-03-15-preview"
)
payload = json.loads(body)
url, payload = convert_to_azure_payload(url, payload)
body = json.dumps(payload).encode()
request_url = f"{url}/{path}?api-version={api_config.get('api_version', '2023-03-15-preview')}"
else:
headers["Authorization"] = f"Bearer {key}"
request_url = f"{url}/{path}"
session = aiohttp.ClientSession(trust_env=True) session = aiohttp.ClientSession(trust_env=True)
r = await session.request( r = await session.request(
method=request.method, method=request.method,
url=f"{url}/{path}", url=request_url,
data=body, data=body,
headers={ headers=headers,
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
**(
{
"X-OpenWebUI-User-Name": user.name,
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
) )
r.raise_for_status() r.raise_for_status()
+15 -5
View File
@@ -18,7 +18,7 @@ from pydantic import BaseModel
from starlette.responses import FileResponse from starlette.responses import FileResponse
from typing import Optional from typing import Optional
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
from open_webui.config import CACHE_DIR from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
@@ -69,7 +69,10 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters: for filter in sorted_filters:
urlIdx = filter.get("urlIdx") urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -89,6 +92,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/inlet", f"{url}/{filter['id']}/filter/inlet",
headers=headers, headers=headers,
json=request_data, json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
payload = await response.json() payload = await response.json()
response.raise_for_status() response.raise_for_status()
@@ -118,7 +122,10 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters: for filter in sorted_filters:
urlIdx = filter.get("urlIdx") urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -138,6 +145,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/outlet", f"{url}/{filter['id']}/filter/outlet",
headers=headers, headers=headers,
json=request_data, json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
payload = await response.json() payload = await response.json()
response.raise_for_status() response.raise_for_status()
@@ -197,8 +205,10 @@ async def upload_pipeline(
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}") log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
filename = os.path.basename(file.filename)
# Check if the uploaded file is a python file # Check if the uploaded file is a python file
if not (file.filename and file.filename.endswith(".py")): if not (filename and filename.endswith(".py")):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Only Python (.py) files are allowed.", detail="Only Python (.py) files are allowed.",
@@ -206,7 +216,7 @@ async def upload_pipeline(
upload_folder = f"{CACHE_DIR}/pipelines" upload_folder = f"{CACHE_DIR}/pipelines"
os.makedirs(upload_folder, exist_ok=True) os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, file.filename) file_path = os.path.join(upload_folder, filename)
r = None r = None
try: try:
+39 -1
View File
@@ -36,7 +36,7 @@ from open_webui.models.knowledge import Knowledges
from open_webui.storage.provider import Storage from open_webui.storage.provider import Storage
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
# Document loaders # Document loaders
from open_webui.retrieval.loaders.main import Loader from open_webui.retrieval.loaders.main import Loader
@@ -352,10 +352,13 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
# Content extraction settings # Content extraction settings
"CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
"EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE, "DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG, "DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
@@ -371,6 +374,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
# File upload settings # File upload settings
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings # Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
@@ -492,10 +496,14 @@ class ConfigForm(BaseModel):
# Content extraction settings # Content extraction settings
CONTENT_EXTRACTION_ENGINE: Optional[str] = None CONTENT_EXTRACTION_ENGINE: Optional[str] = None
PDF_EXTRACT_IMAGES: Optional[bool] = None PDF_EXTRACT_IMAGES: Optional[bool] = None
EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None
TIKA_SERVER_URL: Optional[str] = None TIKA_SERVER_URL: Optional[str] = None
DOCLING_SERVER_URL: Optional[str] = None DOCLING_SERVER_URL: Optional[str] = None
DOCLING_OCR_ENGINE: Optional[str] = None DOCLING_OCR_ENGINE: Optional[str] = None
DOCLING_OCR_LANG: Optional[str] = None DOCLING_OCR_LANG: Optional[str] = None
DOCLING_DO_PICTURE_DESCRIPTION: Optional[bool] = None
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
MISTRAL_OCR_API_KEY: Optional[str] = None MISTRAL_OCR_API_KEY: Optional[str] = None
@@ -514,6 +522,7 @@ class ConfigForm(BaseModel):
# File upload settings # File upload settings
FILE_MAX_SIZE: Optional[int] = None FILE_MAX_SIZE: Optional[int] = None
FILE_MAX_COUNT: Optional[int] = None FILE_MAX_COUNT: Optional[int] = None
ALLOWED_FILE_EXTENSIONS: Optional[List[str]] = None
# Integration settings # Integration settings
ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None
@@ -581,6 +590,16 @@ async def update_rag_config(
if form_data.PDF_EXTRACT_IMAGES is not None if form_data.PDF_EXTRACT_IMAGES is not None
else request.app.state.config.PDF_EXTRACT_IMAGES else request.app.state.config.PDF_EXTRACT_IMAGES
) )
request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = (
form_data.EXTERNAL_DOCUMENT_LOADER_URL
if form_data.EXTERNAL_DOCUMENT_LOADER_URL is not None
else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL
)
request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = (
form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY
if form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY is not None
else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY
)
request.app.state.config.TIKA_SERVER_URL = ( request.app.state.config.TIKA_SERVER_URL = (
form_data.TIKA_SERVER_URL form_data.TIKA_SERVER_URL
if form_data.TIKA_SERVER_URL is not None if form_data.TIKA_SERVER_URL is not None
@@ -601,6 +620,13 @@ async def update_rag_config(
if form_data.DOCLING_OCR_LANG is not None if form_data.DOCLING_OCR_LANG is not None
else request.app.state.config.DOCLING_OCR_LANG else request.app.state.config.DOCLING_OCR_LANG
) )
request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = (
form_data.DOCLING_DO_PICTURE_DESCRIPTION
if form_data.DOCLING_DO_PICTURE_DESCRIPTION is not None
else request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION
)
request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = ( request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
form_data.DOCUMENT_INTELLIGENCE_ENDPOINT form_data.DOCUMENT_INTELLIGENCE_ENDPOINT
if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None
@@ -688,6 +714,11 @@ async def update_rag_config(
if form_data.FILE_MAX_COUNT is not None if form_data.FILE_MAX_COUNT is not None
else request.app.state.config.FILE_MAX_COUNT else request.app.state.config.FILE_MAX_COUNT
) )
request.app.state.config.ALLOWED_FILE_EXTENSIONS = (
form_data.ALLOWED_FILE_EXTENSIONS
if form_data.ALLOWED_FILE_EXTENSIONS is not None
else request.app.state.config.ALLOWED_FILE_EXTENSIONS
)
# Integration settings # Integration settings
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ( request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
@@ -809,10 +840,13 @@ async def update_rag_config(
# Content extraction settings # Content extraction settings
"CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
"EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE, "DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG, "DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
@@ -828,6 +862,7 @@ async def update_rag_config(
# File upload settings # File upload settings
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings # Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
@@ -1129,10 +1164,13 @@ def process_file(
file_path = Storage.get_file(file_path) file_path = Storage.get_file(file_path)
loader = Loader( loader = Loader(
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL, DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE=request.app.state.config.DOCLING_OCR_ENGINE, DOCLING_OCR_ENGINE=request.app.state.config.DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG=request.app.state.config.DOCLING_OCR_LANG, DOCLING_OCR_LANG=request.app.state.config.DOCLING_OCR_LANG,
DOCLING_DO_PICTURE_DESCRIPTION=request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
+7 -6
View File
@@ -20,10 +20,7 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.constants import TASKS from open_webui.constants import TASKS
from open_webui.routers.pipelines import process_pipeline_inlet_filter from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.filter import (
get_sorted_filter_ids,
process_filter_functions,
)
from open_webui.utils.task import get_task_model_id from open_webui.utils.task import get_task_model_id
from open_webui.config import ( from open_webui.config import (
@@ -195,15 +192,19 @@ async def generate_title(
}, },
) )
max_tokens = (
models[task_model_id].get("info", {}).get("params", {}).get("max_tokens", 1000)
)
payload = { payload = {
"model": task_model_id, "model": task_model_id,
"messages": [{"role": "user", "content": content}], "messages": [{"role": "user", "content": content}],
"stream": False, "stream": False,
**( **(
{"max_tokens": 1000} {"max_tokens": max_tokens}
if models[task_model_id].get("owned_by") == "ollama" if models[task_model_id].get("owned_by") == "ollama"
else { else {
"max_completion_tokens": 1000, "max_completion_tokens": max_tokens,
} }
), ),
"metadata": { "metadata": {
+29 -1
View File
@@ -13,6 +13,8 @@ import pytz
from pytz import UTC from pytz import UTC
from typing import Optional, Union, List, Dict from typing import Optional, Union, List, Dict
from opentelemetry import trace
from open_webui.models.users import Users from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
@@ -194,7 +196,17 @@ def get_current_user(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
) )
return get_current_user_by_api_key(token) user = get_current_user_by_api_key(token)
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "api_key")
return user
# auth by jwt token # auth by jwt token
try: try:
@@ -213,6 +225,14 @@ def get_current_user(
detail=ERROR_MESSAGES.INVALID_TOKEN, detail=ERROR_MESSAGES.INVALID_TOKEN,
) )
else: else:
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "jwt")
# Refresh the user's last active timestamp asynchronously # Refresh the user's last active timestamp asynchronously
# to prevent blocking the request # to prevent blocking the request
if background_tasks: if background_tasks:
@@ -234,6 +254,14 @@ def get_current_user_by_api_key(api_key: str):
detail=ERROR_MESSAGES.INVALID_TOKEN, detail=ERROR_MESSAGES.INVALID_TOKEN,
) )
else: else:
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "api_key")
Users.update_user_last_active_by_id(user.id) Users.update_user_last_active_by_id(user.id)
return user return user
+4 -1
View File
@@ -309,6 +309,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
metadata = { metadata = {
"chat_id": data["chat_id"], "chat_id": data["chat_id"],
"message_id": data["id"], "message_id": data["id"],
"filter_ids": data.get("filter_ids", []),
"session_id": data["session_id"], "session_id": data["session_id"],
"user_id": user.id, "user_id": user.id,
} }
@@ -330,7 +331,9 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
try: try:
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
result, _ = await process_filter_functions( result, _ = await process_filter_functions(
+27 -10
View File
@@ -9,7 +9,20 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"]) log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model: dict): def get_function_module(request, function_id):
"""
Get the function module by its ID.
"""
if function_id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[function_id]
else:
function_module, _, _ = load_function_module_by_id(function_id)
request.app.state.FUNCTIONS[function_id] = function_module
return function_module
def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None):
def get_priority(function_id): def get_priority(function_id):
function = Functions.get_function_by_id(function_id) function = Functions.get_function_by_id(function_id)
if function is not None: if function is not None:
@@ -21,14 +34,23 @@ def get_sorted_filter_ids(model: dict):
if "info" in model and "meta" in model["info"]: if "info" in model and "meta" in model["info"]:
filter_ids.extend(model["info"]["meta"].get("filterIds", [])) filter_ids.extend(model["info"]["meta"].get("filterIds", []))
filter_ids = list(set(filter_ids)) filter_ids = list(set(filter_ids))
active_filter_ids = [
enabled_filter_ids = [
function.id function.id
for function in Functions.get_functions_by_type("filter", active_only=True) for function in Functions.get_functions_by_type("filter", active_only=True)
] ]
filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] for filter_id in active_filter_ids:
function_module = get_function_module(request, filter_id)
if getattr(function_module, "toggle", None) and (
filter_id not in enabled_filter_ids
):
active_filter_ids.remove(filter_id)
continue
filter_ids = [fid for fid in filter_ids if fid in active_filter_ids]
filter_ids.sort(key=get_priority) filter_ids.sort(key=get_priority)
return filter_ids return filter_ids
@@ -43,12 +65,7 @@ async def process_filter_functions(
if not filter: if not filter:
continue continue
if filter_id in request.app.state.FUNCTIONS: function_module = get_function_module(request, filter_id)
function_module = request.app.state.FUNCTIONS[filter_id]
else:
function_module, _, _ = load_function_module_by_id(filter_id)
request.app.state.FUNCTIONS[filter_id] = function_module
# Prepare handler function # Prepare handler function
handler = getattr(function_module, filter_type, None) handler = getattr(function_module, filter_type, None)
if not handler: if not handler:
+38 -14
View File
@@ -340,6 +340,11 @@ async def chat_web_search_handler(
log.exception(e) log.exception(e)
queries = [user_message] queries = [user_message]
# Check if generated queries are empty
if len(queries) == 1 and queries[0].strip() == "":
queries = [user_message]
# Check if queries are not found
if len(queries) == 0: if len(queries) == 0:
await event_emitter( await event_emitter(
{ {
@@ -651,7 +656,7 @@ def apply_params_to_form_data(form_data, model):
convert_logit_bias_input_to_json(params["logit_bias"]) convert_logit_bias_input_to_json(params["logit_bias"])
) )
except Exception as e: except Exception as e:
print(f"Error parsing logit_bias: {e}") log.exception(f"Error parsing logit_bias: {e}")
return form_data return form_data
@@ -749,9 +754,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
raise e raise e
try: try:
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
form_data, flags = await process_filter_functions( form_data, flags = await process_filter_functions(
@@ -942,21 +950,35 @@ async def process_chat_response(
message = message_map.get(metadata["message_id"]) if message_map else None message = message_map.get(metadata["message_id"]) if message_map else None
if message: if message:
messages = get_message_list(message_map, message.get("id")) message_list = get_message_list(message_map, message.get("id"))
# Remove reasoning details and files from the messages. # Remove details tags and files from the messages.
# as get_message_list creates a new list, it does not affect # as get_message_list creates a new list, it does not affect
# the original messages outside of this handler # the original messages outside of this handler
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
if message.get("files"): messages = []
message["files"] = [] for message in message_list:
content = message.get("content", "")
if isinstance(content, list):
for item in content:
if item.get("type") == "text":
content = item["text"]
break
if isinstance(content, str):
content = re.sub(
r"<details\b[^>]*>.*?<\/details>",
"",
content,
flags=re.S | re.I,
).strip()
messages.append(
{
"role": message["role"],
"content": content,
}
)
if tasks and messages: if tasks and messages:
if TASKS.TITLE_GENERATION in tasks: if TASKS.TITLE_GENERATION in tasks:
@@ -1169,7 +1191,9 @@ async def process_chat_response(
} }
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
# Streaming response # Streaming response
+63 -3
View File
@@ -49,6 +49,7 @@ async def get_all_base_models(request: Request, user: UserModel = None):
"created": int(time.time()), "created": int(time.time()),
"owned_by": "ollama", "owned_by": "ollama",
"ollama": model, "ollama": model,
"connection_type": model.get("connection_type", "local"),
"tags": model.get("tags", []), "tags": model.get("tags", []),
} }
for model in ollama_models["models"] for model in ollama_models["models"]
@@ -110,6 +111,14 @@ async def get_all_models(request, user: UserModel = None):
for function in Functions.get_functions_by_type("action", active_only=True) for function in Functions.get_functions_by_type("action", active_only=True)
] ]
global_filter_ids = [
function.id for function in Functions.get_global_filter_functions()
]
enabled_filter_ids = [
function.id
for function in Functions.get_functions_by_type("filter", active_only=True)
]
custom_models = Models.get_all_models() custom_models = Models.get_all_models()
for custom_model in custom_models: for custom_model in custom_models:
if custom_model.base_model_id is None: if custom_model.base_model_id is None:
@@ -125,13 +134,20 @@ async def get_all_models(request, user: UserModel = None):
model["name"] = custom_model.name model["name"] = custom_model.name
model["info"] = custom_model.model_dump() model["info"] = custom_model.model_dump()
# Set action_ids and filter_ids
action_ids = [] action_ids = []
filter_ids = []
if "info" in model and "meta" in model["info"]: if "info" in model and "meta" in model["info"]:
action_ids.extend( action_ids.extend(
model["info"]["meta"].get("actionIds", []) model["info"]["meta"].get("actionIds", [])
) )
filter_ids.extend(
model["info"]["meta"].get("filterIds", [])
)
model["action_ids"] = action_ids model["action_ids"] = action_ids
model["filter_ids"] = filter_ids
else: else:
models.remove(model) models.remove(model)
@@ -140,7 +156,9 @@ async def get_all_models(request, user: UserModel = None):
): ):
owned_by = "openai" owned_by = "openai"
pipe = None pipe = None
action_ids = [] action_ids = []
filter_ids = []
for model in models: for model in models:
if ( if (
@@ -154,9 +172,13 @@ async def get_all_models(request, user: UserModel = None):
if custom_model.meta: if custom_model.meta:
meta = custom_model.meta.model_dump() meta = custom_model.meta.model_dump()
if "actionIds" in meta: if "actionIds" in meta:
action_ids.extend(meta["actionIds"]) action_ids.extend(meta["actionIds"])
if "filterIds" in meta:
filter_ids.extend(meta["filterIds"])
models.append( models.append(
{ {
"id": f"{custom_model.id}", "id": f"{custom_model.id}",
@@ -168,6 +190,7 @@ async def get_all_models(request, user: UserModel = None):
"preset": True, "preset": True,
**({"pipe": pipe} if pipe is not None else {}), **({"pipe": pipe} if pipe is not None else {}),
"action_ids": action_ids, "action_ids": action_ids,
"filter_ids": filter_ids,
} }
) )
@@ -181,8 +204,11 @@ async def get_all_models(request, user: UserModel = None):
"id": f"{function.id}.{action['id']}", "id": f"{function.id}.{action['id']}",
"name": action.get("name", f"{function.name} ({action['id']})"), "name": action.get("name", f"{function.name} ({action['id']})"),
"description": function.meta.description, "description": function.meta.description,
"icon_url": action.get( "icon": action.get(
"icon_url", function.meta.manifest.get("icon_url", None) "icon_url",
function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
), ),
} }
for action in actions for action in actions
@@ -193,10 +219,25 @@ async def get_all_models(request, user: UserModel = None):
"id": function.id, "id": function.id,
"name": function.name, "name": function.name,
"description": function.meta.description, "description": function.meta.description,
"icon_url": function.meta.manifest.get("icon_url", None), "icon": function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
} }
] ]
# Process filter_ids to get the filters
def get_filter_items_from_module(function, module):
return [
{
"id": function.id,
"name": function.name,
"description": function.meta.description,
"icon": function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
}
]
def get_function_module_by_id(function_id): def get_function_module_by_id(function_id):
if function_id in request.app.state.FUNCTIONS: if function_id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[function_id] function_module = request.app.state.FUNCTIONS[function_id]
@@ -211,6 +252,11 @@ async def get_all_models(request, user: UserModel = None):
for action_id in list(set(model.pop("action_ids", []) + global_action_ids)) for action_id in list(set(model.pop("action_ids", []) + global_action_ids))
if action_id in enabled_action_ids if action_id in enabled_action_ids
] ]
filter_ids = [
filter_id
for filter_id in list(set(model.pop("filter_ids", []) + global_filter_ids))
if filter_id in enabled_filter_ids
]
model["actions"] = [] model["actions"] = []
for action_id in action_ids: for action_id in action_ids:
@@ -222,6 +268,20 @@ async def get_all_models(request, user: UserModel = None):
model["actions"].extend( model["actions"].extend(
get_action_items_from_module(action_function, function_module) get_action_items_from_module(action_function, function_module)
) )
model["filters"] = []
for filter_id in filter_ids:
filter_function = Functions.get_function_by_id(filter_id)
if filter_function is None:
raise Exception(f"Filter not found: {filter_id}")
function_module = get_function_module_by_id(filter_id)
if getattr(function_module, "toggle", None):
model["filters"].extend(
get_filter_items_from_module(filter_function, function_module)
)
log.debug(f"get_all_models() returned {len(models)} models") log.debug(f"get_all_models() returned {len(models)} models")
request.app.state.MODELS = {model["id"]: model for model in models} request.app.state.MODELS = {model["id"]: model for model in models}
+8 -3
View File
@@ -41,6 +41,7 @@ from open_webui.config import (
) )
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import ( from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
WEBUI_NAME, WEBUI_NAME,
WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE, WEBUI_AUTH_COOKIE_SECURE,
@@ -305,8 +306,10 @@ class OAuthManager:
get_kwargs["headers"] = { get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}", "Authorization": f"Bearer {access_token}",
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(picture_url, **get_kwargs) as resp: async with session.get(
picture_url, **get_kwargs, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as resp:
if resp.ok: if resp.ok:
picture = await resp.read() picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode( base64_encoded_picture = base64.b64encode(picture).decode(
@@ -371,7 +374,9 @@ class OAuthManager:
headers = {"Authorization": f"Bearer {access_token}"} headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get( async with session.get(
"https://api.github.com/user/emails", headers=headers "https://api.github.com/user/emails",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as resp: ) as resp:
if resp.ok: if resp.ok:
emails = await resp.json() emails = await resp.json()
+5 -3
View File
@@ -37,6 +37,7 @@ from open_webui.models.tools import Tools
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.utils.plugin import load_tool_module_by_id from open_webui.utils.plugin import load_tool_module_by_id
from open_webui.env import ( from open_webui.env import (
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
) )
@@ -44,6 +45,7 @@ from open_webui.env import (
import copy import copy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
def get_async_tool_function_and_apply_extra_params( def get_async_tool_function_and_apply_extra_params(
@@ -477,7 +479,7 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
"specs": convert_openapi_to_tool_payload(res), "specs": convert_openapi_to_tool_payload(res),
} }
print("Fetched data:", data) log.info("Fetched data:", data)
return data return data
@@ -510,7 +512,7 @@ async def get_tool_servers_data(
results = [] results = []
for (idx, server, url, _), response in zip(server_entries, responses): for (idx, server, url, _), response in zip(server_entries, responses):
if isinstance(response, Exception): if isinstance(response, Exception):
print(f"Failed to connect to {url} OpenAPI tool server") log.error(f"Failed to connect to {url} OpenAPI tool server")
continue continue
results.append( results.append(
@@ -620,5 +622,5 @@ async def execute_tool_server(
except Exception as err: except Exception as err:
error = str(err) error = str(err)
print("API Request Error:", error) log.exception("API Request Error:", error)
return {"error": error} return {"error": error}
+3 -2
View File
@@ -37,7 +37,8 @@ asgiref==3.8.1
# AI libraries # AI libraries
openai openai
anthropic anthropic
google-generativeai==0.8.4 google-genai==1.15.0
google-generativeai==0.8.5
tiktoken tiktoken
langchain==0.3.24 langchain==0.3.24
@@ -98,7 +99,7 @@ pytube==15.0.0
extract_msg extract_msg
pydub pydub
duckduckgo-search~=8.0.0 duckduckgo-search==8.0.2
## Google Drive ## Google Drive
google-api-python-client google-api-python-client
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.6.8", "version": "0.6.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.6.8", "version": "0.6.10",
"dependencies": { "dependencies": {
"@azure/msal-browser": "^4.5.0", "@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
+2 -1
View File
@@ -1,11 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.6.8", "version": "0.6.10",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
"dev:5050": "npm run pyodide:fetch && vite dev --port 5050", "dev:5050": "npm run pyodide:fetch && vite dev --port 5050",
"build": "npm run pyodide:fetch && vite build", "build": "npm run pyodide:fetch && vite build",
"build:watch": "npm run pyodide:fetch && vite build --watch",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+3 -2
View File
@@ -45,7 +45,8 @@ dependencies = [
"openai", "openai",
"anthropic", "anthropic",
"google-generativeai==0.8.4", "google-genai==1.15.0",
"google-generativeai==0.8.5",
"tiktoken", "tiktoken",
"langchain==0.3.24", "langchain==0.3.24",
@@ -105,7 +106,7 @@ dependencies = [
"extract_msg", "extract_msg",
"pydub", "pydub",
"duckduckgo-search~=8.0.0", "duckduckgo-search==8.0.2",
"google-api-python-client", "google-api-python-client",
"google-auth-httplib2", "google-auth-httplib2",
+10 -2
View File
@@ -81,7 +81,7 @@ textarea::placeholder {
} }
.font-primary { .font-primary {
font-family: 'Archivo', sans-serif; font-family: 'Archivo', 'Vazirmatn', sans-serif;
} }
.drag-region { .drag-region {
@@ -314,12 +314,20 @@ input[type='number'] {
.ProseMirror p.is-editor-empty:first-child::before { .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;
color: #adb5bd; /* Below color is from tailwind, and has the proper contrast
text-gray-600 from: https://tailwindcss.com/docs/color */
color: #676767;
pointer-events: none; pointer-events: none;
@apply line-clamp-1 absolute; @apply line-clamp-1 absolute;
} }
@media (prefers-color-scheme: dark) {
.ProseMirror p.is-editor-empty:first-child::before {
color: #757575;
}
}
.ai-autocompletion::after { .ai-autocompletion::after {
color: #a0a0a0; color: #a0a0a0;
+6 -6
View File
@@ -15,7 +15,7 @@ export const getAudioConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -52,7 +52,7 @@ export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -83,7 +83,7 @@ export const transcribeAudio = async (token: string, file: File) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -120,7 +120,7 @@ export const synthesizeOpenAISpeech = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -152,7 +152,7 @@ export const getModels = async (token: string = ''): Promise<AvailableModelsResp
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -180,7 +180,7 @@ export const getVoices = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+25 -24
View File
@@ -15,7 +15,7 @@ export const getAdminDetails = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -42,7 +42,7 @@ export const getAdminConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -70,7 +70,7 @@ export const updateAdminConfig = async (token: string, body: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -98,7 +98,7 @@ export const getSessionUser = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -129,7 +129,7 @@ export const ldapUserSignIn = async (user: string, password: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
@@ -157,7 +157,7 @@ export const getLdapConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -187,7 +187,7 @@ export const updateLdapConfig = async (token: string = '', enable_ldap: boolean)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -214,7 +214,7 @@ export const getLdapServer = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -242,7 +242,7 @@ export const updateLdapServer = async (token: string = '', body: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -273,7 +273,7 @@ export const userSignIn = async (email: string, password: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
@@ -312,7 +312,7 @@ export const userSignUp = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -339,7 +339,7 @@ export const userSignOut = async () => {
return res; return res;
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -347,6 +347,7 @@ export const userSignOut = async () => {
if (error) { if (error) {
throw error; throw error;
} }
return res;
}; };
export const addUser = async ( export const addUser = async (
@@ -378,7 +379,7 @@ export const addUser = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -409,7 +410,7 @@ export const updateUserProfile = async (token: string, name: string, profileImag
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -440,7 +441,7 @@ export const updateUserPassword = async (token: string, password: string, newPas
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -467,7 +468,7 @@ export const getSignUpEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -494,7 +495,7 @@ export const getDefaultUserRole = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -524,7 +525,7 @@ export const updateDefaultUserRole = async (token: string, role: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -551,7 +552,7 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -578,7 +579,7 @@ export const getJWTExpiresDuration = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -608,7 +609,7 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -635,7 +636,7 @@ export const createAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -660,7 +661,7 @@ export const getAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -685,7 +686,7 @@ export const deleteAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
+12 -12
View File
@@ -28,7 +28,7 @@ export const createNewChannel = async (token: string = '', channel: ChannelForm)
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -59,7 +59,7 @@ export const getChannels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -90,7 +90,7 @@ export const getChannelById = async (token: string = '', channel_id: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -126,7 +126,7 @@ export const updateChannelById = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -157,7 +157,7 @@ export const deleteChannelById = async (token: string = '', channel_id: string)
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -196,7 +196,7 @@ export const getChannelMessages = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -236,7 +236,7 @@ export const getChannelThreadMessages = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -275,7 +275,7 @@ export const sendMessage = async (token: string = '', channel_id: string, messag
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -315,7 +315,7 @@ export const updateMessage = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -355,7 +355,7 @@ export const addReaction = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -395,7 +395,7 @@ export const removeReaction = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -429,7 +429,7 @@ export const deleteMessage = async (token: string = '', channel_id: string, mess
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+31 -31
View File
@@ -21,7 +21,7 @@ export const createNewChat = async (token: string, chat: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -61,7 +61,7 @@ export const importChat = async (
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -97,7 +97,7 @@ export const getChatList = async (token: string = '', page: number | null = null
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -131,7 +131,7 @@ export const getChatListByUserId = async (token: string = '', userId: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -165,7 +165,7 @@ export const getArchivedChatList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -196,7 +196,7 @@ export const getAllChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -231,7 +231,7 @@ export const getChatListBySearchText = async (token: string, text: string, page:
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -265,7 +265,7 @@ export const getChatsByFolderId = async (token: string, folderId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -296,7 +296,7 @@ export const getAllArchivedChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -327,7 +327,7 @@ export const getAllUserChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -358,7 +358,7 @@ export const getAllTags = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -389,7 +389,7 @@ export const getPinnedChatList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -426,7 +426,7 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -461,7 +461,7 @@ export const getChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -493,7 +493,7 @@ export const getChatByShareId = async (token: string, share_id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -531,7 +531,7 @@ export const getChatPinnedStatusById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@@ -569,7 +569,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@@ -610,7 +610,7 @@ export const cloneChatById = async (token: string, id: string, title?: string) =
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@@ -648,7 +648,7 @@ export const cloneSharedChatById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@@ -680,7 +680,7 @@ export const shareChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -715,7 +715,7 @@ export const updateChatFolderIdById = async (token: string, id: string, folderId
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -747,7 +747,7 @@ export const archiveChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -779,7 +779,7 @@ export const deleteSharedChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -814,7 +814,7 @@ export const updateChatById = async (token: string, id: string, chat: object) =>
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -846,7 +846,7 @@ export const deleteChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -878,7 +878,7 @@ export const getTagsById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -912,7 +912,7 @@ export const addTagById = async (token: string, id: string, tagName: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -947,7 +947,7 @@ export const deleteTagById = async (token: string, id: string, tagName: string)
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -978,7 +978,7 @@ export const deleteTagsById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -1010,7 +1010,7 @@ export const deleteAllChats = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -1042,7 +1042,7 @@ export const archiveAllChats = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+14 -14
View File
@@ -19,7 +19,7 @@ export const importConfig = async (token: string, config) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -46,7 +46,7 @@ export const exportConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -73,7 +73,7 @@ export const getDirectConnectionsConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -103,7 +103,7 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -130,7 +130,7 @@ export const getToolServerConnections = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -160,7 +160,7 @@ export const setToolServerConnections = async (token: string, connections: objec
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -190,7 +190,7 @@ export const verifyToolServerConnection = async (token: string, connection: obje
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -217,7 +217,7 @@ export const getCodeExecutionConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -247,7 +247,7 @@ export const setCodeExecutionConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -274,7 +274,7 @@ export const getModelsConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -304,7 +304,7 @@ export const setModelsConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -334,7 +334,7 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -361,7 +361,7 @@ export const getBanners = async (token: string): Promise<Banner[]> => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -391,7 +391,7 @@ export const setBanners = async (token: string, banners: Banner[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
+8 -8
View File
@@ -20,7 +20,7 @@ export const getConfig = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -51,7 +51,7 @@ export const updateConfig = async (token: string, config: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -82,7 +82,7 @@ export const getAllFeedbacks = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -113,7 +113,7 @@ export const exportAllFeedbacks = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -144,7 +144,7 @@ export const createNewFeedback = async (token: string, feedback: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -175,7 +175,7 @@ export const getFeedbackById = async (token: string, feedbackId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -206,7 +206,7 @@ export const updateFeedbackById = async (token: string, feedbackId: string, feed
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -234,7 +234,7 @@ export const deleteFeedbackById = async (token: string, feedbackId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+7 -7
View File
@@ -19,7 +19,7 @@ export const uploadFile = async (token: string, file: File) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -76,7 +76,7 @@ export const getFiles = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -107,7 +107,7 @@ export const getFileById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -141,7 +141,7 @@ export const updateFileDataContentById = async (token: string, id: string, conte
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -168,7 +168,7 @@ export const getFileContentById = async (id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -200,7 +200,7 @@ export const deleteFileById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -231,7 +231,7 @@ export const deleteAllFiles = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+7 -7
View File
@@ -50,7 +50,7 @@ export const getFolders = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -81,7 +81,7 @@ export const getFolderById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -115,7 +115,7 @@ export const updateFolderNameById = async (token: string, id: string, name: stri
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -153,7 +153,7 @@ export const updateFolderIsExpandedById = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -187,7 +187,7 @@ export const updateFolderParentIdById = async (token: string, id: string, parent
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -226,7 +226,7 @@ export const updateFolderItemsById = async (token: string, id: string, items: Fo
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -257,7 +257,7 @@ export const deleteFolderById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+14 -14
View File
@@ -20,7 +20,7 @@ export const createNewFunction = async (token: string, func: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -51,7 +51,7 @@ export const getFunctions = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -82,7 +82,7 @@ export const exportFunctions = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -114,7 +114,7 @@ export const getFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -149,7 +149,7 @@ export const updateFunctionById = async (token: string, id: string, func: object
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -181,7 +181,7 @@ export const deleteFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -213,7 +213,7 @@ export const toggleFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -245,7 +245,7 @@ export const toggleGlobalById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -277,7 +277,7 @@ export const getFunctionValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -309,7 +309,7 @@ export const getFunctionValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -344,7 +344,7 @@ export const updateFunctionValvesById = async (token: string, id: string, valves
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -376,7 +376,7 @@ export const getUserValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -408,7 +408,7 @@ export const getUserValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -443,7 +443,7 @@ export const updateUserValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+5 -5
View File
@@ -20,7 +20,7 @@ export const createNewGroup = async (token: string, group: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -51,7 +51,7 @@ export const getGroups = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -83,7 +83,7 @@ export const getGroupById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -118,7 +118,7 @@ export const updateGroupById = async (token: string, id: string, group: object)
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -150,7 +150,7 @@ export const deleteGroupById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+7 -7
View File
@@ -16,7 +16,7 @@ export const getConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -51,7 +51,7 @@ export const updateConfig = async (token: string = '', config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -83,7 +83,7 @@ export const verifyConfigUrl = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -115,7 +115,7 @@ export const getImageGenerationConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -148,7 +148,7 @@ export const updateImageGenerationConfig = async (token: string = '', config: ob
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -180,7 +180,7 @@ export const getImageGenerationModels = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -215,7 +215,7 @@ export const imageGenerations = async (token: string = '', prompt: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
+33 -33
View File
@@ -25,7 +25,7 @@ export const getModels = async (
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -173,7 +173,7 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -212,7 +212,7 @@ export const chatAction = async (token: string, action_id: string, body: ChatAct
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -244,7 +244,7 @@ export const stopTask = async (token: string, id: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -276,7 +276,7 @@ export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -315,7 +315,7 @@ export const getToolServerData = async (token: string, url: string) => {
} }
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -491,7 +491,7 @@ export const getTaskConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -520,7 +520,7 @@ export const updateTaskConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -562,7 +562,7 @@ export const generateTitle = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -634,7 +634,7 @@ export const generateTags = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -706,7 +706,7 @@ export const generateEmoji = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -756,7 +756,7 @@ export const generateQueries = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -828,7 +828,7 @@ export const generateAutoCompletion = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -892,7 +892,7 @@ export const generateMoACompletion = async (
stream: true stream: true
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -920,7 +920,7 @@ export const getPipelinesList = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -954,7 +954,7 @@ export const uploadPipeline = async (token: string, file: File, urlIdx: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -990,7 +990,7 @@ export const downloadPipeline = async (token: string, url: string, urlIdx: strin
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -1026,7 +1026,7 @@ export const deletePipeline = async (token: string, id: string, urlIdx: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -1063,7 +1063,7 @@ export const getPipelines = async (token: string, urlIdx?: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1100,7 +1100,7 @@ export const getPipelineValves = async (token: string, pipeline_id: string, urlI
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1136,7 +1136,7 @@ export const getPipelineValvesSpec = async (token: string, pipeline_id: string,
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1178,7 +1178,7 @@ export const updatePipelineValves = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
@@ -1210,7 +1210,7 @@ export const getBackendConfig = async () => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1236,7 +1236,7 @@ export const getChangelog = async () => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1263,7 +1263,7 @@ export const getVersionUpdates = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1290,7 +1290,7 @@ export const getModelFilterConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1325,7 +1325,7 @@ export const updateModelFilterConfig = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1352,7 +1352,7 @@ export const getWebhookUrl = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1382,7 +1382,7 @@ export const updateWebhookUrl = async (token: string, url: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1409,7 +1409,7 @@ export const getCommunitySharingEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1436,7 +1436,7 @@ export const toggleCommunitySharingEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -1463,7 +1463,7 @@ export const getModelConfig = async (token: string): Promise<GlobalModelConfig>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -1511,7 +1511,7 @@ export const updateModelConfig = async (token: string, config: GlobalModelConfig
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
+11 -11
View File
@@ -27,7 +27,7 @@ export const createNewKnowledge = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -58,7 +58,7 @@ export const getKnowledgeBases = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -89,7 +89,7 @@ export const getKnowledgeBaseList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -121,7 +121,7 @@ export const getKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -166,7 +166,7 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -201,7 +201,7 @@ export const addFileToKnowledgeById = async (token: string, id: string, fileId:
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -236,7 +236,7 @@ export const updateFileFromKnowledgeById = async (token: string, id: string, fil
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -271,7 +271,7 @@ export const removeFileFromKnowledgeById = async (token: string, id: string, fil
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -303,7 +303,7 @@ export const resetKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -335,7 +335,7 @@ export const deleteKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -363,7 +363,7 @@ export const reindexKnowledgeFiles = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+6 -6
View File
@@ -17,7 +17,7 @@ export const getMemories = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -48,7 +48,7 @@ export const addNewMemory = async (token: string, content: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -79,7 +79,7 @@ export const updateMemoryById = async (token: string, id: string, content: strin
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -110,7 +110,7 @@ export const queryMemory = async (token: string, content: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -142,7 +142,7 @@ export const deleteMemoryById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -174,7 +174,7 @@ export const deleteMemoriesByUserId = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+8 -8
View File
@@ -20,7 +20,7 @@ export const getModels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -51,7 +51,7 @@ export const getBaseModels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -80,7 +80,7 @@ export const createNewModel = async (token: string, model: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -115,7 +115,7 @@ export const getModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -150,7 +150,7 @@ export const toggleModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -186,7 +186,7 @@ export const updateModelById = async (token: string, id: string, model: object)
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -221,7 +221,7 @@ export const deleteModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -253,7 +253,7 @@ export const deleteAllModels = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
+5 -5
View File
@@ -28,7 +28,7 @@ export const createNewNote = async (token: string, note: NoteItem) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -59,7 +59,7 @@ export const getNotes = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -108,7 +108,7 @@ export const getNoteById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -143,7 +143,7 @@ export const updateNoteById = async (token: string, id: string, note: NoteItem)
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -175,7 +175,7 @@ export const deleteNoteById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+14 -19
View File
@@ -1,10 +1,6 @@
import { OLLAMA_API_BASE_URL } from '$lib/constants'; import { OLLAMA_API_BASE_URL } from '$lib/constants';
export const verifyOllamaConnection = async ( export const verifyOllamaConnection = async (token: string = '', connection: dict = {}) => {
token: string = '',
url: string = '',
key: string = ''
) => {
let error = null; let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/verify`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/verify`, {
@@ -15,8 +11,7 @@ export const verifyOllamaConnection = async (
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
url, ...connection
key
}) })
}) })
.then(async (res) => { .then(async (res) => {
@@ -51,7 +46,7 @@ export const getOllamaConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -92,7 +87,7 @@ export const updateOllamaConfig = async (token: string = '', config: OllamaConfi
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -124,7 +119,7 @@ export const getOllamaUrls = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -159,7 +154,7 @@ export const updateOllamaUrls = async (token: string = '', urls: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -191,7 +186,7 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -223,7 +218,7 @@ export const getOllamaModels = async (token: string = '', urlIdx: null | number
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -268,7 +263,7 @@ export const generatePrompt = async (token: string = '', model: string, conversa
` `
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@@ -408,11 +403,11 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string
return res.json(); return res.json();
}) })
.then((json) => { .then((json) => {
console.log(json); console.debug(json);
return true; return true;
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@@ -445,7 +440,7 @@ export const pullModel = async (token: string, tagName: string, urlIdx: number |
name: tagName name: tagName
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@@ -481,7 +476,7 @@ export const downloadModel = async (
}) })
} }
).catch((err) => { ).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@@ -512,7 +507,7 @@ export const uploadModel = async (token: string, file: File, urlIdx: string | nu
body: formData body: formData
} }
).catch((err) => { ).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
+12 -11
View File
@@ -16,7 +16,7 @@ export const getOpenAIConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -58,7 +58,7 @@ export const updateOpenAIConfig = async (token: string = '', config: OpenAIConfi
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -90,7 +90,7 @@ export const getOpenAIUrls = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -125,7 +125,7 @@ export const updateOpenAIUrls = async (token: string = '', urls: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -157,7 +157,7 @@ export const getOpenAIKeys = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -192,7 +192,7 @@ export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@@ -267,10 +267,10 @@ export const getOpenAIModels = async (token: string, urlIdx?: number) => {
export const verifyOpenAIConnection = async ( export const verifyOpenAIConnection = async (
token: string = '', token: string = '',
url: string = 'https://api.openai.com/v1', connection: dict = {},
key: string = '',
direct: boolean = false direct: boolean = false
) => { ) => {
const { url, key, config } = connection;
if (!url) { if (!url) {
throw 'OpenAI: URL is required'; throw 'OpenAI: URL is required';
} }
@@ -309,7 +309,8 @@ export const verifyOpenAIConnection = async (
}, },
body: JSON.stringify({ body: JSON.stringify({
url, url,
key key,
config
}) })
}) })
.then(async (res) => { .then(async (res) => {
@@ -346,7 +347,7 @@ export const chatCompletion = async (
}, },
body: JSON.stringify(body) body: JSON.stringify(body)
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -409,7 +410,7 @@ export const synthesizeOpenAISpeech = async (
voice: speaker voice: speaker
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
+6 -6
View File
@@ -28,7 +28,7 @@ export const createNewPrompt = async (token: string, prompt: PromptItem) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -59,7 +59,7 @@ export const getPrompts = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -90,7 +90,7 @@ export const getPromptList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -122,7 +122,7 @@ export const getPromptByCommand = async (token: string, command: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -158,7 +158,7 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) =
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -192,7 +192,7 @@ export const deletePromptByCommand = async (token: string, command: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+12 -12
View File
@@ -15,7 +15,7 @@ export const getRAGConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -77,7 +77,7 @@ export const updateRAGConfig = async (token: string, payload: RAGConfigForm) =>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -104,7 +104,7 @@ export const getQuerySettings = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -140,7 +140,7 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -167,7 +167,7 @@ export const getEmbeddingConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -209,7 +209,7 @@ export const updateEmbeddingConfig = async (token: string, payload: EmbeddingMod
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -236,7 +236,7 @@ export const getRerankingConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -270,7 +270,7 @@ export const updateRerankingConfig = async (token: string, payload: RerankingMod
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -313,7 +313,7 @@ export const processFile = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -344,7 +344,7 @@ export const processYoutubeVideo = async (token: string, url: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -376,7 +376,7 @@ export const processWeb = async (token: string, collection_name: string, url: st
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -410,7 +410,7 @@ export const processWebSearch = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
+13 -13
View File
@@ -20,7 +20,7 @@ export const createNewTool = async (token: string, tool: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -51,7 +51,7 @@ export const getTools = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -82,7 +82,7 @@ export const getToolList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -113,7 +113,7 @@ export const exportTools = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -145,7 +145,7 @@ export const getToolById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -180,7 +180,7 @@ export const updateToolById = async (token: string, id: string, tool: object) =>
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -212,7 +212,7 @@ export const deleteToolById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -244,7 +244,7 @@ export const getToolValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -276,7 +276,7 @@ export const getToolValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -311,7 +311,7 @@ export const updateToolValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -343,7 +343,7 @@ export const getUserValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -375,7 +375,7 @@ export const getUserValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@@ -410,7 +410,7 @@ export const updateUserValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
+15 -15
View File
@@ -16,7 +16,7 @@ export const getUserGroups = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -43,7 +43,7 @@ export const getUserDefaultPermissions = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -73,7 +73,7 @@ export const updateUserDefaultPermissions = async (token: string, permissions: o
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -104,7 +104,7 @@ export const updateUserRole = async (token: string, id: string, role: string) =>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -154,7 +154,7 @@ export const getUsers = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -182,7 +182,7 @@ export const getAllUsers = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -208,7 +208,7 @@ export const getUserSettings = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -238,7 +238,7 @@ export const updateUserSettings = async (token: string, settings: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -265,7 +265,7 @@ export const getUserById = async (token: string, userId: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -291,7 +291,7 @@ export const getUserInfo = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -321,7 +321,7 @@ export const updateUserInfo = async (token: string, info: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -335,7 +335,7 @@ export const updateUserInfo = async (token: string, info: object) => {
export const getAndUpdateUserLocation = async (token: string) => { export const getAndUpdateUserLocation = async (token: string) => {
const location = await getUserPosition().catch((err) => { const location = await getUserPosition().catch((err) => {
console.log(err); console.error(err);
return null; return null;
}); });
@@ -343,7 +343,7 @@ export const getAndUpdateUserLocation = async (token: string) => {
await updateUserInfo(token, { location: location }); await updateUserInfo(token, { location: location });
return location; return location;
} else { } else {
console.log('Failed to get user location'); console.info('Failed to get user location');
return null; return null;
} }
}; };
@@ -363,7 +363,7 @@ export const deleteUserById = async (token: string, userId: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -403,7 +403,7 @@ export const updateUserById = async (token: string, userId: string, user: UserUp
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
+7 -7
View File
@@ -15,7 +15,7 @@ export const getGravatarUrl = async (token: string, email: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -41,7 +41,7 @@ export const executeCode = async (token: string, code: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if (err.detail) { if (err.detail) {
@@ -75,7 +75,7 @@ export const formatPythonCode = async (token: string, code: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if (err.detail) { if (err.detail) {
@@ -110,7 +110,7 @@ export const downloadChatAsPDF = async (token: string, title: string, messages:
return res.blob(); return res.blob();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -136,7 +136,7 @@ export const getHTMLFromMarkdown = async (token: string, md: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@@ -170,7 +170,7 @@ export const downloadDatabase = async (token: string) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@@ -206,7 +206,7 @@ export const downloadLiteLLMConfig = async (token: string) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
+111 -10
View File
@@ -30,8 +30,17 @@
let url = ''; let url = '';
let key = ''; let key = '';
let connectionType = 'external';
let azure = false;
$: azure =
(url.includes('azure.com') || url.includes('cognitive.microsoft.com')) && !direct
? true
: false;
let prefixId = ''; let prefixId = '';
let enable = true; let enable = true;
let apiVersion = '';
let tags = []; let tags = [];
let modelId = ''; let modelId = '';
@@ -40,7 +49,10 @@
let loading = false; let loading = false;
const verifyOllamaHandler = async () => { const verifyOllamaHandler = async () => {
const res = await verifyOllamaConnection(localStorage.token, url, key).catch((error) => { const res = await verifyOllamaConnection(localStorage.token, {
url,
key
}).catch((error) => {
toast.error(`${error}`); toast.error(`${error}`);
}); });
@@ -50,11 +62,20 @@
}; };
const verifyOpenAIHandler = async () => { const verifyOpenAIHandler = async () => {
const res = await verifyOpenAIConnection(localStorage.token, url, key, direct).catch( const res = await verifyOpenAIConnection(
(error) => { localStorage.token,
toast.error(`${error}`); {
} url,
); key,
config: {
azure: azure,
api_version: apiVersion
}
},
direct
).catch((error) => {
toast.error(`${error}`);
});
if (res) { if (res) {
toast.success($i18n.t('Server connection verified')); toast.success($i18n.t('Server connection verified'));
@@ -85,6 +106,28 @@
return; return;
} }
if (azure) {
if (!apiVersion) {
loading = false;
toast.error('API Version is required');
return;
}
if (!key) {
loading = false;
toast.error('Key is required');
return;
}
if (modelIds.length === 0) {
loading = false;
toast.error('Deployment names are required');
return;
}
}
// remove trailing slash from url // remove trailing slash from url
url = url.replace(/\/$/, ''); url = url.replace(/\/$/, '');
@@ -95,7 +138,9 @@
enable: enable, enable: enable,
tags: tags, tags: tags,
prefix_id: prefixId, prefix_id: prefixId,
model_ids: modelIds model_ids: modelIds,
connection_type: connectionType,
...(!ollama && azure ? { azure: true, api_version: apiVersion } : {})
} }
}; };
@@ -120,6 +165,14 @@
tags = connection.config?.tags ?? []; tags = connection.config?.tags ?? [];
prefixId = connection.config?.prefix_id ?? ''; prefixId = connection.config?.prefix_id ?? '';
modelIds = connection.config?.model_ids ?? []; modelIds = connection.config?.model_ids ?? [];
if (ollama) {
connectionType = connection.config?.connection_type ?? 'local';
} else {
connectionType = connection.config?.connection_type ?? 'external';
azure = connection.config?.azure ?? false;
apiVersion = connection.config?.api_version ?? '';
}
} }
}; };
@@ -134,7 +187,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2"> <div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-1.5">
<div class=" text-lg font-medium self-center font-primary"> <div class=" text-lg font-medium self-center font-primary">
{#if edit} {#if edit}
{$i18n.t('Edit Connection')} {$i18n.t('Edit Connection')}
@@ -171,7 +224,31 @@
}} }}
> >
<div class="px-1"> <div class="px-1">
<div class="flex gap-2"> {#if !direct}
<div class="flex gap-2">
<div class="flex w-full justify-between items-center">
<div class=" text-xs text-gray-500">{$i18n.t('Connection Type')}</div>
<div class="">
<button
on:click={() => {
connectionType = connectionType === 'local' ? 'external' : 'local';
}}
type="button"
class=" text-xs text-gray-700 dark:text-gray-300"
>
{#if connectionType === 'local'}
{$i18n.t('Local')}
{:else}
{$i18n.t('External')}
{/if}
</button>
</div>
</div>
</div>
{/if}
<div class="flex gap-2 mt-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div> <div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>
@@ -252,6 +329,25 @@
</div> </div>
</div> </div>
{#if azure}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('API Version')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text"
bind:value={apiVersion}
placeholder={$i18n.t('API Version')}
autocomplete="off"
required
/>
</div>
</div>
</div>
{/if}
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div> <div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
@@ -308,6 +404,11 @@
{$i18n.t('Leave empty to include all models from "{{url}}/api/tags" endpoint', { {$i18n.t('Leave empty to include all models from "{{url}}/api/tags" endpoint', {
url: url url: url
})} })}
{:else if azure}
{$i18n.t('Deployment names are required for Azure OpenAI')}
<!-- {$i18n.t('Leave empty to include all models from "{{url}}" endpoint', {
url: `${url}/openai/deployments`
})} -->
{:else} {:else}
{$i18n.t('Leave empty to include all models from "{{url}}/models" endpoint', { {$i18n.t('Leave empty to include all models from "{{url}}/models" endpoint', {
url: url url: url
@@ -317,7 +418,7 @@
{/if} {/if}
</div> </div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" /> <hr class=" border-gray-100 dark:border-gray-700/10 my-1.5 w-full" />
<div class="flex items-center"> <div class="flex items-center">
<input <input
-4
View File
@@ -3,10 +3,6 @@
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import { models } from '$lib/stores';
import { verifyOpenAIConnection } from '$lib/apis/openai';
import { verifyOllamaConnection } from '$lib/apis/ollama';
import Modal from '$lib/components/common/Modal.svelte'; import Modal from '$lib/components/common/Modal.svelte';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Minus from '$lib/components/icons/Minus.svelte'; import Minus from '$lib/components/icons/Minus.svelte';
+3 -1
View File
@@ -447,7 +447,9 @@
} }
}} }}
> >
<div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Functions')}</div> <div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Functions')} ({$functions.length})
</div>
<div class=" self-center"> <div class=" self-center">
<svg <svg
@@ -277,7 +277,7 @@ class Pipe:
await tick(); await tick();
if (res) { if (res) {
console.log('Code formatted successfully'); console.info('Code formatted successfully');
saveHandler(); saveHandler();
} }
@@ -62,7 +62,7 @@
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10" class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
></div> ></div>
{/if} {/if}
<div class="flex w-full"> <div class="flex w-full gap-2">
<div class="flex-1 relative"> <div class="flex-1 relative">
<input <input
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}" class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
@@ -91,7 +91,7 @@
return; return;
} }
console.log('Update embedding model attempt:', embeddingModel); console.debug('Update embedding model attempt:', embeddingModel);
updateEmbeddingModelLoading = true; updateEmbeddingModelLoading = true;
const res = await updateEmbeddingConfig(localStorage.token, { const res = await updateEmbeddingConfig(localStorage.token, {
@@ -114,7 +114,7 @@
updateEmbeddingModelLoading = false; updateEmbeddingModelLoading = false;
if (res) { if (res) {
console.log('embeddingModelUpdateHandler:', res); console.debug('embeddingModelUpdateHandler:', res);
if (res.status === true) { if (res.status === true) {
toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), { toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
duration: 1000 * 10 duration: 1000 * 10
@@ -124,6 +124,13 @@
}; };
const submitHandler = async () => { const submitHandler = async () => {
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external' &&
RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL === ''
) {
toast.error($i18n.t('External Document Loader URL required.'));
return;
}
if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') { if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') {
toast.error($i18n.t('Tika Server URL required.')); toast.error($i18n.t('Tika Server URL required.'));
return; return;
@@ -163,6 +170,10 @@
await embeddingModelUpdateHandler(); await embeddingModelUpdateHandler();
} }
RAGConfig.ALLOWED_FILE_EXTENSIONS = RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== '');
const res = await updateRAGConfig(localStorage.token, RAGConfig); const res = await updateRAGConfig(localStorage.token, RAGConfig);
dispatch('save'); dispatch('save');
}; };
@@ -185,7 +196,10 @@
onMount(async () => { onMount(async () => {
await setEmbeddingConfig(); await setEmbeddingConfig();
RAGConfig = await getRAGConfig(localStorage.token); const config = await getRAGConfig(localStorage.token);
config.ALLOWED_FILE_EXTENSIONS = (config?.ALLOWED_FILE_EXTENSIONS ?? []).join(', ');
RAGConfig = config;
}); });
</script> </script>
@@ -246,7 +260,7 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5 flex flex-col w-full justify-between"> <div class="mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-medium"> <div class="self-center text-xs font-medium">
{$i18n.t('Content Extraction Engine')} {$i18n.t('Content Extraction Engine')}
</div> </div>
@@ -256,6 +270,7 @@
bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE} bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE}
> >
<option value="">{$i18n.t('Default')}</option> <option value="">{$i18n.t('Default')}</option>
<option value="external">{$i18n.t('External')}</option>
<option value="tika">{$i18n.t('Tika')}</option> <option value="tika">{$i18n.t('Tika')}</option>
<option value="docling">{$i18n.t('Docling')}</option> <option value="docling">{$i18n.t('Docling')}</option>
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option> <option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
@@ -275,11 +290,24 @@
</div> </div>
</div> </div>
</div> </div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2">
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter External Document Loader URL')}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL}
/>
<SensitiveInput
placeholder={$i18n.t('Enter External Document Loader API Key')}
required={false}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_API_KEY}
/>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={RAGConfig.TIKA_SERVER_URL} bind:value={RAGConfig.TIKA_SERVER_URL}
/> />
@@ -288,27 +316,38 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling Server URL')} placeholder={$i18n.t('Enter Docling Server URL')}
bind:value={RAGConfig.DOCLING_SERVER_URL} bind:value={RAGConfig.DOCLING_SERVER_URL}
/> />
</div> </div>
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling OCR Engine')} placeholder={$i18n.t('Enter Docling OCR Engine')}
bind:value={RAGConfig.DOCLING_OCR_ENGINE} bind:value={RAGConfig.DOCLING_OCR_ENGINE}
/> />
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling OCR Language(s)')} placeholder={$i18n.t('Enter Docling OCR Language(s)')}
bind:value={RAGConfig.DOCLING_OCR_LANG} bind:value={RAGConfig.DOCLING_OCR_LANG}
/> />
</div> </div>
<div class="flex w-full mt-2">
<div class="flex-1 flex justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Describe Pictures in Documents')}
</div>
<div class="flex items-center relative">
<Switch bind:state={RAGConfig.DOCLING_DO_PICTURE_DESCRIPTION} />
</div>
</div>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Document Intelligence Endpoint')} placeholder={$i18n.t('Enter Document Intelligence Endpoint')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT} bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT}
/> />
@@ -437,7 +476,7 @@
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
@@ -448,7 +487,7 @@
{:else if embeddingEngine === 'ollama'} {:else if embeddingEngine === 'ollama'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl} bind:value={OllamaUrl}
required required
@@ -471,7 +510,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
bind:value={embeddingModel} bind:value={embeddingModel}
placeholder={$i18n.t('Set embedding model')} placeholder={$i18n.t('Set embedding model')}
required required
@@ -482,7 +521,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
@@ -639,7 +678,7 @@
{#if RAGConfig.RAG_RERANKING_ENGINE === 'external'} {#if RAGConfig.RAG_RERANKING_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL} bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required required
@@ -661,7 +700,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@@ -677,7 +716,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K')} placeholder={$i18n.t('Enter Top K')}
bind:value={RAGConfig.TOP_K} bind:value={RAGConfig.TOP_K}
@@ -692,7 +731,7 @@
<div class="self-center text-xs font-medium">{$i18n.t('Top K Reranker')}</div> <div class="self-center text-xs font-medium">{$i18n.t('Top K Reranker')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K Reranker')} placeholder={$i18n.t('Enter Top K Reranker')}
bind:value={RAGConfig.TOP_K_RERANKER} bind:value={RAGConfig.TOP_K_RERANKER}
@@ -711,7 +750,7 @@
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
step="0.01" step="0.01"
placeholder={$i18n.t('Enter Score')} placeholder={$i18n.t('Enter Score')}
@@ -760,6 +799,26 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Allowed File Extensions')}</div>
<div class="flex items-center relative">
<Tooltip
content={$i18n.t(
'Allowed file extensions for upload. Separate multiple extensions with commas. Leave empty for all file types.'
)}
placement="top-start"
>
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('e.g. pdf, docx, txt')}
bind:value={RAGConfig.ALLOWED_FILE_EXTENSIONS}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@@ -770,7 +829,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_SIZE} bind:value={RAGConfig.FILE_MAX_SIZE}
@@ -791,7 +850,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_COUNT} bind:value={RAGConfig.FILE_MAX_COUNT}
@@ -18,6 +18,7 @@
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Textarea from '$lib/components/common/Textarea.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@@ -58,10 +59,10 @@
}; };
}); });
console.log(version); console.info(version);
updateAvailable = compareVersion(version.latest, version.current); updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable); console.info(updateAvailable);
}; };
const updateLdapServerHandler = async () => { const updateLdapServerHandler = async () => {
@@ -305,6 +306,31 @@
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} /> <Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
</div> </div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Pending User Overlay Title')}
</div>
<Textarea
rows={2}
placeholder={$i18n.t(
'Enter a title for the pending user info overlay. Leave empty for default.'
)}
bind:value={adminConfig.PENDING_USER_OVERLAY_TITLE}
/>
</div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Pending User Overlay Content')}
</div>
<Textarea
placeholder={$i18n.t(
'Enter content for the pending user info overlay. Leave empty for default.'
)}
bind:value={adminConfig.PENDING_USER_OVERLAY_CONTENT}
/>
</div>
<div class="mb-2.5 flex w-full justify-between pr-2"> <div class="mb-2.5 flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
@@ -559,6 +585,13 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center text-xs">
<div class=" font-medium">Validate certificate</div>
<div class="mt-1">
<Switch bind:state={LDAP_SERVER.validate_cert} />
</div>
</div>
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1"> <div class=" self-center text-xs font-medium min-w-fit mb-1">
@@ -625,6 +658,16 @@
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} /> <Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
</div> </div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Response Watermark')}
</div>
<Textarea
placeholder={$i18n.t('Enter a watermark for the response. Leave empty for none.')}
bind:value={adminConfig.RESPONSE_WATERMARK}
/>
</div>
<div class="mb-2.5 w-full justify-between"> <div class="mb-2.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
@@ -198,14 +198,14 @@
2 2
); );
} catch (e) { } catch (e) {
console.log(e); console.error(e);
} }
} }
requiredWorkflowNodes = requiredWorkflowNodes.map((node) => { requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node; const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
console.log(n); console.debug(n);
return { return {
type: n.type, type: n.type,
@@ -2,9 +2,9 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { getBackendConfig, getTaskConfig, updateTaskConfig } from '$lib/apis'; import { getBackendConfig, getModels, getTaskConfig, updateTaskConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs'; import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, settings, user } from '$lib/stores'; import { config, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { banners as _banners } from '$lib/stores'; import { banners as _banners } from '$lib/stores';
@@ -15,6 +15,8 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte'; import Switch from '$lib/components/common/Switch.svelte';
import Textarea from '$lib/components/common/Textarea.svelte'; import Textarea from '$lib/components/common/Textarea.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import { getBaseModels } from '$lib/apis/models';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -49,6 +51,7 @@
}; };
onMount(async () => { onMount(async () => {
await init();
taskConfig = await getTaskConfig(localStorage.token); taskConfig = await getTaskConfig(localStorage.token);
promptSuggestions = $config?.default_prompt_suggestions ?? []; promptSuggestions = $config?.default_prompt_suggestions ?? [];
@@ -58,9 +61,40 @@
const updateBanners = async () => { const updateBanners = async () => {
_banners.set(await setBanners(localStorage.token, banners)); _banners.set(await setBanners(localStorage.token, banners));
}; };
let workspaceModels = null;
let baseModels = null;
let models = null;
const init = async () => {
workspaceModels = await getBaseModels(localStorage.token);
baseModels = await getModels(localStorage.token, null, false);
models = baseModels.map((m) => {
const workspaceModel = workspaceModels.find((wm) => wm.id === m.id);
if (workspaceModel) {
return {
...m,
...workspaceModel
};
} else {
return {
...m,
id: m.id,
name: m.name,
is_active: true
};
}
});
console.debug('models', models);
};
</script> </script>
{#if taskConfig} {#if models !== null && taskConfig}
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
@@ -74,8 +108,8 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-1 font-medium flex items-center"> <div class=" mb-2 font-medium flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div> <div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries' 'A task model is used when performing tasks such as generating titles for chats and web search queries'
@@ -100,32 +134,70 @@
<div class=" mb-2.5 flex w-full gap-2"> <div class=" mb-2.5 flex w-full gap-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div> <div class=" text-xs mb-1">{$i18n.t('Local Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL} bind:value={taskConfig.TASK_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL) {
const model = models.find((m) => m.id === taskConfig.TASK_MODEL);
if (model) {
if (model?.access_control !== null) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
)
);
}
taskConfig.TASK_MODEL = model.id;
} else {
taskConfig.TASK_MODEL = '';
}
}
}}
> >
<option value="" selected>{$i18n.t('Current Model')}</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models.filter((m) => m.owned_by === 'ollama') as model} {#each models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"> <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name} {model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option> </option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div> <div class=" text-xs mb-1">{$i18n.t('External Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL_EXTERNAL} bind:value={taskConfig.TASK_MODEL_EXTERNAL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
on:change={() => {
if (taskConfig.TASK_MODEL_EXTERNAL) {
const model = models.find((m) => m.id === taskConfig.TASK_MODEL_EXTERNAL);
if (model) {
if (model?.access_control !== null) {
toast.error(
$i18n.t(
'This model is not publicly available. Please select another model.'
)
);
}
taskConfig.TASK_MODEL_EXTERNAL = model.id;
} else {
taskConfig.TASK_MODEL_EXTERNAL = '';
}
}
}}
> >
<option value="" selected>{$i18n.t('Current Model')}</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model} {#each models as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"> <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name} {model.name}
{model?.connection_type === 'local' ? `(${$i18n.t('Local')})` : ''}
</option> </option>
{/each} {/each}
</select> </select>
@@ -480,4 +552,8 @@
</button> </button>
</div> </div>
</form> </form>
{:else}
<div class=" h-full w-full flex justify-center items-center">
<Spinner />
</div>
{/if} {/if}
@@ -166,7 +166,7 @@
hidden: !(model?.meta?.hidden ?? false) hidden: !(model?.meta?.hidden ?? false)
}; };
console.log(model); console.debug(model);
toast.success( toast.success(
model.meta.hidden model.meta.hidden
@@ -495,7 +495,7 @@
}} }}
> >
<div class=" self-center mr-2 font-medium line-clamp-1"> <div class=" self-center mr-2 font-medium line-clamp-1">
{$i18n.t('Export Presets')} {$i18n.t('Export Presets')} ({models.length})
</div> </div>
<div class=" self-center"> <div class=" self-center">
@@ -66,7 +66,7 @@
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of ollamaModels) { for (const model of ollamaModels) {
console.log(model); console.debug(model);
updateModelId = model.id; updateModelId = model.id;
const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch( const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
@@ -114,8 +114,8 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
} }
} }
} }
@@ -215,13 +215,13 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
if (typeof error !== 'string') { if (typeof err !== 'string') {
error = error.message; err = err.message;
} }
toast.error(`${error}`); toast.error(`${err}`);
// opts.callback({ success: false, error, modelName: opts.modelName }); // opts.callback({ success: false, error, modelName: opts.modelName });
} }
} }
@@ -319,8 +319,8 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
} }
} }
} else { } else {
@@ -382,9 +382,9 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
toast.error(`${error}`); toast.error(`${err}`);
} }
} }
} }
@@ -514,9 +514,9 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
toast.error(`${error}`); toast.error(`${err}`);
} }
} }
} }
@@ -104,7 +104,7 @@
valves_spec = null; valves_spec = null;
if (PIPELINES_LIST.length > 0) { if (PIPELINES_LIST.length > 0) {
console.log(selectedPipelinesUrlIdx); console.debug(selectedPipelinesUrlIdx);
pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx); pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx);
if (pipelines.length > 0) { if (pipelines.length > 0) {
@@ -151,7 +151,7 @@
const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch( const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch(
(error) => { (error) => {
console.log(error); console.error(error);
toast.error('Something went wrong :/'); toast.error('Something went wrong :/');
return null; return null;
} }
+1 -1
View File
@@ -106,7 +106,7 @@
}; };
const updateDefaultPermissionsHandler = async (group) => { const updateDefaultPermissionsHandler = async (group) => {
console.log(group.permissions); console.debug(group.permissions);
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch( const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
(error) => { (error) => {
@@ -31,10 +31,6 @@
description = ''; description = '';
userIds = []; userIds = [];
}; };
onMount(() => {
console.log('mounted');
});
</script> </script>
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
@@ -91,7 +91,6 @@
} }
onMount(() => { onMount(() => {
console.log(tabs);
selectedTab = tabs[0]; selectedTab = tabs[0];
init(); init();
}); });
@@ -167,7 +167,7 @@
/> />
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} /> <UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
{#if ($config?.license_metadata?.seats ?? null) !== null && users.length > $config?.license_metadata?.seats} {#if ($config?.license_metadata?.seats ?? null) !== null && total && total > $config?.license_metadata?.seats}
<div class=" mt-1 mb-2 text-xs text-red-500"> <div class=" mt-1 mb-2 text-xs text-red-500">
<Banner <Banner
className="mx-0" className="mx-0"
@@ -73,7 +73,7 @@
for (const [idx, row] of rows.entries()) { for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim()); const columns = row.split(',').map((col) => col.trim());
console.log(idx, columns); console.debug(idx, columns);
if (idx > 0) { if (idx > 0) {
if ( if (
@@ -80,7 +80,7 @@
const inputFilesHandler = async (inputFiles) => { const inputFilesHandler = async (inputFiles) => {
inputFiles.forEach((file) => { inputFiles.forEach((file) => {
console.log('Processing file:', { console.info('Processing file:', {
name: file.name, name: file.name,
type: file.type, type: file.type,
size: file.size, size: file.size,
@@ -91,7 +91,7 @@
($config?.file?.max_size ?? null) !== null && ($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024 file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) { ) {
console.log('File exceeds max size limit:', { console.error('File exceeds max size limit:', {
fileSize: file.size, fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024 maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
}); });
@@ -163,14 +163,14 @@
const uploadedFile = await uploadFile(localStorage.token, file); const uploadedFile = await uploadFile(localStorage.token, file);
if (uploadedFile) { if (uploadedFile) {
console.log('File upload completed:', { console.info('File upload completed:', {
id: uploadedFile.id, id: uploadedFile.id,
name: fileItem.name, name: fileItem.name,
collection: uploadedFile?.meta?.collection_name collection: uploadedFile?.meta?.collection_name
}); });
if (uploadedFile.error) { if (uploadedFile.error) {
console.warn('File upload warning:', uploadedFile.error); console.error('File upload warning:', uploadedFile.error);
toast.warning(uploadedFile.error); toast.warning(uploadedFile.error);
} }
@@ -193,7 +193,6 @@
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
console.log('Escape');
draggedOver = false; draggedOver = false;
} }
}; };
@@ -215,7 +214,6 @@
const onDrop = async (e) => { const onDrop = async (e) => {
e.preventDefault(); e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) { if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files); const inputFiles = Array.from(e.dataTransfer?.files);
@@ -270,7 +268,6 @@
}); });
onDestroy(() => { onDestroy(() => {
console.log('destroy');
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
const dropzoneElement = document.getElementById('channel-container'); const dropzoneElement = document.getElementById('channel-container');
@@ -479,12 +476,12 @@
} }
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('Escape'); console.info('Escape');
} }
}} }}
on:paste={async (e) => { on:paste={async (e) => {
e = e.detail.event; e = e.detail.event;
console.log(e); console.info(e);
}} }}
/> />
</div> </div>
+1 -1
View File
@@ -51,7 +51,7 @@
{#if !top} {#if !top}
<Loader <Loader
on:visible={(e) => { on:visible={(e) => {
console.log('visible'); console.info('visible');
if (!messagesLoading) { if (!messagesLoading) {
loadMoreMessages(); loadMoreMessages();
} }
+1 -1
View File
@@ -54,7 +54,7 @@
}; };
const channelEventHandler = async (event) => { const channelEventHandler = async (event) => {
console.log(event); console.debug(event);
if (event.channel_id === channel.id) { if (event.channel_id === channel.id) {
const type = event?.data?.type ?? null; const type = event?.data?.type ?? null;
const data = event?.data?.data ?? null; const data = event?.data?.data ?? null;
+36 -6
View File
@@ -4,13 +4,15 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { chatId, settings, showArtifacts, showControls } from '$lib/stores'; import { artifactCode, chatId, settings, showArtifacts, showControls } from '$lib/stores';
import XMark from '../icons/XMark.svelte';
import { copyToClipboard, createMessagesList } from '$lib/utils'; import { copyToClipboard, createMessagesList } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte'; import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import SvgPanZoom from '../common/SVGPanZoom.svelte'; import SvgPanZoom from '../common/SVGPanZoom.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte'; import ArrowLeft from '../icons/ArrowLeft.svelte';
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
export let overlay = false; export let overlay = false;
export let history; export let history;
@@ -154,7 +156,7 @@
url.pathname + url.search + url.hash url.pathname + url.search + url.hash
); );
} else { } else {
console.log('External navigation blocked:', url.href); console.info('External navigation blocked:', url.href);
} }
} }
}, },
@@ -180,7 +182,26 @@
} }
}; };
onMount(() => {}); const downloadArtifact = () => {
const blob = new Blob([contents[selectedContentIdx].content], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `artifact-${$chatId}-${selectedContentIdx}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
onMount(() => {
artifactCode.subscribe((value) => {
if (contents) {
const codeIdx = contents.findIndex((content) => content.content.includes(value));
selectedContentIdx = codeIdx !== -1 ? codeIdx : 0;
}
});
});
</script> </script>
<div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850"> <div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850">
@@ -198,7 +219,7 @@
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" /> <ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
</button> </button>
<div class="flex-1 flex items-center justify-between"> <div class="flex-1 flex items-center justify-between pr-1">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr"> <div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
<button <button
@@ -252,7 +273,7 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1.5">
<button <button
class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5" class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={() => { on:click={() => {
@@ -265,6 +286,15 @@
}}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button }}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
> >
<Tooltip content={$i18n.t('Download')}>
<button
class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5"
on:click={downloadArtifact}
>
<ArrowDownTray className="size-3.5" />
</button>
</Tooltip>
{#if contents[selectedContentIdx].type === 'iframe'} {#if contents[selectedContentIdx].type === 'iframe'}
<Tooltip content={$i18n.t('Open in full screen')}> <Tooltip content={$i18n.t('Open in full screen')}>
<button <button
+78 -31
View File
@@ -119,6 +119,7 @@
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels; $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
let selectedToolIds = []; let selectedToolIds = [];
let selectedFilterIds = [];
let imageGenerationEnabled = false; let imageGenerationEnabled = false;
let webSearchEnabled = false; let webSearchEnabled = false;
let codeInterpreterEnabled = false; let codeInterpreterEnabled = false;
@@ -146,6 +147,7 @@
prompt = ''; prompt = '';
files = []; files = [];
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
@@ -155,12 +157,15 @@
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`) localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
); );
prompt = input.prompt; if (!$temporaryChatEnabled) {
files = input.files; prompt = input.prompt;
selectedToolIds = input.selectedToolIds; files = input.files;
webSearchEnabled = input.webSearchEnabled; selectedToolIds = input.selectedToolIds;
imageGenerationEnabled = input.imageGenerationEnabled; selectedFilterIds = input.selectedFilterIds;
codeInterpreterEnabled = input.codeInterpreterEnabled; webSearchEnabled = input.webSearchEnabled;
imageGenerationEnabled = input.imageGenerationEnabled;
codeInterpreterEnabled = input.codeInterpreterEnabled;
}
} catch (e) {} } catch (e) {}
} }
@@ -190,10 +195,12 @@
$: if (selectedModels) { $: if (selectedModels) {
setToolIds(); setToolIds();
setFilterIds();
} }
$: if (atSelectedModel || selectedModels) { $: if (atSelectedModel || selectedModels) {
setToolIds(); setToolIds();
setFilterIds();
} }
const setToolIds = async () => { const setToolIds = async () => {
@@ -207,9 +214,19 @@
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]); const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
if (model) { if (model) {
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) => selectedToolIds = [
$tools.find((t) => t.id === id) ...new Set(
); [...selectedToolIds, ...(model?.info?.meta?.toolIds ?? [])].filter((id) =>
$tools.find((t) => t.id === id)
)
)
];
}
};
const setFilterIds = async () => {
if (selectedModels.length !== 1 && !atSelectedModel) {
selectedFilterIds = [];
} }
}; };
@@ -419,25 +436,29 @@
} }
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) { if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
prompt = '';
files = [];
selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false;
imageGenerationEnabled = false;
codeInterpreterEnabled = false;
try { try {
const input = JSON.parse( const input = JSON.parse(
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`) localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
); );
console.log('chat-input', input);
prompt = input.prompt; if (!$temporaryChatEnabled) {
files = input.files; prompt = input.prompt;
selectedToolIds = input.selectedToolIds; files = input.files;
webSearchEnabled = input.webSearchEnabled; selectedToolIds = input.selectedToolIds;
imageGenerationEnabled = input.imageGenerationEnabled; selectedFilterIds = input.selectedFilterIds;
codeInterpreterEnabled = input.codeInterpreterEnabled; webSearchEnabled = input.webSearchEnabled;
} catch (e) { imageGenerationEnabled = input.imageGenerationEnabled;
prompt = ''; codeInterpreterEnabled = input.codeInterpreterEnabled;
files = []; }
selectedToolIds = []; } catch (e) {}
webSearchEnabled = false;
imageGenerationEnabled = false;
codeInterpreterEnabled = false;
}
} }
if (!chatIdProp) { if (!chatIdProp) {
@@ -664,10 +685,16 @@
////////////////////////// //////////////////////////
const initNewChat = async () => { const initNewChat = async () => {
if ($page.url.searchParams.get('models')) { const availableModels = $models
selectedModels = $page.url.searchParams.get('models')?.split(','); .filter((m) => !(m?.info?.meta?.hidden ?? false))
} else if ($page.url.searchParams.get('model')) { .map((m) => m.id);
const urlModels = $page.url.searchParams.get('model')?.split(',');
if ($page.url.searchParams.get('models') || $page.url.searchParams.get('model')) {
const urlModels = (
$page.url.searchParams.get('models') ||
$page.url.searchParams.get('model') ||
''
)?.split(',');
if (urlModels.length === 1) { if (urlModels.length === 1) {
const m = $models.find((m) => m.id === urlModels[0]); const m = $models.find((m) => m.id === urlModels[0]);
@@ -690,6 +717,10 @@
} else { } else {
selectedModels = urlModels; selectedModels = urlModels;
} }
selectedModels = selectedModels.filter((modelId) =>
$models.map((m) => m.id).includes(modelId)
);
} else { } else {
if (sessionStorage.selectedModels) { if (sessionStorage.selectedModels) {
selectedModels = JSON.parse(sessionStorage.selectedModels); selectedModels = JSON.parse(sessionStorage.selectedModels);
@@ -702,12 +733,12 @@
selectedModels = $config?.default_models.split(','); selectedModels = $config?.default_models.split(',');
} }
} }
selectedModels = selectedModels.filter((modelId) => availableModels.includes(modelId));
} }
selectedModels = selectedModels.filter((modelId) => $models.map((m) => m.id).includes(modelId));
if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) { if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
if ($models.length > 0) { if (availableModels.length > 0) {
selectedModels = [$models[0].id]; selectedModels = [availableModels?.at(0) ?? ''];
} else { } else {
selectedModels = ['']; selectedModels = [''];
} }
@@ -877,6 +908,7 @@
...(m.usage ? { usage: m.usage } : {}), ...(m.usage ? { usage: m.usage } : {}),
...(m.sources ? { sources: m.sources } : {}) ...(m.sources ? { sources: m.sources } : {})
})), })),
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
model_item: $models.find((m) => m.id === modelId), model_item: $models.find((m) => m.id === modelId),
chat_id: chatId, chat_id: chatId,
session_id: $socket?.id, session_id: $socket?.id,
@@ -1496,9 +1528,20 @@
}; };
const sendPromptSocket = async (_history, model, responseMessageId, _chatId) => { const sendPromptSocket = async (_history, model, responseMessageId, _chatId) => {
const chatMessages = createMessagesList(history, history.currentId);
const responseMessage = _history.messages[responseMessageId]; const responseMessage = _history.messages[responseMessageId];
const userMessage = _history.messages[responseMessage.parentId]; const userMessage = _history.messages[responseMessage.parentId];
const chatMessageFiles = chatMessages
.filter((message) => message.files)
.flatMap((message) => message.files);
// Filter chatFiles to only include files that are in the chatMessageFiles
chatFiles = chatFiles.filter((item) => {
const fileExists = chatMessageFiles.some((messageFile) => messageFile.id === item.id);
return fileExists;
});
let files = JSON.parse(JSON.stringify(chatFiles)); let files = JSON.parse(JSON.stringify(chatFiles));
files.push( files.push(
...(userMessage?.files ?? []).filter((item) => ...(userMessage?.files ?? []).filter((item) =>
@@ -1602,6 +1645,8 @@
}, },
files: (files?.length ?? 0) > 0 ? files : undefined, files: (files?.length ?? 0) > 0 ? files : undefined,
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
tool_servers: $toolServers, tool_servers: $toolServers,
@@ -2043,6 +2088,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled
@@ -2099,6 +2145,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled
+124 -23
View File
@@ -38,6 +38,7 @@
import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte'; import FilesOverlay from './MessageInput/FilesOverlay.svelte';
import Commands from './MessageInput/Commands.svelte'; import Commands from './MessageInput/Commands.svelte';
import ToolServersModal from './ToolServersModal.svelte';
import RichTextInput from '../common/RichTextInput.svelte'; import RichTextInput from '../common/RichTextInput.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
@@ -47,12 +48,12 @@
import XMark from '../icons/XMark.svelte'; import XMark from '../icons/XMark.svelte';
import Headphone from '../icons/Headphone.svelte'; import Headphone from '../icons/Headphone.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte'; import GlobeAlt from '../icons/GlobeAlt.svelte';
import PhotoSolid from '../icons/PhotoSolid.svelte';
import Photo from '../icons/Photo.svelte'; import Photo from '../icons/Photo.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import ToolServersModal from './ToolServersModal.svelte';
import Wrench from '../icons/Wrench.svelte'; import Wrench from '../icons/Wrench.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import Sparkles from '../icons/Sparkles.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@@ -79,6 +80,7 @@
export let toolServers = []; export let toolServers = [];
export let selectedToolIds = []; export let selectedToolIds = [];
export let selectedFilterIds = [];
export let imageGenerationEnabled = false; export let imageGenerationEnabled = false;
export let webSearchEnabled = false; export let webSearchEnabled = false;
@@ -88,6 +90,7 @@
prompt, prompt,
files: files.filter((file) => file.type !== 'image'), files: files.filter((file) => file.type !== 'image'),
selectedToolIds, selectedToolIds,
selectedFilterIds,
imageGenerationEnabled, imageGenerationEnabled,
webSearchEnabled, webSearchEnabled,
codeInterpreterEnabled codeInterpreterEnabled
@@ -113,10 +116,41 @@
export let placeholder = ''; export let placeholder = '';
let visionCapableModels = []; let visionCapableModels = [];
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter( $: visionCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
); );
let fileUploadCapableModels = [];
$: fileUploadCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.file_upload ?? true
);
let webSearchCapableModels = [];
$: webSearchCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.web_search ?? true
);
let imageGenerationCapableModels = [];
$: imageGenerationCapableModels = (
atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
).filter(
(model) =>
$models.find((m) => m.id === model)?.info?.meta?.capabilities?.image_generation ?? true
);
let codeInterpreterCapableModels = [];
$: codeInterpreterCapableModels = (
atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
).filter(
(model) =>
$models.find((m) => m.id === model)?.info?.meta?.capabilities?.code_interpreter ?? true
);
let toggleFilters = [];
$: toggleFilters = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels)
.map((id) => ($models.find((model) => model.id === id) || {})?.filters ?? [])
.reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
const scrollToBottom = () => { const scrollToBottom = () => {
const element = document.getElementById('messages-container'); const element = document.getElementById('messages-container');
element.scrollTo({ element.scrollTo({
@@ -355,7 +389,6 @@
</script> </script>
<FilesOverlay show={dragged} /> <FilesOverlay show={dragged} />
<ToolServersModal bind:show={showTools} {selectedToolIds} /> <ToolServersModal bind:show={showTools} {selectedToolIds} />
{#if loaded} {#if loaded}
@@ -581,11 +614,15 @@
dismissible={true} dismissible={true}
edit={true} edit={true}
on:dismiss={async () => { on:dismiss={async () => {
if (file.type !== 'collection' && !file?.collection) { try {
if (file.id) { if (file.type !== 'collection' && !file?.collection) {
// This will handle both file deletion and Chroma cleanup if (file.id) {
await deleteFileById(localStorage.token, file.id); // This will handle both file deletion and Chroma cleanup
await deleteFileById(localStorage.token, file.id);
}
} }
} catch (error) {
console.error('Error deleting file:', error);
} }
// Remove from UI state // Remove from UI state
@@ -771,8 +808,11 @@
console.log('Escape'); console.log('Escape');
atSelectedModel = undefined; atSelectedModel = undefined;
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
codeInterpreterEnabled = false;
} }
}} }}
on:paste={async (e) => { on:paste={async (e) => {
@@ -821,9 +861,9 @@
{:else} {:else}
<textarea <textarea
id="chat-input" id="chat-input"
dir="auto" dir={$settings?.chatDirection ?? 'auto'}
bind:this={chatInputElement} bind:this={chatInputElement}
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none" class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt} bind:value={prompt}
on:compositionstart={() => (isComposing = true)} on:compositionstart={() => (isComposing = true)}
@@ -869,7 +909,7 @@
console.log(userMessageElement); console.log(userMessageElement);
userMessageElement.scrollIntoView({ block: 'center' }); userMessageElement?.scrollIntoView({ block: 'center' });
editButton?.click(); editButton?.click();
} }
@@ -878,20 +918,38 @@
e.preventDefault(); e.preventDefault();
commandsElement.selectUp(); commandsElement.selectUp();
const container = document.getElementById('command-options-container');
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
if (commandOptionButton && container) {
const elTop = commandOptionButton.offsetTop;
const elHeight = commandOptionButton.offsetHeight;
const containerHeight = container.clientHeight;
// Center the selected button in the container
container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
}
} }
if (commandsContainerElement && e.key === 'ArrowDown') { if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
commandsElement.selectDown(); commandsElement.selectDown();
const container = document.getElementById('command-options-container');
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
if (commandOptionButton && container) {
const elTop = commandOptionButton.offsetTop;
const elHeight = commandOptionButton.offsetHeight;
const containerHeight = container.clientHeight;
// Center the selected button in the container
container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
}
} }
if (commandsContainerElement && e.key === 'Enter') { if (commandsContainerElement && e.key === 'Enter') {
@@ -939,8 +997,6 @@
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey; : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
console.log('Enter pressed:', enterPressed);
if (enterPressed) { if (enterPressed) {
e.preventDefault(); e.preventDefault();
} }
@@ -978,8 +1034,10 @@
console.log('Escape'); console.log('Escape');
atSelectedModel = undefined; atSelectedModel = undefined;
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
codeInterpreterEnabled = false;
} }
}} }}
rows="1" rows="1"
@@ -1037,6 +1095,8 @@
<div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5"> <div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5">
<InputMenu <InputMenu
bind:selectedToolIds bind:selectedToolIds
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
{fileUploadCapableModels}
{screenCaptureHandler} {screenCaptureHandler}
{inputFilesHandler} {inputFilesHandler}
uploadFilesHandler={() => { uploadFilesHandler={() => {
@@ -1127,7 +1187,48 @@
{/if} {/if}
{#if $_user} {#if $_user}
{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)} {#each toggleFilters as filter, filterIdx (filter.id)}
<Tooltip content={filter?.description} placement="top">
<button
on:click|preventDefault={() => {
if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filter.id
);
} else {
selectedFilterIds = [...selectedFilterIds, filter.id];
}
}}
type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {selectedFilterIds.includes(
filter.id
)
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
>
{#if filter?.icon}
<div class="size-5 items-center flex justify-center">
<img
src={filter.icon}
class="size-4.5 {filter.icon.includes('svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={filter.name}
/>
</div>
{:else}
<Sparkles className="size-5" strokeWidth="1.75" />
{/if}
<span
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
>{filter?.name}</span
>
</button>
</Tooltip>
{/each}
{#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === webSearchCapableModels.length && $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
<Tooltip content={$i18n.t('Search the internet')} placement="top"> <Tooltip content={$i18n.t('Search the internet')} placement="top">
<button <button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)} on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
@@ -1135,7 +1236,7 @@
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled || class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always' ($settings?.webSearch ?? false) === 'always'
? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400' ? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800'}"
> >
<GlobeAlt className="size-5" strokeWidth="1.75" /> <GlobeAlt className="size-5" strokeWidth="1.75" />
<span <span
@@ -1146,7 +1247,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)} {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === imageGenerationCapableModels.length && $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
<Tooltip content={$i18n.t('Generate an image')} placement="top"> <Tooltip content={$i18n.t('Generate an image')} placement="top">
<button <button
on:click|preventDefault={() => on:click|preventDefault={() =>
@@ -1154,7 +1255,7 @@
type="button" type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400' ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
> >
<Photo className="size-5" strokeWidth="1.75" /> <Photo className="size-5" strokeWidth="1.75" />
<span <span
@@ -1165,7 +1266,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)} {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === codeInterpreterCapableModels.length && $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top"> <Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
<button <button
on:click|preventDefault={() => on:click|preventDefault={() =>
@@ -1173,7 +1274,7 @@
type="button" type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 ' ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 '
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
> >
<CommandLine className="size-5" strokeWidth="1.75" /> <CommandLine className="size-5" strokeWidth="1.75" />
<span <span
@@ -170,10 +170,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" <div
> class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden"> id="command-options-container"
>
{#each filteredItems as item, idx} {#each filteredItems as item, idx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx === class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx ===
@@ -71,10 +71,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" <div
> class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden"> id="command-options-container"
>
{#each filteredItems as model, modelIdx} {#each filteredItems as model, modelIdx}
<button <button
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
@@ -180,10 +180,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" <div
> class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
<div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden"> id="command-options-container"
>
{#each filteredPrompts as prompt, promptIdx} {#each filteredPrompts as prompt, promptIdx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
@@ -20,6 +20,11 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let selectedToolIds: string[] = [];
export let selectedModels: string[] = [];
export let fileUploadCapableModels: string[] = [];
export let screenCaptureHandler: Function; export let screenCaptureHandler: Function;
export let uploadFilesHandler: Function; export let uploadFilesHandler: Function;
export let inputFilesHandler: Function; export let inputFilesHandler: Function;
@@ -27,8 +32,6 @@
export let uploadGoogleDriveHandler: Function; export let uploadGoogleDriveHandler: Function;
export let uploadOneDriveHandler: Function; export let uploadOneDriveHandler: Function;
export let selectedToolIds: string[] = [];
export let onClose: Function; export let onClose: Function;
let tools = {}; let tools = {};
@@ -40,7 +43,9 @@
} }
let fileUploadEnabled = true; let fileUploadEnabled = true;
$: fileUploadEnabled = $user?.role === 'admin' || $user?.permissions?.chat?.file_upload; $: fileUploadEnabled =
fileUploadCapableModels.length === selectedModels.length &&
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
const init = async () => { const init = async () => {
if ($_tools === null) { if ($_tools === null) {
@@ -169,7 +174,11 @@
{/if} {/if}
<Tooltip <Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''} content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full" className="w-full"
> >
<DropdownMenu.Item <DropdownMenu.Item
@@ -196,7 +205,11 @@
</Tooltip> </Tooltip>
<Tooltip <Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''} content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full" className="w-full"
> >
<DropdownMenu.Item <DropdownMenu.Item
@@ -214,157 +227,164 @@
</DropdownMenu.Item> </DropdownMenu.Item>
</Tooltip> </Tooltip>
{#if $config?.features?.enable_google_drive_integration} {#if fileUploadEnabled}
<DropdownMenu.Item {#if $config?.features?.enable_google_drive_integration}
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl" <DropdownMenu.Item
on:click={() => { class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
uploadGoogleDriveHandler(); on:click={() => {
}} uploadGoogleDriveHandler();
> }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<path
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
fill="#0066da"
/>
<path
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
fill="#00ac47"
/>
<path
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335"
/>
<path
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
fill="#00832d"
/>
<path
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
fill="#2684fc"
/>
<path
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00"
/>
</svg>
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item>
{/if}
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<mask <path
id="mask0_87_7796" d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
style="mask-type:alpha" fill="#0066da"
maskUnits="userSpaceOnUse" />
x="0" <path
y="6" d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
width="32" fill="#00ac47"
height="20" />
> <path
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z" fill="#ea4335"
fill="#C4C4C4" />
/> <path
</mask> d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
<g mask="url(#mask0_87_7796)"> fill="#00832d"
<path />
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z" <path
fill="url(#paint0_linear_87_7796)" d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
/> fill="#2684fc"
<path />
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z" <path
fill="url(#paint1_linear_87_7796)" d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
/> fill="#ffba00"
<path />
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg> </svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div> <div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.SubTrigger> </DropdownMenu.Item>
<DropdownMenu.SubContent {/if}
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'} {#if $config?.features?.enable_onedrive_integration}
sideOffset={$mobile ? 5 : 0} <DropdownMenu.Sub>
alignOffset={$mobile ? 0 : -8} <DropdownMenu.SubTrigger
> class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
> >
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div> <svg
</DropdownMenu.Item> xmlns="http://www.w3.org/2000/svg"
<DropdownMenu.Item viewBox="0 0 32 32"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl" class="w-5 h-5"
on:click={() => { fill="none"
uploadOneDriveHandler('organizations'); >
}} <mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
> >
<div class="flex flex-col"> <DropdownMenu.Item
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div> class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
<div class="text-xs text-gray-500">Includes SharePoint</div> on:click={() => {
</div> uploadOneDriveHandler('personal');
</DropdownMenu.Item> }}
</DropdownMenu.SubContent> >
</DropdownMenu.Sub> <div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">Includes SharePoint</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
{/if} {/if}
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>
+3 -2
View File
@@ -255,7 +255,7 @@
await updateChat(); await updateChat();
}; };
const editMessage = async (messageId, content, submit = true) => { const editMessage = async (messageId, { content, files }, submit = true) => {
if (history.messages[messageId].role === 'user') { if (history.messages[messageId].role === 'user') {
if (submit) { if (submit) {
// New user message // New user message
@@ -268,7 +268,7 @@
childrenIds: [], childrenIds: [],
role: 'user', role: 'user',
content: userPrompt, content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }), ...(files && { files: files }),
models: selectedModels, models: selectedModels,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch timestamp: Math.floor(Date.now() / 1000) // Unix epoch
}; };
@@ -290,6 +290,7 @@
} else { } else {
// Edit user message // Edit user message
history.messages[messageId].content = content; history.messages[messageId].content = content;
history.messages[messageId].files = files;
await updateChat(); await updateChat();
} }
} else { } else {
@@ -17,16 +17,19 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte'; import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
import CommandLine from '$lib/components/icons/CommandLine.svelte'; import CommandLine from '$lib/components/icons/CommandLine.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let id = ''; export let id = '';
export let onSave = (e) => {}; export let onSave = (e) => {};
export let onCode = (e) => {}; export let onUpdate = (e) => {};
export let onPreview = (e) => {};
export let save = false; export let save = false;
export let run = true; export let run = true;
export let preview = false;
export let collapsed = false; export let collapsed = false;
export let token; export let token;
@@ -88,6 +91,10 @@
}, 1000); }, 1000);
}; };
const previewCode = () => {
onPreview(code);
};
const checkPythonCode = (str) => { const checkPythonCode = (str) => {
// Check if the string contains typical Python syntax characters // Check if the string contains typical Python syntax characters
const pythonSyntax = [ const pythonSyntax = [
@@ -333,6 +340,8 @@
await drawMermaidDiagram(); await drawMermaidDiagram();
})(); })();
} }
onUpdate(token);
}; };
$: if (token) { $: if (token) {
@@ -345,8 +354,6 @@
render(); render();
} }
$: onCode({ lang, code });
$: if (attributes) { $: if (attributes) {
onAttributesUpdate(); onAttributesUpdate();
} }
@@ -379,10 +386,10 @@
onMount(async () => { onMount(async () => {
console.log('codeblock', lang, code); console.log('codeblock', lang, code);
if (token) {
if (lang) { onUpdate(token);
onCode({ lang, code });
} }
if (document.documentElement.classList.contains('dark')) { if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({ mermaid.initialize({
startOnLoad: true, startOnLoad: true,
@@ -430,7 +437,7 @@
class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5" class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={collapseCodeBlock} on:click={collapseCodeBlock}
> >
<div> <div class=" -translate-y-[0.5px]">
<ChevronUpDown className="size-3" /> <ChevronUpDown className="size-3" />
</div> </div>
@@ -439,6 +446,21 @@
</div> </div>
</button> </button>
{#if preview && ['html', 'svg'].includes(lang)}
<button
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={previewCode}
>
<div class=" -translate-y-[0.5px]">
<Cube className="size-3" />
</div>
<div>
{$i18n.t('Preview')}
</div>
</button>
{/if}
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))} {#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
{#if executing} {#if executing}
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed"> <div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
@@ -453,7 +475,7 @@
executePython(code); executePython(code);
}} }}
> >
<div> <div class=" -translate-y-[0.5px]">
<CommandLine className="size-3" /> <CommandLine className="size-3" />
</div> </div>
@@ -1,10 +1,17 @@
<script> <script>
import { onDestroy, onMount, tick, getContext, createEventDispatcher } from 'svelte'; import { onDestroy, onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import Markdown from './Markdown.svelte'; import Markdown from './Markdown.svelte';
import { chatId, mobile, settings, showArtifacts, showControls, showOverview } from '$lib/stores'; import {
artifactCode,
chatId,
mobile,
settings,
showArtifacts,
showControls,
showOverview
} from '$lib/stores';
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte'; import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
import { createMessagesList } from '$lib/utils'; import { createMessagesList } from '$lib/utils';
@@ -15,8 +22,10 @@
export let sources = null; export let sources = null;
export let save = false; export let save = false;
export let preview = false;
export let floatingButtons = true; export let floatingButtons = true;
export let onSave = () => {};
export let onSourceClick = () => {}; export let onSourceClick = () => {};
export let onTaskClick = () => {}; export let onTaskClick = () => {};
@@ -122,6 +131,7 @@
{content} {content}
{model} {model}
{save} {save}
{preview}
sourceIds={(sources ?? []).reduce((acc, s) => { sourceIds={(sources ?? []).reduce((acc, s) => {
let ids = []; let ids = [];
s.document.forEach((document, index) => { s.document.forEach((document, index) => {
@@ -154,11 +164,9 @@
}, [])} }, [])}
{onSourceClick} {onSourceClick}
{onTaskClick} {onTaskClick}
onUpdate={(value) => { {onSave}
dispatch('update', value); onUpdate={(token) => {
}} const { lang, text: code } = token;
onCode={(value) => {
const { lang, code } = value;
if ( if (
($settings?.detectArtifacts ?? true) && ($settings?.detectArtifacts ?? true) &&
@@ -170,6 +178,13 @@
showControls.set(true); showControls.set(true);
} }
}} }}
onPreview={async (value) => {
console.log('Preview', value);
await artifactCode.set(value);
await showControls.set(true);
await showArtifacts.set(true);
await showOverview.set(false);
}}
/> />
</div> </div>
@@ -12,11 +12,14 @@
export let content; export let content;
export let model = null; export let model = null;
export let save = false; export let save = false;
export let preview = false;
export let sourceIds = []; export let sourceIds = [];
export let onSave = () => {};
export let onUpdate = () => {}; export let onUpdate = () => {};
export let onCode = () => {};
export let onPreview = () => {};
export let onSourceClick = () => {}; export let onSourceClick = () => {};
export let onTaskClick = () => {}; export let onTaskClick = () => {};
@@ -40,5 +43,15 @@
</script> </script>
{#key id} {#key id}
<MarkdownTokens {tokens} {id} {save} {onTaskClick} {onSourceClick} {onUpdate} {onCode} /> <MarkdownTokens
{tokens}
{id}
{save}
{preview}
{onTaskClick}
{onSourceClick}
{onSave}
{onUpdate}
{onPreview}
/>
{/key} {/key}
@@ -29,9 +29,11 @@
export let attributes = {}; export let attributes = {};
export let save = false; export let save = false;
export let preview = false;
export let onSave: Function = () => {};
export let onUpdate: Function = () => {}; export let onUpdate: Function = () => {};
export let onCode: Function = () => {}; export let onPreview: Function = () => {};
export let onTaskClick: Function = () => {}; export let onTaskClick: Function = () => {};
export let onSourceClick: Function = () => {}; export let onSourceClick: Function = () => {};
@@ -95,14 +97,16 @@
code={token?.text ?? ''} code={token?.text ?? ''}
{attributes} {attributes}
{save} {save}
{onCode} {preview}
onSave={(value) => { onSave={(value) => {
onUpdate({ onSave({
raw: token.raw, raw: token.raw,
oldContent: token.text, oldContent: token.text,
newContent: value newContent: value
}); });
}} }}
{onUpdate}
{onPreview}
/> />
{:else} {:else}
{token.text} {token.text}
@@ -157,6 +157,10 @@
const copyToClipboard = async (text) => { const copyToClipboard = async (text) => {
text = removeAllDetails(text); text = removeAllDetails(text);
if (($config?.ui?.response_watermark ?? '').trim() !== '') {
text = `${text}\n\n${$config?.ui?.response_watermark}`;
}
const res = await _copyToClipboard(text, $settings?.copyFormatted ?? false); const res = await _copyToClipboard(text, $settings?.copyFormatted ?? false);
if (res) { if (res) {
toast.success($i18n.t('Copying to clipboard was successful!')); toast.success($i18n.t('Copying to clipboard was successful!'));
@@ -377,7 +381,7 @@
const editMessageConfirmHandler = async () => { const editMessageConfirmHandler = async () => {
const messageContent = postprocessAfterEditing(editedContent ? editedContent : ''); const messageContent = postprocessAfterEditing(editedContent ? editedContent : '');
editMessage(message.id, messageContent, false); editMessage(message.id, { content: messageContent }, false);
edit = false; edit = false;
editedContent = ''; editedContent = '';
@@ -388,7 +392,7 @@
const saveAsCopyHandler = async () => { const saveAsCopyHandler = async () => {
const messageContent = postprocessAfterEditing(editedContent ? editedContent : ''); const messageContent = postprocessAfterEditing(editedContent ? editedContent : '');
editMessage(message.id, messageContent); editMessage(message.id, { content: messageContent });
edit = false; edit = false;
editedContent = ''; editedContent = '';
@@ -560,17 +564,30 @@
await tick(); await tick();
if (buttonsContainerElement) { if (buttonsContainerElement) {
console.log(buttonsContainerElement); console.log(buttonsContainerElement);
buttonsContainerElement.addEventListener('wheel', function (event) {
// console.log(event.deltaY);
event.preventDefault(); buttonsContainerElement.addEventListener('wheel', function (event) {
if (event.deltaY !== 0) { if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) {
// Adjust horizontal scroll position based on vertical scroll // If the container is not scrollable, horizontal scroll
buttonsContainerElement.scrollLeft += event.deltaY; return;
} else {
event.preventDefault();
if (event.deltaY !== 0) {
// Adjust horizontal scroll position based on vertical scroll
buttonsContainerElement.scrollLeft += event.deltaY;
}
} }
}); });
} }
}); });
let screenReaderDiv: HTMLDivElement;
$: if (message.done) {
if (screenReaderDiv) {
screenReaderDiv.textContent = message.content;
}
}
</script> </script>
<DeleteConfirmDialog <DeleteConfirmDialog
@@ -581,6 +598,10 @@
}} }}
/> />
<div bind:this={screenReaderDiv} aria-live="polite" class="sr-only">
{message.done ? message.content : ''}
</div>
{#key message.id} {#key message.id}
<div <div
class=" flex w-full message-{message.id}" class=" flex w-full message-{message.id}"
@@ -785,6 +806,7 @@
sources={message.sources} sources={message.sources}
floatingButtons={message?.done && !readOnly} floatingButtons={message?.done && !readOnly}
save={!readOnly} save={!readOnly}
preview={!readOnly}
{model} {model}
onTaskClick={async (e) => { onTaskClick={async (e) => {
console.log(e); console.log(e);
@@ -819,28 +841,13 @@
onAddMessages={({ modelId, parentId, messages }) => { onAddMessages={({ modelId, parentId, messages }) => {
addMessages({ modelId, parentId, messages }); addMessages({ modelId, parentId, messages });
}} }}
on:update={(e) => { onSave={({ raw, oldContent, newContent }) => {
const { raw, oldContent, newContent } = e.detail;
history.messages[message.id].content = history.messages[ history.messages[message.id].content = history.messages[
message.id message.id
].content.replace(raw, raw.replace(oldContent, newContent)); ].content.replace(raw, raw.replace(oldContent, newContent));
updateChat(); updateChat();
}} }}
on:select={(e) => {
const { type, content } = e.detail;
if (type === 'explain') {
submitMessage(
message.id,
`Explain this section to me in more detail\n\n\`\`\`\n${content}\n\`\`\``
);
} else if (type === 'ask') {
const input = e.detail?.input ?? '';
submitMessage(message.id, `\`\`\`\n${content}\n\`\`\`\n${input}`);
}
}}
/> />
{/if} {/if}
@@ -1394,11 +1401,11 @@
actionMessage(action.id, message); actionMessage(action.id, message);
}} }}
> >
{#if action.icon_url} {#if action?.icon}
<div class="size-4"> <div class="size-4">
<img <img
src={action.icon_url} src={action.icon}
class="w-4 h-4 {action.icon_url.includes('svg') class="w-4 h-4 {action.icon.includes('svg')
? 'dark:invert-[80%]' ? 'dark:invert-[80%]'
: ''}" : ''}"
style="fill: currentColor;" style="fill: currentColor;"
@@ -43,6 +43,8 @@
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let editedFiles = [];
let messageEditTextAreaElement: HTMLTextAreaElement; let messageEditTextAreaElement: HTMLTextAreaElement;
let message = JSON.parse(JSON.stringify(history.messages[messageId])); let message = JSON.parse(JSON.stringify(history.messages[messageId]));
@@ -62,6 +64,7 @@
const editMessageHandler = async () => { const editMessageHandler = async () => {
edit = true; edit = true;
editedContent = message.content; editedContent = message.content;
editedFiles = message.files;
await tick(); await tick();
@@ -74,15 +77,17 @@
}; };
const editMessageConfirmHandler = async (submit = true) => { const editMessageConfirmHandler = async (submit = true) => {
editMessage(message.id, editedContent, submit); editMessage(message.id, { content: editedContent, files: editedFiles }, submit);
edit = false; edit = false;
editedContent = ''; editedContent = '';
editedFiles = [];
}; };
const cancelEditMessage = () => { const cancelEditMessage = () => {
edit = false; edit = false;
editedContent = ''; editedContent = '';
editedFiles = [];
}; };
const deleteMessageHandler = async () => { const deleteMessageHandler = async () => {
@@ -141,30 +146,90 @@
{/if} {/if}
<div class="chat-{message.role} w-full min-w-full markdown-prose"> <div class="chat-{message.role} w-full min-w-full markdown-prose">
{#if message.files} {#if edit !== true}
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap"> {#if message.files}
{#each message.files as file} <div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}> {#each message.files as file}
{#if file.type === 'image'} <div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
<Image src={file.url} imageClassName=" max-h-96 rounded-lg" /> {#if file.type === 'image'}
{:else} <Image src={file.url} imageClassName=" max-h-96 rounded-lg" />
<FileItem {:else}
item={file} <FileItem
url={file.url} item={file}
name={file.name} url={file.url}
type={file.type} name={file.name}
size={file?.size} type={file.type}
colorClassName="bg-white dark:bg-gray-850 " size={file?.size}
/> colorClassName="bg-white dark:bg-gray-850 "
{/if} />
</div> {/if}
{/each} </div>
</div> {/each}
</div>
{/if}
{/if} {/if}
{#if message.content !== ''} {#if message.content !== ''}
{#if edit === true} {#if edit === true}
<div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2"> <div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2">
{#if (editedFiles ?? []).length > 0}
<div class="flex items-center flex-wrap gap-2 -mx-2 mb-1">
{#each editedFiles as file, fileIdx}
{#if file.type === 'image'}
<div class=" relative group">
<div class="relative flex items-center">
<Image
src={file.url}
alt="input"
imageClassName=" size-14 rounded-xl object-cover"
/>
</div>
<div class=" absolute -top-1 -right-1">
<button
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
editedFiles.splice(fileIdx, 1);
editedFiles = editedFiles;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
{:else}
<FileItem
item={file}
name={file.name}
type={file.type}
size={file?.size}
loading={file.status === 'uploading'}
dismissible={true}
edit={true}
on:dismiss={async () => {
editedFiles.splice(fileIdx, 1);
editedFiles = editedFiles;
}}
on:click={() => {
console.log(file);
}}
/>
{/if}
{/each}
</div>
{/if}
<div class="max-h-96 overflow-auto"> <div class="max-h-96 overflow-auto">
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
+3 -1
View File
@@ -114,7 +114,9 @@
</div> </div>
{#if showSetDefault} {#if showSetDefault}
<div class=" absolute text-left mt-[1px] ml-1 text-[0.7rem] text-gray-500 font-primary"> <div
class="absolute text-left mt-[1px] ml-1 text-[0.7rem] text-gray-600 dark:text-gray-400 font-primary"
>
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>
{/if} {/if}
@@ -100,10 +100,10 @@
.filter((item) => { .filter((item) => {
if (selectedConnectionType === '') { if (selectedConnectionType === '') {
return true; return true;
} else if (selectedConnectionType === 'ollama') { } else if (selectedConnectionType === 'local') {
return item.model?.owned_by === 'ollama'; return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'openai') { } else if (selectedConnectionType === 'external') {
return item.model?.owned_by === 'openai'; return item.model?.connection_type === 'external';
} else if (selectedConnectionType === 'direct') { } else if (selectedConnectionType === 'direct') {
return item.model?.direct; return item.model?.direct;
} }
@@ -118,10 +118,10 @@
.filter((item) => { .filter((item) => {
if (selectedConnectionType === '') { if (selectedConnectionType === '') {
return true; return true;
} else if (selectedConnectionType === 'ollama') { } else if (selectedConnectionType === 'local') {
return item.model?.owned_by === 'ollama'; return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'openai') { } else if (selectedConnectionType === 'external') {
return item.model?.owned_by === 'openai'; return item.model?.connection_type === 'external';
} else if (selectedConnectionType === 'direct') { } else if (selectedConnectionType === 'direct') {
return item.model?.direct; return item.model?.direct;
} }
@@ -393,7 +393,7 @@
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5" class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
bind:this={tagsContainerElement} bind:this={tagsContainerElement}
> >
{#if (items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')) || items.find((item) => item.model?.direct) || tags.length > 0} {#if (items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')) || items.find((item) => item.model?.direct) || tags.length > 0}
<button <button
class="min-w-fit outline-none p-1.5 {selectedTag === '' && class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === '' selectedConnectionType === ''
@@ -408,25 +408,25 @@
</button> </button>
{/if} {/if}
{#if items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')} {#if items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')}
<button <button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'ollama' class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'local'
? '' ? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize" : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => { on:click={() => {
selectedTag = ''; selectedTag = '';
selectedConnectionType = 'ollama'; selectedConnectionType = 'local';
}} }}
> >
{$i18n.t('Local')} {$i18n.t('Local')}
</button> </button>
<button <button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'openai' class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'external'
? '' ? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize" : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => { on:click={() => {
selectedTag = ''; selectedTag = '';
selectedConnectionType = 'openai'; selectedConnectionType = 'external';
}} }}
> >
{$i18n.t('External')} {$i18n.t('External')}
@@ -34,6 +34,8 @@
export let files = []; export let files = [];
export let selectedToolIds = []; export let selectedToolIds = [];
export let selectedFilterIds = [];
export let imageGenerationEnabled = false; export let imageGenerationEnabled = false;
export let codeInterpreterEnabled = false; export let codeInterpreterEnabled = false;
export let webSearchEnabled = false; export let webSearchEnabled = false;
@@ -192,6 +194,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled
@@ -62,7 +62,7 @@
class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10" class="absolute top-0 bottom-0 left-0 right-0 opacity-60 bg-white dark:bg-gray-900 z-10"
></div> ></div>
{/if} {/if}
<div class="flex w-full"> <div class="flex w-full gap-2">
<div class="flex-1 relative"> <div class="flex-1 relative">
<input <input
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}" class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
@@ -32,6 +32,8 @@
let notificationSound = true; let notificationSound = true;
let notificationSoundAlways = false; let notificationSoundAlways = false;
let highContrastMode = false;
let detectArtifacts = true; let detectArtifacts = true;
let richTextInput = true; let richTextInput = true;
@@ -85,6 +87,11 @@
saveSettings({ splitLargeChunks: splitLargeChunks }); saveSettings({ splitLargeChunks: splitLargeChunks });
}; };
const toggleHighContrastMode = async () => {
highContrastMode = !highContrastMode;
saveSettings({ highContrastMode: highContrastMode });
};
const togglePromptAutocomplete = async () => { const togglePromptAutocomplete = async () => {
promptAutocomplete = !promptAutocomplete; promptAutocomplete = !promptAutocomplete;
saveSettings({ promptAutocomplete: promptAutocomplete }); saveSettings({ promptAutocomplete: promptAutocomplete });
@@ -281,6 +288,8 @@
titleAutoGenerate = $settings?.title?.auto ?? true; titleAutoGenerate = $settings?.title?.auto ?? true;
autoTags = $settings.autoTags ?? true; autoTags = $settings.autoTags ?? true;
highContrastMode = $settings.highContrastMode ?? false;
detectArtifacts = $settings.detectArtifacts ?? true; detectArtifacts = $settings.detectArtifacts ?? true;
responseAutoCopy = $settings.responseAutoCopy ?? false; responseAutoCopy = $settings.responseAutoCopy ?? false;
@@ -370,6 +379,28 @@
<div> <div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('High Contrast Mode')} ({$i18n.t('Beta')})
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleHighContrastMode();
}}
type="button"
>
{#if highContrastMode === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div> <div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>
@@ -149,6 +149,23 @@
</div> </div>
<div class="flex flex-col space-y-3 w-full self-start"> <div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Toggle search')}</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Ctrl/⌘
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
K
</div>
</div>
</div>
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Toggle settings')}</div> <div class=" text-sm">{$i18n.t('Toggle settings')}</div>
+6 -4
View File
@@ -64,7 +64,7 @@
} }
</script> </script>
<div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-400 dark:text-gray-600"> <div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-600 dark:text-gray-400">
{#if filteredPrompts.length > 0} {#if filteredPrompts.length > 0}
<Bolt /> <Bolt />
{$i18n.t('Suggested')} {$i18n.t('Suggested')}
@@ -74,7 +74,7 @@
<div <div
class="flex w-full {$settings?.landingPageMode === 'chat' class="flex w-full {$settings?.landingPageMode === 'chat'
? ' -mt-1' ? ' -mt-1'
: 'text-center items-center justify-center'} self-start text-gray-400 dark:text-gray-600" : 'text-center items-center justify-center'} self-start text-gray-600 dark:text-gray-400"
> >
{$WEBUI_NAME} ‧ v{WEBUI_VERSION} {$WEBUI_NAME} ‧ v{WEBUI_VERSION}
</div> </div>
@@ -98,7 +98,7 @@
> >
{prompt.title[0]} {prompt.title[0]}
</div> </div>
<div class="text-xs text-gray-500 font-normal line-clamp-1"> <div class="text-xs text-gray-600 dark:text-gray-400 font-normal line-clamp-1">
{prompt.title[1]} {prompt.title[1]}
</div> </div>
{:else} {:else}
@@ -107,7 +107,9 @@
> >
{prompt.content} {prompt.content}
</div> </div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">{$i18n.t('Prompt')}</div> <div class="text-xs text-gray-600 dark:text-gray-400 font-normal line-clamp-1">
{$i18n.t('Prompt')}
</div>
{/if} {/if}
</div> </div>
</button> </button>

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