Compare commits

..

323 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 7ec72679f0 Merge pull request #5599 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.24
2024-09-24 13:32:00 +02:00
Timothy J. Baek 31b311c3c9 refac
Co-Authored-By: MicroDev <70126934+microdev1@users.noreply.github.com>
2024-09-24 13:28:00 +02:00
Timothy J. Baek e24ab4c6d2 doc: changelog 2024-09-24 13:23:34 +02:00
Timothy J. Baek 6739983cf1 refac: deprecate interface "stream response" settings for advanced params 2024-09-24 12:49:35 +02:00
Timothy J. Baek ff00815b61 chore: format 2024-09-24 12:41:44 +02:00
Timothy J. Baek 8f6a927be3 enh: update info toast 2024-09-24 12:40:13 +02:00
Timothy J. Baek f6add92702 chore: format 2024-09-24 11:35:51 +02:00
Timothy Jaeryang Baek deedfdceae Merge pull request #5632 from jannikstdl/dev
enh: open PDF citations on the associated page
2024-09-24 11:27:53 +02:00
Timothy J. Baek e268ee5675 enh: audio/x-m4a support 2024-09-24 11:00:47 +02:00
Timothy J. Baek 33d8d818bd fix 2024-09-24 10:54:49 +02:00
Timothy Jaeryang Baek 9a81a37008 Merge pull request #5645 from kivvi3412/main
[feat] Set whether to stream individually for the model
2024-09-24 10:52:00 +02:00
Timothy Jaeryang Baek 8e620b0c2c Merge pull request #5649 from EtiennePerot/temp-file-close
fix: close temporary file after creating it
2024-09-24 10:49:38 +02:00
Etienne Perot fdd27aa321 fix: close temporary file after creating it.
This fixes "The process cannot access the file
because it is being used by another process"
errors on Windows.

The file is still automatically deleted by the
`os.unlink` call later in the function.

Updates #5606
Fixes #5642
2024-09-23 23:22:47 -07:00
kivvi 4ebff2c5ce Merge remote-tracking branch 'origin/main' 2024-09-24 12:29:36 +08:00
kivvi 8266d343bf Merge remote-tracking branch 'origin/main' 2024-09-24 12:29:11 +08:00
kivvi 36ddb19023 Merge remote-tracking branch 'origin/main' 2024-09-24 12:17:40 +08:00
kivvi ebc410d8d4 Merge remote-tracking branch 'origin/main' 2024-09-24 12:17:33 +08:00
kivvi 1772db4712 Merge remote-tracking branch 'origin/main' 2024-09-24 12:14:58 +08:00
kivvi 40d7d7d6dd Set whether to stream responses for this model individually, overriding the global setting 2024-09-24 12:14:51 +08:00
kivvi 0d01ea5f2f Set whether to stream responses for this model individually, overriding the global setting 2024-09-24 11:51:47 +08:00
Timothy J. Baek 822c47c171 fix 2024-09-24 01:35:34 +02:00
Timothy J. Baek fdf02c4e86 fix 2024-09-24 01:12:49 +02:00
Timothy J. Baek 398bc96b1a refac 2024-09-24 01:05:46 +02:00
Timothy J. Baek ad82eae6a9 feat: create new message pair on cmd+shift+enter 2024-09-24 00:57:28 +02:00
Timothy J. Baek d7b64ff447 enh: infinite scroll messages 2024-09-24 00:27:22 +02:00
Timothy J. Baek 93cb2be35d refac 2024-09-23 23:55:18 +02:00
Timothy J. Baek 8f1b9bdf8a refac 2024-09-23 23:52:45 +02:00
Jannik S. f69956bda8 fix: only append link with page attribute if metatada.page is available 2024-09-23 23:43:50 +02:00
Timothy J. Baek e06667ead8 refac 2024-09-23 23:39:33 +02:00
Timothy Jaeryang Baek 35f64cc53f Merge pull request #5640 from open-webui/messages-render-optimisation
refac: messages render optimisation
2024-09-23 23:27:08 +02:00
Timothy J. Baek 12f95555fc fix 2024-09-23 23:23:08 +02:00
Timothy J. Baek 84c1810b6e refac 2024-09-23 23:20:27 +02:00
Timothy J. Baek ba39f9bf56 refac 2024-09-23 17:40:24 +02:00
Jannik Streidl 8eb82265d0 Remove animation import - unsure if animations are desired 2024-09-23 15:50:46 +02:00
Jannik Streidl ac277e8e9e enh: CitationsModal 2024-09-23 15:48:12 +02:00
Timothy J. Baek b1521cacad refac: WIP 2024-09-23 14:24:50 +02:00
Timothy J. Baek 5978e7c9a6 refac: wip 2024-09-23 01:36:46 +02:00
Timothy J. Baek fd5e8b4fcf refac: deprecate messages for history 2024-09-23 00:55:13 +02:00
Timothy Jaeryang Baek 291b6dd744 Merge pull request #5613 from cevheri/dev
Turkish Language Support has been updated
2024-09-22 20:33:29 +02:00
Timothy J. Baek 73ff524a8f refac 2024-09-22 20:31:53 +02:00
cevheri c2b5bf2130 Turkish Language Support has been fixed. 2024-09-22 21:20:43 +03:00
Timothy J. Baek 8532f9da03 fix: many model chat save as copy issue 2024-09-22 20:16:45 +02:00
Timothy Jaeryang Baek 2cfe6830df Merge pull request #5595 from sebdanielsson/dev
feat: Add more variables to prompts
2024-09-22 16:47:51 +02:00
Timothy J. Baek 44355a4bdc enh: 'model' url search param added 2024-09-22 14:52:30 +02:00
Timothy J. Baek 47a858393b fix: hide mobile sidebar after menu button click 2024-09-22 14:50:24 +02:00
Timothy J. Baek d3e80f515d enh: hide pinned chats when searching 2024-09-22 14:49:53 +02:00
Timothy Jaeryang Baek c5b93ca631 Merge pull request #5601 from JamesClarke7283/extend-num-predict-param
fix: Extend num predict param
2024-09-22 14:28:00 +02:00
Timothy J. Baek 3335eee1b9 refac: do not set max for input 2024-09-22 14:27:52 +02:00
Sebastian 29175405a6 Add weekday, timezone, language variables to system prompts 2024-09-22 12:24:05 +02:00
Sebastian d53062a9b0 Add more variables to prompts 2024-09-22 11:49:18 +02:00
James Clarke 96ef15362a Increased max_tokens (num_predict) to 131072, fixes #5600 2024-09-22 05:43:04 +01:00
Timothy J. Baek d6d6098378 refac 2024-09-22 02:57:51 +02:00
Timothy J. Baek 620e629edc refac 2024-09-22 02:57:38 +02:00
Timothy J. Baek c292fd89f9 refac 2024-09-22 02:55:45 +02:00
Timothy J. Baek c346130774 feat: overview favourite response 2024-09-22 02:53:38 +02:00
Timothy J. Baek 768717aaf9 refac 2024-09-22 02:35:25 +02:00
Timothy J. Baek d055e1f888 refac 2024-09-22 02:14:59 +02:00
Timothy J. Baek 5f84145a2d enh: socket full redis support 2024-09-22 02:12:55 +02:00
Timothy J. Baek 47a9395a22 refac: styling 2024-09-22 01:42:18 +02:00
Timothy J. Baek 8dbac0f7e3 enh: show model hash and modified ts 2024-09-21 22:12:41 +02:00
Timothy Jaeryang Baek 6b463164f4 Merge pull request #5584 from open-webui/dev
fix: node tooltip xss issue
2024-09-21 21:44:25 +02:00
Timothy J. Baek 5f15e9ee68 fix: node tooltip xss issue
Co-Authored-By: Valentino Stillhardt <4715129+Fusseldieb@users.noreply.github.com>
2024-09-21 21:43:54 +02:00
Timothy Jaeryang Baek 54d63ece6f Merge pull request #5583 from open-webui/dev
refac
2024-09-21 21:35:54 +02:00
Timothy J. Baek e35883ca9c fix: openai payload issue 2024-09-21 21:35:00 +02:00
Timothy Jaeryang Baek 19e49e43cb Merge pull request #5578 from aleixdorca/dev
i18n: Update catalan translation.json
2024-09-21 17:37:41 +02:00
Aleix Dorca e429b5548c Update catalan translation.json 2024-09-21 17:27:32 +02:00
Timothy J. Baek 839dacc4a4 refac 2024-09-21 15:53:29 +02:00
Timothy J. Baek 4225591a26 fix 2024-09-21 15:48:52 +02:00
Timothy J. Baek b4adffc3af fix 2024-09-21 15:48:16 +02:00
Timothy J. Baek 8fc178ae91 refac: chat controls size 2024-09-21 15:46:39 +02:00
Timothy J. Baek 580ad46036 refac: speedRate -> playbackRate 2024-09-21 15:42:39 +02:00
Timothy Jaeryang Baek f47dffe6e1 Merge pull request #5576 from open-webui/dev
fix: WEBUI_AUTH=False not working issue
2024-09-21 15:35:55 +02:00
Timothy J. Baek 00f6b4bf09 refac 2024-09-21 15:35:35 +02:00
Timothy J. Baek 9126ceac08 fix: WEBUI_AUTH=False not working issue 2024-09-21 15:33:34 +02:00
Timothy Jaeryang Baek ff8a2da751 Merge pull request #5565 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.23
2024-09-21 04:55:29 +02:00
Timothy J. Baek 2f028b45fe refac 2024-09-21 04:53:44 +02:00
Timothy J. Baek d3ef9d980b refac 2024-09-21 04:36:18 +02:00
Timothy J. Baek cb81dfe4ba fix: voice recording not stopping 2024-09-21 04:27:18 +02:00
Timothy J. Baek 426de76690 refac 2024-09-21 04:24:38 +02:00
Timothy J. Baek 16a767e04e doc: changelog 2024-09-21 04:14:10 +02:00
Timothy J. Baek 2adaf9ba3d refac 2024-09-21 04:10:24 +02:00
Timothy J. Baek f381850bb2 refac 2024-09-21 04:04:34 +02:00
Timothy Jaeryang Baek 273787fe78 Merge pull request #5541 from kivvi3412/fix-title-generate
[fix] Fix title generate
2024-09-21 03:58:28 +02:00
Timothy J. Baek 70d16c3904 refac: styling 2024-09-21 03:55:17 +02:00
Timothy J. Baek ee6b1376c3 fix: rag duplicate collection issue 2024-09-21 03:53:53 +02:00
Timothy J. Baek aed8e2156f refac 2024-09-21 03:44:44 +02:00
Timothy J. Baek 5d848ad130 refac 2024-09-21 03:39:30 +02:00
Timothy J. Baek 98928f6bd7 enh: save chat controls width 2024-09-21 03:37:28 +02:00
Timothy J. Baek 692f04d457 enh: width adjustable chat controls 2024-09-21 03:33:06 +02:00
kivvi e1ea0c23eb Fixed the issue of being unable to generate titles 2024-09-21 09:06:28 +08:00
Timothy J. Baek 657d443a3e fix 2024-09-21 02:24:30 +02:00
Timothy J. Baek 95985e7bbb refac 2024-09-21 02:03:58 +02:00
Timothy J. Baek 3a0a1aca11 refac: task ollama stream support 2024-09-21 01:07:57 +02:00
Timothy J. Baek 41926172d3 fix/refac: use ollama /api/chat endpoint for tasks 2024-09-21 00:30:13 +02:00
Timothy J. Baek 585b9eb84a refac 2024-09-20 23:48:03 +02:00
Timothy J. Baek ffd7d74f77 enh: websocket redis support 2024-09-20 23:43:22 +02:00
Timothy Jaeryang Baek 578d52b89d Merge pull request #5562 from open-webui/dev
fix: chat download as plain text issue
2024-09-20 23:19:51 +02:00
Timothy J. Baek e599d5db3c fix: chat download as plain text issue 2024-09-20 23:19:20 +02:00
Timothy Jaeryang Baek 79b5430a9e Merge pull request #5546 from itaybar/bugfix/downgrade_authlib
downgrade authlib to 1.3.1
2024-09-20 17:41:19 +02:00
Timothy J. Baek 9b05fe3c54 Update pyproject.toml 2024-09-20 17:40:30 +02:00
Timothy Jaeryang Baek 5fe2795db8 Merge pull request #5556 from open-webui/dev
refac
2024-09-20 15:44:31 +02:00
Timothy J. Baek 732f730213 refac: click to focus on mesage node 2024-09-20 15:43:18 +02:00
Timothy Jaeryang Baek c89bb01db9 Merge pull request #5554 from KarlLee830/translate
i18n: Update Chinese translation
2024-09-20 15:34:58 +02:00
Karl Lee 1cbbf75807 i18n: Update Chinese translation 2024-09-20 21:06:10 +08:00
itaybar bd81fc8bff downgrade authlib to 1.3.1 2024-09-20 15:13:03 +03:00
Timothy Jaeryang Baek d4df552076 Merge pull request #5537 from open-webui/dev
refac
2024-09-20 03:18:40 +02:00
Timothy J. Baek b7ad82757d refac 2024-09-20 03:18:14 +02:00
Timothy J. Baek d12b6cda4e refac: openai image async gen 2024-09-20 03:16:08 +02:00
Timothy Jaeryang Baek 83855b713b Merge pull request #5362 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.22
2024-09-20 00:25:27 +02:00
Timothy J. Baek bd09b6dbad doc: changelog 2024-09-20 00:25:09 +02:00
Timothy J. Baek eb9ad47ef8 refac: temp tools & functions files 2024-09-20 00:12:52 +02:00
Timothy J. Baek 5b46a252ff doc: changelog 2024-09-20 00:00:45 +02:00
Timothy J. Baek 39c57c0e94 refac 2024-09-19 22:40:06 +02:00
Timothy J. Baek 619dbbe9f5 refac 2024-09-19 22:21:35 +02:00
Timothy J. Baek 9be73ea94a refac 2024-09-19 22:17:32 +02:00
Timothy J. Baek 4e43663448 refac 2024-09-19 22:12:54 +02:00
Timothy J. Baek cd117f5b67 refac 2024-09-19 21:51:45 +02:00
Timothy J. Baek 2dad9b9432 refac 2024-09-19 20:56:13 +02:00
Timothy J. Baek b0bc36f2af fix 2024-09-19 18:47:02 +02:00
Timothy J. Baek 1688f5ecaf refac 2024-09-19 18:42:01 +02:00
Timothy J. Baek cba2d31175 refac 2024-09-19 18:40:23 +02:00
Timothy J. Baek a2b77fd072 refac 2024-09-19 18:08:52 +02:00
Timothy J. Baek 9b83e57372 refac 2024-09-19 17:46:11 +02:00
Timothy J. Baek 687cae9b79 refac 2024-09-19 17:31:59 +02:00
Timothy J. Baek f8fffdd288 refac: max_tokens -> max_completion_tokens 2024-09-19 17:19:31 +02:00
Timothy J. Baek 60d6279055 chore: format 2024-09-19 17:09:05 +02:00
Timothy J. Baek 7b330d1490 refac 2024-09-19 17:05:49 +02:00
Timothy J. Baek 7078af635c refac 2024-09-19 17:02:59 +02:00
Timothy J. Baek e1b57d80a4 chore: format 2024-09-19 16:57:34 +02:00
Timothy J. Baek 8426874426 fix 2024-09-19 16:54:34 +02:00
Timothy J. Baek 70dd790afc refac: default rag params 2024-09-19 16:44:33 +02:00
Timothy J. Baek e99cba53fe enh: stream=false support 2024-09-19 16:25:59 +02:00
Timothy J. Baek 628d7ae72d refac 2024-09-19 16:20:07 +02:00
Timothy J. Baek ff737a9e25 enh: openai error message handling 2024-09-19 16:20:00 +02:00
Timothy J. Baek 5c16631ec5 refac 2024-09-19 15:35:01 +02:00
Timothy Jaeryang Baek d52eaf19d6 Merge pull request #5511 from EtiennePerot/import-file
fix: restore `__file__` variable for imported toolkits and functions
2024-09-19 15:29:34 +02:00
Timothy J. Baek 27dd6ef14e refac 2024-09-19 05:15:06 +02:00
Timothy J. Baek ed75f72358 refac 2024-09-19 03:53:07 +02:00
Timothy J. Baek 276d629a14 fix: message delete 2024-09-19 03:49:35 +02:00
Timothy J. Baek aedd77b81d refac 2024-09-19 03:27:54 +02:00
Timothy J. Baek 1053863175 chore: format 2024-09-19 03:24:39 +02:00
Timothy J. Baek f448341211 chore: format 2024-09-19 03:24:34 +02:00
Timothy Jaeryang Baek 9fc6b999d0 Merge pull request #5509 from open-webui/dev-playback-controls
feat: playback controls
2024-09-19 03:23:16 +02:00
Timothy J. Baek ff2fff857a refac: styling 2024-09-19 03:22:55 +02:00
Timothy Jaeryang Baek dd4cf102cc Merge pull request #5313 from zabirauf/u/zabirauf/speech-speed
feat: Added speech playback speed control for Call mode
2024-09-19 02:44:27 +02:00
Timothy Jaeryang Baek 5d3a89dd25 Merge pull request #5496 from pawel-ochman/azure-tts
Integrate Azure Speech service for TTS
2024-09-19 02:42:45 +02:00
Timothy J. Baek b4f1a0b5a6 refac 2024-09-19 02:42:24 +02:00
Timothy J. Baek afa42dd2e4 refac 2024-09-19 02:40:54 +02:00
Timothy J. Baek be44af4680 enh: focus on current message 2024-09-19 01:25:46 +02:00
Timothy J. Baek e6b6f42139 refac: display error content on node 2024-09-18 16:46:13 +02:00
Pawel Ochman 4d9677e808 Update configuration page, expose all Azure settings through ENV variables 2024-09-18 14:13:42 +01:00
Pawel Ochman eacb69074e remove dependency and migrate to raw rest calls 2024-09-18 12:24:55 +01:00
Etienne Perot 6477bf37fe fix: restore __file__ variable for imported toolkits and functions
Commit cf86ba7786 changed the way toolkits
and functions were imported to use `exec`, whereas they previously were
written to files and `import`ed. The use of `exec` means that
module-global variables such as `__file__` are no longer defined.

This breaks https://github.com/EtiennePerot/open-webui-code-execution
(code execution tool for Open WebUI), as the module needs to re-execute
its own code in a subprocess in order to properly sandbox itself. This
is done using `__file__` in order to know where the module's code is
located.

This PR creates a temporary in-memory file that contains the imported
toolkit or function's code and exists only during the import process.
Then it injects the path to this in-memory file as the `__file__`
variable in the `exec` context. This restores the ability for the
toolkit or function being imported to rely on `__file__`.
2024-09-18 01:08:30 -07:00
Timothy J. Baek 1743d3c6c1 fix 2024-09-18 03:47:04 +02:00
Timothy J. Baek e723b2a4c6 refac 2024-09-18 03:19:32 +02:00
Timothy J. Baek 67f704c98d enh: scroll to message from overview 2024-09-18 03:13:37 +02:00
Timothy J. Baek 8d92093570 enh: node show content as tooltip 2024-09-18 02:55:25 +02:00
Timothy J. Baek 1ccac9111b refac: only animate active edges 2024-09-18 02:42:19 +02:00
Timothy J. Baek 98984166f9 refac: styling 2024-09-18 02:11:25 +02:00
Timothy J. Baek d1bf18eeb0 fix 2024-09-18 01:57:27 +02:00
Timothy J. Baek 2018a6c000 fix: styling 2024-09-18 01:53:08 +02:00
Timothy J. Baek 56152230f8 refac 2024-09-18 01:39:22 +02:00
Timothy J. Baek 7e8cf5504a refac: styling 2024-09-18 01:35:22 +02:00
Timothy J. Baek 3c03d5069d refac: styling 2024-09-18 01:31:49 +02:00
Timothy J. Baek 705508a674 refac: allow chat menu in mobile 2024-09-18 01:30:09 +02:00
Timothy J. Baek 771482c6e6 refac 2024-09-18 01:25:28 +02:00
Timothy J. Baek d25ccfba5f refac: styling 2024-09-18 01:21:32 +02:00
Timothy J. Baek 614c219010 enh: prepend image filename with prompt 2024-09-18 01:04:20 +02:00
Timothy J. Baek 63b3076d64 refac 2024-09-18 00:52:06 +02:00
Timothy J. Baek 8e3ad45ce4 enh: model move to top 2024-09-18 00:49:27 +02:00
Timothy J. Baek 984e0c533e refac 2024-09-18 00:18:47 +02:00
Timothy J. Baek 5065291f72 feat: save as new response message 2024-09-17 23:36:48 +02:00
Timothy J. Baek 8c273ba58a refac 2024-09-17 23:23:38 +02:00
Timothy J. Baek 7a9c0946a4 refac 2024-09-17 23:13:51 +02:00
Timothy J. Baek 7e0a26ef4e refac 2024-09-17 23:07:04 +02:00
Timothy J. Baek 65b7c9898b fix 2024-09-17 23:02:41 +02:00
Timothy J. Baek 67f95ddfdc refac 2024-09-17 22:58:06 +02:00
Timothy J. Baek d1dbb9a3be feat: chat overview 2024-09-17 22:05:19 +02:00
Timothy J. Baek bb087a5989 enh: chat copy button 2024-09-17 21:32:39 +02:00
Timothy Jaeryang Baek a0d24105fc Merge pull request #5473 from aleixdorca/dev
i18n: Update Catalan Translation
2024-09-17 17:18:40 +02:00
Timothy Jaeryang Baek a5c9160b7d Merge pull request #5466 from phil-ogb/add-security-response-headers
feat: security response headers
2024-09-17 17:18:28 +02:00
Aleix Dorca 0e2cdbe9fb Minor typo 2024-09-17 16:49:05 +02:00
Aleix Dorca 3c55a46484 i18n: Update Catalan translation.json 2024-09-17 16:46:52 +02:00
Pawel Ochman d6b68f405e added azure speech service support 2024-09-17 09:13:10 +01:00
Pawel Ochman 351bbdb36c Added Azure speach service option (UI) 2024-09-17 08:47:30 +01:00
Phil Ogbonna 896baf021b update comment block 2024-09-16 22:02:55 -03:00
Phil Ogbonna 499e5e4f60 feat: security response headers 2024-09-16 21:53:30 -03:00
Timothy J. Baek 3ad003bccb fix 2024-09-16 16:42:18 +02:00
Timothy J. Baek af92184c93 chore: colbert-ai requirement 2024-09-16 16:19:40 +02:00
Timothy J. Baek 06debb322b refac: colbert cuda support 2024-09-16 12:42:48 +02:00
Timothy J. Baek b7f0759485 refac 2024-09-16 12:36:43 +02:00
Timothy J. Baek bc6f23f82f fix 2024-09-16 12:33:55 +02:00
Timothy J. Baek cb9e76c7f9 refac: default rag template 2024-09-16 12:01:04 +02:00
Timothy J. Baek b38986a0aa enh: colbert rerank support 2024-09-16 11:46:39 +02:00
Timothy J. Baek db0c576f48 refac 2024-09-16 07:14:17 +02:00
Timothy Jaeryang Baek 40d7e5089d Merge pull request #5436 from khanh-alice/fix-transparent-icons
fix: model transparent icons
2024-09-16 07:05:17 +02:00
Timothy Jaeryang Baek 9b8d42c670 Merge pull request #5440 from cheahjs/feat/capabilities-help
feat: add help text for capabilities
2024-09-16 05:17:32 +02:00
Jun Siang Cheah bed3b71860 feat: add help text for capabilities 2024-09-15 14:14:09 +01:00
Khanh Le 253791b92c save model icon as png 2024-09-15 16:43:42 +07:00
Timothy J. Baek 5dd6ae6ec4 chore: npm 2024-09-14 23:30:56 +01:00
Timothy J. Baek 902f30c123 enh: table view allow overflow 2024-09-14 23:23:52 +01:00
Timothy J. Baek fa8d7bd9c6 enh: allow new lines in default prompts 2024-09-14 23:17:58 +01:00
Timothy J. Baek d0df2cbe53 fix: disable "enable new sign ups" not working issue 2024-09-14 23:13:52 +01:00
Timothy Jaeryang Baek 27baa00afb Merge pull request #5419 from cheahjs/fix/remove-doc-support
fix: remove unsupported .doc file ingest
2024-09-14 22:52:40 +01:00
Timothy J. Baek 869063c743 refac: better reranking model error handling 2024-09-14 20:58:42 +01:00
Timothy Jaeryang Baek 2f9f568dd9 Merge pull request #5400 from thiswillbeyourgithub/fix_fallback_cuda
fix: if cuda is not available fallback to cpu
2024-09-14 20:57:14 +01:00
Timothy Jaeryang Baek 937ce5f797 Merge pull request #5411 from KarlLee830/translate
i18n: Update Chinese translation
2024-09-14 20:56:13 +01:00
Timothy Jaeryang Baek 1b612209a3 Merge pull request #5429 from OriginalSimon/dev
i18n: Update Ukrainian translation
2024-09-14 20:56:03 +01:00
Simon d00d3341af Update translation.json 2024-09-14 20:32:04 +02:00
Jun Siang Cheah 3199e26500 fix: remove unsupported .doc file ingest 2024-09-14 12:44:51 +01:00
Karl Lee bc06e7a282 i18n: Update Chinese translation 2024-09-14 10:10:15 +08:00
Timothy Jaeryang Baek 1eec9e2b59 Merge pull request #5407 from Peter-De-Ath/citations-page-no
feat: add page no to citations
2024-09-13 21:55:45 +01:00
Peter De-Ath d46f652f7e enh: show page number in citation if known 2024-09-13 21:05:28 +01:00
Timothy Jaeryang Baek d75d638b9a Merge pull request #5406 from open-webui/main
refac
2024-09-13 19:26:51 +01:00
Timothy J. Baek b64c9d966a refac 2024-09-13 14:26:32 -04:00
Timothy Jaeryang Baek 8a928e5356 Merge pull request #5405 from open-webui/main
refac: filter non chat completions models
2024-09-13 19:23:02 +01:00
Timothy J. Baek adf958559b refac: filter non chat completions models 2024-09-13 14:20:27 -04:00
thiswillbeyourgithub 82b35492af pep8 2024-09-13 17:18:44 +02:00
thiswillbeyourgithub a7ea07036e fix: if cuda is not available fallback to cpu 2024-09-13 17:12:00 +02:00
Timothy Jaeryang Baek c1d3481c41 Merge pull request #5399 from open-webui/main
fix: allow o1
2024-09-13 15:01:48 +01:00
Timothy J. Baek bc0baa35e6 fix: allow o1 2024-09-13 09:59:41 -04:00
Timothy J. Baek f549cb1f87 fix 2024-09-13 01:30:30 -04:00
Timothy J. Baek 823093eea6 fix: hybrid search 2024-09-13 01:21:47 -04:00
Timothy J. Baek 939bfd153e refac 2024-09-13 01:18:20 -04:00
Timothy J. Baek b943b7d337 refac 2024-09-13 01:08:02 -04:00
Timothy J. Baek a1f3ece528 refac 2024-09-13 01:07:03 -04:00
Timothy J. Baek 8a4b3e6bc9 refac: allow multiple [context] in prompt 2024-09-13 01:06:51 -04:00
Timothy J. Baek a9c497612b refac: rm assert 2024-09-13 00:56:50 -04:00
Timothy J. Baek 7df997c992 chore: format 2024-09-13 00:49:23 -04:00
Timothy J. Baek bebc0d2073 chore: format 2024-09-13 00:48:54 -04:00
Timothy Jaeryang Baek 857fc3ce99 Merge pull request #5276 from sebdanielsson/add-theme-color
fix: Set theme colors to fix white top bar for PWA
2024-09-13 05:43:25 +01:00
Timothy J. Baek 47e9c12fc2 refac 2024-09-13 00:41:20 -04:00
Timothy Jaeryang Baek 5970dadead Merge pull request #5256 from Bazsalanszky/main
feat: More options for AUTOMATIC1111
2024-09-13 05:40:09 +01:00
Timothy Jaeryang Baek 7dc4cb30b2 Merge pull request #5378 from thiswillbeyourgithub/fix_RAG_and_web
fix: RAG and Web Search + RAG enhancements
2024-09-13 05:38:53 +01:00
Timothy Jaeryang Baek fb122e67d0 Merge pull request #5382 from Peter-De-Ath/hotfix/chroma-metadata-s
fix: change metadata to metadatas
2024-09-12 22:30:08 +01:00
Peter De-Ath ca9874757f fix: change metadata to metadatas 2024-09-12 20:14:14 +01:00
thiswillbeyourgithub 65d5545cf0 added a few type hints 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub e872f5dc78 log: added a debug log if detecting a potential prompt injection attack 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub b4ad64586a fix: add check that the context for RAG is not empty if the threshold is 0 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub 9661fee554 fix: handle case where [query] happens in the RAG context 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub adf26789b8 logs: crash if rag_template would be wrong 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub 209e246e6f fix: much improved RAG template 2024-09-12 17:22:22 +02:00
thiswillbeyourgithub ed2a1e7db9 enh: use non hybrid search as fallback if hybrid search failed 2024-09-12 17:22:22 +02:00
Timothy J. Baek 143ac08c35 refac 2024-09-12 10:56:16 -04:00
thiswillbeyourgithub 53f03f6556 fix: log exception when issues of collection querying 2024-09-12 16:51:37 +02:00
Timothy J. Baek 62f1933e3c refac: audio input (audio/ogg support) 2024-09-12 09:18:20 -04:00
Timothy J. Baek 0b30dc357c refac: remove prints 2024-09-12 09:13:21 -04:00
Timothy J. Baek d5b595a842 fix: ol numbering 2024-09-12 09:11:45 -04:00
Balazs Toldi d05ba042c0 Make the optional AUTOMATIC1111 values nullable
This commit makes the optional AUTOMATIC1111 options default to None, and if the value is removed, it resets to None.

Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2024-09-12 14:00:24 +02:00
Timothy J. Baek eed2d735a1 refac 2024-09-12 02:06:02 -04:00
Timothy Jaeryang Baek 675403d26d Merge pull request #5364 from open-webui/multiple-vector-dbs
refac: vector db clients
2024-09-12 07:00:55 +01:00
Timothy J. Baek 8be6e16513 refac: vector db clients 2024-09-12 02:00:31 -04:00
Timothy Jaeryang Baek c7fc17da69 Merge pull request #5312 from open-webui/multiple-vector-dbs
feat: various vector db support
2024-09-12 06:53:47 +01:00
Timothy J. Baek 4775fe43d8 feat: milvus support 2024-09-12 01:52:19 -04:00
Sebastian 2a04dd0f9b Set theme colors to fix white top bar for PWA 2024-09-11 01:46:39 +02:00
Timothy Jaeryang Baek 61ee4bd629 Merge pull request #5319 from Liuzhch1/fix-openai_timeout
fix: get OpenAI models only if OpenAI API Enabled; timeout for querying OpenAI models
2024-09-10 23:03:20 +01:00
Timothy Jaeryang Baek dbd661a417 Merge pull request #5318 from moblangeois/french-translation
i18n: Update French (fr-FR) translation
2024-09-10 23:02:34 +01:00
Liuzhch1 26700ac4ac fix: get OpenAI models only if OpenAI Enabled;timeout for query OpenAI models 2024-09-10 19:22:05 +08:00
Morgan Blangeois b0defe5524 i18n: Update French (fr-FR) translation 2024-09-10 12:54:47 +02:00
Zohaib Rauf 91cd8c7608 Undid a unnecessary change 2024-09-09 21:59:13 -07:00
Zohaib Rauf 62b01c5f8e Cleaned up 2024-09-09 21:57:53 -07:00
Zohaib Rauf b33ab6c5fd Updated to use Dropdown menu which is being used elsewhere in app 2024-09-09 21:33:48 -07:00
Timothy J. Baek 0886b3a0a4 refac: comments 2024-09-10 04:46:40 +01:00
Timothy J. Baek 522afbb0a0 refac 2024-09-10 04:37:06 +01:00
Timothy Jaeryang Baek d5f13dd9e0 Merge pull request #5311 from open-webui/dev
dev
2024-09-10 02:30:02 +01:00
Timothy Jaeryang Baek 3d6d8c91dd Merge branch 'multiple-vector-dbs' into dev 2024-09-10 02:29:55 +01:00
Timothy J. Baek 4354f270ce refac 2024-09-10 02:27:50 +01:00
Timothy J. Baek 28087ccf40 refac 2024-09-10 01:37:36 +01:00
Timothy J. Baek eb0e683b47 refac 2024-09-10 01:34:27 +01:00
Timothy Jaeryang Baek 1023ff8454 Merge pull request #5270 from cheahjs/fix/websocket-take-2
fix: socket.io connections failing when websockets are not available
2024-09-09 23:19:05 +01:00
Jun Siang Cheah 9401f6c821 fix: workaround socketio upstream bug when websockets are not available 2024-09-09 23:17:34 +01:00
Jun Siang Cheah 827c419251 feat: add ENABLE_WEBSOCKET_SUPPORT to force socket.io to ignore websocket upgrades 2024-09-09 23:17:17 +01:00
Timothy Jaeryang Baek 82db64a700 Merge pull request #5279 from afritzler/enh/kustomization
feat: Improve `kustomization` usage
2024-09-09 23:04:29 +01:00
Timothy J. Baek f1fae805a2 fix: separate /embed and /embedding ollama endpoint 2024-09-09 23:02:26 +01:00
Timothy Jaeryang Baek 272e2386dd Merge pull request #5300 from FINNSEEFLY/task/adjust-ru-localization
i18n: Update ru-RU localization
2024-09-09 22:59:44 +01:00
FINNSEEFLY 67e1c0a10b Adjust ru-RU localization 2024-09-09 22:49:55 +03:00
Zohaib Rauf f88a86f9b0 Fixed build error 2024-09-08 22:57:33 -07:00
Timothy J. Baek 601982f52b fix: lengthy chat title delete ui issue 2024-09-09 04:52:12 +01:00
Timothy Jaeryang Baek fc839011f6 Merge pull request #5275 from sebdanielsson/safe-area
fix: Add padding to compensate for iPhone nav bar
2024-09-09 04:42:45 +01:00
Timothy Jaeryang Baek b407f24950 Merge pull request #5277 from msurma/dev
fix: incorrect casting of top_p and frequency_penalty
2024-09-09 04:36:22 +01:00
Timothy Jaeryang Baek d15f57cfff Merge pull request #5266 from KarlLee830/translate
i18n: Update Chinese translation
2024-09-09 04:34:47 +01:00
Andreas Fritzler 82fbfd69a5 Improve kustomization usage
Ensure that the `manifest/base` folder can be used as a standalone
kustomization resource.

Add a new subfolder `manifest/gpu` which uses `manifest/base` with
additional GPU related patches.
2024-09-08 20:43:00 +02:00
Michał Surmaczewski 8e6ea49e0e fix: incorrect casting of top_p and frequency_penalty 2024-09-08 17:52:58 +02:00
Sebastian 83a3e53d8d Add padding to compensate for iPhone nav bar 2024-09-08 17:52:10 +02:00
Jun Siang Cheah 698976add0 feat: add ENABLE_WEBSOCKET_SUPPORT to force socket.io to ignore websocket upgrades 2024-09-08 12:00:36 +01:00
Karl Lee cc1d3c48e0 i18n: Update Chinese translation 2024-09-08 16:51:14 +08:00
Timothy Jaeryang Baek e510c8b11f Merge pull request #5263 from open-webui/dev
refac
2024-09-08 01:52:27 +01:00
Timothy J. Baek 666086a806 refac 2024-09-08 01:17:02 +01:00
Timothy J. Baek 9bdbe88bda refac: default search query prompt 2024-09-08 01:04:57 +01:00
Timothy Jaeryang Baek 50db51ebe0 Merge pull request #5262 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.21
2024-09-08 00:59:40 +01:00
Timothy J. Baek 0beaab51ae doc: changelog 2024-09-08 00:57:59 +01:00
Timothy Jaeryang Baek 0e30b0f9b4 Merge pull request #5010 from jannikstdl/show-total-docs
feat: show total number of documents
2024-09-08 00:52:45 +01:00
Timothy J. Baek 214722d39e enh: search query generation prompt 2024-09-08 00:51:12 +01:00
Balazs Toldi 7f6dae41f0 More options for AUTOMATIC1111
This commit adds 3 new options to the AUTOMATIC1111 settings:
- CFG Scale
- Sampler
- Scheduler

These options allow users to configure these parameters directly through the admin settings, without needing to modify the source code, which was previously required to change the default values in  AUTOMATIC1111.

Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2024-09-07 17:21:17 +02:00
Jannik S. 9aa8eff44e styling 2024-09-07 15:08:25 +02:00
Timothy Jaeryang Baek 2544f7eaf0 Merge pull request #5249 from open-webui/dev
refac: default search generation prompt
2024-09-07 06:42:12 +02:00
Timothy J. Baek 1a6ce1d5d9 refac: default search generation prompt 2024-09-07 05:41:55 +01:00
Timothy Jaeryang Baek 98eaec22d4 Merge pull request #5248 from open-webui/dev
fix
2024-09-07 06:20:35 +02:00
Timothy J. Baek 2e40719f4e fix 2024-09-07 05:18:52 +01:00
Timothy Jaeryang Baek 71a2bd2fea Merge pull request #5247 from open-webui/dev
refac: enable /api/embed
2024-09-07 06:13:02 +02:00
Timothy J. Baek 1c20db775c refac: enable /api/embed 2024-09-07 05:12:46 +01:00
Timothy Jaeryang Baek e2ef36b582 Merge pull request #5238 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.20
2024-09-07 06:08:23 +02:00
Timothy J. Baek 90a064972c fix 2024-09-07 05:07:37 +01:00
Timothy J. Baek f4a0b845be doc: wording 2024-09-07 05:06:32 +01:00
Timothy J. Baek 1f22aa99d8 doc: changelog 2024-09-07 05:05:54 +01:00
Timothy J. Baek 4c9ea084d5 chore: format 2024-09-07 05:03:26 +01:00
Timothy J. Baek a3094bcd1b fix: all response messages re-rendering on new message 2024-09-07 04:54:48 +01:00
Timothy J. Baek 5c8fb4b3d5 refac: web search 2024-09-07 04:50:29 +01:00
Timothy J. Baek ff46fe2b4a refac 2024-09-07 03:09:57 +01:00
Timothy J. Baek 8d6a424604 fix: markdown proper spacing 2024-09-07 02:56:58 +01:00
Timothy J. Baek 8dfbdbd883 fix: mic kept alive after call issue 2024-09-07 01:28:07 +01:00
Timothy J. Baek f533c9750b fix: close chat controls after call ends 2024-09-07 01:12:43 +01:00
Timothy J. Baek 2f841f9f5a fix: enable inline link image rendering 2024-09-06 23:40:35 +02:00
Timothy J. Baek 02d5bca44d fix: tools & function not installing requirements 2024-09-06 18:35:43 +02:00
Timothy J. Baek 14eda1bf5b fix: pdf download FONTS_DIR issue 2024-09-06 15:52:23 +02:00
Timothy Jaeryang Baek 2d4cdc5be9 Merge pull request #5209 from aleixdorca/dev
i18n: Update Catalan translation.json
2024-09-06 14:52:55 +02:00
Aleix Dorca d038e831dc i18n: Update Catalan translation.json 2024-09-06 07:20:21 +02:00
Timothy J. Baek 9fe62fc80d refac 2024-09-06 05:30:16 +02:00
Timothy J. Baek bfb12a7851 refac 2024-09-06 04:59:20 +02:00
Timothy Jaeryang Baek 4617f3a4e2 Merge pull request #5200 from open-webui/dev
fix
2024-09-05 21:01:12 +02:00
Timothy J. Baek 97e331632c fix: call overlay issue 2024-09-05 21:00:53 +02:00
Timothy J. Baek 4cf777ab5a refac: styling 2024-09-05 20:52:27 +02:00
Jannik Streidl 0ead3ed37a feat: show total number of documents 2024-08-29 16:30:34 +02:00
141 changed files with 6256 additions and 2351 deletions
+114
View File
@@ -5,6 +5,120 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.24] - 2024-09-24
### Added
- **🚀 Rendering Optimization**: Significantly improved message rendering performance, enhancing user experience and webui responsiveness.
- **💖 Favorite Response Feature in Chat Overview**: Users can now mark responses as favorite directly from the chat overview, enhancing ease of retrieval and organization of preferred responses.
- **💬 Create Message Pairs with Shortcut**: Implemented creation of new message pairs using Cmd/Ctrl+Shift+Enter, making conversation editing faster and more intuitive.
- **🌍 Expanded User Prompt Variables**: Added weekday, timezone, and language information variables to user prompts to match system prompt variables.
- **🎵 Enhanced Audio Support**: Now includes support for 'audio/x-m4a' files, broadening compatibility with audio content within the platform.
- **🔏 Model URL Search Parameter**: Added an ability to select a model directly via URL parameters, streamlining navigation and model access.
- **📄 Enhanced PDF Citations**: PDF citations now open at the associated page, streamlining reference checks and document handling.
- **🔧Use of Redis in Sockets**: Enhanced socket implementation to fully support Redis, enabling effective stateless instances suitable for scalable load balancing.
- **🌍 Stream Individual Model Responses**: Allows specific models to have individualized streaming settings, enhancing performance and customization.
- **🕒 Display Model Hash and Last Modified Timestamp for Ollama Models**: Provides critical model details directly in the Models workspace for enhanced tracking.
- **❗ Update Info Notification for Admins**: Ensures administrators receive immediate updates upon login, keeping them informed of the latest changes and system statuses.
### Fixed
- **🗑️ Temporary File Handling On Windows**: Fixed an issue causing errors when accessing a temporary file being used by another process, Tools & Functions should now work as intended.
- **🔓 Authentication Toggle Issue**: Resolved the malfunction where setting 'WEBUI_AUTH=False' did not appropriately disable authentication, ensuring that user experience and system security settings function as configured.
- **🔧 Save As Copy Issue for Many Model Chats**: Resolved an error preventing users from save messages as copies in many model chats.
- **🔒 Sidebar Closure on Mobile**: Resolved an issue where the mobile sidebar remained open after menu engagement, improving user interface responsivity and comfort.
- **🛡️ Tooltip XSS Vulnerability**: Resolved a cross-site scripting (XSS) issue within tooltips, ensuring enhanced security and data integrity during user interactions.
### Changed
- **↩️ Deprecated Interface Stream Response Settings**: Moved to advanced parameters to streamline interface settings and enhance user clarity.
- **⚙️ Renamed 'speedRate' to 'playbackRate'**: Standardizes terminology, improving usability and understanding in media settings.
## [0.3.23] - 2024-09-21
### Added
- **🚀 WebSocket Redis Support**: Enhanced load balancing capabilities for multiple instance setups, promoting better performance and reliability in WebUI.
- **🔧 Adjustable Chat Controls**: Introduced width-adjustable chat controls, enabling a personalized and more comfortable user interface.
- **🌎 i18n Updates**: Improved and updated the Chinese translations.
### Fixed
- **🌐 Task Model Unloading Issue**: Modified task handling to use the Ollama /api/chat endpoint instead of OpenAI compatible endpoint, ensuring models stay loaded and ready with custom parameters, thus minimizing delays in task execution.
- **📝 Title Generation Fix for OpenAI Compatible APIs**: Resolved an issue preventing the generation of titles, enhancing consistency and reliability when using multiple API providers.
- **🗃️ RAG Duplicate Collection Issue**: Fixed a bug causing repeated processing of the same uploaded file. Now utilizes indexed files to prevent unnecessary duplications, optimizing resource usage.
- **🖼️ Image Generation Enhancement**: Refactored OpenAI image generation endpoint to be asynchronous, preventing the WebUI from becoming unresponsive during processing, thus enhancing user experience.
- **🔓 Downgrade Authlib**: Reverted Authlib to version 1.3.1 to address and resolve issues concerning OAuth functionality.
### Changed
- **🔍 Improved Message Interaction**: Enhanced the message node interface to allow for easier focus redirection with a simple click, streamlining user interaction.
- **✨ Styling Refactor**: Updated WebUI styling for a cleaner, more modern look, enhancing user experience across the platform.
## [0.3.22] - 2024-09-19
### Added
- **⭐ Chat Overview**: Introducing a node-based interactive messages diagram for improved visualization of conversation flows.
- **🔗 Multiple Vector DB Support**: Now supports multiple vector databases, including the newly added Milvus support. Community contributions for additional database support are highly encouraged!
- **📡 Experimental Non-Stream Chat Completion**: Experimental feature allowing the use of OpenAI o1 models, which do not support streaming, ensuring more versatile model deployment.
- **🔍 Experimental Colbert-AI Reranker Integration**: Added support for "jinaai/jina-colbert-v2" as a reranker, enhancing search relevance and accuracy. Note: it may not function at all on low-spec computers.
- **🕸️ ENABLE_WEBSOCKET_SUPPORT**: Added environment variable for instances to ignore websocket upgrades, stabilizing connections on platforms with websocket issues.
- **🔊 Azure Speech Service Integration**: Added support for Azure Speech services for Text-to-Speech (TTS).
- **🎚️ Customizable Playback Speed**: Playback speed control is now available in Call mode settings, allowing users to adjust audio playback speed to their preferences.
- **🧠 Enhanced Error Messaging**: System now displays helpful error messages directly to users during chat completion issues.
- **📂 Save Model as Transparent PNG**: Model profile images are now saved as PNGs, supporting transparency and improving visual integration.
- **📱 iPhone Compatibility Adjustments**: Added padding to accommodate the iPhone navigation bar, improving UI display on these devices.
- **🔗 Secure Response Headers**: Implemented security response headers, bolstering web application security.
- **🔧 Enhanced AUTOMATIC1111 Settings**: Users can now configure 'CFG Scale', 'Sampler', and 'Scheduler' parameters directly in the admin settings, enhancing workflow flexibility without source code modifications.
- **🌍 i18n Updates**: Enhanced translations for Chinese, Ukrainian, Russian, and French, fostering a better localized experience.
### Fixed
- **🛠️ Chat Message Deletion**: Resolved issues with chat message deletion, ensuring a smoother user interaction and system stability.
- **🔢 Ordered List Numbering**: Fixed the incorrect ordering in lists.
### Changed
- **🎨 Transparent Icon Handling**: Allowed model icons to be displayed on transparent backgrounds, improving UI aesthetics.
- **📝 Improved RAG Template**: Enhanced Retrieval-Augmented Generation template, optimizing context handling and error checking for more precise operation.
## [0.3.21] - 2024-09-08
### Added
- **📊 Document Count Display**: Now displays the total number of documents directly within the dashboard.
- **🚀 Ollama Embed API Endpoint**: Enabled /api/embed endpoint proxy support.
### Fixed
- **🐳 Docker Launch Issue**: Resolved the problem preventing Open-WebUI from launching correctly when using Docker.
### Changed
- **🔍 Enhanced Search Prompts**: Improved the search query generation prompts for better accuracy and user interaction, enhancing the overall search experience.
## [0.3.20] - 2024-09-07
### Added
- **🌐 Translation Update**: Updated Catalan translations to improve user experience for Catalan speakers.
### Fixed
- **📄 PDF Download**: Resolved a configuration issue with fonts directory, ensuring PDFs are now downloaded with the correct formatting.
- **🛠️ Installation of Tools & Functions Requirements**: Fixed a bug where necessary requirements for tools and functions were not properly installing.
- **🔗 Inline Image Link Rendering**: Enabled rendering of images directly from links in chat.
- **📞 Post-Call User Interface Cleanup**: Adjusted UI behavior to automatically close chat controls after a voice call ends, reducing screen clutter.
- **🎙️ Microphone Deactivation Post-Call**: Addressed an issue where the microphone remained active after calls.
- **✍️ Markdown Spacing Correction**: Corrected spacing in Markdown rendering, ensuring text appears neatly and as expected.
- **🔄 Message Re-rendering**: Fixed an issue causing all response messages to re-render with each new message, now improving chat performance.
### Changed
- **🌐 Refined Web Search Integration**: Deprecated the Search Query Generation Prompt threshold; introduced a toggle button for "Enable Web Search Query Generation" allowing users to opt-in to using web search more judiciously.
- **📝 Default Prompt Templates Update**: Emptied environment variable templates for search and title generation now default to the Open WebUI default prompt templates, simplifying configuration efforts.
## [0.3.19] - 2024-09-05
### Added
+7 -2
View File
@@ -74,6 +74,10 @@ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
## Hugging Face download cache ##
ENV HF_HOME="/app/backend/data/cache/embedding/models"
## Torch Extensions ##
# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
#### Other models ##########################################################
WORKDIR /app/backend
@@ -96,7 +100,7 @@ RUN chown -R $UID:$GID /app $HOME
RUN if [ "$USE_OLLAMA" = "true" ]; then \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
apt-get install -y --no-install-recommends gcc python3-dev && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
@@ -109,7 +113,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
else \
apt-get update && \
# Install pandoc, netcat and gcc
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
apt-get install -y --no-install-recommends gcc python3-dev && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
@@ -157,5 +161,6 @@ USER $UID:$GID
ARG BUILD_HASH
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
ENV DOCKER true
CMD [ "bash", "start.sh"]
+1 -5
View File
@@ -8,9 +8,5 @@ _test
Pipfile
!/data
/data/*
!/data/litellm
/data/litellm/*
!data/litellm/config.yaml
!data/config.json
/open_webui/data/*
.webui_secret_key
+14
View File
@@ -16,6 +16,7 @@ def serve(
host: str = "0.0.0.0",
port: int = 8080,
):
os.environ["FROM_INIT_PY"] = "true"
if os.getenv("WEBUI_SECRET_KEY") is None:
typer.echo(
"Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
@@ -38,6 +39,19 @@ def serve(
"/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
]
)
try:
import torch
assert torch.cuda.is_available(), "CUDA not available"
typer.echo("CUDA seems to be working")
except Exception as e:
typer.echo(
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
"Resetting USE_CUDA_DOCKER to false and removing "
f"LD_LIBRARY_PATH modifications: {e}"
)
os.environ["USE_CUDA_DOCKER"] = "false"
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
import open_webui.main # we need set environment variables before importing main
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
+70 -4
View File
@@ -19,16 +19,18 @@ from open_webui.config import (
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_TTS_SPLIT_ON,
AUDIO_TTS_VOICE,
AUDIO_TTS_AZURE_SPEECH_REGION,
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
CACHE_DIR,
CORS_ALLOW_ORIGIN,
DEVICE_TYPE,
WHISPER_MODEL,
WHISPER_MODEL_AUTO_UPDATE,
WHISPER_MODEL_DIR,
AppConfig,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
@@ -62,6 +64,9 @@ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
log.info(f"whisper_device_type: {whisper_device_type}")
@@ -78,6 +83,8 @@ class TTSConfigForm(BaseModel):
MODEL: str
VOICE: str
SPLIT_ON: str
AZURE_SPEECH_REGION: str
AZURE_SPEECH_OUTPUT_FORMAT: str
class STTConfigForm(BaseModel):
@@ -130,6 +137,8 @@ async def get_audio_config(user=Depends(get_admin_user)):
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
@@ -151,6 +160,10 @@ async def update_audio_config(
app.state.config.TTS_MODEL = form_data.tts.MODEL
app.state.config.TTS_VOICE = form_data.tts.VOICE
app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
)
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
@@ -166,6 +179,8 @@ async def update_audio_config(
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
@@ -301,6 +316,42 @@ async def speech(request: Request, user=Depends(get_verified_user)):
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "azure":
payload = None
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
region = app.state.config.TTS_AZURE_SPEECH_REGION
language = app.state.config.TTS_VOICE
locale = "-".join(app.state.config.TTS_VOICE.split("-")[:1])
output_format = app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
headers = {
"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": output_format,
}
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
<voice name="{language}">{payload["input"]}</voice>
</speak>"""
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
with open(file_path, "wb") as f:
f.write(response.content)
return FileResponse(file_path)
else:
log.error(f"Error synthesizing speech - {response.reason}")
raise HTTPException(
status_code=500, detail=f"Error synthesizing speech - {response.reason}"
)
@app.post("/transcriptions")
def transcribe(
@@ -309,7 +360,7 @@ def transcribe(
):
log.info(f"file.content_type: {file.content_type}")
if file.content_type not in ["audio/mpeg", "audio/wav"]:
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
@@ -443,7 +494,7 @@ def get_available_models() -> list[dict]:
try:
response = requests.get(
"https://api.elevenlabs.io/v1/models", headers=headers
"https://api.elevenlabs.io/v1/models", headers=headers, timeout=5
)
response.raise_for_status()
models = response.json()
@@ -478,6 +529,21 @@ def get_available_voices() -> dict:
except Exception:
# Avoided @lru_cache with exception
pass
elif app.state.config.TTS_ENGINE == "azure":
try:
region = app.state.config.TTS_AZURE_SPEECH_REGION
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
headers = {"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY}
response = requests.get(url, headers=headers)
response.raise_for_status()
voices = response.json()
for voice in voices:
ret[voice["ShortName"]] = (
f"{voice['DisplayName']} ({voice['ShortName']})"
)
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return ret
+43 -1
View File
@@ -17,6 +17,9 @@ from open_webui.apps.images.utils.comfyui import (
from open_webui.config import (
AUTOMATIC1111_API_AUTH,
AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_CFG_SCALE,
AUTOMATIC1111_SAMPLER,
AUTOMATIC1111_SCHEDULER,
CACHE_DIR,
COMFYUI_BASE_URL,
COMFYUI_WORKFLOW,
@@ -65,6 +68,9 @@ app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
@@ -85,6 +91,9 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
@@ -102,6 +111,9 @@ class OpenAIConfigForm(BaseModel):
class Automatic1111ConfigForm(BaseModel):
AUTOMATIC1111_BASE_URL: str
AUTOMATIC1111_API_AUTH: str
AUTOMATIC1111_CFG_SCALE: Optional[str]
AUTOMATIC1111_SAMPLER: Optional[str]
AUTOMATIC1111_SCHEDULER: Optional[str]
class ComfyUIConfigForm(BaseModel):
@@ -133,6 +145,22 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
form_data.automatic1111.AUTOMATIC1111_API_AUTH
)
app.state.config.AUTOMATIC1111_CFG_SCALE = (
float(form_data.automatic1111.AUTOMATIC1111_CFG_SCALE)
if form_data.automatic1111.AUTOMATIC1111_CFG_SCALE
else None
)
app.state.config.AUTOMATIC1111_SAMPLER = (
form_data.automatic1111.AUTOMATIC1111_SAMPLER
if form_data.automatic1111.AUTOMATIC1111_SAMPLER
else None
)
app.state.config.AUTOMATIC1111_SCHEDULER = (
form_data.automatic1111.AUTOMATIC1111_SCHEDULER
if form_data.automatic1111.AUTOMATIC1111_SCHEDULER
else None
)
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL.strip("/")
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
@@ -147,6 +175,9 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
@@ -439,7 +470,9 @@ async def image_generations(
"response_format": "b64_json",
}
r = requests.post(
# Use asyncio.to_thread for the requests.post call
r = await asyncio.to_thread(
requests.post,
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
json=data,
headers=headers,
@@ -524,6 +557,15 @@ async def image_generations(
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
if app.state.config.AUTOMATIC1111_CFG_SCALE:
data["cfg_scale"] = app.state.config.AUTOMATIC1111_CFG_SCALE
if app.state.config.AUTOMATIC1111_SAMPLER:
data["sampler_name"] = app.state.config.AUTOMATIC1111_SAMPLER
if app.state.config.AUTOMATIC1111_SCHEDULER:
data["scheduler"] = app.state.config.AUTOMATIC1111_SCHEDULER
# Use asyncio.to_thread for the requests.post call
r = await asyncio.to_thread(
requests.post,
+60 -2
View File
@@ -28,11 +28,15 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict
from starlette.background import BackgroundTask
from open_webui.utils.misc import (
calculate_sha256,
)
from open_webui.utils.payload import (
apply_model_params_to_body_ollama,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
calculate_sha256,
)
from open_webui.utils.utils import get_admin_user, get_verified_user
@@ -539,6 +543,57 @@ class GenerateEmbeddingsForm(BaseModel):
keep_alive: Optional[Union[int, str]] = None
@app.post("/api/embed")
@app.post("/api/embed/{url_idx}")
async def generate_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx is None:
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/embed",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r.raise_for_status()
return r.json()
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
status_code=r.status_code if r else 500,
detail=error_detail,
)
@app.post("/api/embeddings")
@app.post("/api/embeddings/{url_idx}")
async def generate_embeddings(
@@ -761,7 +816,10 @@ async def generate_chat_completion(
log.debug(payload)
return await post_streaming_url(
f"{url}/api/chat", json.dumps(payload), content_type="application/x-ndjson"
f"{url}/api/chat",
json.dumps(payload),
stream=form_data.stream,
content_type="application/x-ndjson",
)
+60 -20
View File
@@ -26,10 +26,13 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
from starlette.background import BackgroundTask
from open_webui.utils.misc import (
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from open_webui.utils.utils import get_admin_user, get_verified_user
log = logging.getLogger(__name__)
@@ -217,7 +220,17 @@ def merge_models_lists(model_lists):
for model in models
if "api.openai.com"
not in app.state.config.OPENAI_API_BASE_URLS[idx]
or "gpt" in model["id"]
or not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
)
@@ -320,10 +333,24 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
r.raise_for_status()
response_data = r.json()
if "api.openai.com" in url:
response_data["data"] = list(
filter(lambda model: "gpt" in model["id"], response_data["data"])
)
# Filter the response data
response_data["data"] = [
model
for model in response_data["data"]
if not any(
name in model["id"]
for name in [
"babbage",
"dall-e",
"davinci",
"embedding",
"tts",
"whisper",
]
)
]
return response_data
except Exception as e:
@@ -378,14 +405,24 @@ async def generate_chat_completion(
"role": user.role,
}
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
# Change max_completion_tokens to max_tokens (Backward compatible)
if "api.openai.com" not in url and not payload["model"].lower().startswith("o1-"):
if "max_completion_tokens" in payload:
# Remove "max_completion_tokens" from the payload
payload["max_tokens"] = payload["max_completion_tokens"]
del payload["max_completion_tokens"]
else:
if "max_tokens" in payload and "max_completion_tokens" in payload:
del payload["max_tokens"]
# Convert the modified body back to JSON
payload = json.dumps(payload)
log.debug(payload)
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
@@ -396,6 +433,7 @@ async def generate_chat_completion(
r = None
session = None
streaming = False
response = None
try:
session = aiohttp.ClientSession(
@@ -408,8 +446,6 @@ async def generate_chat_completion(
headers=headers,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
@@ -422,19 +458,23 @@ async def generate_chat_completion(
),
)
else:
response_data = await r.json()
return response_data
try:
response = await r.json()
except Exception as e:
log.error(e)
response = await r.text()
r.raise_for_status()
return response
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = await r.json()
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except Exception:
error_detail = f"External: {e}"
if isinstance(response, dict):
if "error" in response:
error_detail = f"{response['error']['message'] if 'message' in response['error'] else response['error']}"
elif isinstance(response, str):
error_detail = response
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
if not streaming and session:
+156 -61
View File
@@ -10,13 +10,21 @@ from datetime import datetime
from pathlib import Path
from typing import Iterator, Optional, Sequence, Union
import numpy as np
import torch
import requests
import validators
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from open_webui.apps.rag.search.main import SearchResult
from open_webui.apps.rag.search.brave import search_brave
from open_webui.apps.rag.search.duckduckgo import search_duckduckgo
from open_webui.apps.rag.search.google_pse import search_google_pse
from open_webui.apps.rag.search.jina_search import search_jina
from open_webui.apps.rag.search.main import SearchResult
from open_webui.apps.rag.search.searchapi import search_searchapi
from open_webui.apps.rag.search.searxng import search_searxng
from open_webui.apps.rag.search.serper import search_serper
@@ -33,15 +41,12 @@ from open_webui.apps.rag.utils import (
)
from open_webui.apps.webui.models.documents import DocumentForm, Documents
from open_webui.apps.webui.models.files import Files
from chromadb.utils.batch_utils import create_batches
from open_webui.config import (
BRAVE_SEARCH_API_KEY,
CHROMA_CLIENT,
CHUNK_OVERLAP,
CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE,
CORS_ALLOW_ORIGIN,
DEVICE_TYPE,
DOCS_DIR,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
@@ -64,6 +69,7 @@ from open_webui.config import (
RAG_RERANKING_MODEL,
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
DEFAULT_RAG_TEMPLATE,
RAG_TEMPLATE,
RAG_TOP_K,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
@@ -84,9 +90,16 @@ from open_webui.config import (
AppConfig,
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
from fastapi.middleware.cors import CORSMiddleware
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE, DOCKER
from open_webui.utils.misc import (
calculate_sha256,
calculate_sha256_string,
extract_folders_after_data_docs,
sanitize_filename,
)
from open_webui.utils.utils import get_admin_user, get_verified_user
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import (
BSHTMLLoader,
@@ -105,14 +118,8 @@ from langchain_community.document_loaders import (
YoutubeLoader,
)
from langchain_core.documents import Document
from pydantic import BaseModel
from open_webui.utils.misc import (
calculate_sha256,
calculate_sha256_string,
extract_folders_after_data_docs,
sanitize_filename,
)
from open_webui.utils.utils import get_admin_user, get_verified_user
from colbert.infra import ColBERTConfig
from colbert.modeling.checkpoint import Checkpoint
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
@@ -143,13 +150,11 @@ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SI
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.YOUTUBE_LOADER_TRANSLATION = None
@@ -175,13 +180,13 @@ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_
def update_embedding_model(
embedding_model: str,
update_model: bool = False,
auto_update: bool = False,
):
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
import sentence_transformers
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
get_model_path(embedding_model, update_model),
get_model_path(embedding_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
@@ -191,16 +196,108 @@ def update_embedding_model(
def update_reranking_model(
reranking_model: str,
update_model: bool = False,
auto_update: bool = False,
):
if reranking_model:
import sentence_transformers
if any(model in reranking_model for model in ["jinaai/jina-colbert-v2"]):
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, update_model),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
)
class ColBERT:
def __init__(self, name) -> None:
print("ColBERT: Loading model", name)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
if DOCKER:
# This is a workaround for the issue with the docker container
# where the torch extension is not loaded properly
# and the following error is thrown:
# /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
lock_file = "/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock"
if os.path.exists(lock_file):
os.remove(lock_file)
self.ckpt = Checkpoint(
name,
colbert_config=ColBERTConfig(model_name=name),
).to(self.device)
pass
def calculate_similarity_scores(
self, query_embeddings, document_embeddings
):
query_embeddings = query_embeddings.to(self.device)
document_embeddings = document_embeddings.to(self.device)
# Validate dimensions to ensure compatibility
if query_embeddings.dim() != 3:
raise ValueError(
f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}."
)
if document_embeddings.dim() != 3:
raise ValueError(
f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}."
)
if query_embeddings.size(0) not in [1, document_embeddings.size(0)]:
raise ValueError(
"There should be either one query or queries equal to the number of documents."
)
# Transpose the query embeddings to align for matrix multiplication
transposed_query_embeddings = query_embeddings.permute(0, 2, 1)
# Compute similarity scores using batch matrix multiplication
computed_scores = torch.matmul(
document_embeddings, transposed_query_embeddings
)
# Apply max pooling to extract the highest semantic similarity across each document's sequence
maximum_scores = torch.max(computed_scores, dim=1).values
# Sum up the maximum scores across features to get the overall document relevance scores
final_scores = maximum_scores.sum(dim=1)
normalized_scores = torch.softmax(final_scores, dim=0)
return normalized_scores.detach().cpu().numpy().astype(np.float32)
def predict(self, sentences):
query = sentences[0][0]
docs = [i[1] for i in sentences]
# Embedding the documents
embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0]
# Embedding the queries
embedded_queries = self.ckpt.queryFromText([query], bsize=32)
embedded_query = embedded_queries[0]
# Calculate retrieval scores for the query against all documents
scores = self.calculate_similarity_scores(
embedded_query.unsqueeze(0), embedded_docs
)
return scores
try:
app.state.sentence_transformer_rf = ColBERT(
get_model_path(reranking_model, auto_update)
)
except Exception as e:
log.error(f"ColBERT: {e}")
app.state.sentence_transformer_rf = None
app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
else:
import sentence_transformers
try:
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
)
except:
log.error("CrossEncoder error")
app.state.sentence_transformer_rf = None
app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
else:
app.state.sentence_transformer_rf = None
@@ -593,7 +690,7 @@ async def update_query_settings(
form_data: QuerySettingsForm, user=Depends(get_admin_user)
):
app.state.config.RAG_TEMPLATE = (
form_data.template if form_data.template else RAG_TEMPLATE
form_data.template if form_data.template != "" else DEFAULT_RAG_TEMPLATE
)
app.state.config.TOP_K = form_data.k if form_data.k else 4
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
@@ -998,41 +1095,39 @@ def store_docs_in_vector_db(
try:
if overwrite:
for collection in CHROMA_CLIENT.list_collections():
if collection_name == collection.name:
log.info(f"deleting existing collection {collection_name}")
CHROMA_CLIENT.delete_collection(name=collection_name)
if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
log.info(f"deleting existing collection {collection_name}")
VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
collection = CHROMA_CLIENT.create_collection(name=collection_name)
embedding_func = get_embedding_function(
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
embeddings = embedding_func(embedding_texts)
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid4()) for _ in texts],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
):
collection.add(*batch)
return True
except Exception as e:
if e.__class__.__name__ == "UniqueConstraintError":
if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
log.info(f"collection {collection_name} already exists")
return True
else:
embedding_function = get_embedding_function(
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
VECTOR_DB_CLIENT.insert(
collection_name=collection_name,
items=[
{
"id": str(uuid.uuid4()),
"text": text,
"vector": embedding_function(text.replace("\n", " ")),
"metadata": metadatas[idx],
}
for idx, text in enumerate(texts)
],
)
return True
except Exception as e:
log.exception(e)
return False
@@ -1158,7 +1253,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext in ["doc", "docx"]
or file_ext == "docx"
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
@@ -1396,7 +1491,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
@app.post("/reset/db")
def reset_vector_db(user=Depends(get_admin_user)):
CHROMA_CLIENT.reset()
VECTOR_DB_CLIENT.reset()
@app.post("/reset/uploads")
@@ -1437,7 +1532,7 @@ def reset(user=Depends(get_admin_user)) -> bool:
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
try:
CHROMA_CLIENT.reset()
VECTOR_DB_CLIENT.reset()
except Exception as e:
log.exception(e)
+121 -83
View File
@@ -1,24 +1,68 @@
import logging
import os
import uuid
from typing import Optional, Union
import requests
from open_webui.apps.ollama.main import (
GenerateEmbeddingsForm,
generate_ollama_embeddings,
)
from open_webui.config import CHROMA_CLIENT
from open_webui.env import SRC_LOG_LEVELS
from huggingface_hub import snapshot_download
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from open_webui.apps.ollama.main import (
GenerateEmbeddingsForm,
generate_ollama_embeddings,
)
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.misc import get_last_user_message
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
from typing import Any
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.retrievers import BaseRetriever
class VectorSearchRetriever(BaseRetriever):
collection_name: Any
embedding_function: Any
top_k: int
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> list[Document]:
result = VECTOR_DB_CLIENT.search(
collection_name=self.collection_name,
vectors=[self.embedding_function(query)],
limit=self.top_k,
)
ids = result.ids[0]
metadatas = result.metadatas[0]
documents = result.documents[0]
results = []
for idx in range(len(ids)):
results.append(
Document(
metadata=metadatas[idx],
page_content=documents[idx],
)
)
return results
def query_doc(
collection_name: str,
query: str,
@@ -26,17 +70,18 @@ def query_doc(
k: int,
):
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
query_embeddings = embedding_function(query)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
result = VECTOR_DB_CLIENT.search(
collection_name=collection_name,
vectors=[embedding_function(query)],
limit=k,
)
print("result", result)
log.info(f"query_doc:result {result}")
return result
except Exception as e:
print(e)
raise e
@@ -47,27 +92,25 @@ def query_doc_with_hybrid_search(
k: int,
reranking_function,
r: float,
):
) -> dict:
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
documents = collection.get() # get all documents
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
bm25_retriever = BM25Retriever.from_texts(
texts=documents.get("documents"),
metadatas=documents.get("metadatas"),
texts=result.documents[0],
metadatas=result.metadatas[0],
)
bm25_retriever.k = k
chroma_retriever = ChromaRetriever(
collection=collection,
vector_search_retriever = VectorSearchRetriever(
collection_name=collection_name,
embedding_function=embedding_function,
top_n=k,
top_k=k,
)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
retrievers=[bm25_retriever, vector_search_retriever], weights=[0.5, 0.5]
)
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k,
@@ -92,7 +135,9 @@ def query_doc_with_hybrid_search(
raise e
def merge_and_sort_query_results(query_results, k, reverse=False):
def merge_and_sort_query_results(
query_results: list[dict], k: int, reverse: bool = False
) -> list[dict]:
# Initialize lists to store combined data
combined_distances = []
combined_documents = []
@@ -138,7 +183,7 @@ def query_collection(
query: str,
embedding_function,
k: int,
):
) -> dict:
results = []
for collection_name in collection_names:
if collection_name:
@@ -149,9 +194,9 @@ def query_collection(
k=k,
embedding_function=embedding_function,
)
results.append(result)
except Exception:
pass
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
@@ -165,8 +210,9 @@ def query_collection_with_hybrid_search(
k: int,
reranking_function,
r: float,
):
) -> dict:
results = []
error = False
for collection_name in collection_names:
try:
result = query_doc_with_hybrid_search(
@@ -178,14 +224,39 @@ def query_collection_with_hybrid_search(
r=r,
)
results.append(result)
except Exception:
pass
except Exception as e:
log.exception(
"Error when querying the collection with " f"hybrid_search: {e}"
)
error = True
if error:
raise Exception(
"Hybrid search failed for all collections. Using Non hybrid search as fallback."
)
return merge_and_sort_query_results(results, k=k, reverse=True)
def rag_template(template: str, context: str, query: str):
template = template.replace("[context]", context)
template = template.replace("[query]", query)
count = template.count("[context]")
assert "[context]" in template, "RAG template does not contain '[context]'"
if "<context>" in context and "</context>" in context:
log.debug(
"WARNING: Potential prompt injection attack: the RAG "
"context contains '<context>' and '</context>'. This might be "
"nothing, or the user might be trying to hack something."
)
if "[query]" in context:
query_placeholder = f"[query-{str(uuid.uuid4())}]"
template = template.replace("[query]", query_placeholder)
template = template.replace("[context]", context)
template = template.replace(query_placeholder, query)
else:
template = template.replace("[context]", context)
template = template.replace("[query]", query)
return template
@@ -262,19 +333,27 @@ def get_rag_context(
continue
try:
context = None
if file["type"] == "text":
context = file["content"]
else:
if hybrid_search:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
else:
try:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
except Exception as e:
log.debug(
"Error when using hybrid search, using"
" non hybrid search as fallback."
)
if (not hybrid_search) or (context is None):
context = query_collection(
collection_names=collection_names,
query=query,
@@ -283,7 +362,6 @@ def get_rag_context(
)
except Exception as e:
log.exception(e)
context = None
if context:
relevant_contexts.append({**context, "source": file})
@@ -391,51 +469,11 @@ def generate_openai_batch_embeddings(
return None
from typing import Any
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.retrievers import BaseRetriever
class ChromaRetriever(BaseRetriever):
collection: Any
embedding_function: Any
top_n: int
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> list[Document]:
query_embeddings = self.embedding_function(query)
results = self.collection.query(
query_embeddings=[query_embeddings],
n_results=self.top_n,
)
ids = results["ids"][0]
metadatas = results["metadatas"][0]
documents = results["documents"][0]
results = []
for idx in range(len(ids)):
results.append(
Document(
metadata=metadatas[idx],
page_content=documents[idx],
)
)
return results
import operator
from typing import Optional, Sequence
from langchain_core.callbacks import Callbacks
from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.pydantic_v1 import Extra
class RerankCompressor(BaseDocumentCompressor):
@@ -445,7 +483,7 @@ class RerankCompressor(BaseDocumentCompressor):
r_score: float
class Config:
extra = Extra.forbid
extra = "forbid"
arbitrary_types_allowed = True
def compress_documents(
@@ -0,0 +1,10 @@
from open_webui.apps.rag.vector.dbs.chroma import ChromaClient
from open_webui.apps.rag.vector.dbs.milvus import MilvusClient
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
VECTOR_DB_CLIENT = MilvusClient()
else:
VECTOR_DB_CLIENT = ChromaClient()
@@ -0,0 +1,122 @@
import chromadb
from chromadb import Settings
from chromadb.utils.batch_utils import create_batches
from typing import Optional
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
CHROMA_HTTP_PORT,
CHROMA_HTTP_HEADERS,
CHROMA_HTTP_SSL,
CHROMA_TENANT,
CHROMA_DATABASE,
)
class ChromaClient:
def __init__(self):
if CHROMA_HTTP_HOST != "":
self.client = chromadb.HttpClient(
host=CHROMA_HTTP_HOST,
port=CHROMA_HTTP_PORT,
headers=CHROMA_HTTP_HEADERS,
ssl=CHROMA_HTTP_SSL,
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
)
else:
self.client = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
)
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
collections = self.client.list_collections()
return collection_name in [collection.name for collection in collections]
def delete_collection(self, collection_name: str):
# Delete the collection based on the collection name.
return self.client.delete_collection(name=collection_name)
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 and return 'limit' number of results.
collection = self.client.get_collection(name=collection_name)
if collection:
result = collection.query(
query_embeddings=vectors,
n_results=limit,
)
return SearchResult(
**{
"ids": result["ids"],
"distances": result["distances"],
"documents": result["documents"],
"metadatas": result["metadatas"],
}
)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
collection = self.client.get_collection(name=collection_name)
if collection:
result = collection.get()
return GetResult(
**{
"ids": [result["ids"]],
"documents": [result["documents"]],
"metadatas": [result["metadatas"]],
}
)
return None
def insert(self, collection_name: str, items: list[VectorItem]):
# Insert the items into the collection, if the collection does not exist, it will be created.
collection = self.client.get_or_create_collection(name=collection_name)
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
embeddings = [item["vector"] for item in items]
metadatas = [item["metadata"] for item in items]
for batch in create_batches(
api=self.client,
documents=documents,
embeddings=embeddings,
ids=ids,
metadatas=metadatas,
):
collection.add(*batch)
def upsert(self, collection_name: str, items: list[VectorItem]):
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
collection = self.client.get_or_create_collection(name=collection_name)
ids = [item["id"] for item in items]
documents = [item["text"] for item in items]
embeddings = [item["vector"] for item in items]
metadatas = [item["metadata"] for item in items]
collection.upsert(
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
)
def delete(self, collection_name: str, ids: list[str]):
# Delete the items from the collection based on the ids.
collection = self.client.get_collection(name=collection_name)
if collection:
collection.delete(ids=ids)
def reset(self):
# Resets the database. This will delete all collections and item entries.
return self.client.reset()
@@ -0,0 +1,205 @@
from pymilvus import MilvusClient as Client
from pymilvus import FieldSchema, DataType
import json
from typing import Optional
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import (
MILVUS_URI,
)
class MilvusClient:
def __init__(self):
self.collection_prefix = "open_webui"
self.client = Client(uri=MILVUS_URI)
def _result_to_get_result(self, result) -> GetResult:
print(result)
ids = []
documents = []
metadatas = []
for match in result:
_ids = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
_documents.append(item.get("data", {}).get("text"))
_metadatas.append(item.get("metadata"))
ids.append(_ids)
documents.append(_documents)
metadatas.append(_metadatas)
return GetResult(
**{
"ids": ids,
"documents": documents,
"metadatas": metadatas,
}
)
def _result_to_search_result(self, result) -> SearchResult:
print(result)
ids = []
distances = []
documents = []
metadatas = []
for match in result:
_ids = []
_distances = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
_distances.append(item.get("distance"))
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
_metadatas.append(item.get("entity", {}).get("metadata"))
ids.append(_ids)
distances.append(_distances)
documents.append(_documents)
metadatas.append(_metadatas)
return SearchResult(
**{
"ids": ids,
"distances": distances,
"documents": documents,
"metadatas": metadatas,
}
)
def _create_collection(self, collection_name: str, dimension: int):
schema = self.client.create_schema(
auto_id=False,
enable_dynamic_field=True,
)
schema.add_field(
field_name="id",
datatype=DataType.VARCHAR,
is_primary=True,
max_length=65535,
)
schema.add_field(
field_name="vector",
datatype=DataType.FLOAT_VECTOR,
dim=dimension,
description="vector",
)
schema.add_field(field_name="data", datatype=DataType.JSON, description="data")
schema.add_field(
field_name="metadata", datatype=DataType.JSON, description="metadata"
)
index_params = self.client.prepare_index_params()
index_params.add_index(
field_name="vector", index_type="HNSW", metric_type="COSINE", params={}
)
self.client.create_collection(
collection_name=f"{self.collection_prefix}_{collection_name}",
schema=schema,
index_params=index_params,
)
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
return self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
)
def delete_collection(self, collection_name: str):
# Delete the collection based on the collection name.
return self.client.drop_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
)
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 and return 'limit' number of results.
result = self.client.search(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=vectors,
limit=limit,
output_fields=["data", "metadata"],
)
return self._result_to_search_result(result)
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
result = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter='id != ""',
)
return self._result_to_get_result([result])
def insert(self, collection_name: str, items: list[VectorItem]):
# Insert the items into the collection, if the collection does not exist, it will be created.
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
return self.client.insert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
{
"id": item["id"],
"vector": item["vector"],
"data": {"text": item["text"]},
"metadata": item["metadata"],
}
for item in items
],
)
def upsert(self, collection_name: str, items: list[VectorItem]):
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
return self.client.upsert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
{
"id": item["id"],
"vector": item["vector"],
"data": {"text": item["text"]},
"metadata": item["metadata"],
}
for item in items
],
)
def delete(self, collection_name: str, ids: list[str]):
# Delete the items from the collection based on the ids.
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
ids=ids,
)
def reset(self):
# Resets the database. This will delete all collections and item entries.
collection_names = self.client.list_collections()
for collection_name in collection_names:
if collection_name.startswith(self.collection_prefix):
self.client.drop_collection(collection_name=collection_name)
@@ -0,0 +1,19 @@
from pydantic import BaseModel
from typing import Optional, List, Any
class VectorItem(BaseModel):
id: str
text: str
vector: List[float | int]
metadata: Any
class GetResult(BaseModel):
ids: Optional[List[List[str]]]
documents: Optional[List[List[str]]]
metadatas: Optional[List[List[Any]]]
class SearchResult(GetResult):
distances: Optional[List[List[float | int]]]
+118 -69
View File
@@ -1,21 +1,123 @@
import asyncio
import socketio
from open_webui.apps.webui.models.users import Users
from open_webui.utils.utils import decode_token
import logging
import sys
import time
from open_webui.apps.webui.models.users import Users
from open_webui.env import (
ENABLE_WEBSOCKET_SUPPORT,
WEBSOCKET_MANAGER,
WEBSOCKET_REDIS_URL,
)
from open_webui.utils.utils import decode_token
from open_webui.apps.socket.utils import RedisDict
from open_webui.env import (
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["SOCKET"])
if WEBSOCKET_MANAGER == "redis":
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
sio = socketio.AsyncServer(
cors_allowed_origins=[],
async_mode="asgi",
transports=(
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
always_connect=True,
client_manager=mgr,
)
else:
sio = socketio.AsyncServer(
cors_allowed_origins=[],
async_mode="asgi",
transports=(
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
),
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
always_connect=True,
)
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
# Dictionary to maintain the user pool
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
if WEBSOCKET_MANAGER == "redis":
SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL)
USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL)
USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL)
else:
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
# Timeout duration in seconds
TIMEOUT_DURATION = 3
async def periodic_usage_pool_cleanup():
while True:
now = int(time.time())
for model_id, connections in list(USAGE_POOL.items()):
# Creating a list of sids to remove if they have timed out
expired_sids = [
sid
for sid, details in connections.items()
if now - details["updated_at"] > TIMEOUT_DURATION
]
for sid in expired_sids:
del connections[sid]
if not connections:
log.debug(f"Cleaning up model {model_id} from usage pool")
del USAGE_POOL[model_id]
else:
USAGE_POOL[model_id] = connections
# Emit updated usage information after cleaning
await sio.emit("usage", {"models": get_models_in_use()})
await asyncio.sleep(TIMEOUT_DURATION)
# Start the cleanup task when your app starts
asyncio.create_task(periodic_usage_pool_cleanup())
def get_models_in_use():
# List models that are currently in use
models_in_use = list(USAGE_POOL.keys())
return models_in_use
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
@sio.event
async def connect(sid, environ, auth):
user = None
@@ -32,15 +134,14 @@ async def connect(sid, environ, auth):
else:
USER_POOL[user.id] = [sid]
print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(USER_POOL.items())})
await sio.emit("usage", {"models": get_models_in_use()})
@sio.on("user-join")
async def user_join(sid, data):
print("user-join", sid, data)
# print("user-join", sid, data)
auth = data["auth"] if "auth" in data else None
if not auth or "token" not in auth:
@@ -60,67 +161,14 @@ async def user_join(sid, data):
else:
USER_POOL[user.id] = [sid]
print(f"user {user.name}({user.id}) connected with session ID {sid}")
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
await sio.emit("user-count", {"count": len(USER_POOL.items())})
@sio.on("user-count")
async def user_count(sid):
await sio.emit("user-count", {"count": len(set(USER_POOL))})
def get_models_in_use():
# Aggregate all models in use
models_in_use = []
for model_id, data in USAGE_POOL.items():
models_in_use.append(model_id)
return models_in_use
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Cancel previous callback if there is one
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["callback"].cancel()
# Store the new usage data and task
if model_id in USAGE_POOL:
USAGE_POOL[model_id]["sids"].append(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
else:
USAGE_POOL[model_id] = {"sids": [sid]}
# Schedule a task to remove the usage data after TIMEOUT_DURATION
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
remove_after_timeout(sid, model_id)
)
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
async def remove_after_timeout(sid, model_id):
try:
await asyncio.sleep(TIMEOUT_DURATION)
if model_id in USAGE_POOL:
print(USAGE_POOL[model_id]["sids"])
USAGE_POOL[model_id]["sids"].remove(sid)
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
if len(USAGE_POOL[model_id]["sids"]) == 0:
del USAGE_POOL[model_id]
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
except asyncio.CancelledError:
# Task was cancelled due to new 'usage' event
pass
await sio.emit("user-count", {"count": len(USER_POOL.items())})
@sio.event
@@ -129,14 +177,15 @@ async def disconnect(sid):
user_id = SESSION_POOL[sid]
del SESSION_POOL[sid]
USER_POOL[user_id].remove(sid)
USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid]
if len(USER_POOL[user_id]) == 0:
del USER_POOL[user_id]
await sio.emit("user-count", {"count": len(USER_POOL)})
else:
print(f"Unknown session ID {sid} disconnected")
pass
# print(f"Unknown session ID {sid} disconnected")
def get_event_emitter(request_info):
+59
View File
@@ -0,0 +1,59 @@
import json
import redis
class RedisDict:
def __init__(self, name, redis_url):
self.name = name
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
def __setitem__(self, key, value):
serialized_value = json.dumps(value)
self.redis.hset(self.name, key, serialized_value)
def __getitem__(self, key):
value = self.redis.hget(self.name, key)
if value is None:
raise KeyError(key)
return json.loads(value)
def __delitem__(self, key):
result = self.redis.hdel(self.name, key)
if result == 0:
raise KeyError(key)
def __contains__(self, key):
return self.redis.hexists(self.name, key)
def __len__(self):
return self.redis.hlen(self.name)
def keys(self):
return self.redis.hkeys(self.name)
def values(self):
return [json.loads(v) for v in self.redis.hvals(self.name)]
def items(self):
return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()]
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def clear(self):
self.redis.delete(self.name)
def update(self, other=None, **kwargs):
if other is not None:
for k, v in other.items() if hasattr(other, "items") else other:
self[k] = v
for k, v in kwargs.items():
self[k] = v
def setdefault(self, key, default=None):
if key not in self:
self[key] = default
return self[key]
+6 -2
View File
@@ -51,11 +51,15 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from open_webui.utils.misc import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
)
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from open_webui.utils.tools import get_tools
app = FastAPI()
+13 -8
View File
@@ -188,14 +188,19 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, response: Response, form_data: SignupForm):
if (
not request.app.state.config.ENABLE_SIGNUP
and request.app.state.config.ENABLE_LOGIN_FORM
and WEBUI_AUTH
):
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
if WEBUI_AUTH:
if (
not request.app.state.config.ENABLE_SIGNUP
or not request.app.state.config.ENABLE_LOGIN_FORM
):
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
else:
if Users.get_num_users() != 0:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
if not validate_email_format(form_data.email.lower()):
raise HTTPException(
@@ -1,12 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
import logging
from typing import Optional
from open_webui.apps.webui.models.memories import Memories, MemoryModel
from open_webui.config import CHROMA_CLIENT
from open_webui.env import SRC_LOG_LEVELS
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
from open_webui.utils.utils import get_verified_user
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -49,14 +50,17 @@ async def add_memory(
user=Depends(get_verified_user),
):
memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[{"created_at": memory.created_at}],
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
items=[
{
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
"metadata": {"created_at": memory.created_at},
}
],
)
return memory
@@ -76,12 +80,10 @@ class QueryMemoryForm(BaseModel):
async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
):
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
results = collection.query(
query_embeddings=[query_embedding],
n_results=form_data.k, # how many results to return
results = VECTOR_DB_CLIENT.search(
collection_name=f"user-memory-{user.id}",
vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content)],
limit=form_data.k,
)
return results
@@ -94,17 +96,25 @@ async def query_memory(
async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user)
):
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id)
for memory in memories:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
)
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
items=[
{
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
"metadata": {
"created_at": memory.created_at,
"updated_at": memory.updated_at,
},
}
for memory in memories
],
)
return True
@@ -119,7 +129,7 @@ async def delete_memory_by_user_id(user=Depends(get_verified_user)):
if result:
try:
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
except Exception as e:
log.error(e)
return True
@@ -144,16 +154,18 @@ async def update_memory_by_id(
raise HTTPException(status_code=404, detail="Memory not found")
if form_data.content is not None:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
)
collection.upsert(
documents=[form_data.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[
{"created_at": memory.created_at, "updated_at": memory.updated_at}
VECTOR_DB_CLIENT.upsert(
collection_name=f"user-memory-{user.id}",
items=[
{
"id": memory.id,
"text": memory.content,
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
"metadata": {
"created_at": memory.created_at,
"updated_at": memory.updated_at,
},
}
],
)
@@ -170,10 +182,9 @@ async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
if result:
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
VECTOR_DB_CLIENT.delete(
collection_name=f"user-memory-{user.id}", ids=[memory_id]
)
collection.delete(ids=[memory_id])
return True
return False
+12 -20
View File
@@ -18,8 +18,18 @@ router = APIRouter()
@router.get("/", response_model=list[ModelResponse])
async def get_models(user=Depends(get_verified_user)):
return Models.get_all_models()
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
if id:
model = Models.get_model_by_id(id)
if model:
return [model]
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
return Models.get_all_models()
############################
@@ -50,24 +60,6 @@ async def add_new_model(
)
############################
# GetModelById
############################
@router.get("/", response_model=Optional[ModelModel])
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelById
############################
@@ -4,6 +4,7 @@ from pathlib import Path
import black
import markdown
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
from open_webui.env import FONTS_DIR
from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fpdf import FPDF
@@ -57,14 +58,11 @@ class ChatForm(BaseModel):
async def download_chat_as_pdf(
form_data: ChatForm,
):
global FONTS_DIR
pdf = FPDF()
pdf.add_page()
# When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts
FONTS_DIR = Path("./static/fonts")
# Non Docker Installation
# When running using `pip install` the static directory is in the site packages.
if not FONTS_DIR.exists():
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
+34 -21
View File
@@ -4,7 +4,7 @@ import subprocess
import sys
from importlib import util
import types
import tempfile
from open_webui.apps.webui.models.functions import Functions
from open_webui.apps.webui.models.tools import Tools
@@ -65,6 +65,7 @@ def replace_imports(content):
def load_toolkit_module_by_id(toolkit_id, content=None):
if content is None:
tool = Tools.get_tool_by_id(toolkit_id)
if not tool:
@@ -74,33 +75,40 @@ def load_toolkit_module_by_id(toolkit_id, content=None):
content = replace_imports(content)
Tools.update_tool_by_id(toolkit_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
# Install required packages found within the frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
module_name = f"tool_{toolkit_id}"
module = types.ModuleType(module_name)
sys.modules[module_name] = module
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:
with open(temp_file.name, "w", encoding="utf-8") as f:
f.write(content)
module.__dict__["__file__"] = temp_file.name
# Executing the modified content in the created module's namespace
exec(content, module.__dict__)
# Extract frontmatter, assuming content can be treated directly as a string
frontmatter = extract_frontmatter(
content
) # Ensure this method is adaptable to handle content strings
# Install required packages found within the frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
frontmatter = extract_frontmatter(content)
print(f"Loaded module: {module.__name__}")
# Create and return the object if the class 'Tools' is found in the module
if hasattr(module, "Tools"):
return module.Tools(), frontmatter
else:
raise Exception("No Tools class found in the module")
except Exception as e:
print(f"Error loading module: {toolkit_id}")
print(f"Error loading module: {toolkit_id}: {e}")
del sys.modules[module_name] # Clean up
raise e
finally:
os.unlink(temp_file.name)
def load_function_module_by_id(function_id, content=None):
@@ -112,23 +120,26 @@ def load_function_module_by_id(function_id, content=None):
content = replace_imports(content)
Functions.update_function_by_id(function_id, {"content": content})
else:
frontmatter = extract_frontmatter(content)
install_frontmatter_requirements(frontmatter.get("requirements", ""))
module_name = f"function_{function_id}"
module = types.ModuleType(module_name)
sys.modules[module_name] = module
# Create a temporary file and use it to define `__file__` so
# that it works as expected from the module's perspective.
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:
with open(temp_file.name, "w", encoding="utf-8") as f:
f.write(content)
module.__dict__["__file__"] = temp_file.name
# Execute the modified content in the created module's namespace
exec(content, module.__dict__)
# Extract the frontmatter from the content, simulate file-like behaviour
frontmatter = extract_frontmatter(
content
) # This function needs to handle string inputs
# Install necessary requirements specified in frontmatter
install_frontmatter_requirements(frontmatter.get("requirements", ""))
frontmatter = extract_frontmatter(content)
print(f"Loaded module: {module.__name__}")
# Create appropriate object based on available class type in the module
@@ -141,11 +152,13 @@ def load_function_module_by_id(function_id, content=None):
else:
raise Exception("No Function class found in the module")
except Exception as e:
print(f"Error loading module: {function_id}")
print(f"Error loading module: {function_id}: {e}")
del sys.modules[module_name] # Cleanup by removing the module in case of error
Functions.update_function_by_id(function_id, {"is_active": False})
raise e
finally:
os.unlink(temp_file.name)
def install_frontmatter_requirements(requirements):
+94 -124
View File
@@ -11,7 +11,6 @@ import chromadb
import requests
import yaml
from open_webui.apps.webui.internal.db import Base, get_db
from chromadb import Settings
from open_webui.env import (
OPEN_WEBUI_DIR,
DATA_DIR,
@@ -540,40 +539,6 @@ Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# LITELLM_CONFIG
####################################
def create_config_file(file_path):
directory = os.path.dirname(file_path)
# Check if directory exists, if not, create it
if not os.path.exists(directory):
os.makedirs(directory)
# Data to write into the YAML file
config_data = {
"general_settings": {},
"litellm_settings": {},
"model_list": [],
"router_settings": {},
}
# Write data to YAML file
with open(file_path, "w") as file:
yaml.dump(config_data, file)
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
# if not os.path.exists(LITELLM_CONFIG_PATH):
# log.info("Config file doesn't exist. Creating...")
# create_config_file(LITELLM_CONFIG_PATH)
# log.info("Config file created successfully.")
####################################
# OLLAMA_BASE_URL
####################################
@@ -898,76 +863,37 @@ TASK_MODEL_EXTERNAL = PersistentConfig(
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"task.title.prompt_template",
os.environ.get(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"""Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
)
Examples of titles:
📉 Stock Market Trends
🍪 Perfect Chocolate Chip Recipe
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights
Prompt: {{prompt:middletruncate:8000}}""",
),
ENABLE_SEARCH_QUERY = PersistentConfig(
"ENABLE_SEARCH_QUERY",
"task.search.enable",
os.environ.get("ENABLE_SEARCH_QUERY", "True").lower() == "true",
)
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"task.search.prompt_template",
os.environ.get(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
Question:
{{prompt:end:4000}}""",
),
os.environ.get("SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE", ""),
)
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
"task.search.prompt_length_threshold",
int(
os.environ.get(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
100,
)
),
)
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"task.tools.prompt_template",
os.environ.get(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"""Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text.""",
),
os.environ.get("TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", ""),
)
####################################
# RAG document content extraction
# Vector Database
####################################
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
"rag.CONTENT_EXTRACTION_ENGINE",
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
)
TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL",
"rag.tika_server_url",
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
)
####################################
# RAG
####################################
VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
# Chroma
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
@@ -984,8 +910,29 @@ else:
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
# Milvus
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
####################################
# RAG
####################################
# RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
"rag.CONTENT_EXTRACTION_ENGINE",
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
)
TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL",
"rag.tika_server_url",
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
)
RAG_TOP_K = PersistentConfig(
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
)
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
"RAG_RELEVANCE_THRESHOLD",
@@ -1074,36 +1021,8 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
if CHROMA_HTTP_HOST != "":
CHROMA_CLIENT = chromadb.HttpClient(
host=CHROMA_HTTP_HOST,
port=CHROMA_HTTP_PORT,
headers=CHROMA_HTTP_HEADERS,
ssl=CHROMA_HTTP_SSL,
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
)
else:
CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
)
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
if USE_CUDA.lower() == "true":
DEVICE_TYPE = "cuda"
else:
DEVICE_TYPE = "cpu"
CHUNK_SIZE = PersistentConfig(
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
)
CHUNK_OVERLAP = PersistentConfig(
"CHUNK_OVERLAP",
@@ -1111,19 +1030,25 @@ CHUNK_OVERLAP = PersistentConfig(
int(os.environ.get("CHUNK_OVERLAP", "100")),
)
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
DEFAULT_RAG_TEMPLATE = """You are given a user query, some textual context and rules, all inside xml tags. You have to answer the query based on the context while respecting the rules.
<context>
[context]
[context]
</context>
When answer to user:
- If you don't know, just say that you don't know.
- If you don't know when you are not sure, ask for clarification.
Avoid mentioning that you obtained the information from the context.
And answer according to the language of the user's question.
<rules>
- If you don't know, just say so.
- If you are not sure, ask for clarification.
- Answer in the same language as the user query.
- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
- Answer directly and without using xml tags.
</rules>
Given the context information, answer the query.
Query: [query]"""
<user_query>
[query]
</user_query>
"""
RAG_TEMPLATE = PersistentConfig(
"RAG_TEMPLATE",
@@ -1293,6 +1218,37 @@ AUTOMATIC1111_API_AUTH = PersistentConfig(
os.getenv("AUTOMATIC1111_API_AUTH", ""),
)
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
"AUTOMATIC1111_CFG_SCALE",
"image_generation.automatic1111.cfg_scale",
(
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
else None
),
)
AUTOMATIC1111_SAMPLER = PersistentConfig(
"AUTOMATIC1111_SAMPLERE",
"image_generation.automatic1111.sampler",
(
os.environ.get("AUTOMATIC1111_SAMPLER")
if os.environ.get("AUTOMATIC1111_SAMPLER")
else None
),
)
AUTOMATIC1111_SCHEDULER = PersistentConfig(
"AUTOMATIC1111_SCHEDULER",
"image_generation.automatic1111.scheduler",
(
os.environ.get("AUTOMATIC1111_SCHEDULER")
if os.environ.get("AUTOMATIC1111_SCHEDULER")
else None
),
)
COMFYUI_BASE_URL = PersistentConfig(
"COMFYUI_BASE_URL",
"image_generation.comfyui.base_url",
@@ -1516,3 +1472,17 @@ AUDIO_TTS_SPLIT_ON = PersistentConfig(
"audio.tts.split_on",
os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
)
AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
"AUDIO_TTS_AZURE_SPEECH_REGION",
"audio.tts.azure.speech_region",
os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", "eastus"),
)
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT",
"audio.tts.azure.speech_output_format",
os.getenv(
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
),
)
+40 -16
View File
@@ -31,6 +31,28 @@ try:
except ImportError:
print("dotenv not installed, skipping...")
DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
if USE_CUDA.lower() == "true":
try:
import torch
assert torch.cuda.is_available(), "CUDA not available"
DEVICE_TYPE = "cuda"
except Exception as e:
cuda_error = (
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
f"Resetting USE_CUDA_DOCKER to false: {e}"
)
os.environ["USE_CUDA_DOCKER"] = "false"
USE_CUDA = "false"
DEVICE_TYPE = "cpu"
else:
DEVICE_TYPE = "cpu"
####################################
# LOGGING
@@ -47,6 +69,9 @@ else:
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
if "cuda_error" in locals():
log.exception(cuda_error)
log_sources = [
"AUDIO",
"COMFYUI",
@@ -59,6 +84,7 @@ log_sources = [
"OPENAI",
"RAG",
"WEBHOOK",
"SOCKET",
]
SRC_LOG_LEVELS = {}
@@ -88,21 +114,9 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
ENV = os.environ.get("ENV", "dev")
PIP_INSTALL = False
try:
importlib.metadata.version("open-webui")
PIP_INSTALL = True
except importlib.metadata.PackageNotFoundError:
pass
FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
PIP_INSTALL = (
os.environ.get("PIP_INSTALL", "False").lower() == "true"
if os.environ.get("PIP_INSTALL")
else PIP_INSTALL
)
if PIP_INSTALL:
if FROM_INIT_PY:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
else:
try:
@@ -193,7 +207,7 @@ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
if PIP_INSTALL:
if FROM_INIT_PY:
NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
@@ -210,9 +224,11 @@ if PIP_INSTALL:
DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
if PIP_INSTALL:
if FROM_INIT_PY:
FRONTEND_BUILD_DIR = Path(
os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
).resolve()
@@ -283,3 +299,11 @@ WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
ENABLE_WEBSOCKET_SUPPORT = (
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
)
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", "redis://localhost:6379/0")
+138 -45
View File
@@ -19,7 +19,9 @@ from open_webui.apps.audio.main import app as audio_app
from open_webui.apps.images.main import app as images_app
from open_webui.apps.ollama.main import app as ollama_app
from open_webui.apps.ollama.main import (
generate_openai_chat_completion as generate_ollama_chat_completion,
GenerateChatCompletionForm,
generate_chat_completion as generate_ollama_chat_completion,
generate_openai_chat_completion as generate_ollama_openai_chat_completion,
)
from open_webui.apps.ollama.main import get_all_models as get_ollama_models
from open_webui.apps.openai.main import app as openai_app
@@ -63,8 +65,8 @@ from open_webui.config import (
MODEL_FILTER_LIST,
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
OAUTH_PROVIDERS,
ENABLE_SEARCH_QUERY,
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
STATIC_DIR,
TASK_MODEL,
TASK_MODEL_EXTERNAL,
@@ -109,6 +111,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import RedirectResponse, Response, StreamingResponse
from open_webui.utils.security_headers import SecurityHeadersMiddleware
from open_webui.utils.misc import (
add_or_update_system_message,
@@ -134,6 +137,12 @@ from open_webui.utils.utils import (
)
from open_webui.utils.webhook import post_webhook
from open_webui.utils.payload import convert_payload_openai_to_ollama
from open_webui.utils.response import (
convert_response_ollama_to_openai,
convert_streaming_response_ollama_to_openai,
)
if SAFE_MODE:
print("SAFE MODE ENABLED")
Functions.deactivate_all_functions()
@@ -199,9 +208,7 @@ app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMP
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
)
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
)
app.state.config.ENABLE_SEARCH_QUERY = ENABLE_SEARCH_QUERY
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
)
@@ -397,8 +404,13 @@ async def chat_completion_tools_handler(
specs = [tool["spec"] for tool in tools.values()]
tools_specs = json.dumps(specs)
if app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE != "":
template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
else:
template = """Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text."""
tools_function_calling_prompt = tools_function_calling_generation_template(
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, tools_specs
template, tools_specs
)
log.info(f"{tools_function_calling_prompt=}")
payload = get_tools_function_calling_payload(
@@ -583,8 +595,17 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
if len(contexts) > 0:
context_string = "/n".join(contexts).strip()
prompt = get_last_user_message(body["messages"])
if prompt is None:
raise Exception("No user message found")
if (
rag_app.state.config.RELEVANCE_THRESHOLD == 0
and context_string.strip() == ""
):
log.debug(
f"With a 0 relevancy threshold for RAG, the context cannot be empty"
)
# Workaround for Ollama 2.0+ system prompt issue
# TODO: replace with add_or_update_system_message
if model["owned_by"] == "ollama":
@@ -777,6 +798,8 @@ app.add_middleware(
allow_headers=["*"],
)
app.add_middleware(SecurityHeadersMiddleware)
@app.middleware("http")
async def commit_session_after_request(request: Request, call_next):
@@ -809,6 +832,24 @@ async def update_embedding_function(request: Request, call_next):
return response
@app.middleware("http")
async def inspect_websocket(request: Request, call_next):
if (
"/ws/socket.io" in request.url.path
and request.query_params.get("transport") == "websocket"
):
upgrade = (request.headers.get("Upgrade") or "").lower()
connection = (request.headers.get("Connection") or "").lower().split(",")
# Check that there's the correct headers for an upgrade, else reject the connection
# This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367
if upgrade != "websocket" or "upgrade" not in connection:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "Invalid WebSocket upgrade request"},
)
return await call_next(request)
app.mount("/ws", socket_app)
app.mount("/ollama", ollama_app)
@@ -1015,7 +1056,18 @@ async def generate_chat_completions(form_data: dict, user=Depends(get_verified_u
if model.get("pipe"):
return await generate_function_chat_completion(form_data, user=user)
if model["owned_by"] == "ollama":
return await generate_ollama_chat_completion(form_data, user=user)
# Using /ollama/api/chat endpoint
form_data = convert_payload_openai_to_ollama(form_data)
form_data = GenerateChatCompletionForm(**form_data)
response = await generate_ollama_chat_completion(form_data=form_data, user=user)
if form_data.stream:
response.headers["content-type"] = "text/event-stream"
return StreamingResponse(
convert_streaming_response_ollama_to_openai(response),
headers=dict(response.headers),
)
else:
return convert_response_ollama_to_openai(response)
else:
return await generate_openai_chat_completion(form_data, user=user)
@@ -1312,8 +1364,8 @@ async def get_task_config(user=Depends(get_verified_user)):
"TASK_MODEL": app.state.config.TASK_MODEL,
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
}
@@ -1323,7 +1375,7 @@ class TaskConfigForm(BaseModel):
TASK_MODEL_EXTERNAL: Optional[str]
TITLE_GENERATION_PROMPT_TEMPLATE: str
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
ENABLE_SEARCH_QUERY: bool
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str
@@ -1337,9 +1389,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
)
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
)
app.state.config.ENABLE_SEARCH_QUERY = form_data.ENABLE_SEARCH_QUERY
app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
)
@@ -1349,7 +1399,7 @@ async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_u
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
"ENABLE_SEARCH_QUERY": app.state.config.ENABLE_SEARCH_QUERY,
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
}
@@ -1367,11 +1417,25 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
# Check if the user has a custom task model
# If the user has a custom task model, use that model
model_id = get_task_model_id(model_id)
task_model_id = get_task_model_id(model_id)
print(task_model_id)
print(model_id)
model = app.state.MODELS[task_model_id]
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
else:
template = """Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
Examples of titles:
📉 Stock Market Trends
🍪 Perfect Chocolate Chip Recipe
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights
Prompt: {{prompt:middletruncate:8000}}"""
content = title_generation_template(
template,
@@ -1383,16 +1447,22 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
)
payload = {
"model": model_id,
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"max_tokens": 50,
**(
{"max_tokens": 50}
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
else {
"max_completion_tokens": 50,
}
),
"chat_id": form_data.get("chat_id", None),
"metadata": {"task": str(TASKS.TITLE_GENERATION)},
}
log.debug(payload)
# Handle pipeline filters
try:
payload = filter_pipeline(payload, user)
except Exception as e:
@@ -1406,7 +1476,6 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
)
if "chat_id" in payload:
del payload["chat_id"]
@@ -1416,11 +1485,10 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
@app.post("/api/task/query/completions")
async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
print("generate_search_query")
if len(form_data["prompt"]) < app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD:
if not app.state.config.ENABLE_SEARCH_QUERY:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Skip search query generation for short prompts (< {app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} characters)",
detail=f"Search query generation is disabled",
)
model_id = form_data["model"]
@@ -1432,26 +1500,46 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
# Check if the user has a custom task model
# If the user has a custom task model, use that model
model_id = get_task_model_id(model_id)
task_model_id = get_task_model_id(model_id)
print(task_model_id)
print(model_id)
model = app.state.MODELS[task_model_id]
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
else:
template = """Given the user's message and interaction history, decide if a web search is necessary. You must be concise and exclusively provide a search query if one is necessary. Refrain from verbose responses or any additional commentary. Prefer suggesting a search if uncertain to provide comprehensive or updated information. If a search isn't needed at all, respond with an empty string. Default to a search query when in doubt. Today's date is {{CURRENT_DATE}}.
User Message:
{{prompt:end:4000}}
Interaction History:
{{MESSAGES:END:6}}
Search Query:"""
content = search_query_generation_template(
template, form_data["prompt"], {"name": user.name}
template, form_data["messages"], {"name": user.name}
)
print("content", content)
payload = {
"model": model_id,
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"max_tokens": 30,
**(
{"max_tokens": 30}
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
else {
"max_completion_tokens": 30,
}
),
"metadata": {"task": str(TASKS.QUERY_GENERATION)},
}
log.debug(payload)
print(payload)
# Handle pipeline filters
try:
payload = filter_pipeline(payload, user)
except Exception as e:
@@ -1465,7 +1553,6 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
)
if "chat_id" in payload:
del payload["chat_id"]
@@ -1485,16 +1572,16 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
# Check if the user has a custom task model
# If the user has a custom task model, use that model
model_id = get_task_model_id(model_id)
task_model_id = get_task_model_id(model_id)
print(task_model_id)
print(model_id)
model = app.state.MODELS[task_model_id]
template = '''
Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
Message: """{{prompt}}"""
'''
content = title_generation_template(
template,
form_data["prompt"],
@@ -1505,16 +1592,22 @@ Message: """{{prompt}}"""
)
payload = {
"model": model_id,
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": False,
"max_tokens": 4,
**(
{"max_tokens": 4}
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
else {
"max_completion_tokens": 4,
}
),
"chat_id": form_data.get("chat_id", None),
"metadata": {"task": str(TASKS.EMOJI_GENERATION)},
}
log.debug(payload)
# Handle pipeline filters
try:
payload = filter_pipeline(payload, user)
except Exception as e:
@@ -1528,7 +1621,6 @@ Message: """{{prompt}}"""
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
)
if "chat_id" in payload:
del payload["chat_id"]
@@ -1548,8 +1640,10 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)
# Check if the user has a custom task model
# If the user has a custom task model, use that model
model_id = get_task_model_id(model_id)
print(model_id)
task_model_id = get_task_model_id(model_id)
print(task_model_id)
model = app.state.MODELS[task_model_id]
template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
@@ -1564,13 +1658,12 @@ Responses from models: {{responses}}"""
)
payload = {
"model": model_id,
"model": task_model_id,
"messages": [{"role": "user", "content": content}],
"stream": form_data.get("stream", False),
"chat_id": form_data.get("chat_id", None),
"metadata": {"task": str(TASKS.MOA_RESPONSE_GENERATION)},
}
log.debug(payload)
try:
@@ -1586,7 +1679,6 @@ Responses from models: {{responses}}"""
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(e)},
)
if "chat_id" in payload:
del payload["chat_id"]
@@ -2237,10 +2329,11 @@ async def get_manifest_json():
return {
"name": WEBUI_NAME,
"short_name": WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "portrait-primary",
"orientation": "any",
"icons": [
{
"src": "/static/logo.png",
@@ -0,0 +1,19 @@
from alembic import command
from alembic.config import Config
from open_webui.env import OPEN_WEBUI_DIR
alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini")
# Set the script location dynamically
migrations_path = OPEN_WEBUI_DIR / "migrations"
alembic_cfg.set_main_option("script_location", str(migrations_path))
def revision(message: str) -> None:
command.revision(alembic_cfg, message=message, autogenerate=False)
if __name__ == "__main__":
input_message = input("Enter the revision message: ")
revision(input_message)
@@ -11,8 +11,8 @@ from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
import open_webui.apps.webui.internal.db
from open_webui.apps.webui.internal.db import JSONField
from open_webui.migrations.util import get_existing_tables
# revision identifiers, used by Alembic.
@@ -82,9 +82,7 @@ def upgrade() -> None:
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("filename", sa.Text(), nullable=True),
sa.Column(
"meta", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column("meta", JSONField(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
@@ -97,12 +95,8 @@ def upgrade() -> None:
sa.Column("name", sa.Text(), nullable=True),
sa.Column("type", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column(
"meta", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column(
"valves", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column("meta", JSONField(), nullable=True),
sa.Column("valves", JSONField(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("is_global", sa.Boolean(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
@@ -128,12 +122,8 @@ def upgrade() -> None:
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("base_model_id", sa.Text(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column(
"params", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column(
"meta", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column("params", JSONField(), nullable=True),
sa.Column("meta", JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
@@ -167,15 +157,9 @@ def upgrade() -> None:
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column(
"specs", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column(
"meta", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column(
"valves", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column("specs", JSONField(), nullable=True),
sa.Column("meta", JSONField(), nullable=True),
sa.Column("valves", JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
@@ -193,12 +177,8 @@ def upgrade() -> None:
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("api_key", sa.String(), nullable=True),
sa.Column(
"settings", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column(
"info", open_webui.apps.webui.internal.db.JSONField(), nullable=True
),
sa.Column("settings", JSONField(), nullable=True),
sa.Column("info", JSONField(), nullable=True),
sa.Column("oauth_sub", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("api_key"),
+20 -88
View File
@@ -6,7 +6,14 @@ from datetime import timedelta
from pathlib import Path
from typing import Callable, Optional
from open_webui.utils.task import prompt_template
def get_messages_content(messages: list[dict]) -> str:
return "\n".join(
[
f"{message['role'].upper()}: {get_content_from_message(message)}"
for message in messages
]
)
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
@@ -30,7 +37,6 @@ def get_last_user_message(messages: list[dict]) -> Optional[str]:
message = get_last_user_message_item(messages)
if message is None:
return None
return get_content_from_message(message)
@@ -99,103 +105,29 @@ def openai_chat_message_template(model: str):
}
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
def openai_chat_chunk_message_template(
model: str, message: Optional[str] = None
) -> dict:
template = openai_chat_message_template(model)
template["object"] = "chat.completion.chunk"
template["choices"][0]["delta"] = {"content": message}
if message:
template["choices"][0]["delta"] = {"content": message}
else:
template["choices"][0]["finish_reason"] = "stop"
return template
def openai_chat_completion_message_template(model: str, message: str) -> dict:
def openai_chat_completion_message_template(
model: str, message: Optional[str] = None
) -> dict:
template = openai_chat_message_template(model)
template["object"] = "chat.completion"
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
if message:
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
template["choices"][0]["finish_reason"] = "stop"
return template
# inplace function: form_data is modified
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
system = params.get("system", None)
if not system:
return form_data
if user:
template_params = {
"user_name": user.name,
"user_location": user.info.get("location") if user.info else None,
}
else:
template_params = {}
system = prompt_template(system, **template_params)
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
return form_data
# inplace function: form_data is modified
def apply_model_params_to_body(
params: dict, form_data: dict, mappings: dict[str, Callable]
) -> dict:
if not params:
return form_data
for key, cast_func in mappings.items():
if (value := params.get(key)) is not None:
form_data[key] = cast_func(value)
return form_data
# inplace function: form_data is modified
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
mappings = {
"temperature": float,
"top_p": int,
"max_tokens": int,
"frequency_penalty": int,
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
}
return apply_model_params_to_body(params, form_data, mappings)
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
opts = [
"temperature",
"top_p",
"seed",
"mirostat",
"mirostat_eta",
"mirostat_tau",
"num_ctx",
"num_batch",
"num_keep",
"repeat_last_n",
"tfs_z",
"top_k",
"min_p",
"use_mmap",
"use_mlock",
"num_thread",
"num_gpu",
]
mappings = {i: lambda x: x for i in opts}
form_data = apply_model_params_to_body(params, form_data, mappings)
name_differences = {
"max_tokens": "num_predict",
"frequency_penalty": "repeat_penalty",
}
for key, value in name_differences.items():
if (param := params.get(key, None)) is not None:
form_data[value] = param
return form_data
def get_gravatar_url(email):
# Trim leading and trailing whitespace from
# an email address and force all characters
+134
View File
@@ -0,0 +1,134 @@
from open_webui.utils.task import prompt_template
from open_webui.utils.misc import (
add_or_update_system_message,
)
from typing import Callable, Optional
# inplace function: form_data is modified
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
system = params.get("system", None)
if not system:
return form_data
if user:
template_params = {
"user_name": user.name,
"user_location": user.info.get("location") if user.info else None,
}
else:
template_params = {}
system = prompt_template(system, **template_params)
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
return form_data
# inplace function: form_data is modified
def apply_model_params_to_body(
params: dict, form_data: dict, mappings: dict[str, Callable]
) -> dict:
if not params:
return form_data
for key, cast_func in mappings.items():
if (value := params.get(key)) is not None:
form_data[key] = cast_func(value)
return form_data
# inplace function: form_data is modified
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
mappings = {
"temperature": float,
"top_p": float,
"max_tokens": int,
"frequency_penalty": float,
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
}
return apply_model_params_to_body(params, form_data, mappings)
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
opts = [
"temperature",
"top_p",
"seed",
"mirostat",
"mirostat_eta",
"mirostat_tau",
"num_ctx",
"num_batch",
"num_keep",
"repeat_last_n",
"tfs_z",
"top_k",
"min_p",
"use_mmap",
"use_mlock",
"num_thread",
"num_gpu",
]
mappings = {i: lambda x: x for i in opts}
form_data = apply_model_params_to_body(params, form_data, mappings)
name_differences = {
"max_tokens": "num_predict",
"frequency_penalty": "repeat_penalty",
}
for key, value in name_differences.items():
if (param := params.get(key, None)) is not None:
form_data[value] = param
return form_data
def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
"""
Converts a payload formatted for OpenAI's API to be compatible with Ollama's API endpoint for chat completions.
Args:
openai_payload (dict): The payload originally designed for OpenAI API usage.
Returns:
dict: A modified payload compatible with the Ollama API.
"""
ollama_payload = {}
# Mapping basic model and message details
ollama_payload["model"] = openai_payload.get("model")
ollama_payload["messages"] = openai_payload.get("messages")
ollama_payload["stream"] = openai_payload.get("stream", False)
# If there are advanced parameters in the payload, format them in Ollama's options field
ollama_options = {}
# Handle parameters which map directly
for param in ["temperature", "top_p", "seed"]:
if param in openai_payload:
ollama_options[param] = openai_payload[param]
# Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
if "max_completion_tokens" in openai_payload:
ollama_options["num_predict"] = openai_payload["max_completion_tokens"]
elif "max_tokens" in openai_payload:
ollama_options["num_predict"] = openai_payload["max_tokens"]
# Handle frequency / presence_penalty, which needs renaming and checking
if "frequency_penalty" in openai_payload:
ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"]
if "presence_penalty" in openai_payload and "penalty" not in ollama_options:
# We are assuming presence penalty uses a similar concept in Ollama, which needs custom handling if exists.
ollama_options["new_topic_penalty"] = openai_payload["presence_penalty"]
# Add options to payload if any have been set
if ollama_options:
ollama_payload["options"] = ollama_options
return ollama_payload
+32
View File
@@ -0,0 +1,32 @@
import json
from open_webui.utils.misc import (
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
)
def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
model = ollama_response.get("model", "ollama")
message_content = ollama_response.get("message", {}).get("content", "")
response = openai_chat_completion_message_template(model, message_content)
return response
async def convert_streaming_response_ollama_to_openai(ollama_streaming_response):
async for data in ollama_streaming_response.body_iterator:
data = json.loads(data)
model = data.get("model", "ollama")
message_content = data.get("message", {}).get("content", "")
done = data.get("done", False)
data = openai_chat_chunk_message_template(
model, message_content if not done else None
)
line = f"data: {json.dumps(data)}\n\n"
if done:
line += "data: [DONE]\n\n"
yield line
@@ -0,0 +1,115 @@
import re
import os
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Dict
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers.update(set_security_headers())
return response
def set_security_headers() -> Dict[str, str]:
"""
Sets security headers based on environment variables.
This function reads specific environment variables and uses their values
to set corresponding security headers. The headers that can be set are:
- cache-control
- strict-transport-security
- referrer-policy
- x-content-type-options
- x-download-options
- x-frame-options
- x-permitted-cross-domain-policies
Each environment variable is associated with a specific setter function
that constructs the header. If the environment variable is set, the
corresponding header is added to the options dictionary.
Returns:
dict: A dictionary containing the security headers and their values.
"""
options = {}
header_setters = {
"CACHE_CONTROL": set_cache_control,
"HSTS": set_hsts,
"REFERRER_POLICY": set_referrer,
"XCONTENT_TYPE": set_xcontent_type,
"XDOWNLOAD_OPTIONS": set_xdownload_options,
"XFRAME_OPTIONS": set_xframe,
"XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
}
for env_var, setter in header_setters.items():
value = os.environ.get(env_var, None)
if value:
header = setter(value)
if header:
options.update(header)
return options
# Set HTTP Strict Transport Security(HSTS) response header
def set_hsts(value: str):
pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
match = re.match(pattern, value, re.IGNORECASE)
if not match:
return "max-age=31536000;includeSubDomains"
return {"Strict-Transport-Security": value}
# Set X-Frame-Options response header
def set_xframe(value: str):
pattern = r"^(DENY|SAMEORIGIN)$"
match = re.match(pattern, value, re.IGNORECASE)
if not match:
value = "DENY"
return {"X-Frame-Options": value}
# Set Referrer-Policy response header
def set_referrer(value: str):
pattern = r"^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$"
match = re.match(pattern, value, re.IGNORECASE)
if not match:
value = "no-referrer"
return {"Referrer-Policy": value}
# Set Cache-Control response header
def set_cache_control(value: str):
pattern = r"^(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable)(,\s*(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable))*$"
match = re.match(pattern, value, re.IGNORECASE)
if not match:
value = "no-store, max-age=0"
return {"Cache-Control": value}
# Set X-Download-Options response header
def set_xdownload_options(value: str):
if value != "noopen":
value = "noopen"
return {"X-Download-Options": value}
# Set X-Content-Type-Options response header
def set_xcontent_type(value: str):
if value != "nosniff":
value = "nosniff"
return {"X-Content-Type-Options": value}
# Set X-Permitted-Cross-Domain-Policies response header
def set_xpermitted_cross_domain_policies(value: str):
pattern = r"^(none|master-only|by-content-type|by-ftp-filename)$"
match = re.match(pattern, value, re.IGNORECASE)
if not match:
value = "none"
return {"X-Permitted-Cross-Domain-Policies": value}
+38 -17
View File
@@ -4,6 +4,9 @@ from datetime import datetime
from typing import Optional
from open_webui.utils.misc import get_last_user_message, get_messages_content
def prompt_template(
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
) -> str:
@@ -37,9 +40,7 @@ def prompt_template(
return template
def title_generation_template(
template: str, prompt: str, user: Optional[dict] = None
) -> str:
def replace_prompt_variable(template: str, prompt: str) -> str:
def replacement_function(match):
full_match = match.group(0)
start_length = match.group(1)
@@ -66,7 +67,13 @@ def title_generation_template(
replacement_function,
template,
)
return template
def title_generation_template(
template: str, prompt: str, user: Optional[dict] = None
) -> str:
template = replace_prompt_variable(template, prompt)
template = prompt_template(
template,
**(
@@ -79,36 +86,50 @@ def title_generation_template(
return template
def search_query_generation_template(
template: str, prompt: str, user: Optional[dict] = None
) -> str:
def replace_messages_variable(template: str, messages: list[str]) -> str:
def replacement_function(match):
full_match = match.group(0)
start_length = match.group(1)
end_length = match.group(2)
middle_length = match.group(3)
if full_match == "{{prompt}}":
return prompt
# Process messages based on the number of messages required
if full_match == "{{MESSAGES}}":
return get_messages_content(messages)
elif start_length is not None:
return prompt[: int(start_length)]
return get_messages_content(messages[: int(start_length)])
elif end_length is not None:
return prompt[-int(end_length) :]
return get_messages_content(messages[-int(end_length) :])
elif middle_length is not None:
middle_length = int(middle_length)
if len(prompt) <= middle_length:
return prompt
start = prompt[: math.ceil(middle_length / 2)]
end = prompt[-math.floor(middle_length / 2) :]
return f"{start}...{end}"
mid = int(middle_length)
if len(messages) <= mid:
return get_messages_content(messages)
# Handle middle truncation: split to get start and end portions of the messages list
half = mid // 2
start_msgs = messages[:half]
end_msgs = messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :]
formatted_start = get_messages_content(start_msgs)
formatted_end = get_messages_content(end_msgs)
return f"{formatted_start}\n{formatted_end}"
return ""
template = re.sub(
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
r"{{MESSAGES}}|{{MESSAGES:START:(\d+)}}|{{MESSAGES:END:(\d+)}}|{{MESSAGES:MIDDLETRUNCATE:(\d+)}}",
replacement_function,
template,
)
return template
def search_query_generation_template(
template: str, messages: list[dict], user: Optional[dict] = None
) -> str:
prompt = get_last_user_message(messages)
template = replace_prompt_variable(template, prompt)
template = replace_messages_variable(template, messages)
template = prompt_template(
template,
**(
+5
View File
@@ -40,7 +40,12 @@ langchain-chroma==0.1.2
fake-useragent==1.5.1
chromadb==0.5.5
pymilvus==2.4.6
sentence-transformers==3.0.1
colbert-ai==0.2.21
einops==0.8.0
pypdf==4.3.1
docx2txt==0.8
python-pptx==1.0.0
@@ -0,0 +1,8 @@
resources:
- open-webui.yaml
- ollama-service.yaml
- ollama-statefulset.yaml
- webui-deployment.yaml
- webui-service.yaml
- webui-ingress.yaml
- webui-pvc.yaml
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: ollama-statefulset-gpu.yaml
-13
View File
@@ -1,13 +0,0 @@
resources:
- base/open-webui.yaml
- base/ollama-service.yaml
- base/ollama-statefulset.yaml
- base/webui-deployment.yaml
- base/webui-service.yaml
- base/webui-ingress.yaml
- base/webui-pvc.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
- path: patches/ollama-statefulset-gpu.yaml
+187 -120
View File
@@ -1,18 +1,19 @@
{
"name": "open-webui",
"version": "0.3.19",
"version": "0.3.24",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.3.19",
"version": "0.3.24",
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/theme-one-dark": "^6.1.2",
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
@@ -31,6 +32,7 @@
"katex": "^0.16.9",
"marked": "^9.1.0",
"mermaid": "^10.9.1",
"paneforge": "^0.0.6",
"pyodide": "^0.26.1",
"socket.io-client": "^4.2.0",
"sortablejs": "^1.15.2",
@@ -45,7 +47,6 @@
"@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/typography": "^0.5.13",
"@types/bun": "latest",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
@@ -1166,9 +1167,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
"integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
"cpu": [
"arm"
],
@@ -1178,9 +1179,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
"integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
"cpu": [
"arm64"
],
@@ -1190,9 +1191,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
"integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
"cpu": [
"arm64"
],
@@ -1202,9 +1203,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
"integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
"cpu": [
"x64"
],
@@ -1214,9 +1215,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
"integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
"cpu": [
"arm"
],
@@ -1226,9 +1227,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
"integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
"cpu": [
"arm"
],
@@ -1238,9 +1239,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
"integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
"cpu": [
"arm64"
],
@@ -1250,9 +1251,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
"integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
"cpu": [
"arm64"
],
@@ -1262,9 +1263,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
"integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
"cpu": [
"ppc64"
],
@@ -1274,9 +1275,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
"integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
"cpu": [
"riscv64"
],
@@ -1286,9 +1287,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
"integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
"cpu": [
"s390x"
],
@@ -1298,9 +1299,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
"integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
"cpu": [
"x64"
],
@@ -1310,9 +1311,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
"integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
"cpu": [
"x64"
],
@@ -1322,9 +1323,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
"integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
"cpu": [
"arm64"
],
@@ -1334,9 +1335,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
"integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
"cpu": [
"ia32"
],
@@ -1346,9 +1347,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
"integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
"cpu": [
"x64"
],
@@ -1368,6 +1369,14 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@svelte-put/shortcut": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
"peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@sveltejs/adapter-auto": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz",
@@ -1494,20 +1503,32 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@types/bun": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.0.10.tgz",
"integrity": "sha512-Jaz6YYAdm1u3NVlgSyEK+qGmrlLQ20sbWeEoXD64b9w6z/YKYNWlfaphu+xF2Kiy5Tpykm5Q9jIquLegwXx4ng==",
"dev": true,
"dependencies": {
"bun-types": "1.0.33"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
@@ -1521,11 +1542,33 @@
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
"integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw=="
},
"node_modules/@types/d3-selection": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz",
"integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg=="
},
"node_modules/@types/d3-time": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
},
"node_modules/@types/d3-transition": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz",
"integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -1568,7 +1611,7 @@
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"devOptional": true,
"optional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -1613,15 +1656,6 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -1942,6 +1976,33 @@
"resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz",
"integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g=="
},
"node_modules/@xyflow/svelte": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.19.tgz",
"integrity": "sha512-yW5w5aI+Yqkob4kLQpVDo/ZmX+E9Pw7459kqwLfv4YG4N1NYXrsDRh9cyph/rapbuDnPi6zqK5E8LKrgaCQC0w==",
"dependencies": {
"@svelte-put/shortcut": "^3.1.0",
"@xyflow/system": "0.0.42",
"classcat": "^5.0.4"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.42.tgz",
"integrity": "sha512-kWYj+Y0GOct0jKYTdyRMNOLPxGNbb2TYvPg2gTmJnZ31DOOMkL5uRBLX825DR2gOACDu+i5FHLxPJUPf/eGOJw==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -2533,16 +2594,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bun-types": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.33.tgz",
"integrity": "sha512-L5tBIf9g6rBBkvshqysi5NoLQ9NnhSPU1pfJ9FzqoSfofYdyac3WLUnOIuQ+M5za/sooVUOP2ko+E6Tco0OLIA==",
"dev": true,
"dependencies": {
"@types/node": "~20.11.3",
"@types/ws": "~8.5.10"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -2777,6 +2828,11 @@
"node": ">=8"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -6931,6 +6987,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/paneforge": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz",
"integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==",
"dependencies": {
"nanoid": "^5.0.4"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -7094,9 +7161,9 @@
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -7162,9 +7229,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -7181,8 +7248,8 @@
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -7839,9 +7906,9 @@
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
},
"node_modules/rollup": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
"integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -7853,22 +7920,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.20.0",
"@rollup/rollup-android-arm64": "4.20.0",
"@rollup/rollup-darwin-arm64": "4.20.0",
"@rollup/rollup-darwin-x64": "4.20.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
"@rollup/rollup-linux-arm64-musl": "4.20.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
"@rollup/rollup-linux-x64-gnu": "4.20.0",
"@rollup/rollup-linux-x64-musl": "4.20.0",
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
"@rollup/rollup-win32-x64-msvc": "4.20.0",
"@rollup/rollup-android-arm-eabi": "4.22.4",
"@rollup/rollup-android-arm64": "4.22.4",
"@rollup/rollup-darwin-arm64": "4.22.4",
"@rollup/rollup-darwin-x64": "4.22.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
"@rollup/rollup-linux-arm64-musl": "4.22.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
"@rollup/rollup-linux-x64-gnu": "4.22.4",
"@rollup/rollup-linux-x64-musl": "4.22.4",
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
"@rollup/rollup-win32-x64-msvc": "4.22.4",
"fsevents": "~2.3.2"
}
},
@@ -8195,9 +8262,9 @@
"integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
@@ -8657,11 +8724,11 @@
}
},
"node_modules/svelte-sonner": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
"version": "0.3.28",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz",
"integrity": "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==",
"peerDependencies": {
"svelte": ">=3 <5"
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/svelte/node_modules/estree-walker": {
@@ -9112,7 +9179,7 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"devOptional": true
"optional": true
},
"node_modules/unist-util-stringify-position": {
"version": "3.0.3",
@@ -9329,13 +9396,13 @@
}
},
"node_modules/vite": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.40",
"rollup": "^4.13.0"
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.3.19",
"version": "0.3.24",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -25,7 +25,6 @@
"@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/typography": "^0.5.13",
"@types/bun": "latest",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
@@ -54,6 +53,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
@@ -72,6 +72,7 @@
"katex": "^0.16.9",
"marked": "^9.1.0",
"mermaid": "^10.9.1",
"paneforge": "^0.0.6",
"pyodide": "^0.26.1",
"socket.io-client": "^4.2.0",
"sortablejs": "^1.15.2",
+5
View File
@@ -47,7 +47,12 @@ dependencies = [
"fake-useragent==1.5.1",
"chromadb==0.5.5",
"pymilvus==2.4.6",
"sentence-transformers==3.0.1",
"colbert-ai==0.2.21",
"einops==0.8.0",
"pypdf==4.3.1",
"docx2txt==0.8",
"python-pptx==1.0.0",
+17 -15
View File
@@ -50,21 +50,6 @@ iframe {
@apply rounded-lg;
}
ol > li {
counter-increment: list-number;
display: block;
margin-bottom: 0;
margin-top: 0;
min-height: 28px;
}
.prose ol > li::before {
content: counters(list-number, '.') '.';
padding-right: 0.5rem;
color: var(--tw-prose-counters);
font-weight: 400;
}
li p {
display: inline;
}
@@ -171,3 +156,20 @@ input[type='number'] {
font-weight: 600;
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
}
.svelte-flow {
background-color: transparent !important;
}
.svelte-flow__edge > path {
stroke-width: 0.5;
}
.svelte-flow__edge.animated > path {
stroke-width: 2;
@apply stroke-gray-600 dark:stroke-gray-500;
}
.bg-gray-950-90 {
background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
}
+20 -14
View File
@@ -4,7 +4,11 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
/>
<meta name="theme-color" content="#171717" />
<meta name="robots" content="noindex,nofollow" />
<meta name="description" content="Open WebUI" />
<link
@@ -23,33 +27,33 @@
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => {
const metaThemeColorTag = document.querySelector('meta[name="theme-color"]');
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (!localStorage?.theme) {
localStorage.theme = 'system';
}
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
if (localStorage.theme === 'system') {
document.documentElement.classList.add(prefersDarkTheme ? 'dark' : 'light');
metaThemeColorTag.setAttribute('content', prefersDarkTheme ? '#171717' : '#ffffff');
} else if (localStorage.theme === 'oled-dark') {
document.documentElement.style.setProperty('--color-gray-800', '#101010');
document.documentElement.style.setProperty('--color-gray-850', '#050505');
document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark');
} else if (
localStorage.theme === 'light' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) {
metaThemeColorTag.setAttribute('content', '#000000');
} else if (localStorage.theme === 'light') {
document.documentElement.classList.add('light');
} else if (localStorage.theme && localStorage.theme !== 'system') {
localStorage.theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
});
} else if (localStorage.theme && localStorage.theme === 'system') {
systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(systemTheme ? 'dark' : 'light');
} else if (localStorage.theme && localStorage.theme === 'her') {
metaThemeColorTag.setAttribute('content', '#ffffff');
} else if (localStorage.theme === 'her') {
document.documentElement.classList.add('dark');
document.documentElement.classList.add('her');
metaThemeColorTag.setAttribute('content', '#983724');
} else {
document.documentElement.classList.add('dark');
metaThemeColorTag.setAttribute('content', '#171717');
}
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
@@ -57,9 +61,11 @@
if (e.matches) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
metaThemeColorTag.setAttribute('content', '#171717');
} else {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
metaThemeColorTag.setAttribute('content', '#ffffff');
}
}
});
+69 -1
View File
@@ -31,6 +31,8 @@
let TTS_MODEL = '';
let TTS_VOICE = '';
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
let TTS_AZURE_SPEECH_REGION = '';
let TTS_AZURE_SPEECH_OUTPUT_FORMAT = '';
let STT_OPENAI_API_BASE_URL = '';
let STT_OPENAI_API_KEY = '';
@@ -87,7 +89,9 @@
ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL,
VOICE: TTS_VOICE,
SPLIT_ON: TTS_SPLIT_ON
SPLIT_ON: TTS_SPLIT_ON,
AZURE_SPEECH_REGION: TTS_AZURE_SPEECH_REGION,
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT
},
stt: {
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
@@ -120,6 +124,9 @@
TTS_SPLIT_ON = res.tts.SPLIT_ON || TTS_RESPONSE_SPLIT.PUNCTUATION;
TTS_AZURE_SPEECH_OUTPUT_FORMAT = res.tts.AZURE_SPEECH_OUTPUT_FORMAT;
TTS_AZURE_SPEECH_REGION = res.tts.AZURE_SPEECH_REGION;
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
@@ -224,6 +231,7 @@
<option value="">{$i18n.t('Web API')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
</select>
</div>
</div>
@@ -252,6 +260,23 @@
/>
</div>
</div>
{:else if TTS_ENGINE === 'azure'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={TTS_API_KEY}
required
/>
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Azure Region')}
bind:value={TTS_AZURE_SPEECH_REGION}
required
/>
</div>
</div>
{/if}
<hr class=" dark:border-gray-850 my-2" />
@@ -359,6 +384,49 @@
</div>
</div>
</div>
{:else if TTS_ENGINE === 'azure'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">
{$i18n.t('Output format')}
<a
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
target="_blank"
>
<small>{$i18n.t('Available list')}</small>
</a>
</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
placeholder="Select a output format"
/>
</div>
</div>
</div>
</div>
{/if}
<hr class="dark:border-gray-850 my-2" />
@@ -150,18 +150,20 @@
})()
]);
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
const res = await getOpenAIModels(localStorage.token, idx);
if (res.pipelines) {
pipelineUrls[url] = true;
}
});
const ollamaConfig = await getOllamaConfig(localStorage.token);
const openaiConfig = await getOpenAIConfig(localStorage.token);
ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
if (ENABLE_OPENAI_API) {
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
const res = await getOpenAIModels(localStorage.token, idx);
if (res.pipelines) {
pipelineUrls[url] = true;
}
});
}
}
});
</script>
@@ -732,11 +732,17 @@
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea
bind:value={querySettings.template}
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="4"
/>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<textarea
bind:value={querySettings.template}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="4"
/>
</Tooltip>
</div>
</div>
@@ -27,6 +27,43 @@
let models = null;
let samplers = [
'DPM++ 2M',
'DPM++ SDE',
'DPM++ 2M SDE',
'DPM++ 2M SDE Heun',
'DPM++ 2S a',
'DPM++ 3M SDE',
'Euler a',
'Euler',
'LMS',
'Heun',
'DPM2',
'DPM2 a',
'DPM fast',
'DPM adaptive',
'Restart',
'DDIM',
'DDIM CFG++',
'PLMS',
'UniPC'
];
let schedulers = [
'Automatic',
'Uniform',
'Karras',
'Exponential',
'Polyexponential',
'SGM Uniform',
'KL Optimal',
'Align Your Steps',
'Simple',
'Normal',
'DDIM',
'Beta'
];
let requiredWorkflowNodes = [
{
type: 'prompt',
@@ -326,6 +363,66 @@
</a>
</div>
</div>
<!---Sampler-->
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
<input
list="sampler-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
/>
<datalist id="sampler-list">
{#each samplers ?? [] as sampler}
<option value={sampler}>{sampler}</option>
{/each}
</datalist>
</Tooltip>
</div>
</div>
</div>
<!---Scheduler-->
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
<input
list="scheduler-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
/>
<datalist id="scheduler-list">
{#each schedulers ?? [] as scheduler}
<option value={scheduler}>{scheduler}</option>
{/each}
</datalist>
</Tooltip>
</div>
</div>
</div>
<!---CFG scale-->
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
/>
</Tooltip>
</div>
</div>
</div>
{:else if config?.engine === 'comfyui'}
<div class="">
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
@@ -23,8 +23,8 @@
TASK_MODEL: '',
TASK_MODEL_EXTERNAL: '',
TITLE_GENERATION_PROMPT_TEMPLATE: '',
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: '',
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: 0
ENABLE_SEARCH_QUERY: true,
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: ''
};
let promptSuggestions = [];
@@ -43,7 +43,6 @@
taskConfig = await getTaskConfig(localStorage.token);
promptSuggestions = $config?.default_prompt_suggestions;
banners = await getBanners(localStorage.token);
});
@@ -119,33 +118,50 @@
</div>
<div class="mt-3">
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
<textarea
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="6"
/>
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<textarea
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="3"
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
/>
</Tooltip>
</div>
<div class="mt-3">
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
<textarea
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="6"
/>
</div>
<hr class=" dark:border-gray-850 my-3" />
<div class="mt-3">
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('Search Query Generation Prompt Length Threshold')}
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Web Search Query Generation')}
</div>
<input
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
type="number"
/>
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY} />
</div>
{#if taskConfig.ENABLE_SEARCH_QUERY}
<div class="">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<textarea
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="3"
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
{/if}
</div>
<hr class=" dark:border-gray-850 my-3" />
@@ -291,9 +307,10 @@
/>
</div>
<input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
<textarea
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
rows="3"
bind:value={prompt.content}
/>
</div>
File diff suppressed because it is too large Load Diff
+148 -58
View File
@@ -1,14 +1,20 @@
<script lang="ts">
import { SvelteFlowProvider } from '@xyflow/svelte';
import { slide } from 'svelte/transition';
import { onDestroy, onMount } from 'svelte';
import { mobile, showControls, showCallOverlay, showOverview } from '$lib/stores';
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import { onMount } from 'svelte';
import { mobile, showCallOverlay } from '$lib/stores';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import Drawer from '../common/Drawer.svelte';
import Overview from './Overview.svelte';
import { Pane, PaneResizer } from 'paneforge';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
import { get } from 'svelte/store';
export let show = false;
export let history;
export let models = [];
export let chatId = null;
@@ -18,10 +24,13 @@
export let eventTarget: EventTarget;
export let submitPrompt: Function;
export let stopResponse: Function;
export let showMessage: Function;
export let files;
export let modelId;
export let pane;
let largeScreen = false;
onMount(() => {
// listen to resize 1024px
const mediaQuery = window.matchMedia('(min-width: 1024px)');
@@ -31,6 +40,7 @@
largeScreen = true;
} else {
largeScreen = false;
pane = null;
}
};
@@ -42,62 +52,142 @@
mediaQuery.removeEventListener('change', handleMediaQuery);
};
});
onDestroy(() => {
showControls.set(false);
});
$: if (!chatId) {
showOverview.set(false);
}
</script>
{#if !largeScreen}
{#if show}
<Drawer bind:show>
<div class=" px-6 py-4 h-full">
<Controls
on:close={() => {
show = false;
}}
{models}
bind:chatFiles
bind:params
/>
<SvelteFlowProvider>
{#if !largeScreen}
{#if $showControls}
<Drawer
show={$showControls}
on:close={() => {
showControls.set(false);
}}
>
<div
class=" {$showCallOverlay || $showOverview ? ' h-screen w-screen' : 'px-6 py-4'} h-full"
>
{#if $showCallOverlay}
<div
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</Drawer>
{/if}
{:else}
<!-- if $showControls -->
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
<EllipsisVertical className="size-4 invisible group-hover:visible" />
</div>
</Drawer>
{/if}
</PaneResizer>
<Pane
bind:pane
defaultSize={$showControls
? parseInt(localStorage?.chatControlsSize ?? '30')
? parseInt(localStorage?.chatControlsSize ?? '30')
: 30
: 0}
onResize={(size) => {
if (size === 0) {
showControls.set(false);
} else {
if (!$showControls) {
showControls.set(true);
}
localStorage.chatControlsSize = size;
}
}}
>
{#if $showControls}
<div class="pr-4 pb-8 flex max-h-full min-h-full">
<div
class="w-full {$showOverview && !$showCallOverlay
? ' '
: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
{#if $showCallOverlay}
<div class="w-full h-full flex justify-center">
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
showControls.set(false);
}}
/>
</div>
{:else if $showOverview}
<Overview
{history}
on:nodeclick={(e) => {
if (e.detail.node.data.message.favorite) {
history.messages[e.detail.node.data.message.id].favorite = true;
} else {
history.messages[e.detail.node.data.message.id].favorite = null;
}
{#if $showCallOverlay}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
<div
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
show = false;
}}
/>
</div>
</div>
showMessage(e.detail.node.data.message);
}}
on:close={() => {
showControls.set(false);
}}
/>
{:else}
<Controls
on:close={() => {
showControls.set(false);
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</div>
{/if}
</Pane>
{/if}
{:else if show}
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
<div
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
{#if $showCallOverlay}
<CallOverlay bind:files {submitPrompt} {stopResponse} {modelId} {chatId} {eventTarget} />
{:else}
<Controls
on:close={() => {
show = false;
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</div>
</div>
{/if}
</SvelteFlowProvider>
+31 -22
View File
@@ -41,6 +41,7 @@
export let transparentBackground = false;
export let submitPrompt: Function;
export let createMessagePair: Function;
export let stopResponse: Function;
export let autoScroll = false;
@@ -61,15 +62,14 @@
let user = null;
let chatInputPlaceholder = '';
export let files = [];
export let history;
export let prompt = '';
export let files = [];
export let availableToolIds = [];
export let selectedToolIds = [];
export let webSearchEnabled = false;
export let prompt = '';
export let messages = [];
let visionCapableModels = [];
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
@@ -93,20 +93,6 @@
const uploadFileHandler = async (file) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
const fileItem = {
type: 'file',
file: '',
@@ -120,6 +106,23 @@
};
files = [...files, fileItem];
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
fileItem.name = file.name;
fileItem.size = file.size;
}
}
try {
const uploadedFile = await uploadFile(localStorage.token, file);
@@ -269,7 +272,7 @@
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative">
{#if autoScroll === false && messages.length > 0}
{#if autoScroll === false && history?.currentId}
<div
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
>
@@ -349,7 +352,7 @@
</div>
<div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0 pb-safe-bottom">
<div class=" pb-2">
<input
bind:this={filesInputElement}
@@ -552,6 +555,12 @@
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
const commandsContainerElement = document.getElementById('commands-container');
// Command/Ctrl + Shift + Enter to submit a message pair
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
e.preventDefault();
createMessagePair(prompt);
}
// Check if Ctrl + R is pressed
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
e.preventDefault();
@@ -689,7 +698,7 @@
/>
<div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true}
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
<Tooltip content={$i18n.t('Record voice')}>
<button
id="voice-input-button"
@@ -741,7 +750,7 @@
</div>
</div>
<div class="flex items-end w-10">
{#if messages.length == 0 || messages.at(-1).done == true}
{#if !history.currentId || history.messages[history.currentId]?.done == true}
{#if prompt === ''}
<div class=" flex items-center mb-1">
<Tooltip content={$i18n.t('Call')}>
@@ -1,15 +1,13 @@
<script lang="ts">
import { config, models, settings, showCallOverlay } from '$lib/stores';
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
import { DropdownMenu } from 'bits-ui';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import { flyAndScale } from '$lib/utils/transitions';
const dispatch = createEventDispatcher();
import {
blobToFile,
calculateSHA256,
extractSentencesForAudio,
findWordIndices
} from '$lib/utils';
import { blobToFile } from '$lib/utils';
import { generateEmoji } from '$lib/apis';
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
@@ -222,7 +220,9 @@
};
const startRecording = async () => {
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
if (!audioStream) {
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
}
mediaRecorder = new MediaRecorder(audioStream);
mediaRecorder.onstart = () => {
@@ -238,7 +238,7 @@
};
mediaRecorder.onstop = (e) => {
console.log('Recording stopped', e);
console.log('Recording stopped', audioStream, e);
stopRecordingCallback();
};
@@ -246,10 +246,11 @@
};
const stopAudioStream = async () => {
if (audioStream) {
const tracks = audioStream.getTracks();
tracks.forEach((track) => track.stop());
}
if (!audioStream) return;
audioStream.getAudioTracks().forEach(function (track) {
track.stop();
});
audioStream = null;
};
@@ -360,6 +361,7 @@
?.at(0) ?? undefined;
currentUtterance = new SpeechSynthesisUtterance(content);
currentUtterance.rate = $settings.audio?.tts?.playbackRate ?? 1;
if (voice) {
currentUtterance.voice = voice;
@@ -381,11 +383,12 @@
const playAudio = (audio) => {
if ($showCallOverlay) {
return new Promise((resolve) => {
const audioElement = document.getElementById('audioElement');
const audioElement = document.getElementById('audioElement') as HTMLAudioElement;
if (audioElement) {
audioElement.src = audio.src;
audioElement.muted = true;
audioElement.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
audioElement
.play()
@@ -525,6 +528,60 @@
console.log(`Audio monitoring and playing stopped for message ID ${id}`);
};
const chatStartHandler = async (e) => {
const { id } = e.detail;
chatStreaming = true;
if (currentMessageId !== id) {
console.log(`Received chat start event for message ID ${id}`);
currentMessageId = id;
if (audioAbortController) {
audioAbortController.abort();
}
audioAbortController = new AbortController();
assistantSpeaking = true;
// Start monitoring and playing audio for the message ID
monitorAndPlayAudio(id, audioAbortController.signal);
}
};
const chatEventHandler = async (e) => {
const { id, content } = e.detail;
// "id" here is message id
// if "id" is not the same as "currentMessageId" then do not process
// "content" here is a sentence from the assistant,
// there will be many sentences for the same "id"
if (currentMessageId === id) {
console.log(`Received chat event for message ID ${id}: ${content}`);
try {
if (messages[id] === undefined) {
messages[id] = [content];
} else {
messages[id].push(content);
}
console.log(content);
fetchAudio(content);
} catch (error) {
console.error('Failed to fetch or play audio:', error);
}
}
};
const chatFinishHandler = async (e) => {
const { id, content } = e.detail;
// "content" here is the entire message from the assistant
finishedMessages[id] = true;
chatStreaming = false;
};
onMount(async () => {
const setWakeLock = async () => {
try {
@@ -558,65 +615,15 @@
startRecording();
const chatStartHandler = async (e) => {
const { id } = e.detail;
chatStreaming = true;
if (currentMessageId !== id) {
console.log(`Received chat start event for message ID ${id}`);
currentMessageId = id;
if (audioAbortController) {
audioAbortController.abort();
}
audioAbortController = new AbortController();
assistantSpeaking = true;
// Start monitoring and playing audio for the message ID
monitorAndPlayAudio(id, audioAbortController.signal);
}
};
const chatEventHandler = async (e) => {
const { id, content } = e.detail;
// "id" here is message id
// if "id" is not the same as "currentMessageId" then do not process
// "content" here is a sentence from the assistant,
// there will be many sentences for the same "id"
if (currentMessageId === id) {
console.log(`Received chat event for message ID ${id}: ${content}`);
try {
if (messages[id] === undefined) {
messages[id] = [content];
} else {
messages[id].push(content);
}
console.log(content);
fetchAudio(content);
} catch (error) {
console.error('Failed to fetch or play audio:', error);
}
}
};
const chatFinishHandler = async (e) => {
const { id, content } = e.detail;
// "content" here is the entire message from the assistant
finishedMessages[id] = true;
chatStreaming = false;
};
eventTarget.addEventListener('chat:start', chatStartHandler);
eventTarget.addEventListener('chat', chatEventHandler);
eventTarget.addEventListener('chat:finish', chatFinishHandler);
return async () => {
await stopAllAudio();
stopAudioStream();
eventTarget.removeEventListener('chat:start', chatStartHandler);
eventTarget.removeEventListener('chat', chatEventHandler);
eventTarget.removeEventListener('chat:finish', chatFinishHandler);
@@ -633,6 +640,17 @@
onDestroy(async () => {
await stopAllAudio();
stopAudioStream();
eventTarget.removeEventListener('chat:start', chatStartHandler);
eventTarget.removeEventListener('chat', chatEventHandler);
eventTarget.removeEventListener('chat:finish', chatFinishHandler);
audioAbortController.abort();
await tick();
await stopAllAudio();
await stopRecordingCallback(false);
await stopCamera();
});
@@ -922,8 +940,12 @@
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
on:click={async () => {
stopAudioStream();
stopVideoStream();
await stopAudioStream();
await stopVideoStream();
console.log(audioStream);
console.log(cameraStream);
showCallOverlay.set(false);
dispatch('close');
}}
@@ -1,6 +1,14 @@
<script lang="ts">
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import {
findWordIndices,
getUserPosition,
getFormattedDate,
getFormattedTime,
getCurrentDateTime,
getUserTimezone,
getWeekday
} from '$lib/utils';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
@@ -39,8 +47,6 @@
return '{{CLIPBOARD}}';
});
console.log(clipboardText);
const clipboardItems = await navigator.clipboard.read();
let imageUrl = null;
@@ -50,7 +56,6 @@
if (type.startsWith('image/')) {
const blob = await item.getType(type);
imageUrl = URL.createObjectURL(blob);
console.log(`Image URL (${type}): ${imageUrl}`);
}
}
}
@@ -65,7 +70,42 @@
];
}
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
}
if (command.content.includes('{{USER_LOCATION}}')) {
const location = await getUserPosition();
text = text.replaceAll('{{USER_LOCATION}}', String(location));
}
if (command.content.includes('{{USER_LANGUAGE}}')) {
const language = localStorage.getItem('locale') || 'en-US';
text = text.replaceAll('{{USER_LANGUAGE}}', language);
}
if (command.content.includes('{{CURRENT_DATE}}')) {
const date = getFormattedDate();
text = text.replaceAll('{{CURRENT_DATE}}', date);
}
if (command.content.includes('{{CURRENT_TIME}}')) {
const time = getFormattedTime();
text = text.replaceAll('{{CURRENT_TIME}}', time);
}
if (command.content.includes('{{CURRENT_DATETIME}}')) {
const dateTime = getCurrentDateTime();
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
}
if (command.content.includes('{{CURRENT_TIMEZONE}}')) {
const timezone = getUserTimezone();
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
}
if (command.content.includes('{{CURRENT_WEEKDAY}}')) {
const weekday = getWeekday();
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
}
prompt = text;
@@ -9,7 +9,7 @@
let prompts = [];
$: prompts = suggestionPrompts
$: prompts = (suggestionPrompts ?? [])
.reduce((acc, current) => [...acc, ...[current]], [])
.sort(() => Math.random() - 0.5);
// suggestionPrompts.length <= 4
@@ -44,6 +44,7 @@
return `${minutes}:${formattedSeconds}`;
};
let stream;
let speechRecognition;
let mediaRecorder;
@@ -159,7 +160,7 @@
const startRecording = async () => {
startDurationCounter();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.onstart = () => {
console.log('Recording started');
@@ -251,6 +252,13 @@
}
stopDurationCounter();
audioChunks = [];
if (stream) {
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
};
const confirmRecording = async () => {
+198 -218
View File
@@ -7,30 +7,63 @@
import { getChatList, updateChatById } from '$lib/apis/chats';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import UserMessage from './Messages/UserMessage.svelte';
import ResponseMessage from './Messages/ResponseMessage.svelte';
import Placeholder from './Messages/Placeholder.svelte';
import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
import Message from './Messages/Message.svelte';
import Loader from '../common/Loader.svelte';
import Spinner from '../common/Spinner.svelte';
const i18n = getContext('i18n');
export let chatId = '';
export let readOnly = false;
export let user = $_user;
export let prompt;
export let history = {};
export let selectedModels;
let messages = [];
export let sendPrompt: Function;
export let continueGeneration: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
export let mergeResponses: Function;
export let chatActionHandler: Function;
export let showMessage: Function = () => {};
export let readOnly = false;
export let user = $_user;
export let prompt;
export let processing = '';
export let bottomPadding = false;
export let autoScroll;
export let history = {};
export let messages = [];
export let selectedModels;
let messagesCount = 20;
let messagesLoading = false;
const loadMoreMessages = async () => {
// scroll slightly down to disable continuous loading
const element = document.getElementById('messages-container');
element.scrollTop = element.scrollTop + 100;
messagesLoading = true;
messagesCount += 20;
await tick();
messagesLoading = false;
};
$: if (history.currentId) {
let _messages = [];
let message = history.messages[history.currentId];
while (message && _messages.length <= messagesCount) {
_messages.unshift({ ...message });
message = message.parentId !== null ? history.messages[message.parentId] : null;
}
messages = _messages;
} else {
messages = [];
}
$: if (autoScroll && bottomPadding) {
(async () => {
@@ -44,56 +77,9 @@
element.scrollTop = element.scrollHeight;
};
const copyToClipboardWithToast = async (text) => {
const res = await copyToClipboard(text);
if (res) {
toast.success($i18n.t('Copying to clipboard was successful!'));
}
};
const confirmEditMessage = async (messageId, content, submit = true) => {
if (submit) {
let userPrompt = content;
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels
};
let messageParentId = history.messages[messageId].parentId;
if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
} else {
history.messages[messageId].content = content;
await tick();
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}
};
const updateChatMessages = async () => {
const updateChatHistory = async () => {
await tick();
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
@@ -101,22 +87,6 @@
await chats.set(await getChatList(localStorage.token, $currentChatPage));
};
const confirmEditResponseMessage = async (messageId, content) => {
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await updateChatMessages();
};
const rateMessage = async (messageId, rating) => {
history.messages[messageId].annotation = {
...history.messages[messageId].annotation,
rating: rating
};
await updateChatMessages();
};
const showPreviousMessage = async (message) => {
if (message.parentId !== null) {
let messageId =
@@ -215,60 +185,129 @@
}
};
const deleteMessageHandler = async (messageId) => {
const messageToDelete = history.messages[messageId];
const rateMessage = async (messageId, rating) => {
history.messages[messageId].annotation = {
...history.messages[messageId].annotation,
rating: rating
};
await updateChatHistory();
};
const editMessage = async (messageId, content, submit = true) => {
if (history.messages[messageId].role === 'user') {
if (submit) {
// New user message
let userPrompt = content;
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels
};
let messageParentId = history.messages[messageId].parentId;
if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
} else {
// Edit user message
history.messages[messageId].content = content;
await updateChatHistory();
}
} else {
if (submit) {
// New response message
const responseMessageId = uuidv4();
const message = history.messages[messageId];
const parentId = message.parentId;
const responseMessage = {
...message,
id: responseMessageId,
parentId: parentId,
childrenIds: [],
content: content,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
await updateChatHistory();
} else {
// Edit response message
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await updateChatHistory();
}
}
};
const deleteMessage = async (messageId) => {
const messageToDelete = history.messages[messageId];
const parentMessageId = messageToDelete.parentId;
const childMessageIds = messageToDelete.childrenIds ?? [];
const hasDescendantMessages = childMessageIds.some(
(childId) => history.messages[childId]?.childrenIds?.length > 0
// Collect all grandchildren
const grandchildrenIds = childMessageIds.flatMap(
(childId) => history.messages[childId]?.childrenIds ?? []
);
history.currentId = parentMessageId;
await tick();
// Update parent's children
if (parentMessageId && history.messages[parentMessageId]) {
history.messages[parentMessageId].childrenIds = [
...history.messages[parentMessageId].childrenIds.filter((id) => id !== messageId),
...grandchildrenIds
];
}
// Remove the message itself from the parent message's children array
history.messages[parentMessageId].childrenIds = history.messages[
parentMessageId
].childrenIds.filter((id) => id !== messageId);
await tick();
childMessageIds.forEach((childId) => {
const childMessage = history.messages[childId];
if (childMessage && childMessage.childrenIds) {
if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
// If there are no other responses/prompts
history.messages[parentMessageId].childrenIds = [];
} else {
childMessage.childrenIds.forEach((grandChildId) => {
if (history.messages[grandChildId]) {
history.messages[grandChildId].parentId = parentMessageId;
history.messages[parentMessageId].childrenIds.push(grandChildId);
}
});
}
// Update grandchildren's parent
grandchildrenIds.forEach((grandchildId) => {
if (history.messages[grandchildId]) {
history.messages[grandchildId].parentId = parentMessageId;
}
});
// Remove child message id from the parent message's children array
history.messages[parentMessageId].childrenIds = history.messages[
parentMessageId
].childrenIds.filter((id) => id !== childId);
// Delete the message and its children
[messageId, ...childMessageIds].forEach((id) => {
delete history.messages[id];
});
await tick();
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
showMessage({ id: parentMessageId });
// Update the chat
await updateChatHistory();
};
</script>
<div class="h-full flex">
{#if messages.length == 0}
{#if Object.keys(history?.messages ?? {}).length == 0}
<Placeholder
modelIds={selectedModels}
submitPrompt={async (p) => {
@@ -309,114 +348,55 @@
{:else}
<div class="w-full pt-2">
{#key chatId}
{#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
<div
class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group"
<div class="w-full">
{#if messages.at(0)?.parentId !== null}
<Loader
on:visible={(e) => {
console.log('visible');
if (!messagesLoading) {
loadMoreMessages();
}
}}
>
{#if message.role === 'user'}
<UserMessage
on:delete={() => deleteMessageHandler(message.id)}
{user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? (history.messages[message.parentId]?.childrenIds ?? [])
: (Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? [])}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{#key message.id && history.currentId}
<ResponseMessage
{message}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{updateChatMessages}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:action={async (e) => {
console.log('action', e);
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/key}
{:else}
{#key message.parentId}
<MultiResponseMessages
bind:history
isLastMessage={messageIdx + 1 === messages.length}
{messages}
{readOnly}
{chatId}
parentMessage={history.messages[message.parentId]}
{messageIdx}
{updateChatMessages}
{confirmEditResponseMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{mergeResponses}
{regenerateResponse}
on:action={async (e) => {
console.log('action', e);
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:change={async () => {
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
if (autoScroll) {
const element = document.getElementById('messages-container');
autoScroll =
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
}}
/>
{/key}
{/if}
</div>
</div>
{/each}
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
{#each messages as message, messageIdx (message.id)}
<Message
{chatId}
bind:history
messageId={message.id}
idx={messageIdx}
{user}
{showPreviousMessage}
{showNextMessage}
{editMessage}
{deleteMessage}
{rateMessage}
{regenerateResponse}
{continueResponse}
{mergeResponses}
{updateChatHistory}
{chatActionHandler}
{readOnly}
on:scroll={() => {
if (autoScroll) {
const element = document.getElementById('messages-container');
autoScroll =
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
}
}}
/>
{/each}
</div>
<div class="pb-12" />
{#if bottomPadding}
<div class=" pb-6" />
{/if}
@@ -3,14 +3,9 @@
export let citations = [];
let showCitationModal = false;
let selectedCitation = null;
</script>
let _citations = [];
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
{#each citations.reduce((acc, citation) => {
$: _citations = citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
@@ -31,26 +26,42 @@
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
acc.push({
id: id,
source: source,
document: [document],
metadata: metadata ? [metadata] : []
});
}
});
return acc;
}, []) as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<button
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
{idx + 1}
</div>
<div class="flex-1 mx-2 line-clamp-1">
{citation.source.name}
</div>
</button>
</div>
{/each}
</div>
}, []);
let showCitationModal = false;
let selectedCitation = null;
</script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
{#if _citations.length > 0}
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
{#each _citations as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<button
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
{idx + 1}
</div>
<div class="flex-1 mx-2 line-clamp-1">
{citation.source.name}
</div>
</button>
</div>
{/each}
</div>
{/if}
@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let show = false;
@@ -55,18 +56,28 @@
</div>
{#if document.source?.name}
<div class="text-sm dark:text-gray-400">
<a
href={document?.metadata?.file_id
? `/api/v1/files/${document?.metadata?.file_id}/content`
: document.source.name.includes('http')
? document.source.name
: `#`}
target="_blank"
>
{document?.metadata?.name ?? document.source.name}
</a>
</div>
<Tooltip
content={$i18n.t('Open file')}
placement="left"
tippyOptions={{ duration: [500, 0], animation: 'perspective' }}
>
<div class="text-sm dark:text-gray-400">
<a
class="hover:text-gray-500 hover:dark:text-gray-100 underline"
href={document?.metadata?.file_id
? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
: document.source.name.includes('http')
? document.source.name
: `#`}
target="_blank"
>
{document?.metadata?.name ?? document.source.name}
</a>
{document?.metadata?.page
? `(${$i18n.t('page')} ${document.metadata.page + 1})`
: ''}
</div>
</Tooltip>
{:else}
<div class="text-sm dark:text-gray-400">
{$i18n.t('No source available')}
@@ -20,6 +20,8 @@
export let lang = '';
export let code = '';
let _token = null;
let mermaidHtml = null;
let highlightedCode = null;
@@ -226,7 +228,7 @@ __builtins__.input = input`);
}
};
$: if (token.raw) {
const render = async () => {
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
(async () => {
await drawMermaidDiagram();
@@ -242,9 +244,21 @@ __builtins__.input = input`);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
}
};
$: if (token) {
if (JSON.stringify(token) !== JSON.stringify(_token)) {
console.log('hi');
_token = token;
}
}
$: if (_token) {
render();
}
onMount(async () => {
console.log('codeblock', lang, code);
if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({
startOnLoad: true,
@@ -30,7 +30,13 @@
{token.text}
{/if}
{:else if token.type === 'link'}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
{#if token.tokens}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>
<svelte:self id={`${id}-a`} tokens={token.tokens} />
</a>
{:else}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
{/if}
{:else if token.type === 'image'}
<Image src={token.href} alt={token.text} />
{:else if token.type === 'strong'}
@@ -19,7 +19,7 @@
</script>
<!-- {JSON.stringify(tokens)} -->
{#each tokens as token, tokenIdx}
{#each tokens as token, tokenIdx (tokenIdx)}
{#if token.type === 'hr'}
<hr />
{:else if token.type === 'heading'}
@@ -34,34 +34,36 @@
code={revertSanitizedResponseContent(token?.text ?? '')}
/>
{:else if token.type === 'table'}
<table>
<thead>
<tr>
{#each token.header as header, headerIdx}
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
/>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each token.rows as row, rowIdx}
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
<table class="w-full">
<thead>
<tr>
{#each row ?? [] as cell, cellIdx}
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
{#each token.header as header, headerIdx}
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
/>
</td>
</th>
{/each}
</tr>
{/each}
</tbody>
</table>
</thead>
<tbody>
{#each token.rows as row, rowIdx}
<tr>
{#each row ?? [] as cell, cellIdx}
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
{:else if token.type === 'blockquote'}
<blockquote>
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
@@ -142,7 +144,7 @@
/>
{/if}
{:else if token.type === 'space'}
<div class="my-0.5" />
<div class="my-2" />
{:else}
{console.log('Unknown token', token)}
{/if}
@@ -0,0 +1,166 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { settings } from '$lib/stores';
import { copyToClipboard } from '$lib/utils';
import MultiResponseMessages from './MultiResponseMessages.svelte';
import ResponseMessage from './ResponseMessage.svelte';
import UserMessage from './UserMessage.svelte';
import { updateChatById } from '$lib/apis/chats';
export let chatId;
export let idx = 0;
export let history;
export let messageId;
export let user;
export let updateChatHistory;
export let chatActionHandler;
export let showPreviousMessage;
export let showNextMessage;
export let editMessage;
export let deleteMessage;
export let rateMessage;
export let regenerateResponse;
export let continueResponse;
// MultiResponseMessages
export let mergeResponses;
export let autoScroll = false;
export let readOnly = false;
onMount(() => {
// console.log('message', idx);
});
</script>
<div
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group"
>
{#if history.messages[messageId]}
{#if history.messages[messageId].role === 'user'}
<UserMessage
{user}
{history}
{messageId}
isFirstMessage={idx === 0}
siblings={history.messages[messageId].parentId !== null
? (history.messages[history.messages[messageId].parentId]?.childrenIds ?? [])
: (Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? [])}
{showPreviousMessage}
{showNextMessage}
{editMessage}
on:delete={() => deleteMessage(messageId)}
{readOnly}
/>
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
<ResponseMessage
{history}
{messageId}
isLastMessage={messageId === history.currentId}
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
{showPreviousMessage}
{showNextMessage}
{editMessage}
{rateMessage}
{continueResponse}
{regenerateResponse}
on:action={async (e) => {
console.log('action', e);
const message = history.messages[messageId];
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:update={async (e) => {
console.log('update', e);
updateChatHistory();
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
if (message) {
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
history: history
});
} else {
await updateChatById(localStorage.token, chatId, {
history: history
});
}
}}
{readOnly}
/>
{:else}
<MultiResponseMessages
bind:history
{chatId}
{messageId}
isLastMessage={messageId === history?.currentId}
{rateMessage}
{editMessage}
{continueResponse}
{regenerateResponse}
{mergeResponses}
on:action={async (e) => {
console.log('action', e);
const message = history.messages[messageId];
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:update={async (e) => {
console.log('update', e);
updateChatHistory();
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
if (message) {
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
history: history
});
} else {
await updateChatById(localStorage.token, chatId, {
history: history
});
}
}}
on:change={async () => {
await tick();
await updateChatById(localStorage.token, chatId, {
history: history
});
dispatch('scroll');
}}
{readOnly}
/>
{/if}
{/if}
</div>
@@ -20,40 +20,39 @@
const i18n = getContext('i18n');
export let chatId;
export let history;
export let messages = [];
export let messageIdx;
export let messageId;
export let parentMessage;
export let isLastMessage;
export let readOnly = false;
export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function;
export let editMessage: Function;
export let rateMessage: Function;
export let copyToClipboard: Function;
export let continueGeneration: Function;
export let mergeResponses: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
export let mergeResponses: Function;
const dispatch = createEventDispatcher();
let currentMessageId;
let groupedMessages = {};
let groupedMessagesIdx = {};
let parentMessage;
let groupedMessageIds = {};
let groupedMessageIdsIdx = {};
$: if (parentMessage) {
initHandler();
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
$: if (history.messages) {
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
message = JSON.parse(JSON.stringify(history.messages[messageId]));
}
}
const showPreviousMessage = (modelIdx) => {
groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1);
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
console.log(messageId);
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
@@ -66,12 +65,12 @@
};
const showNextMessage = (modelIdx) => {
groupedMessagesIdx[modelIdx] = Math.min(
groupedMessages[modelIdx].messages.length - 1,
groupedMessagesIdx[modelIdx] + 1
groupedMessageIdsIdx[modelIdx] = Math.min(
groupedMessageIds[modelIdx].messageIds.length - 1,
groupedMessageIdsIdx[modelIdx] + 1
);
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
console.log(messageId);
let messageChildrenIds = history.messages[messageId].childrenIds;
@@ -86,33 +85,43 @@
};
const initHandler = async () => {
console.log('multiresponse:initHandler');
await tick();
currentMessageId = messages[messageIdx].id;
groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => {
currentMessageId = messageId;
parentMessage = history.messages[messageId].parentId
? history.messages[history.messages[messageId].parentId]
: null;
groupedMessageIds = parentMessage?.models.reduce((a, model, modelIdx) => {
// Find all messages that are children of the parent message and have the same model
let modelMessages = parentMessage?.childrenIds
let modelMessageIds = parentMessage?.childrenIds
.map((id) => history.messages[id])
.filter((m) => m?.modelIdx === modelIdx);
.filter((m) => m?.modelIdx === modelIdx)
.map((m) => m.id);
if (modelMessages.length === 0) {
modelMessages = parentMessage?.childrenIds
// Legacy support for messages that don't have a modelIdx
// Find all messages that are children of the parent message and have the same model
if (modelMessageIds.length === 0) {
let modelMessages = parentMessage?.childrenIds
.map((id) => history.messages[id])
.filter((m) => m?.model === model);
modelMessages.forEach((m) => {
m.modelIdx = modelIdx;
});
modelMessageIds = modelMessages.map((m) => m.id);
}
return {
...a,
[modelIdx]: { messages: modelMessages }
[modelIdx]: { messageIds: modelMessageIds }
};
}, {});
groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId);
groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId);
if (idx !== -1) {
return {
...a,
@@ -121,39 +130,53 @@
} else {
return {
...a,
[modelIdx]: 0
[modelIdx]: groupedMessageIds[modelIdx].messageIds.length - 1
};
}
}, {});
console.log(groupedMessageIds, groupedMessageIdsIdx);
await tick();
};
const mergeResponsesHandler = async () => {
const responses = Object.keys(groupedMessages).map((modelIdx) => {
const { messages } = groupedMessages[modelIdx];
return messages[groupedMessagesIdx[modelIdx]].content;
const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
const { messageIds } = groupedMessageIds[modelIdx];
const messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
return history.messages[messageId].content;
});
mergeResponses(currentMessageId, responses, chatId);
mergeResponses(messageId, responses, chatId);
};
onMount(async () => {
initHandler();
await initHandler();
await tick();
const messageElement = document.getElementById(`message-${messageId}`);
console.log(messageElement);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
</script>
<div>
<div
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
id="responses-container-{chatId}-{parentMessage.id}"
>
{#key currentMessageId}
{#each Object.keys(groupedMessages) as modelIdx}
{#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
{#if parentMessage}
<div>
<div
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
id="responses-container-{chatId}-{parentMessage.id}"
>
{#each Object.keys(groupedMessageIds) as modelIdx}
{#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
{@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
{@const _messageId =
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
<div
class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
?.modelIdx == modelIdx
? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
$mobile ? 'min-w-full' : 'min-w-[32rem]'
@@ -162,17 +185,13 @@
$mobile ? 'min-w-full' : 'min-w-80'
}`} transition-all p-5 rounded-2xl"
on:click={() => {
if (currentMessageId != message.id) {
currentMessageId = message.id;
let messageId = message.id;
console.log(messageId);
//
let messageChildrenIds = history.messages[messageId].childrenIds;
if (messageId != _messageId) {
let messageChildrenIds = history.messages[_messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
messageChildrenIds = history.messages[_messageId].childrenIds;
}
history.currentId = messageId;
history.currentId = _messageId;
dispatch('change');
}
}}
@@ -180,95 +199,92 @@
{#key history.currentId}
{#if message}
<ResponseMessage
{message}
siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
{history}
messageId={_messageId}
isLastMessage={true}
{updateChatMessages}
{confirmEditResponseMessage}
siblings={groupedMessageIds[modelIdx].messageIds}
showPreviousMessage={() => showPreviousMessage(modelIdx)}
showNextMessage={() => showNextMessage(modelIdx)}
{readOnly}
{rateMessage}
{copyToClipboard}
{continueGeneration}
{editMessage}
{continueResponse}
regenerateResponse={async (message) => {
regenerateResponse(message);
await tick();
groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
groupedMessageIdsIdx[modelIdx] =
groupedMessageIds[modelIdx].messageIds.length - 1;
}}
on:action={async (e) => {
dispatch('action', e.detail);
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
on:update={async (e) => {
dispatch('update', e.detail);
}}
on:save={async (e) => {
dispatch('save', e.detail);
}}
{readOnly}
/>
{/if}
{/key}
</div>
{/if}
{/each}
{/key}
</div>
</div>
{#if !readOnly && isLastMessage}
{#if !Object.keys(groupedMessages).find((modelIdx) => {
const { messages } = groupedMessages[modelIdx];
return !messages[groupedMessagesIdx[modelIdx]]?.done ?? false;
})}
<div class="flex justify-end">
<div class="w-full">
{#if history.messages[currentMessageId]?.merged?.status}
{@const message = history.messages[currentMessageId]?.merged}
{#if !readOnly && isLastMessage}
{#if !Object.keys(groupedMessageIds).find((modelIdx) => {
const { messageIds } = groupedMessageIds[modelIdx];
const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
return !history.messages[_messageId]?.done ?? false;
})}
<div class="flex justify-end">
<div class="w-full">
{#if history.messages[messageId]?.merged?.status}
{@const message = history.messages[messageId]?.merged}
<div class="w-full rounded-xl pl-5 pr-2 py-2">
<Name>
Merged Response
<div class="w-full rounded-xl pl-5 pr-2 py-2">
<Name>
Merged Response
{#if message.timestamp}
<span
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
>
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
</span>
{/if}
</Name>
{#if message.timestamp}
<span
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
>
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
</span>
{/if}
</Name>
<div class="mt-1 markdown-prose w-full min-w-full">
{#if (message?.content ?? '') === ''}
<Skeleton />
{:else}
<Markdown id={`merged`} content={message.content ?? ''} />
{/if}
<div class="mt-1 markdown-prose w-full min-w-full">
{#if (message?.content ?? '') === ''}
<Skeleton />
{:else}
<Markdown id={`merged`} content={message.content ?? ''} />
{/if}
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
<button
type="button"
id="merge-response-button"
class="{true
? 'visible'
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
mergeResponsesHandler();
}}
>
<Merge className=" size-5 " />
</button>
</Tooltip>
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
<button
type="button"
id="merge-response-button"
class="{true
? 'visible'
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
mergeResponsesHandler();
}}
>
<Merge className=" size-5 " />
</button>
</Tooltip>
</div>
</div>
</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}
@@ -1,23 +1,11 @@
<script lang="ts">
import { settings } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
import ProfileImageBase from './ProfileImageBase.svelte';
export let className = 'size-8';
export let src = '/user.png';
export let src = '';
</script>
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
<img
crossorigin="anonymous"
src={src.startsWith(WEBUI_BASE_URL) ||
src.startsWith('https://www.gravatar.com/avatar/') ||
src.startsWith('data:') ||
src.startsWith('/')
? src
: `/user.png`}
class=" {className} object-cover rounded-full -translate-y-[1px]"
alt="profile"
draggable="false"
/>
<ProfileImageBase {src} {className} />
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
export let className = 'size-8';
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
</script>
<img
crossorigin="anonymous"
src={src === ''
? `${WEBUI_BASE_URL}/static/favicon.png`
: src.startsWith(WEBUI_BASE_URL) ||
src.startsWith('https://www.gravatar.com/avatar/') ||
src.startsWith('data:') ||
src.startsWith('/')
? src
: `/user.png`}
class=" {className} object-cover rounded-full -translate-y-[1px]"
alt="profile"
draggable="false"
/>
@@ -13,6 +13,7 @@
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
import { imageGenerations } from '$lib/apis/images';
import {
copyToClipboard as _copyToClipboard,
approximateToHumanReadable,
extractParagraphsForAudio,
extractSentencesForAudio,
@@ -76,23 +77,30 @@
annotation?: { type: string; rating: number };
}
export let message: MessageType;
export let history;
export let messageId;
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
$: if (history.messages) {
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
message = JSON.parse(JSON.stringify(history.messages[messageId]));
}
}
export let siblings;
export let isLastMessage = true;
export let readOnly = false;
export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function;
export let showNextMessage: Function;
export let editMessage: Function;
export let rateMessage: Function;
export let copyToClipboard: Function;
export let continueGeneration: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
export let isLastMessage = true;
export let readOnly = false;
let model = null;
$: model = $models.find((m) => m.id === message.model);
@@ -109,6 +117,13 @@
let showRateComment = false;
const copyToClipboard = async (text) => {
const res = await _copyToClipboard(text);
if (res) {
toast.success($i18n.t('Copying to clipboard was successful!'));
}
};
const playAudio = (idx: number) => {
return new Promise<void>((res) => {
speakingIdx = idx;
@@ -202,6 +217,8 @@
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
audioParts[idx] = audio;
loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
@@ -224,6 +241,7 @@
console.log(voice);
const speak = new SpeechSynthesisUtterance(message.content);
speak.rate = $settings.audio?.tts?.playbackRate ?? 1;
console.log(speak);
@@ -255,11 +273,16 @@
};
const editMessageConfirmHandler = async () => {
if (editedContent === '') {
editedContent = ' ';
}
editMessage(message.id, editedContent ? editedContent : '', false);
confirmEditResponseMessage(message.id, editedContent);
edit = false;
editedContent = '';
await tick();
};
const saveAsCopyHandler = async () => {
editMessage(message.id, editedContent ? editedContent : '');
edit = false;
editedContent = '';
@@ -299,6 +322,8 @@
}
onMount(async () => {
console.log('ResponseMessage mounted');
await tick();
});
</script>
@@ -333,7 +358,7 @@
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
<Image src={file.url} alt={message.content} />
{/if}
</div>
{/each}
@@ -399,31 +424,45 @@
const isEnterPressed = e.key === 'Enter';
if (isCmdOrCtrlPressed && isEnterPressed) {
document.getElementById('save-edit-message-button')?.click();
document.getElementById('confirm-edit-message-button')?.click();
}
}}
/>
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
<div>
<button
id="save-new-message-button"
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
on:click={() => {
saveAsCopyHandler();
}}
>
{$i18n.t('Save As Copy')}
</button>
</div>
<button
id="save-edit-message-button"
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save')}
</button>
<div class="flex space-x-1.5">
<button
id="close-edit-message-button"
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
<button
id="confirm-edit-message-button"
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save')}
</button>
</div>
</div>
</div>
{:else}
@@ -881,7 +920,7 @@
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
continueGeneration();
continueResponse();
(model?.actions ?? [])
.filter((action) => action?.__webui__ ?? false)
@@ -1000,8 +1039,7 @@
bind:show={showRateComment}
bind:message
on:submit={(e) => {
updateChatMessages();
dispatch('update');
(model?.actions ?? [])
.filter((action) => action?.__webui__ ?? false)
.forEach((action) => {
@@ -1,4 +1,4 @@
<div class="w-full mt-2 mb-4">
<div class="w-full mt-2 mb-2">
<div class="animate-pulse flex w-full">
<div class="space-y-2 w-full">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
@@ -1,38 +1,58 @@
<script lang="ts">
import dayjs from 'dayjs';
import { toast } from 'svelte-sonner';
import { tick, createEventDispatcher, getContext, onMount } from 'svelte';
import { models, settings } from '$lib/stores';
import { user as _user } from '$lib/stores';
import {
copyToClipboard as _copyToClipboard,
processResponseContent,
replaceTokens
} from '$lib/utils';
import { tick, createEventDispatcher, getContext } from 'svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import { models, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { user as _user } from '$lib/stores';
import { getFileContentById } from '$lib/apis/files';
import FileItem from '$lib/components/common/FileItem.svelte';
import { marked } from 'marked';
import { processResponseContent, replaceTokens } from '$lib/utils';
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
import Markdown from './Markdown.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let user;
export let message;
export let siblings;
export let isFirstMessage: boolean;
export let readOnly: boolean;
export let confirmEditMessage: Function;
export let history;
export let messageId;
export let siblings;
export let showPreviousMessage: Function;
export let showNextMessage: Function;
export let copyToClipboard: Function;
export let editMessage: Function;
export let isFirstMessage: boolean;
export let readOnly: boolean;
let edit = false;
let editedContent = '';
let messageEditTextAreaElement: HTMLTextAreaElement;
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
$: if (history.messages) {
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
message = JSON.parse(JSON.stringify(history.messages[messageId]));
}
}
const copyToClipboard = async (text) => {
const res = await _copyToClipboard(text);
if (res) {
toast.success($i18n.t('Copying to clipboard was successful!'));
}
};
const editMessageHandler = async () => {
edit = true;
editedContent = message.content;
@@ -46,7 +66,7 @@
};
const editMessageConfirmHandler = async (submit = true) => {
confirmEditMessage(message.id, editedContent, submit);
editMessage(message.id, editedContent, submit);
edit = false;
editedContent = '';
@@ -60,9 +80,13 @@
const deleteMessageHandler = async () => {
dispatch('delete', message.id);
};
onMount(() => {
console.log('UserMessage mounted');
});
</script>
<div class=" flex w-full user-message" dir={$settings.chatDirection}>
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
{#if !($settings?.chatBubble ?? true)}
<ProfileImage
src={message.user
+185
View File
@@ -0,0 +1,185 @@
<script lang="ts">
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { models, showOverview, theme, user } from '$lib/stores';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './Overview/Node.svelte';
import Flow from './Overview/Flow.svelte';
import XMark from '../icons/XMark.svelte';
const { width, height } = useStore();
const { fitView, getViewport } = useSvelteFlow();
const nodesInitialized = useNodesInitialized();
export let history;
let selectedMessageId = null;
const nodes = writable([]);
const edges = writable([]);
const nodeTypes = {
custom: CustomNode
};
$: if (history) {
drawFlow();
}
$: if (history && history.currentId) {
focusNode();
selectedMessageId = null;
}
const focusNode = async () => {
if (selectedMessageId === null) {
await fitView({ nodes: [{ id: history.currentId }] });
}
};
const drawFlow = async () => {
const nodeList = [];
const edgeList = [];
const levelOffset = 150; // Vertical spacing between layers
const siblingOffset = 250; // Horizontal spacing between nodes at the same layer
// Map to keep track of node positions at each level
let positionMap = new Map();
// Helper function to truncate labels
function createLabel(content) {
const maxLength = 100;
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
}
// Create nodes and map children to ensure alignment in width
let layerWidths = {}; // Track widths of each layer
Object.keys(history.messages).forEach((id) => {
const message = history.messages[id];
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
if (!layerWidths[level]) layerWidths[level] = 0;
positionMap.set(id, {
id: message.id,
level,
position: layerWidths[level]++
});
});
// Adjust positions based on siblings count to centralize vertical spacing
Object.keys(history.messages).forEach((id) => {
const pos = positionMap.get(id);
const xOffset = pos.position * siblingOffset;
const y = pos.level * levelOffset;
const x = xOffset;
nodeList.push({
id: pos.id,
type: 'custom',
data: {
user: $user,
message: history.messages[id],
model: $models.find((model) => model.id === history.messages[id].model)
},
position: { x, y }
});
// Create edges
const parentId = history.messages[id].parentId;
if (parentId) {
edgeList.push({
id: parentId + '-' + pos.id,
source: parentId,
target: pos.id,
selectable: false,
class: ' dark:fill-gray-300 fill-gray-300',
type: 'smoothstep',
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
});
}
});
await edges.set([...edgeList]);
await nodes.set([...nodeList]);
};
const recurseCheckChild = (nodeId, currentId) => {
const node = history.messages[nodeId];
return (
node.childrenIds &&
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
);
};
onMount(() => {
drawFlow();
nodesInitialized.subscribe(async (initialized) => {
if (initialized) {
await tick();
const res = await fitView({ nodes: [{ id: history.currentId }] });
}
});
width.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
height.subscribe((value) => {
if (value) {
// fitView();
fitView({ nodes: [{ id: history.currentId }] });
}
});
});
onDestroy(() => {
console.log('Overview destroyed');
nodes.set([]);
edges.set([]);
});
</script>
<div class="w-full h-full relative">
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-5 py-4">
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
<button
class="self-center"
on:click={() => {
dispatch('close');
showOverview.set(false);
}}
>
<XMark className="size-4" />
</button>
</div>
{#if $nodes.length > 0}
<Flow
{nodes}
{nodeTypes}
{edges}
on:nodeclick={(e) => {
console.log(e.detail.node.data);
dispatch('nodeclick', e.detail);
selectedMessageId = e.detail.node.data.message.id;
fitView({ nodes: [{ id: selectedMessageId }] });
}}
/>
{/if}
</div>
@@ -0,0 +1,36 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { theme } from '$lib/stores';
import { Background, Controls, SvelteFlow, BackgroundVariant } from '@xyflow/svelte';
export let nodes;
export let nodeTypes;
export let edges;
</script>
<SvelteFlow
{nodes}
{nodeTypes}
{edges}
fitView
minZoom={0.001}
colorMode={$theme.includes('dark')
? 'dark'
: $theme === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
: 'light'}
nodesConnectable={false}
nodesDraggable={false}
on:nodeclick={(e) => dispatch('nodeclick', e.detail)}
oninit={() => {
console.log('Flow initialized');
}}
>
<Controls showLock={false} />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
@@ -0,0 +1,82 @@
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
import ProfileImageBase from '../Messages/ProfileImageBase.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Heart from '$lib/components/icons/Heart.svelte';
type $$Props = NodeProps;
export let data: $$Props['data'];
</script>
<div
class="px-4 py-3 shadow-md rounded-xl dark:bg-black bg-white border dark:border-gray-900 w-60 h-20 group"
>
<Tooltip
content={data?.message?.error ? data.message.error.content : data.message.content}
class="w-full"
allowHTML={false}
>
{#if data.message.role === 'user'}
<div class="flex w-full">
<ProfileImageBase
src={data.user?.profile_image_url ?? '/user.png'}
className={'size-5 -translate-y-[1px]'}
/>
<div class="ml-2">
<div class=" flex justify-between items-center">
<div class="text-xs text-black dark:text-white font-medium line-clamp-1">
{data?.user?.name ?? 'User'}
</div>
</div>
{#if data?.message?.error}
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">{data.message.error.content}</div>
{:else}
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
{/if}
</div>
</div>
{:else}
<div class="flex w-full">
<ProfileImageBase
src={data?.model?.info?.meta?.profile_image_url ?? ''}
className={'size-5 -translate-y-[1px]'}
/>
<div class="ml-2">
<div class=" flex justify-between items-center">
<div class="text-xs text-black dark:text-white font-medium line-clamp-1">
{data?.model?.name ?? data?.message?.model ?? 'Assistant'}
</div>
<button
class={data?.message?.favorite ? '' : 'invisible group-hover:visible'}
on:click={() => {
data.message.favorite = !(data?.message?.favorite ?? false);
}}
>
<Heart
className="size-3 {data?.message?.favorite
? 'fill-red-500 stroke-red-500'
: 'hover:fill-red-500 hover:stroke-red-500'} "
strokeWidth="2.5"
/>
</button>
</div>
{#if data?.message?.error}
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">
{data.message.error.content}
</div>
{:else}
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
{/if}
</div>
</div>
{/if}
</Tooltip>
<Handle type="target" position={Position.Top} class="w-2 rounded-full dark:bg-gray-900" />
<Handle type="source" position={Position.Bottom} class="w-2 rounded-full dark:bg-gray-900" />
</div>
@@ -10,6 +10,7 @@
export let params = {
// Advanced
stream_response: null, // Set stream responses for this model individually
seed: null,
stop: null,
temperature: null,
@@ -41,7 +42,36 @@
}
</script>
<div class=" space-y-1 text-xs">
<div class=" space-y-1 text-xs pb-safe-bottom">
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Stream Chat Response')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
params.stream_response =
(params?.stream_response ?? null) === null
? true
: params.stream_response
? false
: null;
}}
type="button"
>
{#if params.stream_response === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else if params.stream_response === false}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
@@ -734,7 +764,7 @@
id="steps-range"
type="range"
min="-2"
max="16000"
max="131072"
step="1"
bind:value={params.max_tokens}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
@@ -746,7 +776,6 @@
type="number"
class=" bg-transparent text-center w-14"
min="-2"
max="16000"
step="1"
/>
</div>
@@ -23,6 +23,10 @@
let voices = [];
let voice = '';
// Audio speed control
let playbackRate = 1;
const speedOptions = [2, 1.75, 1.5, 1.25, 1, 0.75, 0.5];
const getVoices = async () => {
if ($config.audio.tts.engine === '') {
const getVoicesLoop = setInterval(async () => {
@@ -56,6 +60,7 @@
};
onMount(async () => {
playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
conversationMode = $settings.conversationMode ?? false;
speechAutoSend = $settings.speechAutoSend ?? false;
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
@@ -83,6 +88,7 @@
engine: STTEngine !== '' ? STTEngine : undefined
},
tts: {
playbackRate: playbackRate,
voice: voice !== '' ? voice : undefined,
defaultVoice: $config?.audio?.tts?.voice ?? '',
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
@@ -153,6 +159,21 @@
{/if}
</button>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Speech Playback Speed')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={playbackRate}
>
{#each speedOptions as option}
<option value={option} selected={playbackRate === option}>{option}x</option>
{/each}
</select>
</div>
</div>
</div>
<hr class=" dark:border-gray-850" />
@@ -45,6 +45,7 @@
let params = {
// Advanced
stream_response: null,
seed: null,
temperature: null,
frequency_penalty: null,
@@ -114,6 +115,29 @@
document.documentElement.classList.add(e);
});
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
if (_theme.includes('system')) {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
console.log('Setting system meta theme color: ' + systemTheme);
metaThemeColor.setAttribute('content', systemTheme === 'light' ? '#ffffff' : '#171717');
} else {
console.log('Setting meta theme color: ' + _theme);
metaThemeColor.setAttribute(
'content',
_theme === 'dark'
? '#171717'
: _theme === 'oled-dark'
? '#000000'
: _theme === 'her'
? '#983724'
: '#ffffff'
);
}
}
console.log(_theme);
};
@@ -304,6 +328,7 @@
saveSettings({
system: system !== '' ? system : undefined,
params: {
stream_response: params.stream_response !== null ? params.stream_response : undefined,
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
temperature: params.temperature !== null ? params.temperature : undefined,
@@ -49,20 +49,20 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={modalElement}
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[9999] overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }}
on:mousedown={() => {
show = false;
}}
>
<div
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 shadow-3xl border border-gray-850"
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl border border-gray-850"
in:flyAndScale
on:mousedown={(e) => {
e.stopPropagation();
}}
>
<div class="px-[1.75rem] py-6">
<div class="px-[1.75rem] py-6 flex flex-col">
<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">
{#if title !== ''}
{title}
@@ -72,7 +72,7 @@
</div>
<slot>
<div class=" text-sm text-gray-500">
<div class=" text-sm text-gray-500 flex-1">
{#if message !== ''}
{message}
{:else}
+13 -4
View File
@@ -3,6 +3,8 @@
import { flyAndScale } from '$lib/utils/transitions';
import { fade, fly, slide } from 'svelte/transition';
const dispatch = createEventDispatcher();
export let show = false;
export let size = 'md';
@@ -42,15 +44,22 @@
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
} else if (modalElement) {
dispatch('close');
window.removeEventListener('keydown', handleKeyDown);
document.body.removeChild(modalElement);
document.body.style.overflow = 'unset';
if (document.body.contains(modalElement)) {
document.body.removeChild(modalElement);
document.body.style.overflow = 'unset';
}
}
onDestroy(() => {
show = false;
if (modalElement) {
document.body.removeChild(modalElement);
if (document.body.contains(modalElement)) {
document.body.removeChild(modalElement);
document.body.style.overflow = 'unset';
}
}
});
</script>
@@ -60,7 +69,7 @@
<div
bind:this={modalElement}
class="modal fixed right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
class="modal fixed right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[9999] overflow-hidden overscroll-contain"
in:fly={{ y: 100, duration: 100 }}
on:mousedown={() => {
show = false;
@@ -9,14 +9,14 @@
let previewElement = null;
const downloadImage = (url, filename) => {
const downloadImage = (url, filename, prefixName = '') => {
fetch(url)
.then((response) => response.blob())
.then((blob) => {
const objectUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = filename;
link.download = `${prefixName}${filename}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -87,7 +87,7 @@
<button
class=" p-5"
on:click={() => {
downloadImage(src, src.substring(src.lastIndexOf('/') + 1));
downloadImage(src, src.substring(src.lastIndexOf('/') + 1), alt);
}}
>
<svg
+19 -3
View File
@@ -1,16 +1,24 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
const dispatch = createEventDispatcher();
let loaderElement: HTMLElement;
let observer;
let intervalId;
onMount(() => {
const observer = new IntersectionObserver(
observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
dispatch('visible');
intervalId = setInterval(() => {
dispatch('visible');
}, 100);
// dispatch('visible');
// observer.unobserve(loaderElement); // Stop observing until content is loaded
} else {
clearInterval(intervalId);
}
});
},
@@ -23,6 +31,14 @@
observer.observe(loaderElement);
});
onDestroy(() => {
observer.disconnect();
if (intervalId) {
clearInterval(intervalId);
}
});
</script>
<div bind:this={loaderElement}>
+1 -1
View File
@@ -61,7 +61,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={modalElement}
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[9999] overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }}
on:mousedown={() => {
show = false;
+10 -5
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { onDestroy } from 'svelte';
import { marked } from 'marked';
@@ -10,22 +12,25 @@
export let touch = true;
export let className = 'flex';
export let theme = '';
export let allowHTML = true;
export let tippyOptions = {};
let tooltipElement;
let tooltipInstance;
$: if (tooltipElement && content) {
if (tooltipInstance) {
tooltipInstance.setContent(content);
tooltipInstance.setContent(DOMPurify.sanitize(content));
} else {
tooltipInstance = tippy(tooltipElement, {
content: content,
content: DOMPurify.sanitize(content),
placement: placement,
allowHTML: true,
allowHTML: allowHTML,
touch: touch,
...(theme !== '' ? { theme } : { theme: 'dark' }),
arrow: false,
offset: [0, 4]
offset: [0, 4],
...tippyOptions
});
}
} else if (tooltipInstance && content === '') {
@@ -41,6 +46,6 @@
});
</script>
<div bind:this={tooltipElement} aria-label={content} class={className}>
<div bind:this={tooltipElement} aria-label={DOMPurify.sanitize(content)} class={className}>
<slot />
</div>
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
/>
</svg>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z"
/>
</svg>
+18 -17
View File
@@ -8,7 +8,7 @@
mobile,
settings,
showArchivedChats,
showSettings,
showControls,
showSidebar,
user
} from '$lib/stores';
@@ -22,6 +22,7 @@
import UserMenu from './Sidebar/UserMenu.svelte';
import MenuLines from '../icons/MenuLines.svelte';
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
import Map from '../icons/Map.svelte';
const i18n = getContext('i18n');
@@ -31,9 +32,7 @@
export let chat;
export let selectedModels;
export let showModelSelector = true;
export let showControls = false;
let showShareChatModal = false;
let showDownloadChatModal = false;
@@ -83,7 +82,7 @@
}}
>
<button
class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
id="chat-context-menu-button"
>
<div class=" m-auto self-center">
@@ -106,19 +105,21 @@
</Menu>
{/if}
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
showControls = !showControls;
}}
aria-label="Controls"
>
<div class=" m-auto self-center">
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
</div>
</button>
</Tooltip>
{#if !$mobile}
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={async () => {
await showControls.set(!$showControls);
}}
aria-label="Controls"
>
<div class=" m-auto self-center">
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
</div>
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('New Chat')}>
<button
+60 -13
View File
@@ -5,13 +5,19 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { showSettings } from '$lib/stores';
import { downloadChatAsPDF } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils';
import { showOverview, showControls, mobile } from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import { downloadChatAsPDF } from '$lib/apis/utils';
import Map from '$lib/components/icons/Map.svelte';
import { get } from 'svelte/store';
import Clipboard from '$lib/components/icons/Clipboard.svelte';
import { toast } from 'svelte-sonner';
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
const i18n = getContext('i18n');
@@ -24,19 +30,23 @@
export let chat;
export let onClose: Function = () => {};
const downloadTxt = async () => {
const getChatAsText = async () => {
const _chat = chat.chat;
console.log('download', chat);
const chatText = _chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
return chatText.trim();
};
const downloadTxt = async () => {
const chatText = await getChatAsText();
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${_chat.title}.txt`);
saveAs(blob, `chat-${chat.chat.title}.txt`);
};
const downloadPdf = async () => {
@@ -117,6 +127,48 @@
<div class="flex items-center">{$i18n.t('Settings')}</div>
</DropdownMenu.Item> -->
{#if $mobile}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-controls-button"
on:click={async () => {
await showControls.set(true);
}}
>
<AdjustmentsHorizontal className=" size-4" strokeWidth="0.5" />
<div class="flex items-center">{$i18n.t('Controls')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-overview-button"
on:click={async () => {
await showControls.set(true);
await showOverview.set(true);
}}
>
<Map className=" size-4" strokeWidth="1.5" />
<div class="flex items-center">{$i18n.t('Overview')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-copy-button"
on:click={async () => {
const res = await copyToClipboard(await getChatAsText()).catch((e) => {
console.error(e);
});
if (res) {
toast.success($i18n.t('Copied to clipboard'));
}
}}
>
<Clipboard className=" size-4" strokeWidth="1.5" />
<div class="flex items-center">{$i18n.t('Copy')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-share-button"
@@ -138,12 +190,7 @@
</svg>
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
<!-- <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
downloadHandler();
}}
/> -->
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
+20 -10
View File
@@ -53,6 +53,8 @@
let showDeleteConfirm = false;
let showDropdown = false;
let selectedTagName = null;
let filteredChatList = [];
// Pagination variables
@@ -240,7 +242,7 @@
deleteChatHandler(deleteChat.id);
}}
>
<div class=" text-sm text-gray-500">
<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
{$i18n.t('This will delete')} <span class=" font-semibold">{deleteChat.title}</span>.
</div>
</DeleteConfirmDialog>
@@ -380,11 +382,13 @@
</div>
{/if}
<div class="relative flex flex-col flex-1 overflow-y-auto">
<div
class="relative flex flex-col flex-1 overflow-y-auto {$temporaryChatEnabled
? 'opacity-20'
: ''}"
>
{#if $temporaryChatEnabled}
<div
class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center"
></div>
<div class="absolute z-40 w-full h-full flex justify-center"></div>
{/if}
<div class="px-2 mt-0.5 mb-2 flex justify-center space-x-2">
@@ -419,10 +423,13 @@
</div>
{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
<div class="px-3.5 mb-1 flex gap-0.5 flex-wrap">
<button
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 py-[1px] text-xs transition {selectedTagName === null
? 'bg-gray-100 dark:bg-gray-900'
: ' '} rounded-md font-medium"
on:click={async () => {
selectedTagName = null;
await enablePagination();
}}
>
@@ -430,8 +437,11 @@
</button>
{#each $tags.filter((t) => t.name !== 'pinned') as tag}
<button
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 py-[1px] text-xs transition {selectedTagName === tag.name
? 'bg-gray-100 dark:bg-gray-900'
: ''} rounded-md font-medium"
on:click={async () => {
selectedTagName = tag.name;
scrollPaginationEnabled.set(false);
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
@@ -451,7 +461,7 @@
</div>
{/if}
{#if $pinnedChats.length > 0}
{#if !search && $pinnedChats.length > 0}
<div class="pl-2 py-2 flex flex-col space-y-1">
<div class="">
<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
@@ -551,7 +561,7 @@
</div>
</div>
<div class="px-2.5">
<div class="px-2.5 pb-safe-bottom">
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
<div class="flex flex-col font-primary">
@@ -5,7 +5,7 @@
import { flyAndScale } from '$lib/utils/transitions';
import { goto } from '$app/navigation';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores';
import { showSettings, activeUserCount, USAGE_POOL, mobile, showSidebar } from '$lib/stores';
import { fade, slide } from 'svelte/transition';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -41,6 +41,10 @@
on:click={async () => {
await showSettings.set(true);
show = false;
if ($mobile) {
showSidebar.set(false);
}
}}
>
<div class=" self-center mr-3">
@@ -72,6 +76,10 @@
on:click={() => {
dispatch('show', 'archived-chat');
show = false;
if ($mobile) {
showSidebar.set(false);
}
}}
>
<div class=" self-center mr-3">
@@ -86,6 +94,10 @@
on:click={() => {
goto('/playground');
show = false;
if ($mobile) {
showSidebar.set(false);
}
}}
>
<div class=" self-center mr-3">
@@ -112,6 +124,10 @@
on:click={() => {
goto('/admin');
show = false;
if ($mobile) {
showSidebar.set(false);
}
}}
>
<div class=" self-center mr-3">
@@ -0,0 +1,44 @@
<script lang="ts">
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import { WEBUI_VERSION } from '$lib/constants';
import XMark from '../icons/XMark.svelte';
export let version = {
current: WEBUI_VERSION,
latest: WEBUI_VERSION
};
</script>
<div
class="flex items-start bg-[--info-bg] border border-[--info-border] text-[--info-text] rounded-lg px-3.5 py-3 text-xs"
>
<div class="flex-1 font-medium">
{$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, {
LATEST_VERSION: version.latest
})}
<a
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
target="_blank"
class="underline"
>
{$i18n.t('Update for the latest features and improvements.')}</a
>
</div>
<div class=" flex-shrink-0 pl-2">
<button
class=" hover:text-blue-900 dark:hover:text-blue-300 transition"
on:click={() => {
console.log('closeToast');
dispatch('closeToast');
}}
>
<XMark />
</button>
</div>
</div>
@@ -55,7 +55,7 @@
const uploadDoc = async (file, tags?: object) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
@@ -215,7 +215,11 @@
<div class="mb-3">
<div class="flex justify-between items-center">
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
<div class="flex md:self-center text-lg font-medium px-0.5">
{$i18n.t('Documents')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$documents.length}</span>
</div>
</div>
</div>
+73 -4
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { marked } from 'marked';
import { toast } from 'svelte-sonner';
import Sortable from 'sortablejs';
@@ -94,6 +96,58 @@
window.addEventListener('message', messageHandler, false);
};
const moveToTopHandler = async (model) => {
// find models with position 0 and set them to 1
const topModels = _models.filter((m) => m.info?.meta?.position === 0);
for (const m of topModels) {
let info = m.info;
if (!info) {
info = {
id: m.id,
name: m.name,
meta: {
position: 1
},
params: {}
};
}
info.meta = {
...info.meta,
position: 1
};
await updateModelById(localStorage.token, info.id, info);
}
let info = model.info;
if (!info) {
info = {
id: model.id,
name: model.name,
meta: {
position: 0
},
params: {}
};
}
info.meta = {
...info.meta,
position: 0
};
const res = await updateModelById(localStorage.token, info.id, info);
if (res) {
toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
}
await models.set(await getModels(localStorage.token));
_models = $models;
};
const hideModelHandler = async (model) => {
let info = model.info;
@@ -322,7 +376,7 @@
>
<div class=" self-start w-8 pt-0.5">
<div
class=" rounded-full bg-stone-700 {(model?.info?.meta?.hidden ?? false)
class=" rounded-full object-cover {(model?.info?.meta?.hidden ?? false)
? 'brightness-90 dark:brightness-50'
: ''} "
>
@@ -337,9 +391,21 @@
<div
class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
>
<div class=" font-semibold line-clamp-1">{model.name}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
<Tooltip
content={marked.parse(
model?.ollama?.digest
? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
: ''
)}
className=" w-fit"
placement="top-start"
>
<div class=" font-semibold line-clamp-1">{model.name}</div>
</Tooltip>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
{!!model?.info?.meta?.description
? model?.info?.meta?.description
: (model?.ollama?.digest ?? model.id)}
</div>
</div>
</a>
@@ -440,6 +506,9 @@
exportHandler={() => {
exportModelHandler(model);
}}
moveToTopHandler={() => {
moveToTopHandler(model);
}}
hideHandler={() => {
hideModelHandler(model);
}}
@@ -0,0 +1,44 @@
<script lang="ts">
import { getContext } from 'svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { marked } from 'marked';
const i18n = getContext('i18n');
const helpText = {
vision: $i18n.t('Model accepts image inputs'),
usage: $i18n.t(
'Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.'
)
};
export let capabilities: {
vision?: boolean;
usage?: boolean;
} = {};
</script>
<div>
<div class="flex w-full justify-between mb-1">
<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
</div>
<div class="flex flex-col">
{#each Object.keys(capabilities) as capability}
<div class=" flex items-center gap-2">
<Checkbox
state={capabilities[capability] ? 'checked' : 'unchecked'}
on:change={(e) => {
capabilities[capability] = e.detail === 'checked';
}}
/>
<div class=" py-0.5 text-sm capitalize">
<Tooltip content={marked.parse(helpText[capability])}>
{$i18n.t(capability)}
</Tooltip>
</div>
</div>
{/each}
</div>
</div>
@@ -12,6 +12,7 @@
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
const i18n = getContext('i18n');
@@ -21,6 +22,7 @@
export let cloneHandler: Function;
export let exportHandler: Function;
export let moveToTopHandler: Function;
export let hideHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
@@ -80,6 +82,17 @@
<div class="flex items-center">{$i18n.t('Export')}</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-md"
on:click={() => {
moveToTopHandler();
}}
>
<ArrowUpCircle />
<div class="flex items-center">{$i18n.t('Move to Top')}</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-md"
on:click={() => {
+3 -1
View File
@@ -31,7 +31,9 @@ export const SUPPORTED_FILE_TYPE = [
'application/x-javascript',
'text/markdown',
'audio/mpeg',
'audio/wav'
'audio/wav',
'audio/ogg',
'audio/x-m4a'
];
export const SUPPORTED_FILE_EXTENSIONS = [
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "دردشات {{user}}",
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب",
"a user": "مستخدم",
"About": "عن",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
"Available list": "",
"available!": "متاح",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "خلف",
"Bad Response": "استجابة خطاء",
"Banners": "لافتات",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "UI الدردشة",
"Chat Controls": "",
"Chat direction": "اتجاه المحادثة",
"Chat Overview": "",
"Chats": "المحادثات",
"Check Again": "تحقق مرة اخرى",
"Check for updates": "تحقق من التحديثات",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
"Enable Web Search": "تمكين بحث الويب",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "أدخل مفتاح واجهة برمجة تطبيقات البحث الشجاع",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "أدخل الChunk Overlap",
"Enter Chunk Size": "أدخل Chunk الحجم",
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "أدخل النتيجة",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "آخر نشاط",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "فاتح",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
"Model {{modelName}} is not vision capable": "نموذج {{modelName}} غير قادر على الرؤية",
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
"Model ID": "رقم الموديل",
@@ -417,6 +429,7 @@
"Modelfile Content": "محتوى الملف النموذجي",
"Models": "الموديلات",
"More": "المزيد",
"Move to Top": "",
"Name": "الأسم",
"Name Tag": "أسم التاق",
"Name your model": "قم بتسمية النموذج الخاص بك",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
"Open file": "",
"Open new chat": "فتح محادثة جديده",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "OpenAI",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
"or": "أو",
"Other": "آخر",
"Output format": "",
"Overview": "",
"page": "",
"Password": "الباسورد",
"PDF document (.pdf)": "PDF ملف (.pdf)",
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
@@ -524,6 +541,7 @@
"Save": "حفظ",
"Save & Create": "حفظ وإنشاء",
"Save & Update": "حفظ وتحديث",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
"Scan": "مسح",
@@ -538,7 +556,6 @@
"Search Models": "نماذج البحث",
"Search Prompts": "أبحث حث",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "عدد نتائج البحث",
"Search Tools": "",
"SearchApi API Key": "",
@@ -571,16 +588,20 @@
"Send": "تم",
"Send a Message": "يُرجى إدخال طلبك هنا",
"Send message": "يُرجى إدخال طلبك هنا.",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "سبتمبر",
"Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر",
"Serply API Key": "",
"Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack",
"Server connection verified": "تم التحقق من اتصال الخادم",
"Set as default": "الافتراضي",
"Set CFG Scale": "",
"Set Default Model": "تفعيد الموديل الافتراضي",
"Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})",
"Set Image Size": "حجم الصورة",
"Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "ضبط الخطوات",
"Set Task Model": "تعيين نموذج المهمة",
"Set Voice": "ضبط الصوت",
@@ -601,9 +622,11 @@
"Sign up": "تسجيل",
"Signing in": "جاري الدخول",
"Source": "المصدر",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
"Stop Sequence": "وقف التسلسل",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT اعدادات",
"Submit": "إرسال",
@@ -683,6 +706,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "تحديث ونسخ الرابط",
"Update for the latest features and improvements.": "",
"Update password": "تحديث كلمة المرور",
"Updated at": "",
"Upload": "",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "{{user}}'s чатове",
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Моделът на задачите се използва при изпълнение на задачи като генериране на заглавия за чатове и заявки за търсене в мрежата",
"a user": "потребител",
"About": "Относно",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.",
"Available list": "",
"available!": "наличен!",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "Назад",
"Bad Response": "Невалиден отговор от API",
"Banners": "Банери",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "UI за чат бублон",
"Chat Controls": "",
"Chat direction": "Направление на чата",
"Chat Overview": "",
"Chats": "Чатове",
"Check Again": "Проверете Още Веднъж",
"Check for updates": "Проверка за актуализации",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "Вклюване на Нови Потребители",
"Enable Web Search": "Разрешаване на търсене в уеб",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Уверете се, че вашият CSV файл включва 4 колони в следния ред: Име, Имейл, Парола, Роля.",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "Въведете подробности за себе си, за да се herinnerат вашите LLMs",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "Въведете Brave Search API ключ",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "Въведете Chunk Overlap",
"Enter Chunk Size": "Въведете Chunk Size",
"Enter Github Raw URL": "Въведете URL адреса на Github Raw",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "Въведете оценка",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "Последни активни",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "Светъл",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
"Model {{modelName}} is not vision capable": "Моделът {{modelName}} не може да се вижда",
"Model {{name}} is now {{status}}": "Моделът {{name}} сега е {{status}}",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Открит е път до файловата система на модела. За актуализацията се изисква съкратено име на модела, не може да продължи.",
"Model ID": "ИД на модел",
@@ -417,6 +429,7 @@
"Modelfile Content": "Съдържание на модфайл",
"Models": "Модели",
"More": "Повече",
"Move to Top": "",
"Name": "Име",
"Name Tag": "Име Таг",
"Name your model": "Дайте име на вашия модел",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
"Open file": "",
"Open new chat": "Отвори нов чат",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "OpenAI",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "OpenAI URL/Key е задължителен.",
"or": "или",
"Other": "Other",
"Output format": "",
"Overview": "",
"page": "",
"Password": "Парола",
"PDF document (.pdf)": "PDF документ (.pdf)",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
@@ -524,6 +541,7 @@
"Save": "Запис",
"Save & Create": "Запис & Създаване",
"Save & Update": "Запис & Актуализиране",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
"Scan": "Сканиране",
@@ -538,7 +556,6 @@
"Search Models": "Търсене на модели",
"Search Prompts": "Търси Промптове",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "Брой резултати от търсенето",
"Search Tools": "",
"SearchApi API Key": "",
@@ -567,16 +584,20 @@
"Send": "Изпрати",
"Send a Message": "Изпращане на Съобщение",
"Send message": "Изпращане на съобщение",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "Септември",
"Serper API Key": "Serper API ключ",
"Serply API Key": "",
"Serpstack API Key": "Serpstack API ключ",
"Server connection verified": "Server connection verified",
"Set as default": "Задай по подразбиране",
"Set CFG Scale": "",
"Set Default Model": "Задай Модел По Подразбиране",
"Set embedding model (e.g. {{model}})": "Задай embedding model (e.g. {{model}})",
"Set Image Size": "Задай Размер на Изображението",
"Set reranking model (e.g. {{model}})": "Задай reranking model (e.g. {{model}})",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "Задай Стъпки",
"Set Task Model": "Задаване на модел на задача",
"Set Voice": "Задай Глас",
@@ -597,9 +618,11 @@
"Sign up": "Регистрация",
"Signing in": "Вписване",
"Source": "Източник",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
"Speech-to-Text Engine": "Speech-to-Text Engine",
"Stop Sequence": "Stop Sequence",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT Настройки",
"Submit": "Изпращане",
@@ -679,6 +702,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "Обнови и копирай връзка",
"Update for the latest features and improvements.": "",
"Update password": "Обновяване на парола",
"Updated at": "",
"Upload": "",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "{{user}}র চ্যাটস",
"{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "চ্যাট এবং ওয়েব অনুসন্ধান প্রশ্নের জন্য শিরোনাম তৈরি করার মতো কাজগুলি সম্পাদন করার সময় একটি টাস্ক মডেল ব্যবহার করা হয়",
"a user": "একজন ব্যাবহারকারী",
"About": "সম্পর্কে",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 বেজ ইউআরএল",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 বেজ ইউআরএল আবশ্যক",
"Available list": "",
"available!": "উপলব্ধ!",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "পেছনে",
"Bad Response": "খারাপ প্রতিক্রিয়া",
"Banners": "ব্যানার",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "চ্যাট বাবল UI",
"Chat Controls": "",
"Chat direction": "চ্যাট দিকনির্দেশ",
"Chat Overview": "",
"Chats": "চ্যাটসমূহ",
"Check Again": "আবার চেক করুন",
"Check for updates": "নতুন আপডেট আছে কিনা চেক করুন",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "নতুন সাইনআপ চালু করুন",
"Enable Web Search": "ওয়েব অনুসন্ধান সক্ষম করুন",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "আপনার সিএসভি ফাইলটিতে এই ক্রমে 4 টি কলাম অন্তর্ভুক্ত রয়েছে তা নিশ্চিত করুন: নাম, ইমেল, পাসওয়ার্ড, ভূমিকা।.",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "আপনার এলএলএমগুলি স্মরণ করার জন্য নিজের সম্পর্কে একটি বিশদ লিখুন",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "সাহসী অনুসন্ধান API কী লিখুন",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
"Enter Chunk Size": "চাংক সাইজ লিখুন",
"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "স্কোর দিন",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "সর্বশেষ সক্রিয়",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "লাইট",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "LLM ভুল করতে পারে। গুরুত্বপূর্ণ তথ্য যাচাই করে নিন।",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি",
"Model {{modelName}} is not vision capable": "মডেল {{modelName}} দৃষ্টি সক্ষম নয়",
"Model {{name}} is now {{status}}": "মডেল {{name}} এখন {{status}}",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "মডেল ফাইলসিস্টেম পাথ পাওয়া গেছে। আপডেটের জন্য মডেলের শর্টনেম আবশ্যক, এগিয়ে যাওয়া যাচ্ছে না।",
"Model ID": "মডেল ID",
@@ -417,6 +429,7 @@
"Modelfile Content": "মডেলফাইল কনটেন্ট",
"Models": "মডেলসমূহ",
"More": "আরো",
"Move to Top": "",
"Name": "নাম",
"Name Tag": "নামের ট্যাগ",
"Name your model": "আপনার মডেলের নাম দিন",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
"Open file": "",
"Open new chat": "নতুন চ্যাট খুলুন",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "OpenAI",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "OpenAI URL/Key আবশ্যক",
"or": "অথবা",
"Other": "অন্যান্য",
"Output format": "",
"Overview": "",
"page": "",
"Password": "পাসওয়ার্ড",
"PDF document (.pdf)": "PDF ডকুমেন্ট (.pdf)",
"PDF Extract Images (OCR)": "পিডিএফ এর ছবি থেকে লেখা বের করুন (OCR)",
@@ -524,6 +541,7 @@
"Save": "সংরক্ষণ",
"Save & Create": "সংরক্ষণ এবং তৈরি করুন",
"Save & Update": "সংরক্ষণ এবং আপডেট করুন",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "মাধ্যমে",
"Scan": "স্ক্যান",
@@ -538,7 +556,6 @@
"Search Models": "অনুসন্ধান মডেল",
"Search Prompts": "প্রম্পটসমূহ অনুসন্ধান করুন",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "অনুসন্ধানের ফলাফল গণনা",
"Search Tools": "",
"SearchApi API Key": "",
@@ -567,16 +584,20 @@
"Send": "পাঠান",
"Send a Message": "একটি মেসেজ পাঠান",
"Send message": "মেসেজ পাঠান",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "সেপ্টেম্বর",
"Serper API Key": "Serper API Key",
"Serply API Key": "",
"Serpstack API Key": "Serpstack API Key",
"Server connection verified": "সার্ভার কানেকশন যাচাই করা হয়েছে",
"Set as default": "ডিফল্ট হিসেবে নির্ধারণ করুন",
"Set CFG Scale": "",
"Set Default Model": "ডিফল্ট মডেল নির্ধারণ করুন",
"Set embedding model (e.g. {{model}})": "ইমেম্বিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
"Set Image Size": "ছবির সাইজ নির্ধারণ করুন",
"Set reranking model (e.g. {{model}})": "রি-র্যাংকিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "পরবর্তী ধাপসমূহ",
"Set Task Model": "টাস্ক মডেল সেট করুন",
"Set Voice": "কন্ঠস্বর নির্ধারণ করুন",
@@ -597,9 +618,11 @@
"Sign up": "সাইন আপ",
"Signing in": "সাইন ইন",
"Source": "উৎস",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "স্পিচ রিকগনিশনে সমস্যা: {{error}}",
"Speech-to-Text Engine": "স্পিচ-টু-টেক্সট ইঞ্জিন",
"Stop Sequence": "সিকোয়েন্স থামান",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT সেটিংস",
"Submit": "সাবমিট",
@@ -679,6 +702,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
"Update for the latest features and improvements.": "",
"Update password": "পাসওয়ার্ড আপডেট করুন",
"Updated at": "",
"Upload": "",
+31 -7
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "Els xats de {{user}}",
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
"*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca per a la web",
"a user": "un usuari",
"About": "Sobre",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "Cadena d'autenticació de l'API d'AUTOMATIC1111",
"AUTOMATIC1111 Base URL": "URL Base d'AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base d'AUTOMATIC1111.",
"Available list": "Llista de disponibles",
"available!": "disponible!",
"Azure AI Speech": "Azure AI Speech",
"Azure Region": "Regió d'Azure",
"Back": "Enrere",
"Bad Response": "Resposta errònia",
"Banners": "Banners",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "Chat Bubble UI",
"Chat Controls": "Controls de xat",
"Chat direction": "Direcció del xat",
"Chat Overview": "Vista general del xat",
"Chats": "Xats",
"Check Again": "Comprovar-ho de nou",
"Check for updates": "Comprovar si hi ha actualitzacions",
@@ -230,6 +235,7 @@
"Enable Message Rating": "Permetre la qualificació de missatges",
"Enable New Sign Ups": "Permetre nous registres",
"Enable Web Search": "Activar la cerca web",
"Enable Web Search Query Generation": "Activa la generació de consultes de cerca web",
"Enabled": "Habilitat",
"Engine": "Motor",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assegura't que els teus fitxers CSV inclouen 4 columnes en aquest ordre: Nom, Correu electrònic, Contrasenya, Rol.",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "Introdueix un detall sobre tu què els teus models de llenguatge puguin recordar",
"Enter api auth string (e.g. username:password)": "Entra la cadena d'autenticació api (p. ex. nom d'usuari:contrasenya)",
"Enter Brave Search API Key": "Introdueix la clau API de Brave Search",
"Enter CFG Scale (e.g. 7.0)": "Entra l'escala CFG (p.ex. 7.0)",
"Enter Chunk Overlap": "Introdueix la mida de solapament de blocs",
"Enter Chunk Size": "Introdueix la mida del bloc",
"Enter Github Raw URL": "Introdueix l'URL en brut de Github",
@@ -247,9 +254,11 @@
"Enter Model ID": "Introdueix l'identificador del model",
"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)",
"Enter Sampler (e.g. Euler a)": "Introdueix el mostrejador (p.ex. Euler a)",
"Enter Scheduler (e.g. Karras)": "Entra el programador (p.ex. Karras)",
"Enter Score": "Introdueix la puntuació",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
"Enter SearchApi API Key": "Introdueix la clau API SearchApi",
"Enter SearchApi Engine": "Introdueix el motor SearchApi",
"Enter Searxng Query URL": "Introdueix l'URL de consulta de Searxng",
"Enter Serper API Key": "Introdueix la clau API Serper",
"Enter Serply API Key": "Introdueix la clau API Serply",
@@ -272,7 +281,7 @@
"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
"Export chat (.json)": "Exportar el xat (.json)",
"Export Chats": "Exportar els xats",
"Export Config to JSON File": "",
"Export Config to JSON File": "Exportar la configuració a un arxiu JSON",
"Export Documents Mapping": "Exportar el mapatge de documents",
"Export Functions": "Exportar funcions",
"Export LiteLLM config.yaml": "Exportar la configuració LiteLLM config.yaml",
@@ -337,7 +346,7 @@
"Image Settings": "Preferències d'imatges",
"Images": "Imatges",
"Import Chats": "Importar xats",
"Import Config from JSON File": "",
"Import Config from JSON File": "Importar la configuració des d'un arxiu JSON",
"Import Documents Mapping": "Importar el mapatge de documents",
"Import Functions": "Importar funcions",
"Import Models": "Importar models",
@@ -367,6 +376,7 @@
"Last Active": "Activitat recent",
"Last Modified": "Modificació",
"Leave empty for unlimited": "Deixar-ho buit per il·limitat",
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
"Light": "Clar",
"Listening...": "Escoltant...",
"LLMs can make mistakes. Verify important information.": "Els models de llenguatge poden cometre errors. Verifica la informació important.",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "No s'ha trobat el model {{modelId}}",
"Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió",
"Model {{name}} is now {{status}}": "El model {{name}} ara és {{status}}",
"Model {{name}} is now at the top": "El model {{name}} està ara a dalt de tot",
"Model accepts image inputs": "El model accepta entrades d'imatge",
"Model created successfully!": "Model creat correctament",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "S'ha detectat el camí del sistema de fitxers del model. És necessari un nom curt del model per actualitzar, no es pot continuar.",
"Model ID": "Identificador del model",
@@ -417,6 +429,7 @@
"Modelfile Content": "Contingut del Modelfile",
"Models": "Models",
"More": "Més",
"Move to Top": "Moure a dalt de tot",
"Name": "Nom",
"Name Tag": "Etiqueta de nom",
"Name your model": "Posa un nom al teu model",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL no és vàlida. Si us plau, revisa-la i torna-ho a provar.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "Ui! Hi ha hagut un error en la resposta anterior. Torna a provar-ho o contacta amb un administrador",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
"Open file": "",
"Open new chat": "Obre un xat nou",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La versió d'Open WebUI (v{{OPEN_WEBUI_VERSION}}) és inferior a la versió requerida (v{{REQUIRED_VERSION}})",
"OpenAI": "OpenAI",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.",
"or": "o",
"Other": "Altres",
"Output format": "Format de sortida",
"Overview": "Vista general",
"page": "",
"Password": "Contrasenya",
"PDF document (.pdf)": "Document PDF (.pdf)",
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
@@ -524,6 +541,7 @@
"Save": "Desar",
"Save & Create": "Desar i crear",
"Save & Update": "Desar i actualitzar",
"Save As Copy": "Desar com a còpia",
"Save Tag": "Desar l'etiqueta",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
"Scan": "Escanejar",
@@ -538,11 +556,10 @@
"Search Models": "Cercar models",
"Search Prompts": "Cercar indicacions",
"Search Query Generation Prompt": "Indicació de cerca de generació de consultes",
"Search Query Generation Prompt Length Threshold": "Mida màxima de la indicació de cerca de generació de consultes",
"Search Result Count": "Recompte de resultats de cerca",
"Search Tools": "Cercar eines",
"SearchApi API Key": "",
"SearchApi Engine": "",
"SearchApi API Key": "Clau API de SearchApi",
"SearchApi Engine": "Motor de SearchApi",
"Searched {{count}} sites_one": "S'ha cercat {{count}} una pàgina",
"Searched {{count}} sites_many": "S'han cercat {{count}} pàgines",
"Searched {{count}} sites_other": "S'han cercat {{count}} pàgines",
@@ -568,16 +585,20 @@
"Send": "Enviar",
"Send a Message": "Enviar un missatge",
"Send message": "Enviar missatge",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia `stream_options: { include_usage: true }` a la sol·licitud.\nEls proveïdors compatibles retornaran la informació d'ús del token a la resposta quan s'estableixi.",
"September": "Setembre",
"Serper API Key": "Clau API de Serper",
"Serply API Key": "Clau API de Serply",
"Serpstack API Key": "Clau API de Serpstack",
"Server connection verified": "Connexió al servidor verificada",
"Set as default": "Establir com a predeterminat",
"Set CFG Scale": "Establir l'escala CFG",
"Set Default Model": "Establir el model predeterminat",
"Set embedding model (e.g. {{model}})": "Establir el model d'incrustació (p.ex. {{model}})",
"Set Image Size": "Establir la mida de la image",
"Set reranking model (e.g. {{model}})": "Establir el model de reavaluació (p.ex. {{model}})",
"Set Sampler": "Establir el mostrejador",
"Set Scheduler": "Establir el programador",
"Set Steps": "Establir el nombre de passos",
"Set Task Model": "Establir el model de tasca",
"Set Voice": "Establir la veu",
@@ -598,9 +619,11 @@
"Sign up": "Registrar-se",
"Signing in": "Iniciant sessió",
"Source": "Font",
"Speech Playback Speed": "Velocitat de la parla",
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
"Speech-to-Text Engine": "Motor de veu a text",
"Stop Sequence": "Atura la seqüència",
"Stream Chat Response": "Fer streaming de la resposta del xat",
"STT Model": "Model SST",
"STT Settings": "Preferències de STT",
"Submit": "Enviar",
@@ -680,6 +703,7 @@
"Unpin": "Alliberar",
"Update": "Actualitzar",
"Update and Copy Link": "Actualitzar i copiar l'enllaç",
"Update for the latest features and improvements.": "",
"Update password": "Actualitzar la contrasenya",
"Updated at": "Actualitzat",
"Upload": "Pujar",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "",
"{{webUIName}} Backend Required": "Backend {{webUIName}} gikinahanglan",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
"a user": "usa ka user",
"About": "Mahitungod sa",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "Base URL AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "Ang AUTOMATIC1111 base URL gikinahanglan.",
"Available list": "",
"available!": "magamit!",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "Balik",
"Bad Response": "",
"Banners": "",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "",
"Chat Controls": "",
"Chat direction": "",
"Chat Overview": "",
"Chats": "Mga panaghisgot",
"Check Again": "Susiha pag-usab",
"Check for updates": "Susiha ang mga update",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "I-enable ang bag-ong mga rehistro",
"Enable Web Search": "",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "Pagsulod sa block overlap",
"Enter Chunk Size": "Isulod ang block size",
"Enter Github Raw URL": "",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "Kahayag",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "Ang mga LLM mahimong masayop. ",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "Modelo {{modelId}} wala makit-an",
"Model {{modelName}} is not vision capable": "",
"Model {{name}} is now {{status}}": "",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
"Model ID": "",
@@ -417,6 +429,7 @@
"Modelfile Content": "Mga sulod sa template file",
"Models": "Mga modelo",
"More": "",
"Move to Top": "",
"Name": "Ngalan",
"Name Tag": "Tag sa ngalan",
"Name your model": "",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! ",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! ",
"Open file": "",
"Open new chat": "Ablihi ang bag-ong diskusyon",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "",
"or": "O",
"Other": "",
"Output format": "",
"Overview": "",
"page": "",
"Password": "Password",
"PDF document (.pdf)": "",
"PDF Extract Images (OCR)": "PDF Image Extraction (OCR)",
@@ -524,6 +541,7 @@
"Save": "Tipigi",
"Save & Create": "I-save ug Paghimo",
"Save & Update": "I-save ug I-update",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ang pag-save sa mga chat log direkta sa imong browser storage dili na suportado. ",
"Scan": "Aron ma-scan",
@@ -538,7 +556,6 @@
"Search Models": "",
"Search Prompts": "Pangitaa ang mga prompt",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "",
"Search Tools": "",
"SearchApi API Key": "",
@@ -567,16 +584,20 @@
"Send": "",
"Send a Message": "Magpadala ug mensahe",
"Send message": "Magpadala ug mensahe",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "",
"Serper API Key": "",
"Serply API Key": "",
"Serpstack API Key": "",
"Server connection verified": "Gipamatud-an nga koneksyon sa server",
"Set as default": "Define pinaagi sa default",
"Set CFG Scale": "",
"Set Default Model": "Ibutang ang default template",
"Set embedding model (e.g. {{model}})": "",
"Set Image Size": "Ibutang ang gidak-on sa hulagway",
"Set reranking model (e.g. {{model}})": "",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "Ipasabot ang mga lakang",
"Set Task Model": "",
"Set Voice": "Ibutang ang tingog",
@@ -597,9 +618,11 @@
"Sign up": "Pagrehistro",
"Signing in": "",
"Source": "Tinubdan",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "Sayop sa pag-ila sa tingog: {{error}}",
"Speech-to-Text Engine": "Engine sa pag-ila sa tingog",
"Stop Sequence": "Pagkasunod-sunod sa pagsira",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "Mga setting sa STT",
"Submit": "Isumite",
@@ -679,6 +702,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "",
"Update for the latest features and improvements.": "",
"Update password": "I-update ang password",
"Updated at": "",
"Upload": "",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "{{user}}s Unterhaltungen",
"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Aufgabenmodelle können Unterhaltungstitel oder Websuchanfragen generieren.",
"a user": "ein Benutzer",
"About": "Über",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111-API-Authentifizierungszeichenfolge",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111-Basis-URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111-Basis-URL ist erforderlich.",
"Available list": "",
"available!": "Verfügbar!",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "Zurück",
"Bad Response": "Schlechte Antwort",
"Banners": "Banner",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "Chat Bubble UI",
"Chat Controls": "",
"Chat direction": "Textrichtung",
"Chat Overview": "",
"Chats": "Unterhaltungen",
"Check Again": "Erneut überprüfen",
"Check for updates": "Nach Updates suchen",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "Registrierung erlauben",
"Enable Web Search": "Websuche aktivieren",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "Engine",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Stellen Sie sicher, dass Ihre CSV-Datei 4 Spalten in dieser Reihenfolge enthält: Name, E-Mail, Passwort, Rolle.",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "Geben Sie ein Detail über sich selbst ein, das Ihre Sprachmodelle (LLMs) sich merken sollen",
"Enter api auth string (e.g. username:password)": "Geben Sie die API-Authentifizierungszeichenfolge ein (z. B. Benutzername:Passwort)",
"Enter Brave Search API Key": "Geben Sie den Brave Search API-Schlüssel ein",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein",
"Enter Chunk Size": "Geben Sie die Blockgröße ein",
"Enter Github Raw URL": "Geben Sie die Github Raw-URL ein",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "Gebn Sie den Model-Tag ein",
"Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "Punktzahl eingeben",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "Zuletzt aktiv",
"Last Modified": "Zuletzt bearbeitet",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "Hell",
"Listening...": "Höre zu...",
"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
"Model {{modelName}} is not vision capable": "Das Modell {{modelName}} ist nicht für die Bildverarbeitung geeignet",
"Model {{name}} is now {{status}}": "Modell {{name}} ist jetzt {{status}}",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "Modell erfolgreich erstellt!",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modell-Dateisystempfad erkannt. Modellkurzname ist für das Update erforderlich, Fortsetzung nicht möglich.",
"Model ID": "Modell-ID",
@@ -417,6 +429,7 @@
"Modelfile Content": "Modelfile-Inhalt",
"Models": "Modelle",
"More": "Mehr",
"Move to Top": "",
"Name": "Name",
"Name Tag": "Namens-Tag",
"Name your model": "Benennen Sie Ihr Modell",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppla! Es scheint, dass die URL ungültig ist. Bitte überprüfen Sie diese und versuchen Sie es erneut.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "Hoppla! Es gab einen Fehler in der vorherigen Antwort. Bitte versuchen Sie es erneut oder kontaktieren Sie den Administrator.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hoppla! Sie verwenden eine nicht unterstützte Methode (nur Frontend). Bitte stellen Sie die WebUI vom Backend bereit.",
"Open file": "Datei öffnen",
"Open new chat": "Neuen Chat öffnen",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Die installierte Open-WebUI-Version (v{{OPEN_WEBUI_VERSION}}) ist niedriger als die erforderliche Version (v{{REQUIRED_VERSION}})",
"OpenAI": "OpenAI",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "OpenAI-URL/Schlüssel erforderlich.",
"or": "oder",
"Other": "Andere",
"Output format": "",
"Overview": "",
"page": "Seite",
"Password": "Passwort",
"PDF document (.pdf)": "PDF-Dokument (.pdf)",
"PDF Extract Images (OCR)": "Text von Bildern aus PDFs extrahieren (OCR)",
@@ -524,6 +541,7 @@
"Save": "Speichern",
"Save & Create": "Erstellen",
"Save & Update": "Aktualisieren",
"Save As Copy": "",
"Save Tag": "Tag speichern",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Das direkte Speichern von Unterhaltungen im Browser-Speicher wird nicht mehr unterstützt. Bitte nehmen Sie einen Moment Zeit, um Ihre Unterhaltungen zu exportieren und zu löschen, indem Sie auf die Schaltfläche unten klicken. Keine Sorge, Sie können Ihre Unterhaltungen problemlos über das Backend wieder importieren.",
"Scan": "Scannen",
@@ -538,7 +556,6 @@
"Search Models": "Modelle durchsuchen...",
"Search Prompts": "Prompts durchsuchen...",
"Search Query Generation Prompt": "Suchanfragengenerierungsvorlage",
"Search Query Generation Prompt Length Threshold": "Längenschwelle für Suchanfragengenerierung",
"Search Result Count": "Anzahl der Suchergebnisse",
"Search Tools": "Werkzeuge durchsuchen...",
"SearchApi API Key": "",
@@ -567,16 +584,20 @@
"Send": "Senden",
"Send a Message": "Eine Nachricht senden",
"Send message": "Nachricht senden",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "September",
"Serper API Key": "Serper-API-Schlüssel",
"Serply API Key": "Serply-API-Schlüssel",
"Serpstack API Key": "Serpstack-API-Schlüssel",
"Server connection verified": "Serververbindung überprüft",
"Set as default": "Als Standard festlegen",
"Set CFG Scale": "",
"Set Default Model": "Standardmodell festlegen",
"Set embedding model (e.g. {{model}})": "Einbettungsmodell festlegen (z. B. {{model}})",
"Set Image Size": "Bildgröße festlegen",
"Set reranking model (e.g. {{model}})": "Rerankingmodell festlegen (z. B. {{model}})",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "Schrittgröße festlegen",
"Set Task Model": "Aufgabenmodell festlegen",
"Set Voice": "Stimme festlegen",
@@ -597,9 +618,11 @@
"Sign up": "Registrieren",
"Signing in": "Anmeldung",
"Source": "Quelle",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
"Speech-to-Text Engine": "Sprache-zu-Text-Engine",
"Stop Sequence": "Stop-Sequenz",
"Stream Chat Response": "",
"STT Model": "STT-Modell",
"STT Settings": "STT-Einstellungen",
"Submit": "Senden",
@@ -679,6 +702,7 @@
"Unpin": "Lösen",
"Update": "Aktualisieren",
"Update and Copy Link": "Aktualisieren und Link kopieren",
"Update for the latest features and improvements.": "",
"Update password": "Passwort aktualisieren",
"Updated at": "Aktualisiert am",
"Upload": "Hochladen",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Much Required",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
"a user": "such user",
"About": "Much About",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Base URL is required.",
"Available list": "",
"available!": "available! So excite!",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "Back",
"Bad Response": "",
"Banners": "",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "",
"Chat Controls": "",
"Chat direction": "",
"Chat Overview": "",
"Chats": "Chats",
"Check Again": "Check Again",
"Check for updates": "Check for updates",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "Enable New Bark Ups",
"Enable Web Search": "",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "Enter Overlap of Chunks",
"Enter Chunk Size": "Enter Size of Chunk",
"Enter Github Raw URL": "",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "Light",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "LLMs can make borks. Verify important info.",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "Model {{modelId}} not found",
"Model {{modelName}} is not vision capable": "",
"Model {{name}} is now {{status}}": "",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem bark detected. Model shortname is required for update, cannot continue.",
"Model ID": "",
@@ -417,6 +429,7 @@
"Modelfile Content": "Modelfile Content",
"Models": "Wowdels",
"More": "",
"Move to Top": "",
"Name": "Name",
"Name Tag": "Name Tag",
"Name your model": "",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.",
"Open file": "",
"Open new chat": "Open new bark",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "",
"or": "or",
"Other": "",
"Output format": "",
"Overview": "",
"page": "",
"Password": "Barkword",
"PDF document (.pdf)": "",
"PDF Extract Images (OCR)": "PDF Extract Wowmages (OCR)",
@@ -524,6 +541,7 @@
"Save": "Save much wow",
"Save & Create": "Save & Create much create",
"Save & Update": "Save & Update much update",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs in browser storage not support anymore. Pls download and delete your chat logs by click button below. Much easy re-import to backend through",
"Scan": "Scan much scan",
@@ -538,7 +556,6 @@
"Search Models": "",
"Search Prompts": "Search Prompts much wow",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "",
"Search Tools": "",
"SearchApi API Key": "",
@@ -569,16 +586,20 @@
"Send": "",
"Send a Message": "Send a Message much message",
"Send message": "Send message very send",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "",
"Serper API Key": "",
"Serply API Key": "",
"Serpstack API Key": "",
"Server connection verified": "Server connection verified much secure",
"Set as default": "Set as default very default",
"Set CFG Scale": "",
"Set Default Model": "Set Default Model much model",
"Set embedding model (e.g. {{model}})": "",
"Set Image Size": "Set Image Size very size",
"Set reranking model (e.g. {{model}})": "",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "Set Steps so many steps",
"Set Task Model": "",
"Set Voice": "Set Voice so speak",
@@ -599,9 +620,11 @@
"Sign up": "Sign up much join",
"Signing in": "",
"Source": "Source",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}} so error",
"Speech-to-Text Engine": "Speech-to-Text Engine much speak",
"Stop Sequence": "Stop Sequence much stop",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT Settings very settings",
"Submit": "Submit much submit",
@@ -681,6 +704,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "",
"Update for the latest features and improvements.": "",
"Update password": "Update password much change",
"Updated at": "",
"Upload": "",
+25 -1
View File
@@ -9,6 +9,7 @@
"{{user}}'s Chats": "",
"{{webUIName}} Backend Required": "",
"*Prompt node ID(s) are required for image generation": "",
"A new version (v{{LATEST_VERSION}}) is now available.": "",
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
"a user": "",
"About": "",
@@ -73,7 +74,10 @@
"AUTOMATIC1111 Api Auth String": "",
"AUTOMATIC1111 Base URL": "",
"AUTOMATIC1111 Base URL is required.": "",
"Available list": "",
"available!": "",
"Azure AI Speech": "",
"Azure Region": "",
"Back": "",
"Bad Response": "",
"Banners": "",
@@ -94,6 +98,7 @@
"Chat Bubble UI": "",
"Chat Controls": "",
"Chat direction": "",
"Chat Overview": "",
"Chats": "",
"Check Again": "",
"Check for updates": "",
@@ -230,6 +235,7 @@
"Enable Message Rating": "",
"Enable New Sign Ups": "",
"Enable Web Search": "",
"Enable Web Search Query Generation": "",
"Enabled": "",
"Engine": "",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
@@ -237,6 +243,7 @@
"Enter a detail about yourself for your LLMs to recall": "",
"Enter api auth string (e.g. username:password)": "",
"Enter Brave Search API Key": "",
"Enter CFG Scale (e.g. 7.0)": "",
"Enter Chunk Overlap": "",
"Enter Chunk Size": "",
"Enter Github Raw URL": "",
@@ -247,6 +254,8 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "",
"Enter Sampler (e.g. Euler a)": "",
"Enter Scheduler (e.g. Karras)": "",
"Enter Score": "",
"Enter SearchApi API Key": "",
"Enter SearchApi Engine": "",
@@ -367,6 +376,7 @@
"Last Active": "",
"Last Modified": "",
"Leave empty for unlimited": "",
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Light": "",
"Listening...": "",
"LLMs can make mistakes. Verify important information.": "",
@@ -406,6 +416,8 @@
"Model {{modelId}} not found": "",
"Model {{modelName}} is not vision capable": "",
"Model {{name}} is now {{status}}": "",
"Model {{name}} is now at the top": "",
"Model accepts image inputs": "",
"Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
"Model ID": "",
@@ -417,6 +429,7 @@
"Modelfile Content": "",
"Models": "",
"More": "",
"Move to Top": "",
"Name": "",
"Name Tag": "",
"Name your model": "",
@@ -453,6 +466,7 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "",
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
"Open file": "",
"Open new chat": "",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
"OpenAI": "",
@@ -462,6 +476,9 @@
"OpenAI URL/Key required.": "",
"or": "",
"Other": "",
"Output format": "",
"Overview": "",
"page": "",
"Password": "",
"PDF document (.pdf)": "",
"PDF Extract Images (OCR)": "",
@@ -524,6 +541,7 @@
"Save": "",
"Save & Create": "",
"Save & Update": "",
"Save As Copy": "",
"Save Tag": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
"Scan": "",
@@ -538,7 +556,6 @@
"Search Models": "",
"Search Prompts": "",
"Search Query Generation Prompt": "",
"Search Query Generation Prompt Length Threshold": "",
"Search Result Count": "",
"Search Tools": "",
"SearchApi API Key": "",
@@ -567,16 +584,20 @@
"Send": "",
"Send a Message": "",
"Send message": "",
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
"September": "",
"Serper API Key": "",
"Serply API Key": "",
"Serpstack API Key": "",
"Server connection verified": "",
"Set as default": "",
"Set CFG Scale": "",
"Set Default Model": "",
"Set embedding model (e.g. {{model}})": "",
"Set Image Size": "",
"Set reranking model (e.g. {{model}})": "",
"Set Sampler": "",
"Set Scheduler": "",
"Set Steps": "",
"Set Task Model": "",
"Set Voice": "",
@@ -597,9 +618,11 @@
"Sign up": "",
"Signing in": "",
"Source": "",
"Speech Playback Speed": "",
"Speech recognition error: {{error}}": "",
"Speech-to-Text Engine": "",
"Stop Sequence": "",
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "",
"Submit": "",
@@ -679,6 +702,7 @@
"Unpin": "",
"Update": "",
"Update and Copy Link": "",
"Update for the latest features and improvements.": "",
"Update password": "",
"Updated at": "",
"Upload": "",

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