Compare commits

..

223 commits

Author SHA1 Message Date
fd5bf819cc
chore: Replace favicon
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Failing after 11s
Python CI / Format Backend (3.11) (push) Failing after 7s
Release / release (push) Successful in 44s
Frontend Build / Format & Build Frontend (push) Successful in 1m13s
2024-04-09 15:40:03 +02:00
Timothy Jaeryang Baek
331fe04df7
Merge pull request #1445 from open-webui/dev
All checks were successful
Python CI / Format Backend (3.11) (pull_request) Successful in 1m30s
Frontend Build / Format & Build Frontend (pull_request) Successful in 1m43s
dev
2024-04-06 13:21:15 -07:00
Timothy Jaeryang Baek
aed4763bfe
Merge pull request #1443 from aguvener/main
feat: Added tr-TR translation
2024-04-06 13:15:44 -07:00
aguvener
8d0f3fbde9
added tr-TR to languages.json 2024-04-06 19:03:59 +03:00
aguvener
318d4d1ce1
Create translation.json
Created Turkish translation file
2024-04-06 19:00:23 +03:00
Timothy J. Baek
5a787ab2c5 refac: pdf styling 2024-04-06 02:04:28 -07:00
Timothy J. Baek
81dbc65853 refac: pdf generation 2024-04-06 01:54:59 -07:00
Timothy Jaeryang Baek
d001d7afb1
Merge pull request #1436 from lainedfles/feat/litellm-local-cost-map
Use locally bundled version of the LiteLLM cost map json
2024-04-05 17:26:20 -07:00
Timothy Jaeryang Baek
183b84beae
Merge pull request #1438 from MurielLima/main
feat: translate portuguese brazil
2024-04-05 17:24:14 -07:00
Muriel.Lima
7d64ada43e feat: translate portuguese brazil 2024-04-05 19:45:10 -03:00
Self Denial
802eb3beec Use locally bundled version of the LiteLLM cost map json
Intended to avoid repetitive startup connections by setting `LITELLM_LOCAL_MODEL_COST_MAP="True"`
2024-04-05 16:27:31 -06:00
Timothy Jaeryang Baek
f87805b46f
Merge pull request #1424 from open-webui/dev
fix
2024-04-04 17:40:53 -07:00
Timothy Jaeryang Baek
7a7d1931d9
Merge pull request #1423 from slash-proc/fix_helm_ollama_base_url
remove deprecated /api from ollama base url
2024-04-04 17:39:21 -07:00
Timothy J. Baek
e61e1b079f fix: file upload issue 2024-04-04 17:38:59 -07:00
Doug Danat
7813c3316f remove deprecated /api from ollama base url 2024-04-04 23:57:32 +02:00
Timothy Jaeryang Baek
46774aa5cd
Merge pull request #1416 from open-webui/dev
0.1.117
2024-04-03 21:43:17 -07:00
Timothy J. Baek
62392aa88a Update CHANGELOG.md 2024-04-03 21:37:15 -07:00
Timothy J. Baek
c32d41b47f refac: about 2024-04-03 21:24:57 -07:00
Timothy Jaeryang Baek
9404b6135a
Merge pull request #1401 from theasp/add-dynamic-manifest
feat: Allow overriding application name in manifest.json
2024-04-03 20:45:51 -07:00
Timothy J. Baek
8caad59cb4 feat: allow webui name customisation 2024-04-03 20:45:23 -07:00
Timothy J. Baek
21132420d8 Update Dockerfile 2024-04-03 20:44:18 -07:00
Timothy J. Baek
c89070797c refac 2024-04-03 20:43:55 -07:00
Timothy J. Baek
1f85354856 feat: stylised pdf 2024-04-03 20:35:32 -07:00
Timothy J. Baek
bfd066cc2f fix: share chat modal 2024-04-03 19:45:08 -07:00
Timothy J. Baek
000bea84ae feat: chat pdf download 2024-04-03 19:30:25 -07:00
Timothy J. Baek
944efd2cd8 feat: sanitise response content 2024-04-03 10:57:58 -07:00
Timothy J. Baek
4a09342a71 chore: npm audit fix 2024-04-03 10:26:24 -07:00
Timothy Jaeryang Baek
aad13607c0
Merge pull request #1403 from helmanofer/main
Added boto3 to  requirements.txt
2024-04-03 10:21:02 -07:00
Timothy Jaeryang Baek
1509c2c2ee
Merge pull request #1406 from jannikstdl/fix-whitemode-onhover
Fix hover in whitemode
2024-04-03 10:20:04 -07:00
Timothy Jaeryang Baek
d94bc21ac8
Merge pull request #1408 from Mmx233/main
fix: manually check the docs' filename
2024-04-03 10:19:10 -07:00
Mmx233
947c392f72
fix: manually check the docs' filename 2024-04-03 23:37:13 +08:00
Jannik Streidl
15d24a6a11 Fix hover in whitemode 2024-04-03 16:00:06 +02:00
Ofer Helman
a3d6343dbe
Update requirements.txt 2024-04-03 16:27:49 +03:00
Ofer Helman
14fcf4d267
Update requirements.txt
In case of using bedrock with litellm import boto3 exception is raised
2024-04-03 11:44:01 +03:00
Andrew Phillips
83ad488e31 Do not use hardcoded manifest.json
Generate manifest.json dynamically, using MANIFEST_NAME and
MANIFEST_SHORT_NAME from the environment.
2024-04-02 16:08:10 -03:00
Timothy Jaeryang Baek
fa61e738c3
Merge pull request #1306 from vaayne/feature/support_auth_by_api_key
Feature/support auth by api key
2024-04-02 10:09:27 -07:00
Timothy J. Baek
0aa4d305c7 chore: print statement removed 2024-04-02 10:07:44 -07:00
Timothy J. Baek
1066096b33 fix 2024-04-02 10:06:58 -07:00
Timothy J. Baek
398b2076cd refac 2024-04-02 10:05:53 -07:00
Timothy J. Baek
fb516ea694 refac: styling 2024-04-02 09:58:34 -07:00
Timothy J. Baek
b35cc36e19 chore: formatting 2024-04-02 09:42:45 -07:00
Timothy J. Baek
6d1ce99537 revert: auth page 2024-04-02 09:40:36 -07:00
Timothy J. Baek
7c5f6d71b3 refac 2024-04-02 09:39:55 -07:00
Timothy J. Baek
e49e04c56a chore: formatting 2024-04-02 09:33:27 -07:00
Timothy J. Baek
da8646cae9 refac 2024-04-02 09:27:35 -07:00
Timothy J. Baek
ba0523cd69 refac: api_key field moved to user 2024-04-02 09:23:55 -07:00
Timothy J. Baek
38c34b4444 fix: import 2024-04-02 09:18:36 -07:00
Timothy J. Baek
b11af9ea0e fix 2024-04-02 09:18:15 -07:00
Timothy Jaeryang Baek
56369fea3a
Merge branch 'dev' into feature/support_auth_by_api_key 2024-04-02 09:12:19 -07:00
Timothy Jaeryang Baek
24fb77759e
Merge pull request #1347 from cheahjs/feat/trusted-email-header
feat: allow authenticating with a trusted email header
2024-04-02 09:11:25 -07:00
Timothy Jaeryang Baek
f9106cf394
Merge pull request #1364 from cheahjs/feat/local-sharing
feat: add local sharing of chats
2024-04-02 09:10:15 -07:00
Timothy J. Baek
807a60cccf fix: styling 2024-04-02 09:01:15 -07:00
Timothy J. Baek
ac4675900e Merge branch 'feat/local-sharing' of https://github.com/cheahjs/open-webui-fork into pr/1364 2024-04-02 08:58:38 -07:00
Timothy J. Baek
f7a1fc207a refac: wording 2024-04-02 08:58:03 -07:00
Timothy J. Baek
b9b1d64bd5 refac: styling 2024-04-02 08:58:03 -07:00
Timothy J. Baek
7e6a00b009 refac: share chat page 2024-04-02 08:58:03 -07:00
Timothy J. Baek
5aad328040 refac: toast message wording 2024-04-02 08:58:03 -07:00
Timothy J. Baek
03a7f88dca chore: remove print statement 2024-04-02 08:58:03 -07:00
Timothy J. Baek
f61869d90d fix: cascade shared chat delete 2024-04-02 08:58:03 -07:00
Timothy J. Baek
95505bc4c1 feat: chat cascade delete 2024-04-02 08:58:03 -07:00
Timothy J. Baek
eca59f759d fix: share chat modal 2024-04-02 08:58:03 -07:00
Timothy J. Baek
9975cb17a9 feat: update shared chat 2024-04-02 08:58:03 -07:00
Timothy J. Baek
0b823f90e6 feat: delete shared chat link 2024-04-02 08:58:03 -07:00
Timothy J. Baek
c0fff4c69f fix: typo 2024-04-02 08:58:03 -07:00
Timothy J. Baek
180dd3966b refac: share chat routes 2024-04-02 08:58:03 -07:00
Timothy J. Baek
e4d101e550 fix: delete shared chat by id 2024-04-02 08:58:03 -07:00
Timothy J. Baek
88c5b7f495 refac: share chat model 2024-04-02 08:58:03 -07:00
Timothy J. Baek
4e9ca31146 Update chats.py 2024-04-02 08:58:03 -07:00
Timothy J. Baek
48e7376336 refac: shared chat user id 2024-04-02 08:58:03 -07:00
Timothy J. Baek
488301f150 Update 001_initial_schema.py 2024-04-02 08:58:03 -07:00
Timothy J. Baek
287c95e72d refac: dedicated shared chat page 2024-04-02 08:58:03 -07:00
Timothy J. Baek
af5a7a35c0 refac: copy to clipboard message 2024-04-02 08:58:03 -07:00
Timothy J. Baek
5faa05a117 refac: share chat modal styling update 2024-04-02 08:58:03 -07:00
Jun Siang Cheah
e273a99153 feat: migrate db schema for local sharing 2024-04-02 08:58:03 -07:00
Jun Siang Cheah
151055590d feat: add ability to run db migrations 2024-04-02 08:58:03 -07:00
Jun Siang Cheah
7978adbf45 feat: add frontend support for locally sharing chats 2024-04-02 08:58:03 -07:00
Jun Siang Cheah
94976e5ed3 feat: add backend functions for sharing chats 2024-04-02 08:58:03 -07:00
Timothy J. Baek
3a1d747677 refac: wording 2024-04-02 08:18:47 -07:00
Timothy J. Baek
7121465eda refac: styling 2024-04-02 08:12:55 -07:00
Timothy J. Baek
52e83891b4 refac: share chat page 2024-04-02 08:11:48 -07:00
Timothy J. Baek
456a78ada9 refac: toast message wording 2024-04-02 08:01:45 -07:00
Timothy J. Baek
cd60dbe08c chore: remove print statement 2024-04-02 08:00:47 -07:00
Timothy J. Baek
a0d932b986 fix: cascade shared chat delete 2024-04-02 08:00:26 -07:00
Timothy J. Baek
10cad60566 feat: chat cascade delete 2024-04-02 07:55:56 -07:00
Timothy J. Baek
c6296e5108 fix: share chat modal 2024-04-02 07:45:33 -07:00
Timothy J. Baek
dd9cbd0ab9 feat: update shared chat 2024-04-02 07:42:37 -07:00
Timothy J. Baek
865c268312 feat: delete shared chat link 2024-04-02 07:16:25 -07:00
Timothy J. Baek
dcac8a773b fix: typo 2024-04-02 07:06:07 -07:00
Timothy J. Baek
96d75dfc1b refac: share chat routes 2024-04-02 07:04:29 -07:00
Timothy J. Baek
49428eb7ef fix: delete shared chat by id 2024-04-02 07:04:04 -07:00
Timothy J. Baek
6c0e3972dc refac: share chat model 2024-04-02 06:42:57 -07:00
Timothy J. Baek
458c317736 Update chats.py 2024-04-02 06:36:02 -07:00
Timothy J. Baek
98f68b8b08 refac: shared chat user id 2024-04-02 06:33:59 -07:00
Timothy J. Baek
b76e934140 Update 001_initial_schema.py 2024-04-02 06:27:11 -07:00
Timothy J. Baek
0cdcc2deb6 refac: dedicated shared chat page 2024-04-02 06:17:51 -07:00
Timothy J. Baek
58992d592c refac: copy to clipboard message 2024-04-02 06:14:54 -07:00
Timothy J. Baek
04e172deac refac: share chat modal styling update 2024-04-02 06:12:45 -07:00
Timothy J. Baek
a363c1f2f1 fix: styling 2024-04-02 04:37:35 -07:00
Timothy Jaeryang Baek
6425e794f6
Merge pull request #1394 from Axodouble/patch-1
Update Dutch nl-NL translations again.
2024-04-02 03:40:26 -07:00
Timothy Jaeryang Baek
f340178bb7
Merge pull request #1396 from open-webui/main
dev
2024-04-02 03:39:09 -07:00
Timothy J. Baek
aa6c8b1bfd feat: ascii art on startup 2024-04-02 03:03:55 -07:00
Axodouble
d1ca6b50d9
Update Dutch nl-NL translations again.
Updated all translations to have a Dutch translation.
2024-04-02 11:51:58 +02:00
Timothy J. Baek
57530d87f6 feat: /ollama head route 2024-04-02 02:03:13 -07:00
Timothy J. Baek
2c030218f8 fix: latex dollar sign issue 2024-04-01 19:25:47 -07:00
Timothy J. Baek
5558514ff1 fix 2024-04-01 15:23:12 -07:00
Timothy Jaeryang Baek
83c7633acb
Merge pull request from GHSA-9pgh-j74g-qj6m
Suggested mitigation for KL-CAN-2024-002.
2024-04-01 15:21:56 -07:00
Timothy Jaeryang Baek
6c3f2f8d38
Merge pull request #1382 from open-webui/main
dev
2024-04-01 14:16:35 -07:00
Timothy J. Baek
d72653cdea fix: download allowed hosts 2024-04-01 14:01:05 -07:00
KoreLogic Disclosures
6c96361402
Suggested mitigation for KL-CAN-2024-002. 2024-04-01 15:55:14 -05:00
Timothy Jaeryang Baek
554e56687c
Merge pull request from GHSA-39wr-r5vm-3jxj
fix: allowed hosts
2024-04-01 13:27:01 -07:00
Timothy J. Baek
77b1edcd0f fix: allowed hosts 2024-04-01 13:24:48 -07:00
Jun Siang Cheah
d667faea33 feat: migrate db schema for local sharing 2024-04-01 11:13:02 +01:00
Jun Siang Cheah
4cb2cc62d2 feat: add ability to run db migrations 2024-04-01 11:12:46 +01:00
Timothy J. Baek
698bfcf87f fix: navbar styling 2024-04-01 01:48:44 -07:00
Timothy Jaeryang Baek
edeff20e1d
Merge pull request #1374 from open-webui/dev
fix
2024-03-31 21:04:43 -07:00
Timothy J. Baek
eda157e303 fix: openai issue 2024-03-31 21:02:29 -07:00
Timothy J. Baek
653a0ff02f fix: katex overflow issue 2024-03-31 19:50:46 -07:00
Timothy Jaeryang Baek
8175a323e9
Merge pull request #1372 from cheahjs/chore/update-npm-format-scripts
chore: add format:backend to npm scripts
2024-03-31 15:34:51 -07:00
Jun Siang Cheah
11850b75c8 chore: add format:backend npm script to match ci 2024-03-31 22:13:39 +01:00
Jun Siang Cheah
562e40a7bd Merge branch 'dev' into feat/trusted-email-header 2024-03-31 22:08:58 +01:00
Jun Siang Cheah
0e3b7a11e3 chore: python formatting 2024-03-31 22:07:43 +01:00
Jun Siang Cheah
196f91d68c feat: add frontend support for locally sharing chats 2024-03-31 22:03:28 +01:00
Jun Siang Cheah
bfbfdae1c5 feat: add backend functions for sharing chats 2024-03-31 22:02:40 +01:00
Timothy J. Baek
a6c154d839 feat: rag context logging 2024-03-31 14:02:31 -07:00
Timothy Jaeryang Baek
7563619e26
Merge pull request #1368 from Patrice-Gaudicheau/fix/data-dir-handling
Update of main.py
2024-03-31 14:00:39 -07:00
Timothy J. Baek
1b5c92d66c revert: prompt suggestion change 2024-03-31 13:59:39 -07:00
Timothy Jaeryang Baek
2f23325c90
Merge branch 'dev' into fix/data-dir-handling 2024-03-31 13:57:34 -07:00
Patrice-Gaudicheau
cef676429f Spellchecking 2024-03-31 22:50:17 +02:00
Timothy Jaeryang Baek
a0935dec60
Merge branch 'dev' into feature/support_auth_by_api_key 2024-03-31 13:48:36 -07:00
Timothy Jaeryang Baek
587a8c5992
Merge pull request #1370 from lainedfles/logging-improvements
Improve logging.
2024-03-31 13:46:37 -07:00
Timothy J. Baek
4ddb7e124e fix: error message 2024-03-31 13:40:57 -07:00
Patrice-Gaudicheau
68cfdd89c4 Small simplification 2024-03-31 22:35:44 +02:00
Self Denial
144c9059a3 Improve logging. Move print() statements to appropiate log().
Add COMFYUI and WEBHOOK logging and associated environment variable
control. Add WEBHOOK payload & request debug logs.
2024-03-31 13:17:29 -06:00
Patrice-Gaudicheau
ee38b3c84d Enhance DATA_DIR handling in main.py and docker-compose configuration 2024-03-31 20:43:09 +02:00
Timothy Jaeryang Baek
86aa2ca6cb
Merge pull request #1363 from open-webui/dev
fix
2024-03-31 02:28:20 -07:00
Timothy J. Baek
1d7ca44017 fix: config data issue 2024-03-31 02:27:03 -07:00
Timothy J. Baek
174c082c69 chore: frontend formatting 2024-03-31 02:04:15 -07:00
Timothy J. Baek
bbb63104ea chore: frontend format 2024-03-31 02:01:42 -07:00
Timothy J. Baek
43832d165f chore: frontend formatting 2024-03-31 01:59:10 -07:00
Timothy J. Baek
d630485051 chore: delete test.json 2024-03-31 01:57:21 -07:00
Timothy J. Baek
784572590f chore: frontend i18n formatting 2024-03-31 01:54:54 -07:00
Timothy J. Baek
90a03c331d refac: ci 2024-03-31 01:51:13 -07:00
Timothy J. Baek
b391ad12ae refac: format backend ci 2024-03-31 01:47:53 -07:00
Timothy J. Baek
6c2eb814ed Update format-backend.yaml 2024-03-31 01:43:52 -07:00
Timothy Jaeryang Baek
3b0cb7945f
Merge pull request #1362 from open-webui/dev
0.1.116
2024-03-31 01:17:32 -07:00
Timothy J. Baek
46a181042b Update CHANGELOG.md 2024-03-31 01:17:03 -07:00
Timothy J. Baek
5af8d0612a chore: py formatting 2024-03-31 01:13:39 -07:00
Timothy J. Baek
d4fabeee3c doc: changelog
Co-Authored-By: 深度学习机器 <wuzsgdutee@163.com>
2024-03-31 01:11:18 -07:00
Timothy J. Baek
e47d6ce1f6 refac: return only last 5 changelog 2024-03-31 01:10:57 -07:00
Timothy J. Baek
15f4153fda fix: openai isseu 2024-03-29 18:24:30 -07:00
Timothy J. Baek
1371e66ed9 fix: litellm title generation issue 2024-03-29 18:14:05 -07:00
Timothy J. Baek
2fbb92aceb fix: tag validation 2024-03-29 18:00:08 -07:00
Timothy J. Baek
f96beeee8f fix: litellm issue 2024-03-29 17:55:40 -07:00
Timothy J. Baek
0c367412c3 feat: navbar ui update 2024-03-29 17:20:07 -07:00
Jun Siang Cheah
150152ddbd fix: accidental indent during format changed logic 2024-03-29 21:04:30 +00:00
Timothy J. Baek
12287f8680 chore: code formatting 2024-03-29 13:06:18 -07:00
Timothy J. Baek
047c9fe82c fix: styling 2024-03-29 13:02:38 -07:00
Timothy Jaeryang Baek
ce20f05ef1
Merge pull request #1343 from asedmammad/fix/add-model-selector-to-playground
fix: Add new model selector to playground
2024-03-29 12:44:31 -07:00
Timothy J. Baek
242311aee1 fix: styling 2024-03-29 12:44:16 -07:00
Timothy Jaeryang Baek
bbc34d9be5
Merge pull request #1339 from asedmammad/feat/chat-autocomplete-tags
feat: Chat autocomplete tags
2024-03-29 12:41:15 -07:00
Timothy Jaeryang Baek
09da5e71e2
Merge pull request #1348 from cheahjs/fix/format-actions-enforce
fix: enforce formatters on GitHub Actions
2024-03-29 12:12:40 -07:00
Timothy J. Baek
74638bbd6e fix: config json backward compatibility issue 2024-03-28 21:15:05 -07:00
Jun Siang Cheah
f34fe49bdd fix: enforce formatters on GitHub Actions 2024-03-28 23:46:11 +00:00
Ased Mammad
50089710f3 fix: Add new model selector to playground 2024-03-29 00:19:53 +03:30
Ased Mammad
4bf38e5287 feat: Add autocomplete to chat tags 2024-03-28 21:07:34 +03:30
Jun Siang Cheah
50f6addd6f feat: auto signup/login with WEBUI_AUTH_TRUSTED_EMAIL_HEADER 2024-03-28 10:34:57 +00:00
Timothy J. Baek
c42cb438f9 fix: litellm model filter issue 2024-03-28 02:45:56 -07:00
Timothy J. Baek
b93bdfb222 fix: theme issue 2024-03-28 02:44:16 -07:00
Timothy Jaeryang Baek
c85a2e8cd4
Merge pull request #1329 from ConnorsApps/fix-pvc-storage-class
fix: Helm Chart storageClassName typo
2024-03-27 23:55:01 -07:00
Timothy Jaeryang Baek
46e3208cde
Merge pull request #1325 from dannyl1u/feat/oled-dark-theme
feat: OLED dark theme
2024-03-27 23:54:07 -07:00
Timothy Jaeryang Baek
c6bb0ded5e
Merge pull request #1313 from asedmammad/feat/i18n-navigator-config
feat: Configurable i18n default translation language
2024-03-27 23:52:26 -07:00
Timothy J. Baek
d187b1615a fix: language ordering 2024-03-27 23:51:26 -07:00
Timothy Jaeryang Baek
5e24e03fe5
Merge pull request #1323 from shing100/feat/ko-KR_translation
feat: add ko-KR translation
2024-03-27 23:50:35 -07:00
Timothy Jaeryang Baek
2fcc4aec4b
Merge pull request #1316 from ryankupk/svelte-warnings
Update svelte.config.js to suppress unused CSS selector warnings
2024-03-27 23:49:42 -07:00
Danny Liu
67865fc5ad dynamically adjust --color-gray-950 value for OLED black sidebar 2024-03-27 22:39:47 -07:00
ConnorsApps
9ad097d265 fix: pvc storageClassName typo 2024-03-27 20:35:21 -04:00
Danny Liu
572eefe181 use 🌃 emoji for oled-dark option 2024-03-27 16:19:57 -07:00
Danny Liu
cf44209730 refac: code style 2024-03-27 16:19:02 -07:00
Danny Liu
dfeadf9595 ordering of themes in select menu, group dark themes together 2024-03-27 16:05:37 -07:00
Danny Liu
25c71d8ac2 Update theme options and persist OLED dark theme if selected 2024-03-27 16:04:34 -07:00
Timothy J. Baek
19d2121252 fix: docker issue 2024-03-27 13:08:43 -07:00
Danny Liu
c24d9d3bea Add 'oled' theme option to themes list 2024-03-27 11:23:00 -07:00
Danny Liu
976c714063 feat: OLED dark theme 2024-03-27 10:50:30 -07:00
Lim Geun
f87d5d3d9b add ko-KR translation 2024-03-27 23:05:45 +09:00
Jun Siang Cheah
29f13f34d3 feat: add WEBUI_AUTH_TRUSTED_EMAIL_HEADER for authenticating users by a trusted header
This is very yolo code, use at your own risk
2024-03-27 09:28:03 +00:00
Timothy Jaeryang Baek
7d45d2762f
Merge pull request #1311 from AndrewRyanChama/arc/openaitick
Add wait for tick before scrolling down in openai
2024-03-26 21:51:04 -07:00
Timothy Jaeryang Baek
e5408995d8
Merge pull request #1305 from changchiyou/feat/Teams_Incoming_Webhook
enhance: support Teams Incoming Webhook
2024-03-26 21:43:54 -07:00
changchiyou
d69dfb7c8b
feat: support google chat webhook
https://github.com/open-webui/open-webui/pull/1305/\#issuecomment-2021151237
2024-03-27 10:25:57 +08:00
ryankupk
addee2f248 Update svelte.config.js to suppress unused CSS selector warnings 2024-03-26 23:07:01 +00:00
Ased Mammad
0c021e42f5 rename config key to default_locale 2024-03-26 23:15:51 +03:30
Ased Mammad
64e6003627 refac: Remove translation values for en-US
Translation values for en-US are not necessary, the keys will be
used as fallback.
2024-03-26 23:04:01 +03:30
Ased Mammad
26121c5d88 wip: Set default language from config 2024-03-26 23:01:23 +03:30
Andrew Ryan
d245b756a3 Add wait for tick before scrolling down in openai
We had this wait on ollama but not for openai, causing it to fail
to scroll down in certain cases.

This fixes the issue.
2024-03-26 11:46:22 -07:00
liu.vaayne
b4b56f9c85 frontend support api key 2024-03-26 18:24:14 +08:00
liu.vaayne
81e928030f backend support api key 2024-03-26 18:22:17 +08:00
Chris
0b62bbb52e
Merge branch 'dev' into feat/Teams_Incoming_Webhook 2024-03-26 16:59:42 +08:00
Timothy J. Baek
cb364f0ac7 feat: focus search input by default 2024-03-26 01:51:24 -07:00
Timothy J. Baek
d12a7247bc fix: missing import 2024-03-26 01:47:33 -07:00
Timothy J. Baek
941bb7c603 refac: chat[id] page 2024-03-26 01:45:25 -07:00
Timothy J. Baek
3edc547389 feat: title auto generation for external models 2024-03-26 00:59:57 -07:00
changchiyou
427ca4e3f5
refactor: dynamically swap activitySubtitle and activityImage if CUSTOM_NAME has been set 2024-03-26 15:45:36 +08:00
changchiyou
5eac5c54f8
feat: Teams MessageCard
refer to https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using\?tabs\=cURL\#example-of-connector-message
2024-03-26 15:04:17 +08:00
Timothy Jaeryang Baek
a1fc2f4df0
Merge pull request #1292 from ddanat-smm/dev
Add htm/html support for RAG documents
2024-03-25 23:51:20 -07:00
Timothy J. Baek
3688955c77 fix: encoding issue 2024-03-25 23:50:52 -07:00
Timothy J. Baek
6307adfba1 feat: better error handling 2024-03-25 23:47:08 -07:00
Timothy Jaeryang Baek
4f78acaa5c
Merge pull request #1299 from jannikstdl/k8s-pvc-naming-fix
K8s fixes
2024-03-25 23:20:39 -07:00
Timothy J. Baek
7f405c74cd refac: rename 2024-03-25 23:20:16 -07:00
Timothy J. Baek
5d6cee0c58 feat: model selector w/ more info 2024-03-25 14:04:46 -07:00
Jannik S
af366cf732
fix 2024-03-25 21:50:58 +01:00
Timothy J. Baek
32cb5f3ceb feat: external model info 2024-03-25 13:46:06 -07:00
Jannik S
eecc7f3529
Deleted unused checks 2024-03-25 21:32:21 +01:00
Jannik S
8962d948cd
Check if running on K8s to correctly allocate Ollama Base URL 2024-03-25 21:21:32 +01:00
Doug Danat
c91a5d8b1f switch to using BeautifulSoup HTML loader so title is also captured 2024-03-25 11:26:18 +01:00
Doug Danat
77f4ffddc1 add htm/html to supported extensions in ui 2024-03-25 11:21:34 +01:00
Doug Danat
784a6ec85e include html langchain loader for RAG 2024-03-25 09:50:53 +01:00
Timothy J. Baek
eb004ccfc2 refac: icon update 2024-03-24 23:28:25 -07:00
Timothy J. Baek
c6f9e74477 feat: external model icon 2024-03-24 23:26:00 -07:00
Timothy J. Baek
302bd11b33 fix: pull model from selector 2024-03-24 23:11:55 -07:00
Timothy J. Baek
c19ea89a1d feat: model download from model selector 2024-03-24 23:03:26 -07:00
Timothy Jaeryang Baek
9560f9196d
Merge pull request #1283 from pazoff/dev
Fix spelling
2024-03-24 20:30:14 -05:00
Plamen Vatev
42bb1ec089
Fix spelling 2024-03-25 02:31:47 +02:00
Jannik S
c91c95431d
Changed allocation in webui-deployment.yaml 2024-03-24 08:18:28 +01:00
Jannik S
1d64d31275
Updated PVC naming 2024-03-24 08:17:38 +01:00
111 changed files with 6483 additions and 2728 deletions

View file

@ -10,3 +10,7 @@ OPENAI_API_KEY=''
# DO NOT TRACK # DO NOT TRACK
SCARF_NO_ANALYTICS=true SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true DO_NOT_TRACK=true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"

View file

@ -12,16 +12,13 @@ on:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env: env:
REGISTRY: git.depeuter.dev REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
RUNNER_TOOL_CACHE: /toolcache
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs: jobs:
build-and-push-image: build-and-push-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions: permissions:
contents: read contents: read
@ -42,7 +39,7 @@ jobs:
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.CI_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images - name: Extract metadata for Docker images
id: meta id: meta

View file

@ -1,31 +1,39 @@
name: Python CI name: Python CI
on: on:
push: push:
branches: ['main'] branches:
- main
- dev
pull_request: pull_request:
branches:
- main
- dev
jobs: jobs:
build: build:
name: 'Format Backend' name: 'Format Backend'
env:
PUBLIC_API_BASE_URL: ''
RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: python-version: [3.11]
- latest
python-version: [ '3.12.2' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Python
- name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Use Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install yapf pip install black
- name: Format backend - name: Format backend
run: bun run format:backend run: npm run format:backend
- name: Check for changes after format
run: git diff --exit-code

View file

@ -1,22 +1,36 @@
name: Bun CI name: Frontend Build
on: on:
push: push:
branches: ['main'] branches:
- main
- dev
pull_request: pull_request:
branches:
- main
- dev
jobs: jobs:
build: build:
name: 'Format & Build Frontend' name: 'Format & Build Frontend'
env:
PUBLIC_API_BASE_URL: ''
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout Repository
- name: Use Bun uses: actions/checkout@v4
uses: oven-sh/setup-bun@v1
- run: bun --version - name: Setup Node.js
- name: Install frontend dependencies uses: actions/setup-node@v3
run: bun install with:
- name: Format frontend node-version: '20' # Or specify any other version you want to use
run: bun run format
- name: Build frontend - name: Install Dependencies
run: bun run build run: npm install
- name: Format Frontend
run: npm run format
- name: Check for Changes After Format
run: git diff --exit-code
- name: Build Frontend
run: npm run build

View file

@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.117] - 2024-04-03
### Added
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
- 📝 **Improved Logging**: Enhancements to logging functionality.
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
### Fixed
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
- 🔒 **Security Patches**: Applied necessary security patches.
## [0.1.116] - 2024-03-31
### Added
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
- **🌐 Localization**: Korean translation (I18n) now available.
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
### Fixed
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
- **✅ Spelling**: Corrected various spelling issues for improved readability.
## [0.1.115] - 2024-03-24 ## [0.1.115] - 2024-03-24
### Added ### Added

View file

@ -26,10 +26,15 @@ ENV OPENAI_API_BASE_URL ""
ENV OPENAI_API_KEY "" ENV OPENAI_API_KEY ""
ENV WEBUI_SECRET_KEY "" ENV WEBUI_SECRET_KEY ""
ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER ""
ENV SCARF_NO_ANALYTICS true ENV SCARF_NO_ANALYTICS true
ENV DO_NOT_TRACK true ENV DO_NOT_TRACK true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
######## Preloaded models ######## ######## Preloaded models ########
# whisper TTS Settings # whisper TTS Settings
ENV WHISPER_MODEL="base" ENV WHISPER_MODEL="base"

View file

@ -8,6 +8,8 @@ remove:
start: start:
@docker-compose start @docker-compose start
startAndBuild:
docker-compose up -d --build
stop: stop:
@docker-compose stop @docker-compose stop

View file

@ -22,7 +22,13 @@ from utils.utils import (
) )
from utils.misc import calculate_sha256 from utils.misc import calculate_sha256
from config import SRC_LOG_LEVELS, CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["AUDIO"]) log.setLevel(SRC_LOG_LEVELS["AUDIO"])

View file

@ -325,7 +325,7 @@ def save_url_image(url):
return image_id return image_id
except Exception as e: except Exception as e:
print(f"Error saving image: {e}") log.exception(f"Error saving image: {e}")
return None return None
@ -397,7 +397,7 @@ def generate_image(
user.id, user.id,
app.state.COMFYUI_BASE_URL, app.state.COMFYUI_BASE_URL,
) )
print(res) log.debug(f"res: {res}")
images = [] images = []
@ -409,7 +409,7 @@ def generate_image(
with open(file_body_path, "w") as f: with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f) json.dump(data.model_dump(exclude_none=True), f)
print(images) log.debug(f"images: {images}")
return images return images
else: else:
if form_data.model: if form_data.model:

View file

@ -4,6 +4,12 @@ import json
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import random import random
import logging
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
from pydantic import BaseModel from pydantic import BaseModel
@ -121,7 +127,7 @@ COMFYUI_DEFAULT_PROMPT = """
def queue_prompt(prompt, client_id, base_url): def queue_prompt(prompt, client_id, base_url):
print("queue_prompt") log.info("queue_prompt")
p = {"prompt": prompt, "client_id": client_id} p = {"prompt": prompt, "client_id": client_id}
data = json.dumps(p).encode("utf-8") data = json.dumps(p).encode("utf-8")
req = urllib.request.Request(f"{base_url}/prompt", data=data) req = urllib.request.Request(f"{base_url}/prompt", data=data)
@ -129,7 +135,7 @@ def queue_prompt(prompt, client_id, base_url):
def get_image(filename, subfolder, folder_type, base_url): def get_image(filename, subfolder, folder_type, base_url):
print("get_image") log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type} data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data) url_values = urllib.parse.urlencode(data)
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response: with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
@ -137,14 +143,14 @@ def get_image(filename, subfolder, folder_type, base_url):
def get_image_url(filename, subfolder, folder_type, base_url): def get_image_url(filename, subfolder, folder_type, base_url):
print("get_image") log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type} data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data) url_values = urllib.parse.urlencode(data)
return f"{base_url}/view?{url_values}" return f"{base_url}/view?{url_values}"
def get_history(prompt_id, base_url): def get_history(prompt_id, base_url):
print("get_history") log.info("get_history")
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response: with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
return json.loads(response.read()) return json.loads(response.read())
@ -212,15 +218,15 @@ def comfyui_generate_image(
try: try:
ws = websocket.WebSocket() ws = websocket.WebSocket()
ws.connect(f"ws://{host}/ws?clientId={client_id}") ws.connect(f"ws://{host}/ws?clientId={client_id}")
print("WebSocket connection established.") log.info("WebSocket connection established.")
except Exception as e: except Exception as e:
print(f"Failed to connect to WebSocket server: {e}") log.exception(f"Failed to connect to WebSocket server: {e}")
return None return None
try: try:
images = get_images(ws, comfyui_prompt, client_id, base_url) images = get_images(ws, comfyui_prompt, client_id, base_url)
except Exception as e: except Exception as e:
print(f"Error while receiving images: {e}") log.exception(f"Error while receiving images: {e}")
images = None images = None
ws.close() ws.close()

View file

@ -33,7 +33,13 @@ from constants import ERROR_MESSAGES
from utils.utils import decode_token, get_current_user, get_admin_user from utils.utils import decode_token, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, UPLOAD_DIR from config import (
SRC_LOG_LEVELS,
OLLAMA_BASE_URLS,
MODEL_FILTER_ENABLED,
MODEL_FILTER_LIST,
UPLOAD_DIR,
)
from utils.misc import calculate_sha256 from utils.misc import calculate_sha256
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -75,6 +81,12 @@ async def check_url(request: Request, call_next):
return response return response
@app.head("/")
@app.get("/")
async def get_status():
return {"status": True}
@app.get("/urls") @app.get("/urls")
async def get_ollama_api_urls(user=Depends(get_admin_user)): async def get_ollama_api_urls(user=Depends(get_admin_user)):
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
@ -266,7 +278,7 @@ async def pull_model(
if request_id in REQUEST_POOL: if request_id in REQUEST_POOL:
yield chunk yield chunk
else: else:
print("User: canceled request") log.warning("User: canceled request")
break break
finally: finally:
if hasattr(r, "close"): if hasattr(r, "close"):
@ -664,7 +676,7 @@ async def generate_completion(
else: else:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="error_detail", detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
) )
url = app.state.OLLAMA_BASE_URLS[url_idx] url = app.state.OLLAMA_BASE_URLS[url_idx]
@ -770,7 +782,11 @@ async def generate_chat_completion(
r = None r = None
log.debug("form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(form_data.model_dump_json(exclude_none=True).encode())) log.debug(
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
form_data.model_dump_json(exclude_none=True).encode()
)
)
def get_request(): def get_request():
nonlocal form_data nonlocal form_data
@ -1019,6 +1035,14 @@ async def download_model(
url_idx: Optional[int] = None, url_idx: Optional[int] = None,
): ):
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
if not any(form_data.url.startswith(host) for host in allowed_hosts):
raise HTTPException(
status_code=400,
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
)
if url_idx == None: if url_idx == None:
url_idx = 0 url_idx = 0
url = app.state.OLLAMA_BASE_URLS[url_idx] url = app.state.OLLAMA_BASE_URLS[url_idx]
@ -1027,6 +1051,7 @@ async def download_model(
if file_name: if file_name:
file_path = f"{UPLOAD_DIR}/{file_name}" file_path = f"{UPLOAD_DIR}/{file_name}"
return StreamingResponse( return StreamingResponse(
download_file_stream(url, form_data.url, file_path, file_name), download_file_stream(url, form_data.url, file_path, file_name),
) )

View file

@ -8,7 +8,7 @@ from fastapi import (
Form, Form,
) )
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import os, shutil, logging import os, shutil, logging, re
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@ -21,6 +21,7 @@ from langchain_community.document_loaders import (
TextLoader, TextLoader,
PyPDFLoader, PyPDFLoader,
CSVLoader, CSVLoader,
BSHTMLLoader,
Docx2txtLoader, Docx2txtLoader,
UnstructuredEPubLoader, UnstructuredEPubLoader,
UnstructuredWordDocumentLoader, UnstructuredWordDocumentLoader,
@ -114,6 +115,7 @@ class CollectionNameForm(BaseModel):
class StoreWebForm(CollectionNameForm): class StoreWebForm(CollectionNameForm):
url: str url: str
@app.get("/") @app.get("/")
async def get_status(): async def get_status():
return { return {
@ -296,13 +298,18 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
text_splitter = RecursiveCharacterTextSplitter( text_splitter = RecursiveCharacterTextSplitter(
chunk_size=app.state.CHUNK_SIZE, chunk_size=app.state.CHUNK_SIZE,
chunk_overlap=app.state.CHUNK_OVERLAP, chunk_overlap=app.state.CHUNK_OVERLAP,
add_start_index=True, add_start_index=True,
) )
docs = text_splitter.split_documents(data) docs = text_splitter.split_documents(data)
return store_docs_in_vector_db(docs, collection_name, overwrite)
if len(docs) > 0:
return store_docs_in_vector_db(docs, collection_name, overwrite), None
else:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
def store_text_in_vector_db( def store_text_in_vector_db(
@ -318,6 +325,7 @@ def store_text_in_vector_db(
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool: def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
texts = [doc.page_content for doc in docs] texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs] metadatas = [doc.metadata for doc in docs]
@ -325,7 +333,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
if overwrite: if overwrite:
for collection in CHROMA_CLIENT.list_collections(): for collection in CHROMA_CLIENT.list_collections():
if collection_name == collection.name: if collection_name == collection.name:
print(f"deleting existing collection {collection_name}") log.info(f"deleting existing collection {collection_name}")
CHROMA_CLIENT.delete_collection(name=collection_name) CHROMA_CLIENT.delete_collection(name=collection_name)
collection = CHROMA_CLIENT.create_collection( collection = CHROMA_CLIENT.create_collection(
@ -338,7 +346,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
) )
return True return True
except Exception as e: except Exception as e:
print(e) log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError": if e.__class__.__name__ == "UniqueConstraintError":
return True return True
@ -402,6 +410,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
loader = UnstructuredRSTLoader(file_path, mode="elements") loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml": elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path) loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md": elif file_ext == "md":
loader = UnstructuredMarkdownLoader(file_path) loader = UnstructuredMarkdownLoader(file_path)
elif file_content_type == "application/epub+zip": elif file_content_type == "application/epub+zip":
@ -438,8 +448,11 @@ def store_doc(
log.info(f"file.content_type: {file.content_type}") log.info(f"file.content_type: {file.content_type}")
try: try:
filename = file.filename unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
file_path = f"{UPLOAD_DIR}/{filename}" file_path = f"{UPLOAD_DIR}/{filename}"
contents = file.file.read() contents = file.file.read()
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(contents) f.write(contents)
@ -450,8 +463,10 @@ def store_doc(
collection_name = calculate_sha256(f)[:63] collection_name = calculate_sha256(f)[:63]
f.close() f.close()
loader, known_type = get_loader(file.filename, file.content_type, file_path) loader, known_type = get_loader(filename, file.content_type, file_path)
data = loader.load() data = loader.load()
try:
result = store_data_in_vector_db(data, collection_name) result = store_data_in_vector_db(data, collection_name)
if result: if result:
@ -461,10 +476,10 @@ def store_doc(
"filename": filename, "filename": filename,
"known_type": known_type, "known_type": known_type,
} }
else: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(), detail=e,
) )
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
@ -529,6 +544,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
) )
data = loader.load() data = loader.load()
try:
result = store_data_in_vector_db(data, collection_name) result = store_data_in_vector_db(data, collection_name)
if result: if result:
@ -561,6 +577,9 @@ def scan_docs_dir(user=Depends(get_admin_user)):
} }
), ),
) )
except Exception as e:
log.exception(e)
pass
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)

View file

@ -156,6 +156,8 @@ def rag_messages(docs, messages, template, k, embedding_function):
relevant_contexts.append(context) relevant_contexts.append(context)
log.debug(f"relevant_contexts: {relevant_contexts}")
context_string = "" context_string = ""
for context in relevant_contexts: for context in relevant_contexts:
if context: if context:

View file

@ -1,4 +1,5 @@
from peewee import * from peewee import *
from peewee_migrate import Router
from config import SRC_LOG_LEVELS, DATA_DIR from config import SRC_LOG_LEVELS, DATA_DIR
import os import os
import logging import logging
@ -16,4 +17,6 @@ else:
DB = SqliteDatabase(f"{DATA_DIR}/webui.db") DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
DB.connect() router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)

View file

@ -0,0 +1,149 @@
"""Peewee migrations -- 001_initial_schema.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.CharField(max_length=255)
active = pw.BooleanField()
class Meta:
table_name = "auth"
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "chat"
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "chatidtag"
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.CharField()
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "document"
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "modelfile"
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.DateField()
class Meta:
table_name = "prompt"
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = "tag"
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.DateField()
class Meta:
table_name = "user"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("user")
migrator.remove_model("tag")
migrator.remove_model("prompt")
migrator.remove_model("modelfile")
migrator.remove_model("document")
migrator.remove_model("chatidtag")
migrator.remove_model("chat")
migrator.remove_model("auth")

View file

@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("chat", "share_id")

View file

@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "api_key")

View file

@ -0,0 +1,21 @@
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
2. Make your changes to the models.
3. From the `backend` directory, run the following command:
```bash
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.
4. The migration file will be created in the `apps/web/internal/migrations` directory.

View file

@ -20,6 +20,7 @@ from config import (
ENABLE_SIGNUP, ENABLE_SIGNUP,
USER_PERMISSIONS, USER_PERMISSIONS,
WEBHOOK_URL, WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
) )
app = FastAPI() app = FastAPI()
@ -34,7 +35,7 @@ app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.USER_PERMISSIONS = USER_PERMISSIONS app.state.USER_PERMISSIONS = USER_PERMISSIONS
app.state.WEBHOOK_URL = WEBHOOK_URL app.state.WEBHOOK_URL = WEBHOOK_URL
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View file

@ -11,6 +11,7 @@ from utils.utils import verify_password
from apps.web.internal.db import DB from apps.web.internal.db import DB
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])
@ -46,6 +47,10 @@ class Token(BaseModel):
token_type: str token_type: str
class ApiKey(BaseModel):
api_key: Optional[str] = None
class UserResponse(BaseModel): class UserResponse(BaseModel):
id: str id: str
email: str email: str
@ -122,6 +127,28 @@ class AuthsTable:
except: except:
return None return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_api_key: {api_key}")
# if no api_key, return None
if not api_key:
return None
try:
user = Users.get_user_by_api_key(api_key)
return user if user else None
except:
return False
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool: def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try: try:
query = Auth.update(password=new_password).where(Auth.id == id) query = Auth.update(password=new_password).where(Auth.id == id)

View file

@ -20,6 +20,7 @@ class Chat(Model):
title = CharField() title = CharField()
chat = TextField() # Save Chat JSON as Text chat = TextField() # Save Chat JSON as Text
timestamp = DateField() timestamp = DateField()
share_id = CharField(null=True, unique=True)
class Meta: class Meta:
database = DB database = DB
@ -31,6 +32,7 @@ class ChatModel(BaseModel):
title: str title: str
chat: str chat: str
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
share_id: Optional[str] = None
#################### ####################
@ -52,6 +54,7 @@ class ChatResponse(BaseModel):
title: str title: str
chat: dict chat: dict
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
class ChatTitleIdResponse(BaseModel): class ChatTitleIdResponse(BaseModel):
@ -95,6 +98,71 @@ class ChatTable:
except: except:
return None return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"timestamp": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
print("update_shared_chat_by_id")
chat = Chat.get(Chat.id == chat_id)
print(chat)
query = Chat.update(
title=chat.title,
chat=chat.chat,
).where(Chat.id == chat.share_id)
query.execute()
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
query = Chat.update(
share_id=share_id,
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chat_lists_by_user_id( def get_chat_lists_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50 self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]: ) -> List[ChatModel]:
@ -131,6 +199,13 @@ class ChatTable:
.order_by(Chat.timestamp.desc()) .order_by(Chat.timestamp.desc())
] ]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id) chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
@ -149,12 +224,15 @@ class ChatTable:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
query.execute() # Remove the rows, return number of rows removed. query.execute() # Remove the rows, return number of rows removed.
return True return True and self.delete_shared_chat_by_chat_id(id)
except: except:
return False return False
def delete_chats_by_user_id(self, user_id: str) -> bool: def delete_chats_by_user_id(self, user_id: str) -> bool:
try: try:
self.delete_shared_chats_by_user_id(user_id)
query = Chat.delete().where(Chat.user_id == user_id) query = Chat.delete().where(Chat.user_id == user_id)
query.execute() # Remove the rows, return number of rows removed. query.execute() # Remove the rows, return number of rows removed.
@ -162,5 +240,19 @@ class ChatTable:
except: except:
return False return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Chats = ChatTable(DB) Chats = ChatTable(DB)

View file

@ -13,6 +13,7 @@ from apps.web.internal.db import DB
import json import json
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -64,8 +64,8 @@ class ModelfilesTable:
self.db.create_tables([Modelfile]) self.db.create_tables([Modelfile])
def insert_new_modelfile( def insert_new_modelfile(
self, user_id: str, self, user_id: str, form_data: ModelfileForm
form_data: ModelfileForm) -> Optional[ModelfileModel]: ) -> Optional[ModelfileModel]:
if "tagName" in form_data.modelfile: if "tagName" in form_data.modelfile:
modelfile = ModelfileModel( modelfile = ModelfileModel(
**{ **{
@ -73,7 +73,8 @@ class ModelfilesTable:
"tag_name": form_data.modelfile["tagName"], "tag_name": form_data.modelfile["tagName"],
"modelfile": json.dumps(form_data.modelfile), "modelfile": json.dumps(form_data.modelfile),
"timestamp": int(time.time()), "timestamp": int(time.time()),
}) }
)
try: try:
result = Modelfile.create(**modelfile.model_dump()) result = Modelfile.create(**modelfile.model_dump())
@ -87,29 +88,28 @@ class ModelfilesTable:
else: else:
return None return None
def get_modelfile_by_tag_name(self, def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
tag_name: str) -> Optional[ModelfileModel]:
try: try:
modelfile = Modelfile.get(Modelfile.tag_name == tag_name) modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile)) return ModelfileModel(**model_to_dict(modelfile))
except: except:
return None return None
def get_modelfiles(self, def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
skip: int = 0,
limit: int = 50) -> List[ModelfileResponse]:
return [ return [
ModelfileResponse( ModelfileResponse(
**{ **{
**model_to_dict(modelfile), **model_to_dict(modelfile),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) for modelfile in Modelfile.select() )
for modelfile in Modelfile.select()
# .limit(limit).offset(skip) # .limit(limit).offset(skip)
] ]
def update_modelfile_by_tag_name( def update_modelfile_by_tag_name(
self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]: self, tag_name: str, modelfile: dict
) -> Optional[ModelfileModel]:
try: try:
query = Modelfile.update( query = Modelfile.update(
modelfile=json.dumps(modelfile), modelfile=json.dumps(modelfile),

View file

@ -52,8 +52,9 @@ class PromptsTable:
self.db = db self.db = db
self.db.create_tables([Prompt]) self.db.create_tables([Prompt])
def insert_new_prompt(self, user_id: str, def insert_new_prompt(
form_data: PromptForm) -> Optional[PromptModel]: self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
prompt = PromptModel( prompt = PromptModel(
**{ **{
"user_id": user_id, "user_id": user_id,
@ -61,7 +62,8 @@ class PromptsTable:
"title": form_data.title, "title": form_data.title,
"content": form_data.content, "content": form_data.content,
"timestamp": int(time.time()), "timestamp": int(time.time()),
}) }
)
try: try:
result = Prompt.create(**prompt.model_dump()) result = Prompt.create(**prompt.model_dump())
@ -81,13 +83,14 @@ class PromptsTable:
def get_prompts(self) -> List[PromptModel]: def get_prompts(self) -> List[PromptModel]:
return [ return [
PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select() PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select()
# .limit(limit).offset(skip) # .limit(limit).offset(skip)
] ]
def update_prompt_by_command( def update_prompt_by_command(
self, command: str, self, command: str, form_data: PromptForm
form_data: PromptForm) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
try: try:
query = Prompt.update( query = Prompt.update(
title=form_data.title, title=form_data.title,

View file

@ -11,6 +11,7 @@ import logging
from apps.web.internal.db import DB from apps.web.internal.db import DB
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -20,6 +20,7 @@ class User(Model):
role = CharField() role = CharField()
profile_image_url = CharField() profile_image_url = CharField()
timestamp = DateField() timestamp = DateField()
api_key = CharField(null=True, unique=True)
class Meta: class Meta:
database = DB database = DB
@ -32,6 +33,7 @@ class UserModel(BaseModel):
role: str = "pending" role: str = "pending"
profile_image_url: str = "/user.png" profile_image_url: str = "/user.png"
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
api_key: Optional[str] = None
#################### ####################
@ -82,6 +84,13 @@ class UsersTable:
except: except:
return None return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
user = User.get(User.api_key == api_key)
return UserModel(**model_to_dict(user))
except:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]: def get_user_by_email(self, email: str) -> Optional[UserModel]:
try: try:
user = User.get(User.email == email) user = User.get(User.email == email)
@ -149,5 +158,21 @@ class UsersTable:
except: except:
return False return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
query = User.update(api_key=api_key).where(User.id == id)
result = query.execute()
return True if result == 1 else False
except:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
user = User.get(User.id == id)
return user.api_key
except:
return None
Users = UsersTable(DB) Users = UsersTable(DB)

View file

@ -1,13 +1,10 @@
from fastapi import Response, Request from fastapi import Request
from fastapi import Depends, FastAPI, HTTPException, status from fastapi import Depends, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union
from fastapi import APIRouter, status from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import time
import uuid
import re import re
import uuid
from apps.web.models.auths import ( from apps.web.models.auths import (
SigninForm, SigninForm,
@ -17,6 +14,7 @@ from apps.web.models.auths import (
UserResponse, UserResponse,
SigninResponse, SigninResponse,
Auths, Auths,
ApiKey,
) )
from apps.web.models.users import Users from apps.web.models.users import Users
@ -25,10 +23,12 @@ from utils.utils import (
get_current_user, get_current_user,
get_admin_user, get_admin_user,
create_token, create_token,
create_api_key,
) )
from utils.misc import parse_duration, validate_email_format from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
router = APIRouter() router = APIRouter()
@ -79,6 +79,8 @@ async def update_profile(
async def update_password( async def update_password(
form_data: UpdatePasswordForm, session_user=Depends(get_current_user) form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
): ):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
if session_user: if session_user:
user = Auths.authenticate_user(session_user.email, form_data.password) user = Auths.authenticate_user(session_user.email, form_data.password)
@ -98,7 +100,22 @@ async def update_password(
@router.post("/signin", response_model=SigninResponse) @router.post("/signin", response_model=SigninResponse)
async def signin(request: Request, form_data: SigninForm): async def signin(request: Request, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
if not Users.get_user_by_email(trusted_email.lower()):
await signup(
request,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password) user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user: if user:
token = create_token( token = create_token(
data={"id": user.id}, data={"id": user.id},
@ -249,3 +266,40 @@ async def update_token_expires_duration(
return request.app.state.JWT_EXPIRES_IN return request.app.state.JWT_EXPIRES_IN
else: else:
return request.app.state.JWT_EXPIRES_IN return request.app.state.JWT_EXPIRES_IN
############################
# API Key
############################
# create api key
@router.post("/api_key", response_model=ApiKey)
async def create_api_key_(user=Depends(get_current_user)):
api_key = create_api_key()
success = Users.update_user_api_key_by_id(user.id, api_key)
if success:
return {
"api_key": api_key,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)
# delete api key
@router.delete("/api_key", response_model=bool)
async def delete_api_key(user=Depends(get_current_user)):
success = Users.update_user_api_key_by_id(user.id, None)
return success
# get api key
@router.get("/api_key", response_model=ApiKey)
async def get_api_key(user=Depends(get_current_user)):
api_key = Users.get_user_api_key_by_id(user.id)
if api_key:
return {
"api_key": api_key,
}
else:
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)

View file

@ -29,6 +29,7 @@ from apps.web.models.tags import (
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])
@ -188,6 +189,78 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
return result return result
############################
# ShareChatById
############################
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
if not shared_chat:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(),
)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# DeletedSharedChatById
############################
@router.delete("/{id}/share", response_model=Optional[bool])
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if not chat.share_id:
return False
result = Chats.delete_shared_chat_by_chat_id(id)
update_result = Chats.update_chat_share_id_by_id(id, None)
return result and update_result != None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################ ############################
# GetChatTagsById # GetChatTagsById
############################ ############################

View file

@ -10,7 +10,12 @@ import uuid
from apps.web.models.users import Users from apps.web.models.users import Users
from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token from utils.utils import (
get_password_hash,
get_current_user,
get_admin_user,
create_token,
)
from utils.misc import get_gravatar_url, validate_email_format from utils.misc import get_gravatar_url, validate_email_format
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
@ -43,7 +48,6 @@ async def set_global_default_models(
return request.app.state.DEFAULT_MODELS return request.app.state.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=List[PromptSuggestion]) @router.post("/default/suggestions", response_model=List[PromptSuggestion])
async def set_global_default_suggestions( async def set_global_default_suggestions(
request: Request, request: Request,

View file

@ -24,9 +24,9 @@ router = APIRouter()
@router.get("/", response_model=List[ModelfileResponse]) @router.get("/", response_model=List[ModelfileResponse])
async def get_modelfiles(skip: int = 0, async def get_modelfiles(
limit: int = 50, skip: int = 0, limit: int = 50, user=Depends(get_current_user)
user=Depends(get_current_user)): ):
return Modelfiles.get_modelfiles(skip, limit) return Modelfiles.get_modelfiles(skip, limit)
@ -36,17 +36,16 @@ async def get_modelfiles(skip: int = 0,
@router.post("/create", response_model=Optional[ModelfileResponse]) @router.post("/create", response_model=Optional[ModelfileResponse])
async def create_new_modelfile(form_data: ModelfileForm, async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
user=Depends(get_admin_user)):
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data) modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
if modelfile: if modelfile:
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -60,17 +59,18 @@ async def create_new_modelfile(form_data: ModelfileForm,
@router.post("/", response_model=Optional[ModelfileResponse]) @router.post("/", response_model=Optional[ModelfileResponse])
async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, async def get_modelfile_by_tag_name(
user=Depends(get_current_user)): form_data: ModelfileTagNameForm, user=Depends(get_current_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile: if modelfile:
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -84,8 +84,9 @@ async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm,
@router.post("/update", response_model=Optional[ModelfileResponse]) @router.post("/update", response_model=Optional[ModelfileResponse])
async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm, async def update_modelfile_by_tag_name(
user=Depends(get_admin_user)): form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile: if modelfile:
updated_modelfile = { updated_modelfile = {
@ -94,14 +95,15 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
} }
modelfile = Modelfiles.update_modelfile_by_tag_name( modelfile = Modelfiles.update_modelfile_by_tag_name(
form_data.tag_name, updated_modelfile) form_data.tag_name, updated_modelfile
)
return ModelfileResponse( return ModelfileResponse(
**{ **{
**modelfile.model_dump(), **modelfile.model_dump(),
"modelfile": "modelfile": json.loads(modelfile.modelfile),
json.loads(modelfile.modelfile), }
}) )
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -115,7 +117,8 @@ async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm,
@router.delete("/delete", response_model=bool) @router.delete("/delete", response_model=bool)
async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm, async def delete_modelfile_by_tag_name(
user=Depends(get_admin_user)): form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
):
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name) result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
return result return result

View file

@ -16,6 +16,7 @@ from utils.utils import get_current_user, get_password_hash, get_admin_user
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"]) log.setLevel(SRC_LOG_LEVELS["MODELS"])

View file

@ -1,14 +1,11 @@
from fastapi import APIRouter, UploadFile, File, BackgroundTasks from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from starlette.responses import StreamingResponse, FileResponse from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel from pydantic import BaseModel
import requests
import os from fpdf import FPDF
import aiohttp import markdown
import json
from utils.utils import get_admin_user from utils.utils import get_admin_user
@ -16,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
from typing import List
router = APIRouter() router = APIRouter()
@ -28,6 +25,70 @@ async def get_gravatar(
return get_gravatar_url(email) return get_gravatar_url(email)
class MarkdownForm(BaseModel):
md: str
@router.post("/markdown")
async def get_html_from_markdown(
form_data: MarkdownForm,
):
return {"html": markdown.markdown(form_data.md)}
class ChatForm(BaseModel):
title: str
messages: List[dict]
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatForm,
):
pdf = FPDF()
pdf.add_page()
STATIC_DIR = "./static"
FONTS_DIR = f"{STATIC_DIR}/fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_auto_page_break(auto=True, margin=15)
# Adjust the effective page width for multi_cell
effective_page_width = (
pdf.w - 2 * pdf.l_margin - 10
) # Subtracted an additional 10 for extra padding
# Add chat messages
for message in form_data.messages:
role = message["role"]
content = message["content"]
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
pdf.ln(1) # Extra space between messages
pdf.set_font("NotoSans", size=10) # Regular for content
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
pdf.ln(1.5) # Extra space between messages
# Save the pdf with name .pdf
pdf_bytes = pdf.output()
return Response(
content=bytes(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment;filename=chat.pdf"},
)
@router.get("/db/download") @router.get("/db/download")
async def download_db(user=Depends(get_admin_user)): async def download_db(user=Depends(get_admin_user)):

View file

@ -25,7 +25,9 @@ try:
except ImportError: except ImportError:
log.warning("dotenv not installed, skipping...") log.warning("dotenv not installed, skipping...")
WEBUI_NAME = "Aura" WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
shutil.copyfile("../build/favicon.png", "./static/favicon.png") shutil.copyfile("../build/favicon.png", "./static/favicon.png")
#################################### ####################################
@ -116,7 +118,20 @@ else:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = ["AUDIO", "CONFIG", "DB", "IMAGES", "LITELLM", "MAIN", "MODELS", "OLLAMA", "OPENAI", "RAG"] log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {} SRC_LOG_LEVELS = {}
@ -135,13 +150,14 @@ log.setLevel(SRC_LOG_LEVELS["CONFIG"])
#################################### ####################################
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
if CUSTOM_NAME: if CUSTOM_NAME:
try: try:
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
data = r.json() data = r.json()
if r.ok: if r.ok:
if "logo" in data: if "logo" in data:
url = ( WEBUI_FAVICON_URL = url = (
f"https://api.openwebui.com{data['logo']}" f"https://api.openwebui.com{data['logo']}"
if data["logo"][0] == "/" if data["logo"][0] == "/"
else data["logo"] else data["logo"]
@ -157,7 +173,9 @@ if CUSTOM_NAME:
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
pass pass
else:
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
#################################### ####################################
# DATA/FRONTEND BUILD DIR # DATA/FRONTEND BUILD DIR
@ -238,7 +256,7 @@ OLLAMA_API_BASE_URL = os.environ.get(
) )
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
K8S_FLAG = os.environ.get("K8S_FLAG", "")
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = ( OLLAMA_BASE_URL = (
@ -251,6 +269,9 @@ if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama": if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434" OLLAMA_BASE_URL = "http://host.docker.internal:11434"
elif K8S_FLAG:
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
@ -345,6 +366,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
#################################### ####################################
WEBUI_AUTH = True WEBUI_AUTH = True
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
#################################### ####################################
# WEBUI_SECRET_KEY # WEBUI_SECRET_KEY

View file

@ -20,6 +20,7 @@ class ERROR_MESSAGES(str, Enum):
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
USERNAME_TAKEN = ( USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username." "Uh-oh! This username is already registered. Please choose another username."
@ -36,6 +37,7 @@ class ERROR_MESSAGES(str, Enum):
INVALID_PASSWORD = ( INVALID_PASSWORD = (
"The password provided is incorrect. Please check for typos and try again." "The password provided is incorrect. Please check for typos and try again."
) )
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
UNAUTHORIZED = "401 Unauthorized" UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = ( ACTION_PROHIBITED = (
@ -58,5 +60,8 @@ class ERROR_MESSAGES(str, Enum):
RATE_LIMIT_EXCEEDED = "API rate limit exceeded" RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."

View file

@ -1,33 +1,22 @@
{ {
"version": 0, "version": 0,
"ui": { "ui": {
"default_locale": "en-US",
"prompt_suggestions": [ "prompt_suggestions": [
{ {
"title": [ "title": ["Help me study", "vocabulary for a college entrance exam"],
"Help me study",
"vocabulary for a college entrance exam"
],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
}, },
{ {
"title": [ "title": ["Give me ideas", "for what to do with my kids' art"],
"Give me ideas",
"for what to do with my kids' art"
],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
}, },
{ {
"title": [ "title": ["Tell me a fun fact", "about the Roman Empire"],
"Tell me a fun fact",
"about the Roman Empire"
],
"content": "Tell me a random fun fact about the Roman Empire" "content": "Tell me a random fun fact about the Roman Empire"
}, },
{ {
"title": [ "title": ["Show me a code snippet", "of a website's sticky header"],
"Show me a code snippet",
"of a website's sticky header"
],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
} }
] ]

View file

@ -32,6 +32,7 @@ from utils.utils import get_admin_user
from apps.rag.utils import rag_messages from apps.rag.utils import rag_messages
from config import ( from config import (
CONFIG_DATA,
WEBUI_NAME, WEBUI_NAME,
ENV, ENV,
VERSION, VERSION,
@ -49,6 +50,7 @@ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"]) log.setLevel(SRC_LOG_LEVELS["MAIN"])
class SPAStaticFiles(StaticFiles): class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope): async def get_response(self, path: str, scope):
try: try:
@ -60,6 +62,21 @@ class SPAStaticFiles(StaticFiles):
raise ex raise ex
print(
f"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|_|
v{VERSION} - building the best open-source AI user interface.
https://github.com/open-webui/open-webui
"""
)
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
@ -67,7 +84,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.WEBHOOK_URL = WEBHOOK_URL app.state.WEBHOOK_URL = WEBHOOK_URL
origins = ["*"] origins = ["*"]
@ -88,7 +104,6 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Example: Add a new key-value pair or modify existing ones # Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification # data["modified"] = True # Example modification
if "docs" in data: if "docs" in data:
data = {**data} data = {**data}
data["messages"] = rag_messages( data["messages"] = rag_messages(
data["docs"], data["docs"],
@ -163,14 +178,22 @@ app.mount("/rag/api/v1", rag_app)
@app.get("/api/config") @app.get("/api/config")
async def get_app_config(): async def get_app_config():
# Checking and Handling the Absence of 'ui' in CONFIG_DATA
default_locale = "en-US"
if "ui" in CONFIG_DATA:
default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US")
# The Rest of the Function Now Uses the Variables Defined Above
return { return {
"status": True, "status": True,
"name": WEBUI_NAME, "name": WEBUI_NAME,
"version": VERSION, "version": VERSION,
"default_locale": default_locale,
"images": images_app.state.ENABLED, "images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS, "default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
} }
@ -191,7 +214,6 @@ class ModelFilterConfigForm(BaseModel):
async def update_model_filter_config( async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user) form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
): ):
app.state.MODEL_FILTER_ENABLED = form_data.enabled app.state.MODEL_FILTER_ENABLED = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models app.state.MODEL_FILTER_LIST = form_data.models
@ -201,6 +223,9 @@ async def update_model_filter_config(
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
return { return {
"enabled": app.state.MODEL_FILTER_ENABLED, "enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST, "models": app.state.MODEL_FILTER_LIST,
@ -231,7 +256,6 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
@app.get("/api/version") @app.get("/api/version")
async def get_app_config(): async def get_app_config():
return { return {
"version": VERSION, "version": VERSION,
} }
@ -239,7 +263,7 @@ async def get_app_config():
@app.get("/api/changelog") @app.get("/api/changelog")
async def get_app_changelog(): async def get_app_changelog():
return CHANGELOG return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
@app.get("/api/version/updates") @app.get("/api/version/updates")
@ -259,6 +283,20 @@ async def get_app_latest_release_version():
) )
@app.get("/manifest.json")
async def get_manifest_json():
return {
"name": WEBUI_NAME,
"short_name": WEBUI_NAME,
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
}
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")

View file

@ -14,9 +14,12 @@ uuid
requests requests
aiohttp aiohttp
peewee peewee
peewee-migrate
bcrypt bcrypt
litellm==1.30.7 litellm==1.30.7
boto3
argon2-cffi argon2-cffi
apscheduler apscheduler
google-generativeai google-generativeai
@ -39,6 +42,8 @@ xlrd
opencv-python-headless opencv-python-headless
rapidocr-onnxruntime rapidocr-onnxruntime
fpdf2
faster-whisper faster-whisper
PyJWT PyJWT

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,8 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends from fastapi import HTTPException, status, Depends
from apps.web.models.users import Users from apps.web.models.users import Users
from pydantic import BaseModel from pydantic import BaseModel
from typing import Union, Optional from typing import Union, Optional
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
@ -8,6 +10,7 @@ from passlib.context import CryptContext
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests import requests
import jwt import jwt
import uuid
import logging import logging
import config import config
@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
return auth_header[len("Bearer ") :] return auth_header[len("Bearer ") :]
def create_api_key():
key = str(uuid.uuid4()).replace("-", "")
return f"sk-{key}"
def get_http_authorization_cred(auth_header: str): def get_http_authorization_cred(auth_header: str):
try: try:
scheme, credentials = auth_header.split(" ") scheme, credentials = auth_header.split(" ")
@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
def get_current_user( def get_current_user(
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
): ):
# auth by api key
if auth_token.credentials.startswith("sk-"):
return get_current_user_by_api_key(auth_token.credentials)
# auth by jwt token
data = decode_token(auth_token.credentials) data = decode_token(auth_token.credentials)
if data != None and "id" in data: if data != None and "id" in data:
user = Users.get_user_by_id(data["id"]) user = Users.get_user_by_id(data["id"])
@ -85,6 +97,16 @@ def get_current_user(
) )
def get_current_user_by_api_key(api_key: str):
user = Users.get_user_by_api_key(api_key)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
return user
def get_verified_user(user=Depends(get_current_user)): def get_verified_user(user=Depends(get_current_user)):
if user.role not in {"user", "admin"}: if user.role not in {"user", "admin"}:
raise HTTPException( raise HTTPException(

View file

@ -1,20 +1,54 @@
import json
import requests import requests
import logging
from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
def post_webhook(url: str, message: str, event_data: dict) -> bool: def post_webhook(url: str, message: str, event_data: dict) -> bool:
try: try:
payload = {} payload = {}
if "https://hooks.slack.com" in url: # Slack and Google Chat Webhooks
if "https://hooks.slack.com" in url or "https://chat.googleapis.com" in url:
payload["text"] = message payload["text"] = message
# Discord Webhooks
elif "https://discord.com/api/webhooks" in url: elif "https://discord.com/api/webhooks" in url:
payload["content"] = message payload["content"] = message
# Microsoft Teams Webhooks
elif "webhook.office.com" in url:
action = event_data.get("action", "undefined")
facts = [
{"name": name, "value": value}
for name, value in json.loads(event_data.get("user", {})).items()
]
payload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": message,
"sections": [
{
"activityTitle": message,
"activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
"activityImage": WEBUI_FAVICON_URL,
"facts": facts,
"markdown": True,
}
],
}
# Default Payload
else: else:
payload = {**event_data} payload = {**event_data}
log.debug(f"payload: {payload}")
r = requests.post(url, json=payload) r = requests.post(url, json=payload)
r.raise_for_status() r.raise_for_status()
log.debug(f"r.text: {r.text}")
return True return True
except Exception as e: except Exception as e:
print(e) log.exception(e)
return False return False

View file

@ -7,7 +7,7 @@ ollama
{{- end -}} {{- end -}}
{{- define "ollama.url" -}} {{- define "ollama.url" -}}
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }} {{- printf "http://%s.%s.svc.cluster.local:%d/" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }} {{- end }}
{{- define "chart.name" -}} {{- define "chart.name" -}}

View file

@ -88,7 +88,7 @@ spec:
resources: resources:
requests: requests:
storage: {{ .Values.ollama.persistence.size | quote }} storage: {{ .Values.ollama.persistence.size | quote }}
storageClass: {{ .Values.ollama.persistence.storageClass }} storageClassName: {{ .Values.ollama.persistence.storageClass }}
{{- with .Values.ollama.persistence.selector }} {{- with .Values.ollama.persistence.selector }}
selector: selector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View file

@ -17,7 +17,7 @@ spec:
resources: resources:
requests: requests:
storage: {{ .Values.webui.persistence.size }} storage: {{ .Values.webui.persistence.size }}
storageClass: {{ .Values.webui.persistence.storageClass }} storageClassName: {{ .Values.webui.persistence.storageClass }}
{{- with .Values.webui.persistence.selector }} {{- with .Values.webui.persistence.selector }}
selector: selector:
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}

View file

@ -35,4 +35,4 @@ spec:
volumes: volumes:
- name: webui-volume - name: webui-volume
persistentVolumeClaim: persistentVolumeClaim:
claimName: ollama-webui-pvc claimName: open-webui-pvc

View file

@ -2,8 +2,8 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
labels: labels:
app: ollama-webui app: open-webui
name: ollama-webui-pvc name: open-webui-pvc
namespace: open-webui namespace: open-webui
spec: spec:
accessModes: ["ReadWriteOnce"] accessModes: ["ReadWriteOnce"]

200
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.114", "version": "0.1.117",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.1.114", "version": "0.1.117",
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5", "async": "^3.2.5",
@ -19,6 +19,7 @@
"i18next-resources-to-backend": "^1.2.0", "i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1", "idb": "^7.1.1",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"jspdf": "^2.5.1",
"katex": "^0.16.9", "katex": "^0.16.9",
"marked": "^9.1.0", "marked": "^9.1.0",
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",
@ -1067,6 +1068,12 @@
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true "dev": true
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"optional": true
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@ -1402,6 +1409,17 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.19", "version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
@ -1459,6 +1477,15 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -1666,6 +1693,17 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@ -1758,6 +1796,31 @@
} }
] ]
}, },
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/canvg/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"optional": true
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -1944,6 +2007,17 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
"hasInstallScript": true,
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -1964,6 +2038,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@ -2156,6 +2239,12 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dompurify": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz",
"integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==",
"optional": true
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@ -2584,6 +2673,11 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -3003,6 +3097,19 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@ -3403,10 +3510,27 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jspdf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
"dependencies": {
"@babel/runtime": "^7.14.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.4.8"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.2.0",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/katex": { "node_modules/katex": {
"version": "0.16.9", "version": "0.16.10",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://github.com/sponsors/katex"
@ -3971,6 +4095,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true
},
"node_modules/periscopic": { "node_modules/periscopic": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@ -4391,6 +4521,15 @@
"rimraf": "bin.js" "rimraf": "bin.js"
} }
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -4494,6 +4633,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -4814,6 +4962,15 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true "dev": true
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stream-composer": { "node_modules/stream-composer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz",
@ -5215,6 +5372,15 @@
"@types/estree": "*" "@types/estree": "*"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/symlink-or-copy": { "node_modules/symlink-or-copy": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
@ -5353,6 +5519,15 @@
"streamx": "^2.12.5" "streamx": "^2.12.5"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -5583,6 +5758,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@ -5676,9 +5860,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.2", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
"postcss": "^8.4.27", "postcss": "^8.4.27",

View file

@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.115", "version": "0.1.117",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
@ -13,7 +13,7 @@
"lint:types": "npm run check", "lint:types": "npm run check",
"lint:backend": "pylint backend/", "lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", "format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format:backend": "yapf --recursive backend -p -i", "format:backend": "black . --exclude \"/venv/\"",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'" "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
}, },
"devDependencies": { "devDependencies": {
@ -53,6 +53,7 @@
"i18next-resources-to-backend": "^1.2.0", "i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1", "idb": "^7.1.1",
"js-sha256": "^0.10.1", "js-sha256": "^0.10.1",
"jspdf": "^2.5.1",
"katex": "^0.16.9", "katex": "^0.16.9",
"marked": "^9.1.0", "marked": "^9.1.0",
"svelte-sonner": "^0.3.19", "svelte-sonner": "^0.3.19",

View file

@ -78,3 +78,7 @@ select {
/* for Chrome */ /* for Chrome */
-webkit-appearance: none; -webkit-appearance: none;
} }
.katex-mathml {
display: none;
}

View file

@ -9,7 +9,11 @@
<script> <script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC // On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => { (() => {
if ( if (localStorage?.theme && localStorage?.theme.includes('oled')) {
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' || localStorage.theme === 'light' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) { ) {

View file

@ -318,3 +318,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
return res; return res;
}; };
export const createAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const getAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const deleteAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};

View file

@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => {
return res; return res;
}; };
export const getChatByShareId = async (token: string, share_id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const shareChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteSharedChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateChatById = async (token: string, id: string, chat: object) => { export const updateChatById = async (token: string, id: string, chat: object) => {
let error = null; let error = null;

View file

@ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => {
id: model.id, id: model.id,
name: model.name ?? model.id, name: model.name ?? model.id,
external: true, external: true,
source: 'litellm' source: 'LiteLLM'
})) }))
.sort((a, b) => { .sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);

View file

@ -263,3 +263,53 @@ export const synthesizeOpenAISpeech = async (
return res; return res;
}; };
export const generateTitle = async (
token: string = '',
template: string,
model: string,
prompt: string,
url: string = OPENAI_API_BASE_URL
) => {
let error = null;
template = template.replace(/{{prompt}}/g, prompt);
console.log(template);
const res = await fetch(`${url}/chat/completions`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
messages: [
{
role: 'user',
content: template
}
],
stream: false
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
}
return null;
});
if (error) {
throw error;
}
return res?.choices[0]?.message?.content ?? 'New Chat';
};

View file

@ -22,6 +22,57 @@ export const getGravatarUrl = async (email: string) => {
return res; return res;
}; };
export const downloadChatAsPDF = async (chat: object) => {
let error = null;
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: chat.title,
messages: chat.messages
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.blob();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return blob;
};
export const getHTMLFromMarkdown = async (md: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
md: md
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return res.html;
};
export const downloadDatabase = async (token: string) => { export const downloadDatabase = async (token: string) => {
let error = null; let error = null;

View file

@ -16,6 +16,7 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let chatId = ''; export let chatId = '';
export let readOnly = false;
export let sendPrompt: Function; export let sendPrompt: Function;
export let continueGeneration: Function; export let continueGeneration: Function;
export let regenerateResponse: Function; export let regenerateResponse: Function;
@ -317,6 +318,7 @@
<UserMessage <UserMessage
on:delete={() => messageDeleteHandler(message.id)} on:delete={() => messageDeleteHandler(message.id)}
user={$user} user={$user}
{readOnly}
{message} {message}
isFirstMessage={messageIdx === 0} isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null siblings={message.parentId !== null
@ -335,6 +337,7 @@
modelfiles={selectedModelfiles} modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length} isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{confirmEditResponseMessage} {confirmEditResponseMessage}
{showPreviousMessage} {showPreviousMessage}
{showNextMessage} {showNextMessage}

View file

@ -17,7 +17,11 @@
import { config, settings } from '$lib/stores'; import { config, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/openai'; import { synthesizeOpenAISpeech } from '$lib/apis/openai';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
import { extractSentences } from '$lib/utils'; import {
extractSentences,
revertSanitizedResponseContent,
sanitizeResponseContent
} from '$lib/utils';
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
@ -33,6 +37,8 @@
export let isLastMessage = true; export let isLastMessage = true;
export let readOnly = false;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
export let showNextMessage: Function; export let showNextMessage: Function;
@ -54,7 +60,7 @@
let loadingSpeech = false; let loadingSpeech = false;
let generatingImage = false; let generatingImage = false;
$: tokens = marked.lexer(message.content); $: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@ -403,8 +409,10 @@
{:else} {:else}
{#each tokens as token} {#each tokens as token}
{#if token.type === 'code'} {#if token.type === 'code'}
<!-- {token.text} --> <CodeBlock
<CodeBlock lang={token.lang} code={token.text} /> lang={token.lang}
code={revertSanitizedResponseContent(token.text)}
/>
{:else} {:else}
{@html marked.parse(token.raw, { {@html marked.parse(token.raw, {
...defaults, ...defaults,
@ -422,7 +430,7 @@
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
> >
{#if siblings.length > 1} {#if siblings.length > 1}
<div class="flex self-center min-w-fit -mt-1"> <div class="flex self-center min-w-fit">
<button <button
class="self-center dark:hover:text-white hover:text-black transition" class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
@ -469,6 +477,7 @@
</div> </div>
{/if} {/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom"> <Tooltip content="Edit" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -494,6 +503,7 @@
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom"> <Tooltip content="Copy" placement="bottom">
<button <button
@ -521,6 +531,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if !readOnly}
<Tooltip content="Good Response" placement="bottom"> <Tooltip content="Good Response" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -574,6 +585,7 @@
> >
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Read Aloud" placement="bottom"> <Tooltip content="Read Aloud" placement="bottom">
<button <button
@ -656,7 +668,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if $config.images} {#if $config.images && !readOnly}
<Tooltip content="Generate Image" placement="bottom"> <Tooltip content="Generate Image" placement="bottom">
<button <button
class="{isLastMessage class="{isLastMessage
@ -752,7 +764,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if isLastMessage} {#if isLastMessage && !readOnly}
<Tooltip content="Continue Response" placement="bottom"> <Tooltip content="Continue Response" placement="bottom">
<button <button
type="button" type="button"

View file

@ -15,6 +15,7 @@
export let message; export let message;
export let siblings; export let siblings;
export let isFirstMessage: boolean; export let isFirstMessage: boolean;
export let readOnly: boolean;
export let confirmEditMessage: Function; export let confirmEditMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
@ -203,7 +204,7 @@
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> <div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
{#if siblings.length > 1} {#if siblings.length > 1}
<div class="flex self-center -mt-1"> <div class="flex self-center">
<button <button
class="self-center dark:hover:text-white hover:text-black transition" class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
@ -250,6 +251,7 @@
</div> </div>
{/if} {/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom"> <Tooltip content="Edit" placement="bottom">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
@ -273,6 +275,7 @@
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom"> <Tooltip content="Copy" placement="bottom">
<button <button
@ -298,7 +301,7 @@
</button> </button>
</Tooltip> </Tooltip>
{#if !isFirstMessage} {#if !isFirstMessage && !readOnly}
<Tooltip content="Delete" placement="bottom"> <Tooltip content="Delete" placement="bottom">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"

View file

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Collapsible } from 'bits-ui';
import { setDefaultModels } from '$lib/apis/configs'; import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores'; import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Select from '../common/Select.svelte'; import Selector from './ModelSelector/Selector.svelte';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -33,19 +36,19 @@
} }
</script> </script>
<div class="flex flex-col my-2 w-full"> <div class="flex flex-col mt-0.5 w-full">
{#each selectedModels as selectedModel, selectedModelIdx} {#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full"> <div class="flex w-full">
<div class="overflow-hidden w-full"> <div class="overflow-hidden w-full">
<div class="mr-2 max-w-full"> <div class="mr-0.5 max-w-full">
<Select <Selector
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
items={$models items={$models
.filter((model) => model.name !== 'hr') .filter((model) => model.name !== 'hr')
.map((model) => ({ .map((model) => ({
value: model.id, value: model.id,
label: label: model.name,
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}` info: model
}))} }))}
bind:value={selectedModel} bind:value={selectedModel}
/> />
@ -53,10 +56,10 @@
</div> </div>
{#if selectedModelIdx === 0} {#if selectedModelIdx === 0}
<div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600">
<Tooltip content="Add Model">
<button <button
class=" self-center {selectedModelIdx === 0 class=" "
? 'mr-3'
: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
{disabled} {disabled}
on:click={() => { on:click={() => {
selectedModels = [...selectedModels, '']; selectedModels = [...selectedModels, ''];
@ -73,12 +76,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg> </svg>
</button> </button>
</Tooltip>
</div>
{:else} {:else}
<div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2">
<Tooltip content="Remove Model">
<button <button
class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx ===
0
? 'mr-3'
: 'mr-7'}"
{disabled} {disabled}
on:click={() => { on:click={() => {
selectedModels.splice(selectedModelIdx, 1); selectedModels.splice(selectedModelIdx, 1);
@ -96,41 +99,13 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg> </svg>
</button> </button>
{/if} </Tooltip>
</div>
{#if selectedModelIdx === 0}
<button
class=" self-center dark:hover:text-gray-300"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
<div class="text-left mt-1.5 ml-1 text-xs text-gray-500"> <div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>

View file

@ -0,0 +1,389 @@
<script lang="ts">
import { Select } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte';
import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let value = '';
export let placeholder = 'Select a model';
export let searchEnabled = true;
export let searchPlaceholder = 'Search a model';
export let items = [{ value: 'mango', label: 'Mango' }];
let searchValue = '';
let ollamaVersion = null;
$: filteredItems = searchValue
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
: items;
const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim();
console.log($MODEL_DOWNLOAD_POOL);
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
toast.error(
$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
modelTag: sanitizedModelTag
})
);
return;
}
if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) {
toast.error(
$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
);
return;
}
const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => {
toast.error(error);
return null;
});
if (res) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
try {
const { value, done } = await reader.read();
if (done) break;
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
let data = JSON.parse(line);
console.log(data);
if (data.error) {
throw data.error;
}
if (data.detail) {
throw data.detail;
}
if (data.id) {
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
requestId: data.id,
reader,
done: false
}
});
console.log(data);
}
if (data.status) {
if (data.digest) {
let downloadProgress = 0;
if (data.completed) {
downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
} else {
downloadProgress = 100;
}
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
pullProgress: downloadProgress,
digest: data.digest
}
});
} else {
toast.success(data.status);
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL,
[sanitizedModelTag]: {
...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
done: data.status === 'success'
}
});
}
}
}
}
} catch (error) {
console.log(error);
if (typeof error !== 'string') {
error = error.message;
}
toast.error(error);
// opts.callback({ success: false, error, modelName: opts.modelName });
}
}
if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
toast.success(
$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
modelName: sanitizedModelTag
})
);
models.set(await getModels(localStorage.token));
} else {
toast.error('Download canceled');
}
delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL
});
}
};
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
});
const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model];
if (reader) {
await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete $MODEL_DOWNLOAD_POOL[model];
MODEL_DOWNLOAD_POOL.set({
...$MODEL_DOWNLOAD_POOL
});
await deleteModel(localStorage.token, model);
toast.success(`${model} download has been canceled`);
}
};
</script>
<Select.Root
{items}
onOpenChange={async () => {
searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
selected={items.find((item) => item.value === value) ?? ''}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
}}
>
<Select.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
{placeholder}
/>
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class=" z-40 w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
transition={flyAndScale}
sideOffset={4}
>
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
<Search className="size-4" strokeWidth="2.5" />
<input
id="model-search-input"
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none"
placeholder={searchPlaceholder}
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-72 overflow-y-auto">
{#each filteredItems as item}
<Select.Item
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
value={item.value}
label={item.label}
>
<div class="flex items-center gap-2">
<div class="line-clamp-1">
{item.label}
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400"
>{item.info?.details?.parameter_size ?? ''}</span
>
</div>
<!-- {JSON.stringify(item.info)} -->
{#if item.info.external}
<Tooltip content={item.info?.source ?? 'External'}>
<div class=" mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{:else}
<Tooltip
content={`${
item.info?.details?.quantization_level
? item.info?.details?.quantization_level + ' '
: ''
}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}
>
<div class=" mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
</Tooltip>
{/if}
</div>
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{:else}
<div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
</div>
</div>
{/each}
{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'}
<button
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
on:click={() => {
pullModelHandler();
}}
>
Pull "{searchValue}" from Ollama.com
</button>
{/if}
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
<div
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
>
<div class="flex">
<div class="-ml-2 mr-2.5 translate-y-0.5">
<svg
class="size-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
<div class="flex flex-col self-start">
<div class="line-clamp-1">
Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
: ''}
</div>
{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest}
<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1">
{$MODEL_DOWNLOAD_POOL[model].digest}
</div>
{/if}
</div>
</div>
<div class="mr-2 translate-y-0.5">
<Tooltip content="Cancel">
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
cancelModelPullHandler(model);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
{/each}
</div>
</slot>
</Select.Content>
</Select.Root>

View file

@ -6,6 +6,8 @@
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let ollamaVersion = ''; let ollamaVersion = '';
@ -51,8 +53,10 @@
</div> </div>
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> <div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
<div> <div class="flex gap-1">
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}>
v{WEBUI_VERSION} v{WEBUI_VERSION}
</Tooltip>
<a <a
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}" href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
@ -126,7 +130,9 @@
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Created by')} {#if !$WEBUI_NAME.includes('Open WebUI')}
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
{/if}{$i18n.t('Created by')}
<a <a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck" href="https://github.com/tjbck"

View file

@ -3,11 +3,13 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths'; import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
import UpdatePassword from './Account/UpdatePassword.svelte'; import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -15,8 +17,14 @@
let profileImageUrl = ''; let profileImageUrl = '';
let name = ''; let name = '';
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
let APIKey = '';
let showAPIKey = false;
let APIKeyCopied = false;
let profileImageInputElement: HTMLInputElement; let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
@ -33,9 +41,23 @@
return false; return false;
}; };
onMount(() => { const createAPIKeyHandler = async () => {
APIKey = await createAPIKey(localStorage.token);
if (APIKey) {
toast.success($i18n.t('API Key created.'));
} else {
toast.error($i18n.t('Failed to create API Key.'));
}
};
onMount(async () => {
name = $user.name; name = $user.name;
profileImageUrl = $user.profile_image_url; profileImageUrl = $user.profile_image_url;
APIKey = await getAPIKey(localStorage.token).catch((error) => {
console.log(error);
return '';
});
}); });
</script> </script>
@ -170,22 +192,23 @@
<hr class=" dark:border-gray-700 my-4" /> <hr class=" dark:border-gray-700 my-4" />
<div class=" w-full justify-between"> <div class="flex flex-col gap-4">
<div class="flex w-full justify-between"> <div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> <div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div> </div>
<div class="flex mt-2"> <div class="flex mt-2">
<div class="flex w-full"> <div class="flex w-full">
<input <input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showJWTToken ? 'text' : 'password'} type={showJWTToken ? 'text' : 'password'}
value={localStorage.token} value={localStorage.token}
disabled disabled
/> />
<button <button
class="dark:bg-gray-800 px-2 transition rounded-r-lg" class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
on:click={() => { on:click={() => {
showJWTToken = !showJWTToken; showJWTToken = !showJWTToken;
}} }}
@ -225,7 +248,7 @@
</div> </div>
<button <button
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
on:click={() => { on:click={() => {
copyToClipboard(localStorage.token); copyToClipboard(localStorage.token);
JWTTokenCopied = true; JWTTokenCopied = true;
@ -269,6 +292,143 @@
</button> </button>
</div> </div>
</div> </div>
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
</div>
<div class="flex mt-2">
{#if APIKey}
<div class="flex w-full">
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showAPIKey ? 'text' : 'password'}
value={APIKey}
disabled
/>
<button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800"
on:click={() => {
showAPIKey = !showAPIKey;
}}
>
{#if showAPIKey}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
<button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
on:click={() => {
copyToClipboard(APIKey);
APIKeyCopied = true;
setTimeout(() => {
APIKeyCopied = false;
}, 2000);
}}
>
{#if APIKeyCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
<Tooltip content="Create new key">
<button
class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg"
on:click={() => {
createAPIKeyHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button
>
{/if}
</div>
</div>
</div>
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">

View file

@ -14,7 +14,7 @@
export let getModels: Function; export let getModels: Function;
// General // General
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
let selectedTheme = 'system'; let selectedTheme = 'system';
let languages = []; let languages = [];
@ -91,12 +91,17 @@
}); });
const applyTheme = (_theme: string) => { const applyTheme = (_theme: string) => {
let themeToApply = _theme; let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme;
if (_theme === 'system') { if (_theme === 'system') {
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} }
if (themeToApply === 'dark' && !_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#171717');
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
}
themes themes
.filter((e) => e !== themeToApply) .filter((e) => e !== themeToApply)
.forEach((e) => { .forEach((e) => {
@ -115,7 +120,11 @@
const themeChangeHandler = (_theme: string) => { const themeChangeHandler = (_theme: string) => {
theme.set(_theme); theme.set(_theme);
localStorage.setItem('theme', _theme); localStorage.setItem('theme', _theme);
if (_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark');
}
applyTheme(_theme); applyTheme(_theme);
}; };
</script> </script>
@ -136,6 +145,7 @@
> >
<option value="system">⚙️ {$i18n.t('System')}</option> <option value="system">⚙️ {$i18n.t('System')}</option>
<option value="dark">🌑 {$i18n.t('Dark')}</option> <option value="dark">🌑 {$i18n.t('Dark')}</option>
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
<option value="light">☀️ {$i18n.t('Light')}</option> <option value="light">☀️ {$i18n.t('Light')}</option>
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option> <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> <option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs'; import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, user } from '$lib/stores'; import { config, models, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -14,6 +14,7 @@
let titleAutoGenerate = true; let titleAutoGenerate = true;
let responseAutoCopy = false; let responseAutoCopy = false;
let titleAutoGenerateModel = ''; let titleAutoGenerateModel = '';
let titleAutoGenerateModelExternal = '';
let fullScreenMode = false; let fullScreenMode = false;
let titleGenerationPrompt = ''; let titleGenerationPrompt = '';
@ -33,7 +34,12 @@
const toggleTitleAutoGenerate = async () => { const toggleTitleAutoGenerate = async () => {
titleAutoGenerate = !titleAutoGenerate; titleAutoGenerate = !titleAutoGenerate;
saveSettings({ titleAutoGenerate: titleAutoGenerate }); saveSettings({
title: {
...$settings.title,
auto: titleAutoGenerate
}
});
}; };
const toggleResponseAutoCopy = async () => { const toggleResponseAutoCopy = async () => {
@ -65,8 +71,13 @@
} }
saveSettings({ saveSettings({
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined, title: {
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined ...$settings.title,
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
modelExternal:
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
}
}); });
}; };
@ -77,16 +88,18 @@
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
titleAutoGenerate = settings.titleAutoGenerate ?? true; titleAutoGenerate = settings?.title?.auto ?? true;
responseAutoCopy = settings.responseAutoCopy ?? false; titleAutoGenerateModel = settings?.title?.model ?? '';
showUsername = settings.showUsername ?? false; titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
fullScreenMode = settings.fullScreenMode ?? false;
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
titleGenerationPrompt = titleGenerationPrompt =
settings.titleGenerationPrompt ?? settings?.title?.prompt ??
$i18n.t( $i18n.t(
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':" "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
) + ' {{prompt}}'; ) + ' {{prompt}}';
responseAutoCopy = settings.responseAutoCopy ?? false;
showUsername = settings.showUsername ?? false;
fullScreenMode = settings.fullScreenMode ?? false;
}); });
</script> </script>
@ -190,8 +203,9 @@
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div>
<div class="flex w-full"> <div class="flex w-full gap-2 pr-2">
<div class="flex-1 mr-2"> <div class="flex-1">
<div class=" text-xs mb-1">Local Models</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModel} bind:value={titleAutoGenerateModel}
@ -207,6 +221,24 @@
{/each} {/each}
</select> </select>
</div> </div>
<div class="flex-1">
<div class=" text-xs mb-1">External Models</div>
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={titleAutoGenerateModelExternal}
placeholder={$i18n.t('Select a model')}
>
<option value="" selected>{$i18n.t('Current Model')}</option>
{#each $models as model}
{#if model.name !== 'hr'}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
{model.name}
</option>
{/if}
{/each}
</select>
</div>
</div> </div>
<div class="mt-3 mr-2"> <div class="mt-3 mr-2">

View file

@ -258,6 +258,9 @@
console.log(error); console.log(error);
} }
} }
} else {
const error = await fileResponse?.json();
toast.error(error?.detail ?? error);
} }
if (uploaded) { if (uploaded) {

View file

@ -1,19 +1,124 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext, onMount } from 'svelte';
import Modal from '../common/Modal.svelte';
import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { chatId, modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte';
import Link from '../icons/Link.svelte';
let chat = null;
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let downloadChat: Function; const shareLocalChat = async () => {
export let shareChat: Function; const _chat = chat;
const sharedChat = await shareChatById(localStorage.token, $chatId);
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`;
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
copyToClipboard(chatShareUrl);
chat = await getChatById(localStorage.token, $chatId);
};
const shareChat = async () => {
const _chat = chat.chat;
console.log('share', _chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: _chat,
modelfiles: $modelfiles.filter((modelfile) =>
_chat.models.includes(modelfile.tagName)
)
}),
'*'
);
}
},
false
);
};
export let show = false; export let show = false;
$: if (show) {
(async () => {
if ($chatId) {
chat = await getChatById(localStorage.token, $chatId);
} else {
chat = null;
console.log(chat);
}
})();
}
</script> </script>
<Modal bind:show size="xs"> <Modal bind:show size="sm">
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> <div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
<button <button
class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white" class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-800" />
{#if chat}
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank"
>You have shared this chat <span class=" underline">before</span>.</a
>
Click here to
<button
class="underline"
on:click={async () => {
const res = await deleteSharedChatById(localStorage.token, $chatId);
if (res) {
chat = await getChatById(localStorage.token, $chatId);
}
}}>delete this link</button
> and create a new shared link.
{:else}
Messages you send after creating your link won't be shared. Users with the URL will be
able to view the shared chat.
{/if}
</div>
<div class="flex justify-end">
<div class="flex flex-col items-end space-x-1 mt-1.5">
<div class="flex gap-1">
<button
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
type="button" type="button"
on:click={() => { on:click={() => {
shareChat(); shareChat();
@ -23,19 +128,26 @@
{$i18n.t('Share to OpenWebUI Community')} {$i18n.t('Share to OpenWebUI Community')}
</button> </button>
<div class="flex justify-center space-x-1 mt-1.5">
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
<button <button
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button" type="button"
on:click={() => { on:click={() => {
downloadChat(); shareLocalChat();
show = false; show = false;
}} }}
> >
{$i18n.t('Download as a File')} <Link />
{#if chat.share_id}
{$i18n.t('Update and Copy Link')}
{:else}
{$i18n.t('Copy Link')}
{/if}
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
{/if}
</div>
</Modal> </Modal>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Tags from '../common/Tags.svelte';
const i18n = getContext('i18n');
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let show = false;
</script>
<Modal bind:show size="xs">
<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center">
<Tags {tags} {deleteTag} {addTag} />
</div>
</Modal>

View file

@ -12,6 +12,9 @@
export let value = ''; export let value = '';
export let placeholder = 'Select a model'; export let placeholder = 'Select a model';
export let searchEnabled = true;
export let searchPlaceholder = 'Search a model';
export let items = [ export let items = [
{ value: 'mango', label: 'Mango' }, { value: 'mango', label: 'Mango' },
{ value: 'watermelon', label: 'Watermelon' }, { value: 'watermelon', label: 'Watermelon' },
@ -49,22 +52,25 @@
transition={flyAndScale} transition={flyAndScale}
sideOffset={4} sideOffset={4}
> >
<slot>
{#if searchEnabled}
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3"> <div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
<Search className="size-4" strokeWidth="2.5" /> <Search className="size-4" strokeWidth="2.5" />
<input <input
bind:value={searchValue} bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none" class="w-full text-sm bg-transparent outline-none"
placeholder="Search a model" placeholder={searchPlaceholder}
/> />
</div> </div>
<hr class="border-gray-100 dark:border-gray-800" /> <hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-80 overflow-y-auto"> <div class="px-3 my-2 max-h-80 overflow-y-auto">
{#each filteredItems as item} {#each filteredItems as item}
<Select.Item <Select.Item
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 data-[highlighted]:bg-muted" class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
value={item.value} value={item.value}
label={item.label} label={item.label}
> >
@ -77,11 +83,13 @@
{/if} {/if}
</Select.Item> </Select.Item>
{:else} {:else}
<span class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100"> <div>
<div class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found No results found
</span> </div>
</div>
{/each} {/each}
</div> </div>
</slot>
</Select.Content> </Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root> </Select.Root>

View file

@ -1,24 +1,25 @@
<script lang="ts"> <script lang="ts">
export let className: string = 'text-white'; export let className: string = '';
export let theme: 'blue' | 'white' | 'black' = 'white';
</script> </script>
<div class="flex justify-center text-center {className}"> <div class="flex justify-center text-center {className}">
<svg <svg class="size-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
class="animate-spin -ml-1 mr-3 h-5 w-5 {theme === 'blue' ><style>
? 'text-sky-600' .spinner_ajPY {
: theme === 'white' transform-origin: center;
? 'text-white' animation: spinner_AtaB 0.75s infinite linear;
: 'text-gray-600'} " }
xmlns="http://www.w3.org/2000/svg" @keyframes spinner_AtaB {
fill="none" 100% {
viewBox="0 0 24 24" transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div> </div>

View file

@ -8,7 +8,7 @@
export let addTag: Function; export let addTag: Function;
</script> </script>
<div class="flex flex-row space-x-0.5 line-clamp-1"> <div class="flex flex-row flex-wrap gap-0.5 line-clamp-1">
<TagList <TagList
{tags} {tags}
on:delete={(e) => { on:delete={(e) => {
@ -17,6 +17,7 @@
/> />
<TagInput <TagInput
label={tags.length == 0 ? 'Add Tags' : ''}
on:add={(e) => { on:add={(e) => {
addTag(e.detail); addTag(e.detail);
}} }}

View file

@ -1,24 +1,31 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'; import { createEventDispatcher, getContext } from 'svelte';
import { tags } from '$lib/stores';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let label = '';
let showTagInput = false; let showTagInput = false;
let tagName = ''; let tagName = '';
const addTagHandler = async () => {
tagName = tagName.trim();
if (tagName !== '') {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
} else {
toast.error('Invalid Tag');
}
};
</script> </script>
<div class="flex space-x-1 pl-1.5"> <div class="flex space-x-1 pl-1.5">
{#if showTagInput} {#if showTagInput}
<div class="flex items-center"> <div class="flex items-center">
<button <button type="button" on:click={addTagHandler}>
type="button"
on:click={() => {
dispatch('add', tagName);
tagName = '';
showTagInput = false;
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
@ -34,12 +41,21 @@
</button> </button>
<input <input
bind:value={tagName} bind:value={tagName}
class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[8rem]" class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]"
placeholder={$i18n.t('Add a tag')} placeholder={$i18n.t('Add a tag')}
list="tagOptions"
on:keydown={(event) => {
if (event.key === 'Enter') {
addTagHandler();
}
}}
/> />
<datalist id="tagOptions">
{#each $tags as tag}
<option value={tag.name} />
{/each}
</datalist>
</div> </div>
<!-- TODO: Tag Suggestions -->
{/if} {/if}
<button <button
@ -62,4 +78,8 @@
</svg> </svg>
</div> </div>
</button> </button>
{#if label && !showTagInput}
<span class="text-xs pl-1.5 self-center">{label}</span>
{/if}
</div> </div>

View file

@ -7,7 +7,7 @@
{#each tags as tag} {#each tags as tag}
<div <div
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white" class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white"
> >
<div class=" text-[0.7rem] font-medium self-center line-clamp-1"> <div class=" text-[0.7rem] font-medium self-center line-clamp-1">
{tag.name} {tag.name}

View file

@ -29,6 +29,6 @@
}); });
</script> </script>
<div bind:this={tooltipElement} aria-label={content}> <div bind:this={tooltipElement} aria-label={content} class="flex">
<slot /> <slot />
</div> </div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
</svg>

View file

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
/>
</svg>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>

View file

@ -0,0 +1,15 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>

View file

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { getChatById } from '$lib/apis/chats'; import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
import { WEBUI_NAME, chatId, modelfiles, settings } from '$lib/stores';
import { slide } from 'svelte/transition';
import ShareChatModal from '../chat/ShareChatModal.svelte'; import ShareChatModal from '../chat/ShareChatModal.svelte';
import TagInput from '../common/Tags/TagInput.svelte'; import ModelSelector from '../chat/ModelSelector.svelte';
import Tags from '../common/Tags.svelte'; import Tooltip from '../common/Tooltip.svelte';
import Menu from './Navbar/Menu.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -16,71 +16,108 @@
export let title: string = $WEBUI_NAME; export let title: string = $WEBUI_NAME;
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
export let chat;
export let selectedModels;
export let tags = []; export let tags = [];
export let addTag: Function; export let addTag: Function;
export let deleteTag: Function; export let deleteTag: Function;
export let showModelSelector = true;
let showShareChatModal = false; let showShareChatModal = false;
let showDownloadChatModal = false;
let tagName = '';
let showTagInput = false;
const shareChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('share', chat);
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
// const url = 'http://localhost:5173';
const tab = await window.open(`${url}/chats/upload`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(
JSON.stringify({
chat: chat,
modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName))
}),
'*'
);
}
},
false
);
};
const downloadChat = async () => {
const chat = (await getChatById(localStorage.token, $chatId)).chat;
console.log('download', chat);
const chatText = chat.messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${chat.title}.txt`);
};
</script> </script>
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> <ShareChatModal bind:show={showShareChatModal} />
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div <div
class=" flex {$settings?.fullScreenMode ?? null class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
? 'max-w-full' w-full mx-auto px-3"
: 'max-w-3xl'} w-full mx-auto px-3"
> >
<div class="flex items-center w-full max-w-full"> <div class="flex items-center w-full max-w-full">
<div class="pr-2 self-start"> <div class="flex-1 overflow-hidden max-w-full">
{#if showModelSelector}
<ModelSelector bind:selectedModels />
{/if}
</div>
<div class="self-start flex flex-none items-center">
<div class="flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
{#if !shareEnabled}
<Tooltip content="Settings">
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
{:else}
<Menu
{chat}
{shareEnabled}
shareHandler={() => {
showShareChatModal = !showShareChatModal;
}}
downloadHandler={() => {
showDownloadChatModal = !showDownloadChatModal;
}}
{tags}
{deleteTag}
{addTag}
>
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
</div>
</button>
</Menu>
{/if}
<Tooltip content="New Chat">
<button <button
id="new-chat-button" id="new-chat-button"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition" class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
on:click={initNewChat} on:click={() => {
initNewChat();
}}
> >
<div class=" m-auto self-center"> <div class=" m-auto self-center">
<svg <svg
@ -98,41 +135,7 @@
</svg> </svg>
</div> </div>
</button> </button>
</div> </Tooltip>
<div class=" flex-1 self-center font-medium line-clamp-1">
<div>
{title != '' ? title : $WEBUI_NAME}
</div>
</div>
<div class="pl-2 self-center flex items-center space-x-2">
{#if shareEnabled}
<Tags {tags} {deleteTag} {addTag} />
<button
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition"
on:click={async () => {
showShareChatModal = !showShareChatModal;
// console.log(showShareChatModal);
}}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
{/if}
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,223 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { jsPDF } from 'jspdf';
import { showSettings } from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/common/Tags.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { downloadChatAsPDF } from '$lib/apis/utils';
export let shareEnabled: boolean = false;
export let shareHandler: Function;
export let downloadHandler: Function;
// export let tagHandler: Function;
export let chat;
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let onClose: Function = () => {};
const downloadTxt = 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`;
}, '');
let blob = new Blob([chatText], {
type: 'text/plain'
});
saveAs(blob, `chat-${_chat.title}.txt`);
};
const downloadPdf = async () => {
const _chat = chat.chat;
console.log('download', chat);
const blob = await downloadChatAsPDF(_chat);
// Create a URL for the blob
const url = window.URL.createObjectURL(blob);
// Create a link element to trigger the download
const a = document.createElement('a');
a.href = url;
a.download = `chat-${_chat.title}.pdf`;
// Append the link to the body and click it programmatically
document.body.appendChild(a);
a.click();
// Remove the link from the body
document.body.removeChild(a);
// Revoke the URL to release memory
window.URL.revokeObjectURL(url);
};
</script>
<Dropdown
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
>
<slot />
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
sideOffset={8}
side="bottom"
align="end"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
<div class="flex items-center">Settings</div>
</DropdownMenu.Item>
{#if shareEnabled}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
shareHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">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 dark:hover:bg-gray-850 rounded-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<div class="flex items-center">Download</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
downloadTxt();
}}
>
<div class="flex items-center line-clamp-1">Plain text (.txt)</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
downloadPdf();
}}
>
<div class="flex items-center line-clamp-1">PDF document (.pdf)</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags {tags} {deleteTag} {addTag} />
</div>
<!-- <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
tagHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
<div class="flex items-center">Tag</div>
</DropdownMenu.Item> -->
{/if}
</DropdownMenu.Content>
</div>
</Dropdown>

View file

@ -107,7 +107,7 @@
bind:this={navElement} bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show class="h-screen max-h-[100dvh] min-h-screen {show
? 'lg:relative w-[260px]' ? 'lg:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition z-40 fixed top-0 left-0 : '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
" "
> >
<div <div
@ -581,7 +581,7 @@
<div class="py-2 w-full"> <div class="py-2 w-full">
{#if $user.role === 'admin'} {#if $user.role === 'admin'}
<button <button
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
goto('/admin'); goto('/admin');
showDropdown = false; showDropdown = false;
@ -607,7 +607,7 @@
</button> </button>
<button <button
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
goto('/playground'); goto('/playground');
showDropdown = false; showDropdown = false;
@ -634,7 +634,7 @@
{/if} {/if}
<button <button
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => { on:click={async () => {
await showSettings.set(true); await showSettings.set(true);
showDropdown = false; showDropdown = false;
@ -669,7 +669,7 @@
<div class="py-2 w-full"> <div class="py-2 w-full">
<button <button
class="flex py-2.5 px-3.5 w-full dark:hover:bg-gray-800 transition" class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
localStorage.removeItem('token'); localStorage.removeItem('token');
location.href = '/auth'; location.href = '/auth';
@ -706,7 +706,7 @@
</div> </div>
<div <div
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0" class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
> >
<Tooltip <Tooltip
placement="right" placement="right"

View file

@ -22,6 +22,7 @@ export const SUPPORTED_FILE_TYPE = [
'text/plain', 'text/plain',
'text/csv', 'text/csv',
'text/xml', 'text/xml',
'text/html',
'text/x-python', 'text/x-python',
'text/css', 'text/css',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@ -50,6 +51,8 @@ export const SUPPORTED_FILE_EXTENSIONS = [
'h', 'h',
'c', 'c',
'cs', 'cs',
'htm',
'html',
'sql', 'sql',
'log', 'log',
'ini', 'ini',

View file

@ -37,23 +37,28 @@ const createIsLoadingStore = (i18n: i18nType) => {
return isLoading; return isLoading;
}; };
export const initI18n = (defaultLocale: string) => {
let detectionOrder = defaultLocale
? ['querystring', 'localStorage']
: ['querystring', 'localStorage', 'navigator'];
let fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US'];
const loadResource = (language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`);
i18next i18next
.use( .use(resourcesToBackend(loadResource))
resourcesToBackend(
(language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)
)
)
.use(LanguageDetector) .use(LanguageDetector)
.init({ .init({
debug: false, debug: false,
detection: { detection: {
order: ['querystring', 'localStorage', 'navigator'], order: detectionOrder,
caches: ['localStorage'], caches: ['localStorage'],
lookupQuerystring: 'lang', lookupQuerystring: 'lang',
lookupLocalStorage: 'locale' lookupLocalStorage: 'locale'
}, },
fallbackLng: { fallbackLng: {
default: ['en-US'] default: fallbackDefaultLocale
}, },
ns: 'translation', ns: 'translation',
returnEmptyString: false, returnEmptyString: false,
@ -61,6 +66,7 @@ i18next
escapeValue: false // not needed for svelte as it escapes by default escapeValue: false // not needed for svelte as it escapes by default
} }
}); });
};
const i18n = createI18nStore(i18next); const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next); const isLoadingStore = createIsLoadingStore(i18next);

View file

@ -279,7 +279,7 @@
"Send a Message": "Изпращане на Съобщение", "Send a Message": "Изпращане на Съобщение",
"Send message": "Изпращане на съобщение", "Send message": "Изпращане на съобщение",
"Server connection verified": "Server connection verified", "Server connection verified": "Server connection verified",
"Set as default": "Задай като по подразбиране", "Set as default": "Задай по подразбиране",
"Set Default Model": "Задай Модел По Подразбиране", "Set Default Model": "Задай Модел По Подразбиране",
"Set Image Size": "Задай Размер на Изображението", "Set Image Size": "Задай Размер на Изображението",
"Set Steps": "Задай Стъпки", "Set Steps": "Задай Стъпки",
@ -320,7 +320,7 @@
"Title": "Заглавие", "Title": "Заглавие",
"Title Auto-Generation": "Автоматично Генериране на Заглавие", "Title Auto-Generation": "Автоматично Генериране на Заглавие",
"Title Generation Prompt": "Промпт за Генериране на Заглавие", "Title Generation Prompt": "Промпт за Генериране на Заглавие",
"to": "до", "to": "в",
"To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,", "To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,",
"To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,", "To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,",
"to chat input.": "към чат входа.", "to chat input.": "към чат входа.",

View file

@ -1,128 +1,128 @@
{ {
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.", "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "",
"(Beta)": "(Beta)", "(Beta)": "",
"(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", "(e.g. `sh webui.sh --api`)": "",
"(latest)": "(latest)", "(latest)": "",
"{{modelName}} is thinking...": "{{modelName}} is thinking...", "{{modelName}} is thinking...": "",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Required", "{{webUIName}} Backend Required": "",
"a user": "", "a user": "",
"About": "About", "About": "",
"Account": "Account", "Account": "",
"Action": "Action", "Action": "",
"Add a model": "Add a model", "Add a model": "",
"Add a model tag name": "Add a model tag name", "Add a model tag name": "",
"Add a short description about what this modelfile does": "Add a short description about what this modelfile does", "Add a short description about what this modelfile does": "",
"Add a short title for this prompt": "Add a short title for this prompt", "Add a short title for this prompt": "",
"Add a tag": "Add a tag", "Add a tag": "",
"Add Docs": "Add Docs", "Add Docs": "",
"Add Files": "Add Files", "Add Files": "",
"Add message": "Add message", "Add message": "",
"add tags": "add tags", "add tags": "",
"Adjusting these settings will apply changes universally to all users.": "Adjusting these settings will apply changes universally to all users.", "Adjusting these settings will apply changes universally to all users.": "",
"admin": "admin", "admin": "",
"Admin Panel": "Admin Panel", "Admin Panel": "",
"Admin Settings": "Admin Settings", "Admin Settings": "",
"Advanced Parameters": "Advanced Parameters", "Advanced Parameters": "",
"all": "all", "all": "",
"All Users": "All Users", "All Users": "",
"Allow": "Allow", "Allow": "",
"Allow Chat Deletion": "Allow Chat Deletion", "Allow Chat Deletion": "",
"alphanumeric characters and hyphens": "alphanumeric characters and hyphens", "alphanumeric characters and hyphens": "",
"Already have an account?": "Already have an account?", "Already have an account?": "",
"an assistant": "", "an assistant": "",
"and": "and", "and": "",
"API Base URL": "API Base URL", "API Base URL": "",
"API Key": "API Key", "API Key": "",
"API RPM": "API RPM", "API RPM": "",
"are allowed - Activate this command by typing": "are allowed - Activate this command by typing", "are allowed - Activate this command by typing": "",
"Are you sure?": "", "Are you sure?": "",
"Audio": "Audio", "Audio": "",
"Auto-playback response": "Auto-playback response", "Auto-playback response": "",
"Auto-send input after 3 sec.": "Auto-send input after 3 sec.", "Auto-send input after 3 sec.": "",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", "AUTOMATIC1111 Base URL": "",
"AUTOMATIC1111 Base URL is required.": "", "AUTOMATIC1111 Base URL is required.": "",
"available!": "available!", "available!": "",
"Back": "Back", "Back": "",
"Builder Mode": "Builder Mode", "Builder Mode": "",
"Cancel": "Cancel", "Cancel": "",
"Categories": "Categories", "Categories": "",
"Change Password": "Change Password", "Change Password": "",
"Chat": "Chat", "Chat": "",
"Chat History": "Chat History", "Chat History": "",
"Chat History is off for this browser.": "Chat History is off for this browser.", "Chat History is off for this browser.": "",
"Chats": "Chats", "Chats": "",
"Check Again": "Check Again", "Check Again": "",
"Check for updates": "Check for updates", "Check for updates": "",
"Checking for updates...": "Checking for updates...", "Checking for updates...": "",
"Choose a model before saving...": "Choose a model before saving...", "Choose a model before saving...": "",
"Chunk Overlap": "Chunk Overlap", "Chunk Overlap": "",
"Chunk Params": "Chunk Params", "Chunk Params": "",
"Chunk Size": "Chunk Size", "Chunk Size": "",
"Click here for help.": "Click here for help.", "Click here for help.": "",
"Click here to check other modelfiles.": "Click here to check other modelfiles.", "Click here to check other modelfiles.": "",
"Click here to select": "", "Click here to select": "",
"Click here to select documents.": "", "Click here to select documents.": "",
"click here.": "click here.", "click here.": "",
"Click on the user role button to change a user's role.": "Click on the user role button to change a user's role.", "Click on the user role button to change a user's role.": "",
"Close": "Close", "Close": "",
"Collection": "Collection", "Collection": "",
"Command": "Command", "Command": "",
"Confirm Password": "Confirm Password", "Confirm Password": "",
"Connections": "Connections", "Connections": "",
"Content": "Content", "Content": "",
"Context Length": "Context Length", "Context Length": "",
"Conversation Mode": "Conversation Mode", "Conversation Mode": "",
"Copy last code block": "Copy last code block", "Copy last code block": "",
"Copy last response": "Copy last response", "Copy last response": "",
"Copying to clipboard was successful!": "Copying to clipboard was successful!", "Copying to clipboard was successful!": "",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':", "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "",
"Create a modelfile": "Create a modelfile", "Create a modelfile": "",
"Create Account": "Create Account", "Create Account": "",
"Created at": "Created at", "Created at": "",
"Created by": "Created by", "Created by": "",
"Current Model": "Current Model", "Current Model": "",
"Current Password": "Current Password", "Current Password": "",
"Custom": "Custom", "Custom": "",
"Customize Ollama models for a specific purpose": "Customize Ollama models for a specific purpose", "Customize Ollama models for a specific purpose": "",
"Dark": "Dark", "Dark": "",
"Database": "Database", "Database": "",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm", "DD/MM/YYYY HH:mm": "",
"Default": "Default", "Default": "",
"Default (Automatic1111)": "", "Default (Automatic1111)": "",
"Default (Web API)": "Default (Web API)", "Default (Web API)": "",
"Default model updated": "Default model updated", "Default model updated": "",
"Default Prompt Suggestions": "Default Prompt Suggestions", "Default Prompt Suggestions": "",
"Default User Role": "Default User Role", "Default User Role": "",
"delete": "delete", "delete": "",
"Delete a model": "Delete a model", "Delete a model": "",
"Delete chat": "Delete chat", "Delete chat": "",
"Delete Chats": "Delete Chats", "Delete Chats": "",
"Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "",
"Deleted {tagName}": "Deleted {tagName}", "Deleted {tagName}": "",
"Description": "Description", "Description": "",
"Desktop Notifications": "Notification", "Desktop Notifications": "",
"Disabled": "Disabled", "Disabled": "",
"Discover a modelfile": "Discover a modelfile", "Discover a modelfile": "",
"Discover a prompt": "Discover a prompt", "Discover a prompt": "",
"Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts", "Discover, download, and explore custom prompts": "",
"Discover, download, and explore model presets": "Discover, download, and explore model presets", "Discover, download, and explore model presets": "",
"Display the username instead of You in the Chat": "Display the username instead of 'You' in the Chat", "Display the username instead of You in the Chat": "",
"Document": "Document", "Document": "",
"Document Settings": "Document Settings", "Document Settings": "",
"Documents": "Documents", "Documents": "",
"does not make any external connections, and your data stays securely on your locally hosted server.": "does not make any external connections, and your data stays securely on your locally hosted server.", "does not make any external connections, and your data stays securely on your locally hosted server.": "",
"Don't Allow": "Don't Allow", "Don't Allow": "",
"Don't have an account?": "Don't have an account?", "Don't have an account?": "",
"Download as a File": "Download as a File", "Download as a File": "",
"Download Database": "Download Database", "Download Database": "",
"Drop any files here to add to the conversation": "Drop any files here to add to the conversation", "Drop any files here to add to the conversation": "",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.", "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "",
"Edit Doc": "Edit Doc", "Edit Doc": "",
"Edit User": "Edit User", "Edit User": "",
"Email": "Email", "Email": "",
"Enable Chat History": "Enable Chat History", "Enable Chat History": "",
"Enable New Sign Ups": "Enable New Sign Ups", "Enable New Sign Ups": "",
"Enabled": "Enabled", "Enabled": "",
"Enter {{role}} message here": "", "Enter {{role}} message here": "",
"Enter API Key": "", "Enter API Key": "",
"Enter Chunk Overlap": "", "Enter Chunk Overlap": "",
@ -135,229 +135,229 @@
"Enter Max Tokens (litellm_params.max_tokens)": "", "Enter Max Tokens (litellm_params.max_tokens)": "",
"Enter model tag (e.g. {{modelTag}})": "", "Enter model tag (e.g. {{modelTag}})": "",
"Enter Number of Steps (e.g. 50)": "", "Enter Number of Steps (e.g. 50)": "",
"Enter stop sequence": "Enter stop sequence", "Enter stop sequence": "",
"Enter Top K": "", "Enter Top K": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "", "Enter URL (e.g. http://127.0.0.1:7860/)": "",
"Enter Your Email": "Enter Your Email", "Enter Your Email": "",
"Enter Your Full Name": "Enter Your Full Name", "Enter Your Full Name": "",
"Enter Your Password": "Enter Your Password", "Enter Your Password": "",
"Experimental": "Experimental", "Experimental": "",
"Export All Chats (All Users)": "Export All Chats (All Users)", "Export All Chats (All Users)": "",
"Export Chats": "Export Chats", "Export Chats": "",
"Export Documents Mapping": "Export Documents Mapping", "Export Documents Mapping": "",
"Export Modelfiles": "Export Modelfiles", "Export Modelfiles": "",
"Export Prompts": "Export Prompts", "Export Prompts": "",
"Failed to read clipboard contents": "Failed to read clipboard contents", "Failed to read clipboard contents": "",
"File Mode": "File Mode", "File Mode": "",
"File not found.": "File not found.", "File not found.": "",
"Focus chat input": "Focus chat input", "Focus chat input": "",
"Format your variables using square brackets like this:": "Format your variables using square brackets like this:", "Format your variables using square brackets like this:": "",
"From (Base Model)": "From (Base Model)", "From (Base Model)": "",
"Full Screen Mode": "Full Screen Mode", "Full Screen Mode": "",
"General": "General", "General": "",
"General Settings": "General Settings", "General Settings": "",
"Hello, {{name}}": "Hello, {{name}}", "Hello, {{name}}": "",
"Hide": "Hide", "Hide": "",
"Hide Additional Params": "Hide Additional Params", "Hide Additional Params": "",
"How can I help you today?": "How can I help you today?", "How can I help you today?": "",
"Image Generation (Experimental)": "Image Generation (Experimental)", "Image Generation (Experimental)": "",
"Image Generation Engine": "", "Image Generation Engine": "",
"Image Settings": "Image Settings", "Image Settings": "",
"Images": "Images", "Images": "",
"Import Chats": "Import Chats", "Import Chats": "",
"Import Documents Mapping": "Import Documents Mapping", "Import Documents Mapping": "",
"Import Modelfiles": "Import Modelfiles", "Import Modelfiles": "",
"Import Prompts": "Import Prompts", "Import Prompts": "",
"Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui", "Include `--api` flag when running stable-diffusion-webui": "",
"Interface": "Interface", "Interface": "",
"join our Discord for help.": "join our Discord for help.", "join our Discord for help.": "",
"JSON": "JSON", "JSON": "",
"JWT Expiration": "JWT Expiration", "JWT Expiration": "",
"JWT Token": "JWT Token", "JWT Token": "",
"Keep Alive": "Keep Alive", "Keep Alive": "",
"Keyboard shortcuts": "Keyboard shortcuts", "Keyboard shortcuts": "",
"Language": "Language", "Language": "",
"Light": "Light", "Light": "",
"Listening...": "Listening...", "Listening...": "",
"LLMs can make mistakes. Verify important information.": "LLMs can make mistakes. Verify important information.", "LLMs can make mistakes. Verify important information.": "",
"Made by OpenWebUI Community": "Made by OpenWebUI Community", "Made by OpenWebUI Community": "",
"Make sure to enclose them with": "Make sure to enclose them with", "Make sure to enclose them with": "",
"Manage LiteLLM Models": "Manage LiteLLM Models", "Manage LiteLLM Models": "",
"Manage Models": "Manage Models", "Manage Models": "",
"Manage Ollama Models": "Manage Ollama Models", "Manage Ollama Models": "",
"Max Tokens": "Max Tokens", "Max Tokens": "",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximum of 3 models can be downloaded simultaneously. Please try again later.", "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "",
"Mirostat": "Mirostat", "Mirostat": "",
"Mirostat Eta": "Mirostat Eta", "Mirostat Eta": "",
"Mirostat Tau": "Mirostat Tau", "Mirostat Tau": "",
"MMMM DD, YYYY": "MMMM DD, YYYY", "MMMM DD, YYYY": "",
"Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.", "Model '{{modelName}}' has been successfully downloaded.": "",
"Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.", "Model '{{modelTag}}' is already in queue for downloading.": "",
"Model {{modelId}} not found": "Model {{modelId}} not found", "Model {{modelId}} not found": "",
"Model {{modelName}} already exists.": "Model {{modelName}} already exists.", "Model {{modelName}} already exists.": "",
"Model Name": "Model Name", "Model Name": "",
"Model not selected": "Model not selected", "Model not selected": "",
"Model Tag Name": "Model Tag Name", "Model Tag Name": "",
"Model Whitelisting": "Model Whitelisting", "Model Whitelisting": "",
"Model(s) Whitelisted": "Model(s) Whitelisted", "Model(s) Whitelisted": "",
"Modelfile": "Modelfile", "Modelfile": "",
"Modelfile Advanced Settings": "Modelfile Advanced Settings", "Modelfile Advanced Settings": "",
"Modelfile Content": "Modelfile Content", "Modelfile Content": "",
"Modelfiles": "Modelfiles", "Modelfiles": "",
"Models": "Models", "Models": "",
"My Documents": "My Documents", "My Documents": "",
"My Modelfiles": "My Modelfiles", "My Modelfiles": "",
"My Prompts": "My Prompts", "My Prompts": "",
"Name": "Name", "Name": "",
"Name Tag": "Name Tag", "Name Tag": "",
"Name your modelfile": "Name your modelfile", "Name your modelfile": "",
"New Chat": "New Chat", "New Chat": "",
"New Password": "New Password", "New Password": "",
"Not sure what to add?": "Not sure what to add?", "Not sure what to add?": "",
"Not sure what to write? Switch to": "Not sure what to write? Switch to", "Not sure what to write? Switch to": "",
"Off": "Off", "Off": "",
"Okay, Let's Go!": "Okay, Let's Go!", "Okay, Let's Go!": "",
"Ollama Base URL": "", "Ollama Base URL": "",
"Ollama Version": "Ollama Version", "Ollama Version": "",
"On": "On", "On": "",
"Only": "Only", "Only": "",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Only alphanumeric characters and hyphens are allowed in the command string.", "Only alphanumeric characters and hyphens are allowed in the command string.": "",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.", "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
"Open": "Open", "Open": "",
"Open AI": "Open AI", "Open AI": "",
"Open AI (Dall-E)": "", "Open AI (Dall-E)": "",
"Open new chat": "Open new chat", "Open new chat": "",
"OpenAI API": "OpenAI API", "OpenAI API": "",
"OpenAI API Key": "", "OpenAI API Key": "",
"OpenAI API Key is required.": "", "OpenAI API Key is required.": "",
"or": "or", "or": "",
"Parameters": "Parameters", "Parameters": "",
"Password": "Password", "Password": "",
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)", "PDF Extract Images (OCR)": "",
"pending": "pending", "pending": "",
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}", "Permission denied when accessing microphone: {{error}}": "",
"Playground": "Playground", "Playground": "",
"Profile": "Profile", "Profile": "",
"Prompt Content": "Prompt Content", "Prompt Content": "",
"Prompt suggestions": "Prompt suggestions", "Prompt suggestions": "",
"Prompts": "Prompts", "Prompts": "",
"Pull a model from Ollama.com": "Pull a model from Ollama.com", "Pull a model from Ollama.com": "",
"Pull Progress": "Pull Progress", "Pull Progress": "",
"Query Params": "Query Params", "Query Params": "",
"RAG Template": "RAG Template", "RAG Template": "",
"Raw Format": "Raw Format", "Raw Format": "",
"Record voice": "Record voice", "Record voice": "",
"Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community", "Redirecting you to OpenWebUI Community": "",
"Release Notes": "Release Notes", "Release Notes": "",
"Repeat Last N": "Repeat Last N", "Repeat Last N": "",
"Repeat Penalty": "Repeat Penalty", "Repeat Penalty": "",
"Request Mode": "Request Mode", "Request Mode": "",
"Reset Vector Storage": "Reset Vector Storage", "Reset Vector Storage": "",
"Response AutoCopy to Clipboard": "Response AutoCopy to Clipboard", "Response AutoCopy to Clipboard": "",
"Role": "Role", "Role": "",
"Rosé Pine": "Rosé Pine", "Rosé Pine": "",
"Rosé Pine Dawn": "Rosé Pine Dawn", "Rosé Pine Dawn": "",
"Save": "Save", "Save": "",
"Save & Create": "Save & Create", "Save & Create": "",
"Save & Submit": "Save & Submit", "Save & Submit": "",
"Save & Update": "Save & Update", "Save & Update": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through", "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
"Scan": "Scan", "Scan": "",
"Scan complete!": "Scan complete!", "Scan complete!": "",
"Scan for documents from {{path}}": "Scan for documents from {{path}}", "Scan for documents from {{path}}": "",
"Search": "Search", "Search": "",
"Search Documents": "Search Documents", "Search Documents": "",
"Search Prompts": "Search Prompts", "Search Prompts": "",
"See readme.md for instructions": "See readme.md for instructions", "See readme.md for instructions": "",
"See what's new": "See what's new", "See what's new": "",
"Seed": "Seed", "Seed": "",
"Select a mode": "", "Select a mode": "",
"Select a model": "Select a model", "Select a model": "",
"Select an Ollama instance": "", "Select an Ollama instance": "",
"Send a Message": "Send a Message", "Send a Message": "",
"Send message": "Send message", "Send message": "",
"Server connection verified": "Server connection verified", "Server connection verified": "",
"Set as default": "Set as default", "Set as default": "",
"Set Default Model": "Set Default Model", "Set Default Model": "",
"Set Image Size": "Set Image Size", "Set Image Size": "",
"Set Steps": "Set Steps", "Set Steps": "",
"Set Title Auto-Generation Model": "Set Title Auto-Generation Model", "Set Title Auto-Generation Model": "",
"Set Voice": "Set Voice", "Set Voice": "",
"Settings": "Settings", "Settings": "",
"Settings saved successfully!": "Settings saved successfully!", "Settings saved successfully!": "",
"Share to OpenWebUI Community": "Share to OpenWebUI Community", "Share to OpenWebUI Community": "",
"short-summary": "short-summary", "short-summary": "",
"Show": "Show", "Show": "",
"Show Additional Params": "Show Additional Params", "Show Additional Params": "",
"Show shortcuts": "Show shortcuts", "Show shortcuts": "",
"sidebar": "sidebar", "sidebar": "",
"Sign in": "Sign in", "Sign in": "",
"Sign Out": "Sign Out", "Sign Out": "",
"Sign up": "Sign up", "Sign up": "",
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}", "Speech recognition error: {{error}}": "",
"Speech-to-Text Engine": "Speech-to-Text Engine", "Speech-to-Text Engine": "",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.", "SpeechRecognition API is not supported in this browser.": "",
"Stop Sequence": "Stop Sequence", "Stop Sequence": "",
"STT Settings": "STT Settings", "STT Settings": "",
"Submit": "Submit", "Submit": "",
"Success": "Success", "Success": "",
"Successfully updated.": "Successfully updated.", "Successfully updated.": "",
"Sync All": "Sync All", "Sync All": "",
"System": "System", "System": "",
"System Prompt": "System Prompt", "System Prompt": "",
"Tags": "Tags", "Tags": "",
"Temperature": "Temperature", "Temperature": "",
"Template": "Template", "Template": "",
"Text Completion": "Text Completion", "Text Completion": "",
"Text-to-Speech Engine": "Text-to-Speech Engine", "Text-to-Speech Engine": "",
"Tfs Z": "Tfs Z", "Tfs Z": "",
"Theme": "Theme", "Theme": "",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "This ensures that your valuable conversations are securely saved to your backend database. Thank you!", "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "",
"This setting does not sync across browsers or devices.": "This setting does not sync across browsers or devices.", "This setting does not sync across browsers or devices.": "",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.", "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "",
"Title": "Title", "Title": "",
"Title Auto-Generation": "Title Auto-Generation", "Title Auto-Generation": "",
"Title Generation Prompt": "Title Generation Prompt", "Title Generation Prompt": "",
"to": "to", "to": "",
"To access the available model names for downloading,": "To access the available model names for downloading,", "To access the available model names for downloading,": "",
"To access the GGUF models available for downloading,": "To access the GGUF models available for downloading,", "To access the GGUF models available for downloading,": "",
"to chat input.": "to chat input.", "to chat input.": "",
"Toggle settings": "Toggle settings", "Toggle settings": "",
"Toggle sidebar": "Toggle sidebar", "Toggle sidebar": "",
"Top K": "Top K", "Top K": "",
"Top P": "Top P", "Top P": "",
"Trouble accessing Ollama?": "Trouble accessing Ollama?", "Trouble accessing Ollama?": "",
"TTS Settings": "TTS Settings", "TTS Settings": "",
"Type Hugging Face Resolve (Download) URL": "", "Type Hugging Face Resolve (Download) URL": "",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}.", "Uh-oh! There was an issue connecting to {{provider}}.": "",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Unknown File Type '{{file_type}}', but accepting and treating as plain text", "Unknown File Type '{{file_type}}', but accepting and treating as plain text": "",
"Update password": "Update password", "Update password": "",
"Upload a GGUF model": "Upload a GGUF model", "Upload a GGUF model": "",
"Upload files": "Upload files", "Upload files": "",
"Upload Progress": "Upload Progress", "Upload Progress": "",
"URL Mode": "URL Mode", "URL Mode": "",
"Use '#' in the prompt input to load and select your documents.": "Use '#' in the prompt input to load and select your documents.", "Use '#' in the prompt input to load and select your documents.": "",
"Use Gravatar": "", "Use Gravatar": "",
"user": "user", "user": "",
"User Permissions": "User Permissions", "User Permissions": "",
"Users": "Users", "Users": "",
"Utilize": "Utilize", "Utilize": "",
"Valid time units:": "Valid time units:", "Valid time units:": "",
"variable": "variable", "variable": "",
"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content.", "variable to have them replaced with clipboard content.": "",
"Version": "Version", "Version": "",
"Web": "Web", "Web": "",
"WebUI Add-ons": "WebUI Add-ons", "WebUI Add-ons": "",
"WebUI Settings": "WebUI Settings", "WebUI Settings": "",
"WebUI will make requests to": "WebUI will make requests to", "WebUI will make requests to": "",
"Whats New in": "Whats New in", "Whats New in": "",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "When history is turned off, new chats on this browser won't appear in your history on any of your devices.", "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "",
"Whisper (Local)": "Whisper (Local)", "Whisper (Local)": "",
"Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?)", "Write a prompt suggestion (e.g. Who are you?)": "",
"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword].", "Write a summary in 50 words that summarizes [topic or keyword].": "",
"You": "You", "You": "",
"You're a helpful assistant.": "You're a helpful assistant.", "You're a helpful assistant.": "",
"You're now logged in.": "You're now logged in." "You're now logged in.": ""
} }

View file

@ -0,0 +1,362 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'초', '분', '시간', '일', '주' 또는 만료 없음 '-1'",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(예: `sh webui.sh --api`)",
"(latest)": "(latest)",
"{{modelName}} is thinking...": "{{modelName}} 이(가) 생각중입니다....",
"{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.",
"a user": "사용자",
"About": "소개",
"Account": "계정",
"Action": "액션",
"Add a model": "모델 추가",
"Add a model tag name": "모델 태그명 추가",
"Add a short description about what this modelfile does": "이 모델파일이 하는 일에 대한 간단한 설명 추가",
"Add a short title for this prompt": "이 프롬프트에 대한 간단한 제목 추가",
"Add a tag": "태그 추가",
"Add Docs": "문서 추가",
"Add Files": "파일 추가",
"Add message": "메시지 추가",
"add tags": "태그들 추가",
"Adjusting these settings will apply changes universally to all users.": "이 설정을 조정하면 모든 사용자에게 적용됩니다.",
"admin": "관리자",
"Admin Panel": "관리자 패널",
"Admin Settings": "관리자 설정",
"Advanced Parameters": "고급 매개변수",
"all": "모두",
"All Users": "모든 사용자",
"Allow": "허용",
"Allow Chat Deletion": "채팅 삭제 허용",
"alphanumeric characters and hyphens": "영문자,숫자 및 하이픈",
"Already have an account?": "이미 계정이 있으신가요?",
"an assistant": "어시스턴트",
"and": "그리고",
"API Base URL": "API 기본 URL",
"API Key": "API키",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "허용됩니다 - 이 명령을 활성화하려면 입력하세요.",
"Are you sure?": "확실합니까?",
"Audio": "오디오",
"Auto-playback response": "응답 자동 재생",
"Auto-send input after 3 sec.": "3초 후 입력 자동 전송",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Base URL이 필요합니다.",
"available!": "사용 가능!",
"Back": "뒤로가기",
"Builder Mode": "빌더 모드",
"Cancel": "취소",
"Categories": "분류",
"Change Password": "비밀번호 변경",
"Chat": "채팅",
"Chat History": "채팅 기록",
"Chat History is off for this browser.": "이 브라우저에서 채팅 기록이 꺼져 있습니다.",
"Chats": "채팅",
"Check Again": "다시 확인",
"Check for updates": "업데이트 확인",
"Checking for updates...": "업데이트 확인중...",
"Choose a model before saving...": "저장하기 전에 모델을 선택하세요...",
"Chunk Overlap": "Chunk Overlap",
"Chunk Params": "Chunk Params",
"Chunk Size": "Chunk Size",
"Click here for help.": "도움말을 보려면 여기를 클릭하세요.",
"Click here to check other modelfiles.": "다른 모델파일을 확인하려면 여기를 클릭하세요.",
"Click here to select": "선택하려면 여기를 클릭하세요.",
"Click here to select documents.": "문서를 선택하려면 여기를 클릭하세요.",
"click here.": "여기를 클릭하세요.",
"Click on the user role button to change a user's role.": "사용자 역할 버튼을 클릭하여 사용자의 역할을 변경하세요.",
"Close": "닫기",
"Collection": "컬렉션",
"Command": "명령",
"Confirm Password": "비밀번호 확인",
"Connections": "연결",
"Content": "내용",
"Context Length": "내용 길이",
"Conversation Mode": "대화 모드",
"Copy last code block": "마지막 코드 블록 복사",
"Copy last response": "마지막 응답 복사",
"Copying to clipboard was successful!": "클립보드에 복사되었습니다!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "다음 질문에 대한 제목으로 간결한 3-5 단어 문구를 만드되 3-5 단어 제한을 엄격히 준수하고 'title' 단어 사용을 피하세요:",
"Create a modelfile": "모델파일 만들기",
"Create Account": "계정 만들기",
"Created at": "생성일",
"Created by": "생성자",
"Current Model": "현재 모델",
"Current Password": "현재 비밀번호",
"Custom": "사용자 정의",
"Customize Ollama models for a specific purpose": "특정 목적으로 Ollama 모델 사용자 정의",
"Dark": "어두운",
"Database": "데이터베이스",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "기본값",
"Default (Automatic1111)": "기본값 (Automatic1111)",
"Default (Web API)": "기본값 (Web API)",
"Default model updated": "기본 모델이 업데이트되었습니다.",
"Default Prompt Suggestions": "기본 프롬프트 제안",
"Default User Role": "기본 사용자 역할",
"delete": "삭제",
"Delete a model": "모델 삭제",
"Delete chat": "채팅 삭제",
"Delete Chats": "채팅들 삭제",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨",
"Deleted {tagName}": "{tagName} 삭제됨",
"Description": "설명",
"Desktop Notifications": "알림",
"Disabled": "비활성화",
"Discover a modelfile": "모델파일 검색",
"Discover a prompt": "프롬프트 검색",
"Discover, download, and explore custom prompts": "사용자 정의 프롬프트 검색, 다운로드 및 탐색",
"Discover, download, and explore model presets": "모델 사전 설정 검색, 다운로드 및 탐색",
"Display the username instead of You in the Chat": "채팅에서 'You' 대신 사용자 이름 표시",
"Document": "문서",
"Document Settings": "문서 설정",
"Documents": "문서들",
"does not make any external connections, and your data stays securely on your locally hosted server.": "어떠한 외부 연결도 하지 않으며, 데이터는 로컬에서 호스팅되는 서버에 안전하게 유지됩니다.",
"Don't Allow": "허용 안 함",
"Don't have an account?": "계정이 없으신가요?",
"Download as a File": "파일로 다운로드",
"Download Database": "데이터베이스 다운로드",
"Drop any files here to add to the conversation": "대화에 추가할 파일을 여기에 드롭하세요.",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "예: '30s','10m'. 유효한 시간 단위는 's', 'm', 'h'입니다.",
"Edit Doc": "문서 편집",
"Edit User": "사용자 편집",
"Email": "이메일",
"Enable Chat History": "채팅 기록 활성화",
"Enable New Sign Ups": "새 회원가입 활성화",
"Enabled": "활성화",
"Enter {{role}} message here": "여기에 {{role}} 메시지 입력",
"Enter API Key": "API 키 입력",
"Enter Chunk Overlap": "청크 오버랩 입력",
"Enter Chunk Size": "청크 크기 입력",
"Enter Image Size (e.g. 512x512)": "이미지 크기 입력(예: 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "LiteLLM API 기본 URL 입력(litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "LiteLLM API 키 입력(litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM 입력(litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "LiteLLM 모델 입력(litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "최대 토큰 수 입력(litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "단계 수 입력(예: 50)",
"Enter stop sequence": "중지 시퀀스 입력",
"Enter Top K": "Top K 입력",
"Enter URL (e.g. http://127.0.0.1:7860/)": "URL 입력(예: http://127.0.0.1:7860/)",
"Enter Your Email": "이메일 입력",
"Enter Your Full Name": "전체 이름 입력",
"Enter Your Password": "비밀번호 입력",
"Experimental": "실험적",
"Export All Chats (All Users)": "모든 채팅 내보내기 (모든 사용자)",
"Export Chats": "채팅 내보내기",
"Export Documents Mapping": "문서 매핑 내보내기",
"Export Modelfiles": "모델파일 내보내기",
"Export Prompts": "프롬프트 내보내기",
"Failed to read clipboard contents": "클립보드 내용을 읽는 데 실패했습니다.",
"File Mode": "파일 모드",
"File not found.": "파일을 찾을 수 없습니다.",
"Focus chat input": "채팅 입력 포커스",
"Format your variables using square brackets like this:": "이렇게 대괄호를 사용하여 변수를 형식화하세요:",
"From (Base Model)": "출처(기본 모델)",
"Full Screen Mode": "전체 화면 모드",
"General": "일반",
"General Settings": "일반 설정",
"Hello, {{name}}": "안녕하세요, {{name}}",
"Hide": "숨기기",
"Hide Additional Params": "추가 매개변수 숨기기",
"How can I help you today?": "오늘 어떻게 도와드릴까요?",
"Image Generation (Experimental)": "이미지 생성(실험적)",
"Image Generation Engine": "이미지 생성 엔진",
"Image Settings": "이미지 설정",
"Images": "이미지",
"Import Chats": "채팅 가져오기",
"Import Documents Mapping": "문서 매핑 가져오기",
"Import Modelfiles": "모델파일 가져오기",
"Import Prompts": "프롬프트 가져오기",
"Include --api flag when running stable-diffusion-webui": "stable-diffusion-webui를 실행할 때 --api 플래그 포함",
"Interface": "인터페이스",
"join our Discord for help.": "도움말을 보려면 Discord에 가입하세요.",
"JSON": "JSON",
"JWT Expiration": "JWT 만료",
"JWT Token": "JWT 토큰",
"Keep Alive": "계속 유지하기",
"Keyboard shortcuts": "키보드 단축키",
"Language": "언어",
"Light": "밝음",
"Listening...": "청취 중...",
"LLMs can make mistakes. Verify important information.": "LLM은 실수를 할 수 있습니다. 중요한 정보를 확인하세요.",
"Made by OpenWebUI Community": "OpenWebUI 커뮤니티에서 제작",
"Make sure to enclose them with": "다음으로 묶는 것을 잊지 마세요:",
"Manage LiteLLM Models": "LiteLLM 모델 관리",
"Manage Models": "모델 관리",
"Manage Ollama Models": "Ollama 모델 관리",
"Max Tokens": "최대 토큰 수",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "최대 3개의 모델을 동시에 다운로드할 수 있습니다. 나중에 다시 시도하세요.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"Model '{{modelName}}' has been successfully downloaded.": "모델 '{{modelName}}'이(가) 성공적으로 다운로드되었습니다.",
"Model '{{modelTag}}' is already in queue for downloading.": "모델 '{{modelTag}}'이(가) 이미 다운로드 대기열에 있습니다.",
"Model {{modelId}} not found": "모델 {{modelId}}를 찾을 수 없습니다.",
"Model {{modelName}} already exists.": "모델 {{modelName}}이(가) 이미 존재합니다.",
"Model Name": "모델 이름",
"Model not selected": "모델이 선택되지 않았습니다.",
"Model Tag Name": "모델 태그 이름",
"Model Whitelisting": "모델 허용 목록",
"Model(s) Whitelisted": "허용된 모델",
"Modelfile": "모델파일",
"Modelfile Advanced Settings": "모델파일 고급 설정",
"Modelfile Content": "모델파일 내용",
"Modelfiles": "모델파일",
"Models": "모델",
"My Documents": "내 문서",
"My Modelfiles": "내 모델파일",
"My Prompts": "내 프롬프트",
"Name": "이름",
"Name Tag": "이름 태그",
"Name your modelfile": "모델파일 이름 지정",
"New Chat": "새 채팅",
"New Password": "새 비밀번호",
"Not sure what to add?": "추가할 것이 궁금하세요?",
"Not sure what to write? Switch to": "무엇을 쓸지 모르겠나요? 전환하세요.",
"Off": "끄기",
"Okay, Let's Go!": "그렇습니다, 시작합시다!",
"Ollama Base URL": "Ollama 기본 URL",
"Ollama Version": "Ollama 버전",
"On": "켜기",
"Only": "오직",
"Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈만 허용됩니다.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "이런! 잠시만 기다려 주세요! 파일이 아직 처리 중입니다. 완벽하게 준비하고 있습니다. 잠시만 기다려주시면 준비가 되면 알려드리겠습니다.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "이런! 지원되지 않는 방식(프론트엔드 전용)을 사용하고 계십니다. 백엔드에서 WebUI를 제공해주세요.",
"Open": "열기",
"Open AI": "Open AI",
"Open AI (Dall-E)": "OpenAI (Dall-E)",
"Open new chat": "새 채팅 열기",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API 키",
"OpenAI API Key is required.": "OpenAI API 키가 필요합니다.",
"or": "또는",
"Parameters": "매개변수",
"Password": "비밀번호",
"PDF Extract Images (OCR)": "PDF에서 이미지 추출 (OCR)",
"pending": "보류 중",
"Permission denied when accessing microphone: {{error}}": "마이크 액세스가 거부되었습니다: {{error}}",
"Playground": "놀이터",
"Profile": "프로필",
"Prompt Content": "프롬프트 내용",
"Prompt suggestions": "프롬프트 제안",
"Prompts": "프롬프트",
"Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기",
"Pull Progress": "가져오기 진행 상황",
"Query Params": "쿼리 매개변수",
"RAG Template": "RAG 템플릿",
"Raw Format": "Raw 형식",
"Record voice": "음성 녹음",
"Redirecting you to OpenWebUI Community": "OpenWebUI 커뮤니티로 리디렉션하는 중",
"Release Notes": "릴리스 노트",
"Repeat Last N": "마지막 N 반복",
"Repeat Penalty": "반복 패널티",
"Request Mode": "요청 모드",
"Reset Vector Storage": "벡터 스토리지 초기화",
"Response AutoCopy to Clipboard": "응답 자동 클립보드 복사",
"Role": "역할",
"Rosé Pine": "로제 파인",
"Rosé Pine Dawn": "로제 파인 던",
"Save": "저장",
"Save & Create": "저장 및 생성",
"Save & Submit": "저장 및 제출",
"Save & Update": "저장 및 업데이트",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "브라우저의 저장소에 채팅 로그를 직접 저장하는 것은 더 이상 지원되지 않습니다. 아래 버튼을 클릭하여 채팅 로그를 다운로드하고 삭제하세요. 걱정 마세요. 백엔드를 통해 채팅 로그를 쉽게 다시 가져올 수 있습니다.",
"Scan": "스캔",
"Scan complete!": "스캔 완료!",
"Scan for documents from {{path}}": "{{path}}에서 문서 스캔",
"Search": "검색",
"Search Documents": "문서 검색",
"Search Prompts": "프롬프트 검색",
"See readme.md for instructions": "설명은 readme.md를 참조하세요.",
"See what's new": "새로운 기능 보기",
"Seed": "시드",
"Select a mode": "모드 선택",
"Select a model": "모델 선택",
"Select an Ollama instance": "Ollama 인스턴스 선택",
"Send a Message": "메시지 보내기",
"Send message": "메시지 보내기",
"Server connection verified": "서버 연결 확인됨",
"Set as default": "기본값으로 설정",
"Set Default Model": "기본 모델 설정",
"Set Image Size": "이미지 크기 설정",
"Set Steps": "단계 설정",
"Set Title Auto-Generation Model": "제목 자동 생성 모델 설정",
"Set Voice": "음성 설정",
"Settings": "설정",
"Settings saved successfully!": "설정이 성공적으로 저장되었습니다!",
"Share to OpenWebUI Community": "OpenWebUI 커뮤니티에 공유",
"short-summary": "간단한 요약",
"Show": "보이기",
"Show Additional Params": "추가 매개변수 보기",
"Show shortcuts": "단축키 보기",
"sidebar": "사이드바",
"Sign in": "로그인",
"Sign Out": "로그아웃",
"Sign up": "가입",
"Speech recognition error: {{error}}": "음성 인식 오류: {{error}}",
"Speech-to-Text Engine": "음성-텍스트 엔진",
"SpeechRecognition API is not supported in this browser.": "이 브라우저에서는 SpeechRecognition API를 지원하지 않습니다.",
"Stop Sequence": "중지 시퀀스",
"STT Settings": "STT 설정",
"Submit": "제출",
"Success": "성공",
"Successfully updated.": "성공적으로 업데이트되었습니다.",
"Sync All": "모두 동기화",
"System": "시스템",
"System Prompt": "시스템 프롬프트",
"Tags": "Tags",
"Temperature": "Temperature",
"Template": "Template",
"Text Completion": "텍스트 완성",
"Text-to-Speech Engine": "텍스트-음성 엔진",
"Tfs Z": "Tfs Z",
"Theme": "테마",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "이렇게 하면 소중한 대화 내용이 백엔드 데이터베이스에 안전하게 저장됩니다. 감사합니다!",
"This setting does not sync across browsers or devices.": "이 설정은 브라우저 또는 장치 간에 동기화되지 않습니다.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "팁: 각 대체 후 채팅 입력에서 탭 키를 눌러 여러 개의 변수 슬롯을 연속적으로 업데이트하세요.",
"Title": "제목",
"Title Auto-Generation": "제목 자동 생성",
"Title Generation Prompt": "제목 생성 프롬프트",
"to": "~까지",
"To access the available model names for downloading,": "다운로드 가능한 모델명을 확인하려면,",
"To access the GGUF models available for downloading,": "다운로드 가능한 GGUF 모델을 확인하려면,",
"to chat input.": "채팅 입력으로.",
"Toggle settings": "설정 전환",
"Toggle sidebar": "사이드바 전환",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Ollama에 접근하는 데 문제가 있나요?",
"TTS Settings": "TTS 설정",
"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력",
"Uh-oh! There was an issue connecting to {{provider}}.": "앗! {{provider}}에 연결하는 데 문제가 있었습니다.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "알 수 없는 파일 유형 '{{file_type}}', 하지만 일반 텍스트로 허용하고 처리합니다.",
"Update password": "비밀번호 업데이트",
"Upload a GGUF model": "GGUF 모델 업로드",
"Upload files": "파일 업로드",
"Upload Progress": "업로드 진행 상황",
"URL Mode": "URL 모드",
"Use '#' in the prompt input to load and select your documents.": "프롬프트 입력에서 '#'를 사용하여 문서를 로드하고 선택하세요.",
"Use Gravatar": "Gravatar 사용",
"user": "사용자",
"User Permissions": "사용자 권한",
"Users": "사용자",
"Utilize": "활용",
"Valid time units:": "유효한 시간 단위:",
"variable": "변수",
"variable to have them replaced with clipboard content.": "변수를 사용하여 클립보드 내용으로 바꾸세요.",
"Version": "버전",
"Web": "웹",
"WebUI Add-ons": "WebUI 애드온",
"WebUI Settings": "WebUI 설정",
"WebUI will make requests to": "WebUI가 요청할 대상:",
"What's New in": "새로운 기능",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "기록 기능이 꺼져 있으면 이 브라우저의 새 채팅이 다른 장치의 채팅 기록에 나타나지 않습니다.",
"Whisper (Local)": "위스퍼 (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.",
"You": "당신",
"You're a helpful assistant.": "당신은 유용한 어시스턴트입니다.",
"You're now logged in.": "로그인되었습니다."
}

View file

@ -39,6 +39,10 @@
"code": "ja-JP", "code": "ja-JP",
"title": "Japanese" "title": "Japanese"
}, },
{
"code": "ko-KR",
"title": "Korean"
},
{ {
"code": "nl-NL", "code": "nl-NL",
"title": "Dutch (Netherlands)" "title": "Dutch (Netherlands)"
@ -47,10 +51,18 @@
"code": "pt-PT", "code": "pt-PT",
"title": "Portuguese (Portugal)" "title": "Portuguese (Portugal)"
}, },
{
"code": "pt-BR",
"title": "Portuguese (Brazil)"
},
{ {
"code": "ru-RU", "code": "ru-RU",
"title": "Russian (Russia)" "title": "Russian (Russia)"
}, },
{
"code": "tr-TR",
"title": "Turkish"
},
{ {
"code": "uk-UA", "code": "uk-UA",
"title": "Ukrainian" "title": "Ukrainian"

View file

@ -5,7 +5,7 @@
"(latest)": "(nieuwste)", "(latest)": "(nieuwste)",
"{{modelName}} is thinking...": "{{modelName}} is aan het denken...", "{{modelName}} is thinking...": "{{modelName}} is aan het denken...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", "{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht",
"a user": "", "a user": "een gebruiker",
"About": "Over", "About": "Over",
"Account": "Account", "Account": "Account",
"Action": "Actie", "Action": "Actie",
@ -35,12 +35,12 @@
"API Key": "API Key", "API Key": "API Key",
"API RPM": "API RPM", "API RPM": "API RPM",
"are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", "are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen",
"Are you sure?": "", "Are you sure?": "Zeker weten?",
"Audio": "Audio", "Audio": "Audio",
"Auto-playback response": "Automatisch afspelen van antwoord", "Auto-playback response": "Automatisch afspelen van antwoord",
"Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.", "Auto-send input after 3 sec.": "Automatisch verzenden van input na 3 sec.",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
"AUTOMATIC1111 Base URL is required.": "", "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht",
"available!": "beschikbaar!", "available!": "beschikbaar!",
"Back": "Terug", "Back": "Terug",
"Builder Mode": "Bouwer Modus", "Builder Mode": "Bouwer Modus",
@ -60,8 +60,8 @@
"Chunk Size": "Chunk Grootte", "Chunk Size": "Chunk Grootte",
"Click here for help.": "Klik hier voor help.", "Click here for help.": "Klik hier voor help.",
"Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.", "Click here to check other modelfiles.": "Klik hier om andere modelfiles te controleren.",
"Click here to select": "", "Click here to select": "Klik hier om te selecteren",
"Click here to select documents.": "", "Click here to select documents.": "Klik hier om documenten te selecteren",
"click here.": "click here.", "click here.": "click here.",
"Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", "Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.",
"Close": "Sluiten", "Close": "Sluiten",
@ -88,7 +88,7 @@
"Database": "Database", "Database": "Database",
"DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm", "DD/MM/YYYY HH:mm": "YYYY/MM/DD HH:mm",
"Default": "Standaard", "Default": "Standaard",
"Default (Automatic1111)": "", "Default (Automatic1111)": "Standaard (Automatic1111)",
"Default (Web API)": "Standaard (Web API)", "Default (Web API)": "Standaard (Web API)",
"Default model updated": "Standaard model bijgewerkt", "Default model updated": "Standaard model bijgewerkt",
"Default Prompt Suggestions": "Standaard Prompt Suggesties", "Default Prompt Suggestions": "Standaard Prompt Suggesties",
@ -123,21 +123,21 @@
"Enable Chat History": "Schakel Chat Geschiedenis in", "Enable Chat History": "Schakel Chat Geschiedenis in",
"Enable New Sign Ups": "Schakel Nieuwe Registraties in", "Enable New Sign Ups": "Schakel Nieuwe Registraties in",
"Enabled": "Ingeschakeld", "Enabled": "Ingeschakeld",
"Enter {{role}} message here": "", "Enter {{role}} message here": "Voeg {{role}} bericht hier toe",
"Enter API Key": "", "Enter API Key": "Voeg API Key toe",
"Enter Chunk Overlap": "", "Enter Chunk Overlap": "Voeg Chunk Overlap toe",
"Enter Chunk Size": "", "Enter Chunk Size": "Voeg Chunk Size toe",
"Enter Image Size (e.g. 512x512)": "", "Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "", "Enter LiteLLM API Base URL (litellm_params.api_base)": "Voeg LiteLLM API Base URL toe (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "", "Enter LiteLLM API Key (litellm_params.api_key)": "Voeg LiteLLM API Sleutel toe (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "", "Enter LiteLLM API RPM (litellm_params.rpm)": "Voeg LiteLLM API RPM toe (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "", "Enter LiteLLM Model (litellm_params.model)": "Voeg LiteLLM Model toe (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "", "Enter Max Tokens (litellm_params.max_tokens)": "Voeg maximum aantal tokens toe (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "", "Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "", "Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)",
"Enter stop sequence": "Zet stop sequentie", "Enter stop sequence": "Zet stop sequentie",
"Enter Top K": "", "Enter Top K": "Voeg Top K toe",
"Enter URL (e.g. http://127.0.0.1:7860/)": "", "Enter URL (e.g. http://127.0.0.1:7860/)": "Zet URL (Bijv. http://127.0.0.1:7860/)",
"Enter Your Email": "Voer je Email in", "Enter Your Email": "Voer je Email in",
"Enter Your Full Name": "Voer je Volledige Naam in", "Enter Your Full Name": "Voer je Volledige Naam in",
"Enter Your Password": "Voer je Wachtwoord in", "Enter Your Password": "Voer je Wachtwoord in",
@ -161,7 +161,7 @@
"Hide Additional Params": "Verberg Extra Params", "Hide Additional Params": "Verberg Extra Params",
"How can I help you today?": "Hoe kan ik je vandaag helpen?", "How can I help you today?": "Hoe kan ik je vandaag helpen?",
"Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", "Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)",
"Image Generation Engine": "", "Image Generation Engine": "Afbeelding Generatie Engine",
"Image Settings": "Afbeelding Instellingen", "Image Settings": "Afbeelding Instellingen",
"Images": "Afbeeldingen", "Images": "Afbeeldingen",
"Import Chats": "Importeer Chats", "Import Chats": "Importeer Chats",
@ -217,7 +217,7 @@
"Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar", "Not sure what to write? Switch to": "Niet zeker wat te schrijven? Schakel over naar",
"Off": "Uit", "Off": "Uit",
"Okay, Let's Go!": "Okay, Laten we gaan!", "Okay, Let's Go!": "Okay, Laten we gaan!",
"Ollama Base URL": "", "Ollama Base URL": "Ollama Basis URL",
"Ollama Version": "Ollama Versie", "Ollama Version": "Ollama Versie",
"On": "Aan", "On": "Aan",
"Only": "Alleen", "Only": "Alleen",
@ -227,11 +227,11 @@
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.",
"Open": "Open", "Open": "Open",
"Open AI": "Open AI", "Open AI": "Open AI",
"Open AI (Dall-E)": "", "Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Open nieuwe chat", "Open new chat": "Open nieuwe chat",
"OpenAI API": "OpenAI API", "OpenAI API": "OpenAI API",
"OpenAI API Key": "", "OpenAI API Key": "OpenAI API Sleutel",
"OpenAI API Key is required.": "", "OpenAI API Key is required.": "OpenAI API Sleutel is verplicht",
"or": "of", "or": "of",
"Parameters": "Parameters", "Parameters": "Parameters",
"Password": "Wachtwoord", "Password": "Wachtwoord",
@ -273,9 +273,9 @@
"See readme.md for instructions": "Zie readme.md voor instructies", "See readme.md for instructions": "Zie readme.md voor instructies",
"See what's new": "Zie wat er nieuw is", "See what's new": "Zie wat er nieuw is",
"Seed": "Seed", "Seed": "Seed",
"Select a mode": "", "Select a mode": "Selecteer een modus",
"Select a model": "Selecteer een model", "Select a model": "Selecteer een model",
"Select an Ollama instance": "", "Select an Ollama instance": "Selecteer een Ollama instantie",
"Send a Message": "Stuur een Bericht", "Send a Message": "Stuur een Bericht",
"Send message": "Stuur bericht", "Send message": "Stuur bericht",
"Server connection verified": "Server verbinding geverifieerd", "Server connection verified": "Server verbinding geverifieerd",
@ -330,7 +330,7 @@
"Top P": "Top P", "Top P": "Top P",
"Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", "Trouble accessing Ollama?": "Problemen met toegang tot Ollama?",
"TTS Settings": "TTS instellingen", "TTS Settings": "TTS instellingen",
"Type Hugging Face Resolve (Download) URL": "", "Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL",
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst", "Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Onbekend Bestandstype '{{file_type}}', maar accepteren en behandelen als platte tekst",
"Update password": "Wijzig wachtwoord", "Update password": "Wijzig wachtwoord",
@ -339,7 +339,7 @@
"Upload Progress": "Upload Voortgang", "Upload Progress": "Upload Voortgang",
"URL Mode": "URL Modus", "URL Mode": "URL Modus",
"Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", "Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.",
"Use Gravatar": "", "Use Gravatar": "Gebruik Gravatar",
"user": "user", "user": "user",
"User Permissions": "Gebruikers Rechten", "User Permissions": "Gebruikers Rechten",
"Users": "Gebruikers", "Users": "Gebruikers",

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 's' ou '-1' para não expirar.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)",
"(latest)": "(mais recente)",
"{{modelName}} is thinking...": "{{modelName}} está pensando...",
"{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário",
"a user": "um usuário",
"About": "Sobre",
"Account": "Conta",
"Action": "Ação",
"Add a model": "Adicionar um modelo",
"Add a model tag name": "Adicionar um nome de tag de modelo",
"Add a short description about what this modelfile does": "Adicione uma breve descrição sobre o que este arquivo de modelo faz",
"Add a short title for this prompt": "Adicione um título curto para este prompt",
"Add a tag": "Adicionar uma tag",
"Add Docs": "Adicionar Documentos",
"Add Files": "Adicionar Arquivos",
"Add message": "Adicionar mensagem",
"add tags": "adicionar tags",
"Adjusting these settings will apply changes universally to all users.": "Ajustar essas configurações aplicará alterações universalmente a todos os usuários.",
"admin": "administrador",
"Admin Panel": "Painel do Administrador",
"Admin Settings": "Configurações do Administrador",
"Advanced Parameters": "Parâmetros Avançados",
"all": "todos",
"All Users": "Todos os Usuários",
"Allow": "Permitir",
"Allow Chat Deletion": "Permitir Exclusão de Bate-papo",
"alphanumeric characters and hyphens": "caracteres alfanuméricos e hífens",
"Already have an account?": "Já tem uma conta?",
"an assistant": "um assistente",
"and": "e",
"API Base URL": "URL Base da API",
"API Key": "Chave da API",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "são permitidos - Ative este comando digitando",
"Are you sure?": "Tem certeza?",
"Audio": "Áudio",
"Auto-playback response": "Reprodução automática da resposta",
"Auto-send input after 3 sec.": "Enviar entrada automaticamente após 3 segundos.",
"AUTOMATIC1111 Base URL": "URL Base do AUTOMATIC1111",
"AUTOMATIC1111 Base URL is required.": "A URL Base do AUTOMATIC1111 é obrigatória.",
"available!": "disponível!",
"Back": "Voltar",
"Builder Mode": "Modo de Construtor",
"Cancel": "Cancelar",
"Categories": "Categorias",
"Change Password": "Alterar Senha",
"Chat": "Bate-papo",
"Chat History": "Histórico de Bate-papo",
"Chat History is off for this browser.": "O histórico de bate-papo está desativado para este navegador.",
"Chats": "Bate-papos",
"Check Again": "Verifique novamente",
"Check for updates": "Verificar atualizações",
"Checking for updates...": "Verificando atualizações...",
"Choose a model before saving...": "Escolha um modelo antes de salvar...",
"Chunk Overlap": "Sobreposição de Fragmento",
"Chunk Params": "Parâmetros de Fragmento",
"Chunk Size": "Tamanho do Fragmento",
"Click here for help.": "Clique aqui para obter ajuda.",
"Click here to check other modelfiles.": "Clique aqui para verificar outros arquivos de modelo.",
"Click here to select": "Clique aqui para selecionar",
"Click here to select documents.": "Clique aqui para selecionar documentos.",
"click here.": "clique aqui.",
"Click on the user role button to change a user's role.": "Clique no botão de função do usuário para alterar a função de um usuário.",
"Close": "Fechar",
"Collection": "Coleção",
"Command": "Comando",
"Confirm Password": "Confirmar Senha",
"Connections": "Conexões",
"Content": "Conteúdo",
"Context Length": "Comprimento do Contexto",
"Conversation Mode": "Modo de Conversa",
"Copy last code block": "Copiar último bloco de código",
"Copy last response": "Copiar última resposta",
"Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Crie uma frase concisa de 3 a 5 palavras como cabeçalho para a seguinte consulta, aderindo estritamente ao limite de 3 a 5 palavras e evitando o uso da palavra 'título':",
"Create a modelfile": "Criar um arquivo de modelo",
"Create Account": "Criar Conta",
"Created at": "Criado em",
"Created by": "Criado por",
"Current Model": "Modelo Atual",
"Current Password": "Senha Atual",
"Custom": "Personalizado",
"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico",
"Dark": "Escuro",
"Database": "Banco de dados",
"DD/MM/YYYY HH:mm": "DD/MM/AAAA HH:mm",
"Default": "Padrão",
"Default (Automatic1111)": "Padrão (Automatic1111)",
"Default (Web API)": "Padrão (API Web)",
"Default model updated": "Modelo padrão atualizado",
"Default Prompt Suggestions": "Sugestões de Prompt Padrão",
"Default User Role": "Função de Usuário Padrão",
"delete": "excluir",
"Delete a model": "Excluir um modelo",
"Delete chat": "Excluir bate-papo",
"Delete Chats": "Excluir Bate-papos",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído",
"Deleted {tagName}": "{tagName} excluído",
"Description": "Descrição",
"Desktop Notifications": "Notificações da Área de Trabalho",
"Disabled": "Desativado",
"Discover a modelfile": "Descobrir um arquivo de modelo",
"Discover a prompt": "Descobrir um prompt",
"Discover, download, and explore custom prompts": "Descubra, baixe e explore prompts personalizados",
"Discover, download, and explore model presets": "Descubra, baixe e explore predefinições de modelo",
"Display the username instead of You in the Chat": "Exibir o nome de usuário em vez de Você no Bate-papo",
"Document": "Documento",
"Document Settings": "Configurações de Documento",
"Documents": "Documentos",
"does not make any external connections, and your data stays securely on your locally hosted server.": "não faz conexões externas e seus dados permanecem seguros em seu servidor hospedado localmente.",
"Don't Allow": "Não Permitir",
"Don't have an account?": "Não tem uma conta?",
"Download as a File": "Baixar como Arquivo",
"Download Database": "Baixar Banco de Dados",
"Drop any files here to add to the conversation": "Solte os arquivos aqui para adicionar à conversa",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "por exemplo, '30s', '10m'. Unidades de tempo válidas são 's', 'm', 'h'.",
"Edit Doc": "Editar Documento",
"Edit User": "Editar Usuário",
"Email": "E-mail",
"Enable Chat History": "Ativar Histórico de Bate-papo",
"Enable New Sign Ups": "Ativar Novas Inscrições",
"Enabled": "Ativado",
"Enter {{role}} message here": "Digite a mensagem de {{role}} aqui",
"Enter API Key": "Digite a Chave da API",
"Enter Chunk Overlap": "Digite a Sobreposição de Fragmento",
"Enter Chunk Size": "Digite o Tamanho do Fragmento",
"Enter Image Size (e.g. 512x512)": "Digite o Tamanho da Imagem (por exemplo, 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "Digite a URL Base da API LiteLLM (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "Digite a Chave da API LiteLLM (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "Digite o RPM da API LiteLLM (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "Digite o Modelo LiteLLM (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Digite o Máximo de Tokens (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Digite a tag do modelo (por exemplo, {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Digite o Número de Etapas (por exemplo, 50)",
"Enter stop sequence": "Digite a sequência de parada",
"Enter Top K": "Digite o Top K",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Digite a URL (por exemplo, http://127.0.0.1:7860/)",
"Enter Your Email": "Digite seu E-mail",
"Enter Your Full Name": "Digite seu Nome Completo",
"Enter Your Password": "Digite sua Senha",
"Experimental": "Experimental",
"Export All Chats (All Users)": "Exportar Todos os Bate-papos (Todos os Usuários)",
"Export Chats": "Exportar Bate-papos",
"Export Documents Mapping": "Exportar Mapeamento de Documentos",
"Export Modelfiles": "Exportar Arquivos de Modelo",
"Export Prompts": "Exportar Prompts",
"Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência",
"File Mode": "Modo de Arquivo",
"File not found.": "Arquivo não encontrado.",
"Focus chat input": "Focar entrada de bate-papo",
"Format your variables using square brackets like this:": "Formate suas variáveis usando colchetes como este:",
"From (Base Model)": "De (Modelo Base)",
"Full Screen Mode": "Modo de Tela Cheia",
"General": "Geral",
"General Settings": "Configurações Gerais",
"Hello, {{name}}": "Olá, {{name}}",
"Hide": "Ocultar",
"Hide Additional Params": "Ocultar Parâmetros Adicionais",
"How can I help you today?": "Como posso ajudá-lo hoje?",
"Image Generation (Experimental)": "Geração de Imagens (Experimental)",
"Image Generation Engine": "Mecanismo de Geração de Imagens",
"Image Settings": "Configurações de Imagem",
"Images": "Imagens",
"Import Chats": "Importar Bate-papos",
"Import Documents Mapping": "Importar Mapeamento de Documentos",
"Import Modelfiles": "Importar Arquivos de Modelo",
"Import Prompts": "Importar Prompts",
"Include `--api` flag when running stable-diffusion-webui": "Inclua a flag `--api` ao executar stable-diffusion-webui",
"Interface": "Interface",
"join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.",
"JSON": "JSON",
"JWT Expiration": "Expiração JWT",
"JWT Token": "Token JWT",
"Keep Alive": "Manter Vivo",
"Keyboard shortcuts": "Atalhos de teclado",
"Language": "Idioma",
"Light": "Claro",
"Listening...": "Ouvindo...",
"LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.",
"Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI",
"Make sure to enclose them with": "Certifique-se de colocá-los entre",
"Manage LiteLLM Models": "Gerenciar Modelos LiteLLM",
"Manage Models": "Gerenciar Modelos",
"Manage Ollama Models": "Gerenciar Modelos Ollama",
"Max Tokens": "Máximo de Tokens",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Máximo de 3 modelos podem ser baixados simultaneamente. Tente novamente mais tarde.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "MMMM DD, AAAA",
"Model '{{modelName}}' has been successfully downloaded.": "O modelo '{{modelName}}' foi baixado com sucesso.",
"Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para download.",
"Model {{modelId}} not found": "Modelo {{modelId}} não encontrado",
"Model {{modelName}} already exists.": "O modelo {{modelName}} já existe.",
"Model Name": "Nome do Modelo",
"Model not selected": "Modelo não selecionado",
"Model Tag Name": "Nome da Tag do Modelo",
"Model Whitelisting": "Lista de Permissões de Modelo",
"Model(s) Whitelisted": "Modelo(s) na Lista de Permissões",
"Modelfile": "Arquivo de Modelo",
"Modelfile Advanced Settings": "Configurações Avançadas do Arquivo de Modelo",
"Modelfile Content": "Conteúdo do Arquivo de Modelo",
"Modelfiles": "Arquivos de Modelo",
"Models": "Modelos",
"My Documents": "Meus Documentos",
"My Modelfiles": "Meus Arquivos de Modelo",
"My Prompts": "Meus Prompts",
"Name": "Nome",
"Name Tag": "Nome da Tag",
"Name your modelfile": "Nomeie seu arquivo de modelo",
"New Chat": "Novo Bate-papo",
"New Password": "Nova Senha",
"Not sure what to add?": "Não tem certeza do que adicionar?",
"Not sure what to write? Switch to": "Não tem certeza do que escrever? Mude para",
"Off": "Desligado",
"Okay, Let's Go!": "Ok, Vamos Lá!",
"Ollama Base URL": "URL Base do Ollama",
"Ollama Version": "Versão do Ollama",
"On": "Ligado",
"Only": "Somente",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Somente caracteres alfanuméricos e hífens são permitidos na string de comando.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Opa! Aguente firme! Seus arquivos ainda estão no forno de processamento. Estamos cozinhando-os com perfeição. Por favor, seja paciente e avisaremos quando estiverem prontos.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Opa! Parece que a URL é inválida. Verifique novamente e tente outra vez.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Opa! Você está usando um método não suportado (somente frontend). Por favor, sirva o WebUI a partir do backend.",
"Open": "Abrir",
"Open AI": "OpenAI",
"Open AI (Dall-E)": "OpenAI (Dall-E)",
"Open new chat": "Abrir novo bate-papo",
"OpenAI API": "API OpenAI",
"OpenAI API Key": "Chave da API OpenAI",
"OpenAI API Key is required.": "A Chave da API OpenAI é obrigatória.",
"or": "ou",
"Parameters": "Parâmetros",
"Password": "Senha",
"PDF Extract Images (OCR)": "Extrair Imagens de PDF (OCR)",
"pending": "pendente",
"Permission denied when accessing microphone: {{error}}": "Permissão negada ao acessar o microfone: {{error}}",
"Playground": "Playground",
"Profile": "Perfil",
"Prompt Content": "Conteúdo do Prompt",
"Prompt suggestions": "Sugestões de Prompt",
"Prompts": "Prompts",
"Pull a model from Ollama.com": "Extrair um modelo do Ollama.com",
"Pull Progress": "Progresso da Extração",
"Query Params": "Parâmetros de Consulta",
"RAG Template": "Modelo RAG",
"Raw Format": "Formato Bruto",
"Record voice": "Gravar voz",
"Redirecting you to OpenWebUI Community": "Redirecionando você para a Comunidade OpenWebUI",
"Release Notes": "Notas de Lançamento",
"Repeat Last N": "Repetir Últimos N",
"Repeat Penalty": "Penalidade de Repetição",
"Request Mode": "Modo de Solicitação",
"Reset Vector Storage": "Redefinir Armazenamento de Vetor",
"Response AutoCopy to Clipboard": "Cópia Automática da Resposta para a Área de Transferência",
"Role": "Função",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Salvar",
"Save & Create": "Salvar e Criar",
"Save & Submit": "Salvar e Enviar",
"Save & Update": "Salvar e Atualizar",
"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": "Salvar logs de bate-papo diretamente no armazenamento do seu navegador não é mais suportado. Reserve um momento para baixar e excluir seus logs de bate-papo clicando no botão abaixo. Não se preocupe, você pode facilmente reimportar seus logs de bate-papo para o backend através de",
"Scan": "Digitalizar",
"Scan complete!": "Digitalização concluída!",
"Scan for documents from {{path}}": "Digitalizar documentos de {{path}}",
"Search": "Pesquisar",
"Search Documents": "Pesquisar Documentos",
"Search Prompts": "Pesquisar Prompts",
"See readme.md for instructions": "Consulte readme.md para obter instruções",
"See what's new": "Veja o que há de novo",
"Seed": "Semente",
"Select a mode": "Selecione um modo",
"Select a model": "Selecione um modelo",
"Select an Ollama instance": "Selecione uma instância Ollama",
"Send a Message": "Enviar uma Mensagem",
"Send message": "Enviar mensagem",
"Server connection verified": "Conexão com o servidor verificada",
"Set as default": "Definir como padrão",
"Set Default Model": "Definir Modelo Padrão",
"Set Image Size": "Definir Tamanho da Imagem",
"Set Steps": "Definir Etapas",
"Set Title Auto-Generation Model": "Definir Modelo de Geração Automática de Título",
"Set Voice": "Definir Voz",
"Settings": "Configurações",
"Settings saved successfully!": "Configurações salvas com sucesso!",
"Share to OpenWebUI Community": "Compartilhar com a Comunidade OpenWebUI",
"short-summary": "resumo-curto",
"Show": "Mostrar",
"Show Additional Params": "Mostrar Parâmetros Adicionais",
"Show shortcuts": "Mostrar",
"sidebar": "barra lateral",
"Sign in": "Entrar",
"Sign Out": "Sair",
"Sign up": "Inscrever-se",
"Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}",
"Speech-to-Text Engine": "Mecanismo de Fala para Texto",
"SpeechRecognition API is not supported in this browser.": "A API SpeechRecognition não é suportada neste navegador.",
"Stop Sequence": "Sequência de Parada",
"STT Settings": "Configurações STT",
"Submit": "Enviar",
"Success": "Sucesso",
"Successfully updated.": "Atualizado com sucesso.",
"Sync All": "Sincronizar Tudo",
"System": "Sistema",
"System Prompt": "Prompt do Sistema",
"Tags": "Tags",
"Temperature": "Temperatura",
"Template": "Modelo",
"Text Completion": "Complemento de Texto",
"Text-to-Speech Engine": "Mecanismo de Texto para Fala",
"Tfs Z": "Tfs Z",
"Theme": "Tema",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Isso garante que suas conversas valiosas sejam salvas com segurança em seu banco de dados de backend. Obrigado!",
"This setting does not sync across browsers or devices.": "Esta configuração não sincroniza entre navegadores ou dispositivos.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Dica: Atualize vários slots de variáveis consecutivamente pressionando a tecla Tab na entrada de bate-papo após cada substituição.",
"Title": "Título",
"Title Auto-Generation": "Geração Automática de Título",
"Title Generation Prompt": "Prompt de Geração de Título",
"to": "para",
"To access the available model names for downloading,": "Para acessar os nomes de modelo disponíveis para download,",
"To access the GGUF models available for downloading,": "Para acessar os modelos GGUF disponíveis para download,",
"to chat input.": "para a entrada de bate-papo.",
"Toggle settings": "Alternar configurações",
"Toggle sidebar": "Alternar barra lateral",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Problemas para acessar o Ollama?",
"TTS Settings": "Configurações TTS",
"Type Hugging Face Resolve (Download) URL": "Digite a URL do Hugging Face Resolve (Download)",
"Uh-oh! There was an issue connecting to {{provider}}.": "Opa! Houve um problema ao conectar-se a {{provider}}.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de arquivo desconhecido '{{file_type}}', mas aceitando e tratando como texto simples",
"Update password": "Atualizar senha",
"Upload a GGUF model": "Carregar um modelo GGUF",
"Upload files": "Carregar arquivos",
"Upload Progress": "Progresso do Carregamento",
"URL Mode": "Modo de URL",
"Use '#' in the prompt input to load and select your documents.": "Use '#' na entrada do prompt para carregar e selecionar seus documentos.",
"Use Gravatar": "Usar Gravatar",
"user": "usuário",
"User Permissions": "Permissões do Usuário",
"Users": "Usuários",
"Utilize": "Utilizar",
"Valid time units:": "Unidades de tempo válidas:",
"variable": "variável",
"variable to have them replaced with clipboard content.": "variável para que sejam substituídos pelo conteúdo da área de transferência.",
"Version": "Versão",
"Web": "Web",
"WebUI Add-ons": "Complementos WebUI",
"WebUI Settings": "Configurações WebUI",
"WebUI will make requests to": "WebUI fará solicitações para",
"Whats New in": "O que há de novo em",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quando o histórico está desativado, novos bate-papos neste navegador não aparecerão em seu histórico em nenhum dos seus dispositivos.",
"Whisper (Local)": "Whisper (Local)",
"Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem é você?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].",
"You": "Você",
"You're a helpful assistant.": "Você é um assistente útil.",
"You're now logged in.": "Você está conectado agora."
}

View file

@ -0,0 +1,363 @@
{
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' veya süresiz için '-1'.",
"(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(örn. `sh webui.sh --api`)",
"(latest)": "(en son)",
"{{modelName}} is thinking...": "{{modelName}} düşünüyor...",
"{{webUIName}} Backend Required": "{{webUIName}} Arkayüz Gerekli",
"a user": "bir kullanıcı",
"About": "Hakkında",
"Account": "Hesap",
"Action": "Eylem",
"Add a model": "Bir model ekleyin",
"Add a model tag name": "Bir model etiket adı ekleyin",
"Add a short description about what this modelfile does": "Bu model dosyasının ne yaptığı hakkında kısa bir açıklama ekleyin",
"Add a short title for this prompt": "Bu prompt için kısa bir başlık ekleyin",
"Add a tag": "Bir etiket ekleyin",
"Add Docs": "Dökümanlar Ekle",
"Add Files": "Dosyalar Ekle",
"Add message": "Mesaj ekle",
"add tags": "etiketler ekle",
"Adjusting these settings will apply changes universally to all users.": "Bu ayarları ayarlamak değişiklikleri tüm kullanıcılara evrensel olarak uygular.",
"admin": "yönetici",
"Admin Panel": "Yönetici Paneli",
"Admin Settings": "Yönetici Ayarları",
"Advanced Parameters": "Gelişmiş Parametreler",
"all": "tümü",
"All Users": "Tüm Kullanıcılar",
"Allow": "İzin ver",
"Allow Chat Deletion": "Sohbet Silmeye İzin Ver",
"alphanumeric characters and hyphens": "alfanumerik karakterler ve tireler",
"Already have an account?": "Zaten bir hesabınız mı var?",
"an assistant": "bir asistan",
"and": "ve",
"API Base URL": "API Temel URL",
"API Key": "API Anahtarı",
"API RPM": "API RPM",
"are allowed - Activate this command by typing": "izin verilir - Bu komutu yazarak etkinleştirin",
"Are you sure?": "Emin misiniz?",
"Audio": "Ses",
"Auto-playback response": "Yanıtı otomatik oynatma",
"Auto-send input after 3 sec.": "3 saniye sonra otomatik olarak gönder",
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Temel URL",
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Temel URL gereklidir.",
"available!": "mevcut!",
"Back": "Geri",
"Builder Mode": "Oluşturucu Modu",
"Cancel": "İptal",
"Categories": "Kategoriler",
"Change Password": "Parola Değiştir",
"Chat": "Sohbet",
"Chat History": "Sohbet Geçmişi",
"Chat History is off for this browser.": "Bu tarayıcı için sohbet geçmişi kapalı.",
"Chats": "Sohbetler",
"Check Again": "Tekrar Kontrol Et",
"Check for updates": "Güncellemeleri kontrol et",
"Checking for updates...": "Güncellemeler kontrol ediliyor...",
"Choose a model before saving...": "Kaydetmeden önce bir model seçin...",
"Chunk Overlap": "Chunk Çakışması",
"Chunk Params": "Chunk Parametreleri",
"Chunk Size": "Chunk Boyutu",
"Click here for help.": "Yardım için buraya tıklayın.",
"Click here to check other modelfiles.": "Diğer model dosyalarını kontrol etmek için buraya tıklayın.",
"Click here to select": "Seçmek için buraya tıklayın",
"Click here to select documents.": "Belgeleri seçmek için buraya tıklayın.",
"click here.": "buraya tıklayın.",
"Click on the user role button to change a user's role.": "Bir kullanıcının rolünü değiştirmek için kullanıcı rolü düğmesine tıklayın.",
"Close": "Kapat",
"Collection": "Koleksiyon",
"Command": "Komut",
"Confirm Password": "Parolayı Onayla",
"Connections": "Bağlantılar",
"Content": "İçerik",
"Context Length": "Bağlam Uzunluğu",
"Conversation Mode": "Sohbet Modu",
"Copy last code block": "Son kod bloğunu kopyala",
"Copy last response": "Son yanıtı kopyala",
"Copying to clipboard was successful!": "Panoya kopyalama başarılı!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Aşağıdaki sorgu için başlık olarak 3-5 kelimelik kısa ve öz bir ifade oluşturun, 3-5 kelime sınırına kesinlikle uyun ve 'başlık' kelimesini kullanmaktan kaçının:",
"Create a modelfile": "Bir model dosyası oluştur",
"Create Account": "Hesap Oluştur",
"Created at": "Oluşturulma tarihi",
"Created by": "Oluşturan",
"Current Model": "Mevcut Model",
"Current Password": "Mevcut Parola",
"Custom": "Özel",
"Customize Ollama models for a specific purpose": "Ollama modellerini belirli bir amaç için özelleştirin",
"Dark": "Koyu",
"Database": "Veritabanı",
"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Varsayılan",
"Default (Automatic1111)": "Varsayılan (Automatic1111)",
"Default (Web API)": "Varsayılan (Web API)",
"Default model updated": "Varsayılan model güncellendi",
"Default Prompt Suggestions": "Varsayılan Prompt Önerileri",
"Default User Role": "Varsayılan Kullanıcı Rolü",
"delete": "sil",
"Delete a model": "Bir modeli sil",
"Delete chat": "Sohbeti sil",
"Delete Chats": "Sohbetleri Sil",
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi",
"Deleted {tagName}": "{tagName} silindi",
"Description": "Açıklama",
"Desktop Notifications": "Masaüstü Bildirimleri",
"Disabled": "Devre Dışı",
"Discover a modelfile": "Bir model dosyası keşfedin",
"Discover a prompt": "Bir prompt keşfedin",
"Discover, download, and explore custom prompts": "Özel promptları keşfedin, indirin ve inceleyin",
"Discover, download, and explore model presets": "Model ön ayarlarını keşfedin, indirin ve inceleyin",
"Display the username instead of You in the Chat": "Sohbet'te Siz yerine kullanıcı adını göster",
"Document": "Belge",
"Document Settings": "Belge Ayarları",
"Documents": "Belgeler",
"does not make any external connections, and your data stays securely on your locally hosted server.": "herhangi bir harici bağlantı yapmaz ve verileriniz güvenli bir şekilde yerel olarak barındırılan sunucunuzda kalır.",
"Don't Allow": "İzin Verme",
"Don't have an account?": "Hesabınız yok mu?",
"Download as a File": "Dosya olarak indir",
"Download Database": "Veritabanını İndir",
"Drop any files here to add to the conversation": "Sohbete eklemek istediğiniz dosyaları buraya bırakın",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "örn. '30s', '10m'. Geçerli zaman birimleri 's', 'm', 'h'.",
"Edit Doc": "Belgeyi Düzenle",
"Edit User": "Kullanıcıyı Düzenle",
"Email": "E-posta",
"Enable Chat History": "Sohbet Geçmişini Etkinleştir",
"Enable New Sign Ups": "Yeni Kayıtları Etkinleştir",
"Enabled": "Etkin",
"Enter {{role}} message here": "Buraya {{role}} mesajını girin",
"Enter API Key": "API Anahtarını Girin",
"Enter Chunk Overlap": "Chunk Örtüşmesini Girin",
"Enter Chunk Size": "Chunk Boyutunu Girin",
"Enter Image Size (e.g. 512x512)": "Görüntü Boyutunu Girin (örn. 512x512)",
"Enter LiteLLM API Base URL (litellm_params.api_base)": "LiteLLM API Ana URL'sini Girin (litellm_params.api_base)",
"Enter LiteLLM API Key (litellm_params.api_key)": "LiteLLM API Anahtarını Girin (litellm_params.api_key)",
"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM'ini Girin (litellm_params.rpm)",
"Enter LiteLLM Model (litellm_params.model)": "LiteLLM Modelini Girin (litellm_params.model)",
"Enter Max Tokens (litellm_params.max_tokens)": "Maksimum Token Sayısını Girin (litellm_params.max_tokens)",
"Enter model tag (e.g. {{modelTag}})": "Model etiketini girin (örn. {{modelTag}})",
"Enter Number of Steps (e.g. 50)": "Adım Sayısını Girin (örn. 50)",
"Enter stop sequence": "Durdurma dizisini girin",
"Enter Top K": "Top K'yı girin",
"Enter URL (e.g. http://127.0.0.1:7860/)": "URL'yi Girin (örn. http://127.0.0.1:7860/)",
"Enter Your Email": "E-postanızı Girin",
"Enter Your Full Name": "Tam Adınızı Girin",
"Enter Your Password": "Parolanızı Girin",
"Experimental": "Deneysel",
"Export All Chats (All Users)": "Tüm Sohbetleri Dışa Aktar (Tüm Kullanıcılar)",
"Export Chats": "Sohbetleri Dışa Aktar",
"Export Documents Mapping": "Belge Eşlemesini Dışa Aktar",
"Export Modelfiles": "Model Dosyalarını Dışa Aktar",
"Export Prompts": "Promptları Dışa Aktar",
"Failed to read clipboard contents": "Pano içeriği okunamadı",
"File Mode": "Dosya Modu",
"File not found.": "Dosya bulunamadı.",
"Focus chat input": "Sohbet girişine odaklan",
"Format your variables using square brackets like this:": "Değişkenlerinizi şu şekilde kare parantezlerle biçimlendirin:",
"From (Base Model)": "(Temel Model)'den",
"Full Screen Mode": "Tam Ekran Modu",
"General": "Genel",
"General Settings": "Genel Ayarlar",
"Hello, {{name}}": "Merhaba, {{name}}",
"Hide": "Gizle",
"Hide Additional Params": "Ek Parametreleri Gizle",
"How can I help you today?": "Bugün size nasıl yardımcı olabilirim?",
"Image Generation (Experimental)": "Görüntü Oluşturma (Deneysel)",
"Image Generation Engine": "Görüntü Oluşturma Motoru",
"Image Settings": "Görüntü Ayarları",
"Images": "Görüntüler",
"Import Chats": "Sohbetleri İçe Aktar",
"Import Documents Mapping": "Belge Eşlemesini İçe Aktar",
"Import Modelfiles": "Model Dosyalarını İçe Aktar",
"Import Prompts": "Promptları İçe Aktar",
"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui çalıştırılırken `--api` bayrağını dahil edin",
"Interface": "Arayüz",
"join our Discord for help.": "yardım için Discord'umuza katılın.",
"JSON": "JSON",
"JWT Expiration": "JWT Bitişi",
"JWT Token": "JWT Token",
"Keep Alive": "Canlı Tut",
"Keyboard shortcuts": "Klavye kısayolları",
"Language": "Dil",
"Light": "Açık",
"Listening...": "Dinleniyor...",
"LLMs can make mistakes. Verify important information.": "LLM'ler hata yapabilir. Önemli bilgileri doğrulayın.",
"Made by OpenWebUI Community": "OpenWebUI Topluluğu tarafından yapılmıştır",
"Make sure to enclose them with": "Değişkenlerinizi şu şekilde biçimlendirin:",
"Manage LiteLLM Models": "LiteLLM Modellerini Yönet",
"Manage Models": "Modelleri Yönet",
"Manage Ollama Models": "Ollama Modellerini Yönet",
"Max Tokens": "Maksimum Token",
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Aynı anda en fazla 3 model indirilebilir. Lütfen daha sonra tekrar deneyin.",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
"Mirostat Tau": "Mirostat Tau",
"MMMM DD, YYYY": "DD MMMM YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' başarıyla indirildi.",
"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' zaten indirme sırasında.",
"Model {{modelId}} not found": "{{modelId}} bulunamadı",
"Model {{modelName}} already exists.": "{{modelName}} zaten mevcut.",
"Model Name": "Model Adı",
"Model not selected": "Model seçilmedi",
"Model Tag Name": "Model Etiket Adı",
"Model Whitelisting": "Model Beyaz Listeye Alma",
"Model(s) Whitelisted": "Model(ler) Beyaz Listeye Alındı",
"Modelfile": "Model Dosyası",
"Modelfile Advanced Settings": "Model Dosyası Gelişmiş Ayarları",
"Modelfile Content": "Model Dosyası İçeriği",
"Modelfiles": "Model Dosyaları",
"Models": "Modeller",
"My Documents": "Belgelerim",
"My Modelfiles": "Model Dosyalarım",
"My Prompts": "Promptlarım",
"Name": "Ad",
"Name Tag": "Ad Etiketi",
"Name your modelfile": "Model dosyanıza ad verin",
"New Chat": "Yeni Sohbet",
"New Password": "Yeni Parola",
"Not sure what to add?": "Ne ekleyeceğinizden emin değil misiniz?",
"Not sure what to write? Switch to": "Ne yazacağınızdan emin değil misiniz? Şuraya geçin",
"Off": "Kapalı",
"Okay, Let's Go!": "Tamam, Hadi Başlayalım!",
"Ollama Base URL": "Ollama Temel URL",
"Ollama Version": "Ollama Sürümü",
"On": "Açık",
"Only": "Yalnızca",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Komut dizisinde yalnızca alfasayısal karakterler ve tireler kabul edilir.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Hop! Biraz sabırlı ol! Dosyaların hala hazırlama fırınında. Onları ağzınıza layık olana kadar pişiriyoruz :) Lütfen sabırlı olun; hazır olduklarında size haber vereceğiz.",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hop! URL geçersiz gibi görünüyor. Lütfen tekrar kontrol edin ve yeniden deneyin.",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hop! Desteklenmeyen bir yöntem kullanıyorsunuz (yalnızca önyüz). Lütfen WebUI'yi arkayüzden sunun.",
"Open": "Aç",
"Open AI": "Open AI",
"Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Yeni sohbet aç",
"OpenAI API": "OpenAI API",
"OpenAI API Key": "OpenAI API Anahtarı",
"OpenAI API Key is required.": "OpenAI API Anahtarı gereklidir.",
"or": "veya",
"Parameters": "Parametreler",
"Password": "Parola",
"PDF Extract Images (OCR)": "PDF Görüntülerini Çıkart (OCR)",
"pending": "beklemede",
"Permission denied when accessing microphone: {{error}}": "Mikrofona erişim izni reddedildi: {{error}}",
"Playground": "Oyun Alanı",
"Profile": "Profil",
"Prompt Content": "Prompt İçeriği",
"Prompt suggestions": "Prompt önerileri",
"Prompts": "Promptlar",
"Pull a model from Ollama.com": "Ollama.com'dan bir model çekin",
"Pull Progress": "Çekme İlerlemesi",
"Query Params": "Sorgu Parametreleri",
"RAG Template": "RAG Şablonu",
"Raw Format": "Ham Format",
"Record voice": "Ses kaydı yap",
"Redirecting you to OpenWebUI Community": "OpenWebUI Topluluğuna yönlendiriliyorsunuz",
"Release Notes": "Sürüm Notları",
"Repeat Last N": "Son N'yi Tekrar Et",
"Repeat Penalty": "Tekrar Cezası",
"Request Mode": "İstek Modu",
"Reset Vector Storage": "Vektör Depolamayı Sıfırla",
"Response AutoCopy to Clipboard": "Yanıtı Panoya Otomatik Kopyala",
"Role": "Rol",
"Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Kaydet",
"Save & Create": "Kaydet ve Oluştur",
"Save & Submit": "Kaydet ve Gönder",
"Save & Update": "Kaydet ve Güncelle",
"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": "Sohbet kayıtlarının doğrudan tarayıcınızın depolama alanına kaydedilmesi artık desteklenmemektedir. Lütfen aşağıdaki butona tıklayarak sohbet kayıtlarınızı indirmek ve silmek için bir dakikanızı ayırın. Endişelenmeyin, sohbet günlüklerinizi arkayüze kolayca yeniden aktarabilirsiniz:",
"Scan": "Tarama",
"Scan complete!": "Tarama tamamlandı!",
"Scan for documents from {{path}}": "{{path}} dizininden belgeleri tarayın",
"Search": "Ara",
"Search Documents": "Belgeleri Ara",
"Search Prompts": "Prompt Ara",
"See readme.md for instructions": "Yönergeler için readme.md dosyasına bakın",
"See what's new": "Yeniliklere göz atın",
"Seed": "Seed",
"Select a mode": "Bir mod seç",
"Select a model": "Bir model seç",
"Select an Ollama instance": "Bir Ollama örneği seçin",
"Send a Message": "Bir Mesaj Gönder",
"Send message": "Mesaj gönder",
"Server connection verified": "Sunucu bağlantısı doğrulandı",
"Set as default": "Varsayılan olarak ayarla",
"Set Default Model": "Varsayılan Modeli Ayarla",
"Set Image Size": "Görüntü Boyutunu Ayarla",
"Set Steps": "Adımları Ayarla",
"Set Title Auto-Generation Model": "Otomatik Başlık Oluşturma Modelini Ayarla",
"Set Voice": "Ses Ayarla",
"Settings": "Ayarlar",
"Settings saved successfully!": "Ayarlar başarıyla kaydedildi!",
"Share to OpenWebUI Community": "OpenWebUI Topluluğu ile Paylaş",
"short-summary": "kısa-özet",
"Show": "Göster",
"Show Additional Params": "Ek Parametreleri Göster",
"Show shortcuts": "Kısayolları göster",
"sidebar": "kenar çubuğu",
"Sign in": "Oturum aç",
"Sign Out": ıkış Yap",
"Sign up": "Kaydol",
"Speech recognition error: {{error}}": "Konuşma tanıma hatası: {{error}}",
"Speech-to-Text Engine": "Konuşmadan Metne Motoru",
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API bu tarayıcıda desteklenmiyor.",
"Stop Sequence": "Diziyi Durdur",
"STT Settings": "STT Ayarları",
"Submit": "Gönder",
"Success": "Başarılı",
"Successfully updated.": "Başarıyla güncellendi.",
"Sync All": "Tümünü Senkronize Et",
"System": "Sistem",
"System Prompt": "Sistem Promptu",
"Tags": "Etiketler",
"Temperature": "Temperature",
"Template": "Şablon",
"Text Completion": "Metin Tamamlama",
"Text-to-Speech Engine": "Metinden Sese Motoru",
"Tfs Z": "Tfs Z",
"Theme": "Tema",
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Bu, önemli konuşmalarınızın güvenli bir şekilde arkayüz veritabanınıza kaydedildiğini garantiler. Teşekkür ederiz!",
"This setting does not sync across browsers or devices.": "Bu ayar tarayıcılar veya cihazlar arasında senkronize edilmez.",
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "İpucu: Her değiştirmeden sonra sohbet girişinde tab tuşuna basarak birden fazla değişken yuvasını art arda güncelleyin.",
"Title": "Başlık",
"Title Auto-Generation": "Otomatik Başlık Oluşturma",
"Title Generation Prompt": "Başlık Oluşturma Promptu",
"to": "için",
"To access the available model names for downloading,": "İndirilebilir mevcut model adlarına erişmek için,",
"To access the GGUF models available for downloading,": "İndirilebilir mevcut GGUF modellerine erişmek için,",
"to chat input.": "sohbet girişine.",
"Toggle settings": "Ayarları Aç/Kapat",
"Toggle sidebar": "Kenar Çubuğunu Aç/Kapat",
"Top K": "Top K",
"Top P": "Top P",
"Trouble accessing Ollama?": "Ollama'ya erişmede sorun mu yaşıyorsunuz?",
"TTS Settings": "TTS Ayarları",
"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (Download) URL'sini Yazın",
"Uh-oh! There was an issue connecting to {{provider}}.": "Ah! {{provider}}'a bağlanırken bir sorun oluştu.",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Bilinmeyen Dosya Türü '{{file_type}}', ancak düz metin olarak kabul ediliyor ve işleniyor",
"Update password": "Parolayı Güncelle",
"Upload a GGUF model": "Bir GGUF modeli yükle",
"Upload files": "Dosyaları Yükle",
"Upload Progress": "Yükleme İlerlemesi",
"URL Mode": "URL Modu",
"Use '#' in the prompt input to load and select your documents.": "Belgelerinizi yüklemek ve seçmek için promptda '#' kullanın.",
"Use Gravatar": "Gravatar Kullan",
"user": "kullanıcı",
"User Permissions": "Kullanıcı İzinleri",
"Users": "Kullanıcılar",
"Utilize": "Kullan",
"Valid time units:": "Geçerli zaman birimleri:",
"variable": "değişken",
"variable to have them replaced with clipboard content.": "panodaki içerikle değiştirilmesi için değişken.",
"Version": "Sürüm",
"Web": "Web",
"WebUI Add-ons": "WebUI Eklentileri",
"WebUI Settings": "WebUI Ayarları",
"WebUI will make requests to": "WebUI, isteklerde bulunacak:",
"Whats New in": "Yenilikler:",
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Geçmiş kapatıldığında, bu tarayıcıdaki yeni sohbetler hiçbir cihazınızdaki geçmişinizde görünmez.",
"Whisper (Local)": "Whisper (Yerel)",
"Write a prompt suggestion (e.g. Who are you?)": "Bir prompt önerisi yazın (örn. Sen kimsin?)",
"Write a summary in 50 words that summarizes [topic or keyword].": "[Konuyu veya anahtar kelimeyi] özetleyen 50 kelimelik bir özet yazın.",
"You": "Siz",
"You're a helpful assistant.": "Sen yardımcı bir asistansın.",
"You're now logged in.": "Şimdi oturum açtınız."
}

View file

@ -7,8 +7,9 @@ export const config = writable(undefined);
export const user = writable(undefined); export const user = writable(undefined);
// Frontend // Frontend
export const theme = writable('system'); export const MODEL_DOWNLOAD_POOL = writable({});
export const theme = writable('system');
export const chatId = writable(''); export const chatId = writable('');
export const chats = writable([]); export const chats = writable([]);

View file

@ -1,10 +1,55 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import sha256 from 'js-sha256'; import sha256 from 'js-sha256';
import { getOllamaModels } from '$lib/apis/ollama';
import { getOpenAIModels } from '$lib/apis/openai';
import { getLiteLLMModels } from '$lib/apis/litellm';
export const getModels = async (token: string) => {
let models = await Promise.all([
await getOllamaModels(token).catch((error) => {
console.log(error);
return null;
}),
await getOpenAIModels(token).catch((error) => {
console.log(error);
return null;
}),
await getLiteLLMModels(token).catch((error) => {
console.log(error);
return null;
})
]);
models = models
.filter((models) => models)
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
return models;
};
////////////////////////// //////////////////////////
// Helper functions // Helper functions
////////////////////////// //////////////////////////
export const sanitizeResponseContent = (content: string) => {
return content
.replace(/<\|[a-z]*$/, '')
.replace(/<\|[a-z]+\|$/, '')
.replace(/<$/, '')
.replaceAll(/<\|[a-z]+\|>/g, ' ')
.replaceAll(/<br\s?\/?>/gi, '\n')
.replaceAll('<', '&lt;')
.trim();
};
export const revertSanitizedResponseContent = (content: string) => {
return content.replaceAll('&lt;', '<');
};
export const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
export const splitStream = (splitOn) => { export const splitStream = (splitOn) => {
let buffer = ''; let buffer = '';
return new TransformStream({ return new TransformStream({

View file

@ -19,7 +19,7 @@
} from '$lib/stores'; } from '$lib/stores';
import { copyToClipboard, splitStream } from '$lib/utils'; import { copyToClipboard, splitStream } from '$lib/utils';
import { generateChatCompletion, cancelOllamaRequest, generateTitle } from '$lib/apis/ollama'; import { generateChatCompletion, cancelOllamaRequest } from '$lib/apis/ollama';
import { import {
addTagById, addTagById,
createNewChat, createNewChat,
@ -30,14 +30,14 @@
updateChatById updateChatById
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { queryCollection, queryDoc } from '$lib/apis/rag'; import { queryCollection, queryDoc } from '$lib/apis/rag';
import { generateOpenAIChatCompletion } from '$lib/apis/openai'; import { generateOpenAIChatCompletion, generateTitle } from '$lib/apis/openai';
import MessageInput from '$lib/components/chat/MessageInput.svelte'; import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte'; import Messages from '$lib/components/chat/Messages.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants'; import { LITELLM_API_BASE_URL, OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -48,6 +48,7 @@
let messagesContainerElement: HTMLDivElement; let messagesContainerElement: HTMLDivElement;
let currentRequestId = null; let currentRequestId = null;
let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;
@ -511,13 +512,13 @@
if (messages.length == 2 && messages.at(1).content !== '') { if (messages.length == 2 && messages.at(1).content !== '') {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
await generateChatTitle(_chatId, userPrompt); const _title = await generateChatTitle(userPrompt);
await setChatTitle(_chatId, _title);
} }
}; };
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
scrollToBottom();
const docs = messages const docs = messages
.filter((message) => message?.files ?? null) .filter((message) => message?.files ?? null)
@ -528,6 +529,8 @@
console.log(docs); console.log(docs);
console.log(model);
const res = await generateOpenAIChatCompletion( const res = await generateOpenAIChatCompletion(
localStorage.token, localStorage.token,
{ {
@ -580,9 +583,16 @@
max_tokens: $settings?.options?.num_predict ?? undefined, max_tokens: $settings?.options?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined docs: docs.length > 0 ? docs : undefined
}, },
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}` model?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
); );
// Wait until history/message have been updated
await tick();
scrollToBottom();
if (res && res.ok) { if (res && res.ok) {
const reader = res.body const reader = res.body
.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextDecoderStream())
@ -696,11 +706,8 @@
if (messages.length == 2) { if (messages.length == 2) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
if ($settings?.titleAutoGenerateModel) { const _title = await generateChatTitle(userPrompt);
await generateChatTitle(_chatId, userPrompt); await setChatTitle(_chatId, _title);
} else {
await setChatTitle(_chatId, userPrompt);
}
} }
}; };
@ -754,23 +761,46 @@
} }
}; };
const generateChatTitle = async (_chatId, userPrompt) => { const generateChatTitle = async (userPrompt) => {
if ($settings.titleAutoGenerate ?? true) { if ($settings?.title?.auto ?? true) {
const model = $models.find((model) => model.id === selectedModels[0]);
const titleModelId =
model?.external ?? false
? $settings?.title?.modelExternal ?? selectedModels[0]
: $settings?.title?.model ?? selectedModels[0];
const titleModel = $models.find((model) => model.id === titleModelId);
console.log(titleModel);
const title = await generateTitle( const title = await generateTitle(
localStorage.token, localStorage.token,
$settings?.titleGenerationPrompt ?? $settings?.title?.prompt ??
$i18n.t( $i18n.t(
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':" "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
) + ' {{prompt}}', ) + ' {{prompt}}',
$settings?.titleAutoGenerateModel ?? selectedModels[0], titleModelId,
userPrompt userPrompt,
titleModel?.external ?? false
? titleModel?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
); );
if (title) { return title;
await setChatTitle(_chatId, title);
}
} else { } else {
await setChatTitle(_chatId, `${userPrompt}`); return `${userPrompt}`;
}
};
const setChatTitle = async (_chatId, _title) => {
if (_chatId === $chatId) {
title = _title;
}
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token));
} }
}; };
@ -801,17 +831,6 @@
_tags.set(await getAllChatTags(localStorage.token)); _tags.set(await getAllChatTags(localStorage.token));
}; };
const setChatTitle = async (_chatId, _title) => {
if (_chatId === $chatId) {
title = _title;
}
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token));
}
};
</script> </script>
<svelte:head> <svelte:head>
@ -823,7 +842,17 @@
</svelte:head> </svelte:head>
<div class="h-screen max-h-[100dvh] w-full flex flex-col"> <div class="h-screen max-h-[100dvh] w-full flex flex-col">
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} /> <Navbar
{title}
bind:selectedModels
bind:showModelSelector
shareEnabled={messages.length > 0}
{chat}
{initNewChat}
{tags}
{addTag}
{deleteTag}
/>
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto">
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
@ -835,15 +864,7 @@
messagesContainerElement.clientHeight + 5; messagesContainerElement.clientHeight + 5;
}} }}
> >
<div <div class=" h-full w-full flex flex-col pt-2 pb-4">
class="{$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
>
<ModelSelector bind:selectedModels />
</div>
<div class=" h-full w-full flex flex-col py-8">
<Messages <Messages
chatId={$chatId} chatId={$chatId}
{selectedModels} {selectedModels}

View file

@ -19,7 +19,7 @@
} from '$lib/stores'; } from '$lib/stores';
import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils'; import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
import { generateChatCompletion, generateTitle, cancelOllamaRequest } from '$lib/apis/ollama'; import { generateChatCompletion, cancelOllamaRequest } from '$lib/apis/ollama';
import { import {
addTagById, addTagById,
createNewChat, createNewChat,
@ -31,14 +31,19 @@
updateChatById updateChatById
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { queryCollection, queryDoc } from '$lib/apis/rag'; import { queryCollection, queryDoc } from '$lib/apis/rag';
import { generateOpenAIChatCompletion } from '$lib/apis/openai'; import { generateOpenAIChatCompletion, generateTitle } from '$lib/apis/openai';
import MessageInput from '$lib/components/chat/MessageInput.svelte'; import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte'; import Messages from '$lib/components/chat/Messages.svelte';
import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import {
LITELLM_API_BASE_URL,
OPENAI_API_BASE_URL,
OLLAMA_API_BASE_URL,
WEBUI_BASE_URL
} from '$lib/constants';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -51,8 +56,10 @@
let currentRequestId = null; let currentRequestId = null;
// let chatId = $page.params.id; // let chatId = $page.params.id;
let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
let selectedModelfile = null; let selectedModelfile = null;
$: selectedModelfile = $: selectedModelfile =
selectedModels.length === 1 && selectedModels.length === 1 &&
$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0 $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
@ -521,13 +528,13 @@
if (messages.length == 2 && messages.at(1).content !== '') { if (messages.length == 2 && messages.at(1).content !== '') {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
await generateChatTitle(_chatId, userPrompt); const _title = await generateChatTitle(userPrompt);
await setChatTitle(_chatId, _title);
} }
}; };
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
scrollToBottom();
const docs = messages const docs = messages
.filter((message) => message?.files ?? null) .filter((message) => message?.files ?? null)
@ -590,9 +597,16 @@
max_tokens: $settings?.options?.num_predict ?? undefined, max_tokens: $settings?.options?.num_predict ?? undefined,
docs: docs.length > 0 ? docs : undefined docs: docs.length > 0 ? docs : undefined
}, },
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}` model?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
); );
// Wait until history/message have been updated
await tick();
scrollToBottom();
if (res && res.ok) { if (res && res.ok) {
const reader = res.body const reader = res.body
.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextDecoderStream())
@ -706,11 +720,8 @@
if (messages.length == 2) { if (messages.length == 2) {
window.history.replaceState(history.state, '', `/c/${_chatId}`); window.history.replaceState(history.state, '', `/c/${_chatId}`);
if ($settings?.titleAutoGenerateModel) { const _title = await generateChatTitle(userPrompt);
await generateChatTitle(_chatId, userPrompt); await setChatTitle(_chatId, _title);
} else {
await setChatTitle(_chatId, userPrompt);
}
} }
}; };
@ -719,6 +730,19 @@
console.log('stopResponse'); console.log('stopResponse');
}; };
const regenerateResponse = async () => {
console.log('regenerateResponse');
if (messages.length != 0 && messages.at(-1).done == true) {
messages.splice(messages.length - 1, 1);
messages = messages;
let userMessage = messages.at(-1);
let userPrompt = userMessage.content;
await sendPrompt(userPrompt, userMessage.id);
}
};
const continueGeneration = async () => { const continueGeneration = async () => {
console.log('continueGeneration'); console.log('continueGeneration');
const _chatId = JSON.parse(JSON.stringify($chatId)); const _chatId = JSON.parse(JSON.stringify($chatId));
@ -751,36 +775,35 @@
} }
}; };
const regenerateResponse = async () => { const generateChatTitle = async (userPrompt) => {
console.log('regenerateResponse'); if ($settings?.title?.auto ?? true) {
if (messages.length != 0 && messages.at(-1).done == true) { const model = $models.find((model) => model.id === selectedModels[0]);
messages.splice(messages.length - 1, 1);
messages = messages;
let userMessage = messages.at(-1); const titleModelId =
let userPrompt = userMessage.content; model?.external ?? false
? $settings?.title?.modelExternal ?? selectedModels[0]
: $settings?.title?.model ?? selectedModels[0];
const titleModel = $models.find((model) => model.id === titleModelId);
await sendPrompt(userPrompt, userMessage.id); console.log(titleModel);
}
};
const generateChatTitle = async (_chatId, userPrompt) => {
if ($settings.titleAutoGenerate ?? true) {
const title = await generateTitle( const title = await generateTitle(
localStorage.token, localStorage.token,
$settings?.titleGenerationPrompt ?? $settings?.title?.prompt ??
$i18n.t( $i18n.t(
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':" "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
) + ' {{prompt}}', ) + ' {{prompt}}',
$settings?.titleAutoGenerateModel ?? selectedModels[0], titleModelId,
userPrompt userPrompt,
titleModel?.external ?? false
? titleModel?.source?.toLowerCase() === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
); );
if (title) { return title;
await setChatTitle(_chatId, title);
}
} else { } else {
await setChatTitle(_chatId, `${userPrompt}`); return `${userPrompt}`;
} }
}; };
@ -789,8 +812,10 @@
title = _title; title = _title;
} }
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, _chatId, { title: _title }); chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
}
}; };
const getTags = async () => { const getTags = async () => {
@ -840,6 +865,9 @@
<div class="min-h-screen max-h-screen w-full flex flex-col"> <div class="min-h-screen max-h-screen w-full flex flex-col">
<Navbar <Navbar
{title} {title}
{chat}
bind:selectedModels
bind:showModelSelector
shareEnabled={messages.length > 0} shareEnabled={messages.length > 0}
initNewChat={async () => { initNewChat={async () => {
if (currentRequestId !== null) { if (currentRequestId !== null) {
@ -864,15 +892,7 @@
messagesContainerElement.clientHeight + 5; messagesContainerElement.clientHeight + 5;
}} }}
> >
<div <div class=" h-full w-full flex flex-col py-4">
class="{$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
>
<ModelSelector bind:selectedModels />
</div>
<div class=" h-full w-full flex flex-col py-8">
<Messages <Messages
chatId={$chatId} chatId={$chatId}
{selectedModels} {selectedModels}

View file

@ -18,6 +18,7 @@
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte'; import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte';
import Selector from '$lib/components/chat/ModelSelector/Selector.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -315,27 +316,24 @@
</div> </div>
</div> </div>
<div class=" flex gap-1 px-1"> <div class="flex flex-col gap-1 px-1 w-full">
<select <div class="flex w-full">
id="models" <div class="overflow-hidden w-full">
class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400" <div class="max-w-full">
<Selector
placeholder={$i18n.t('Select a model')}
items={$models
.filter((model) => model.name !== 'hr')
.map((model) => ({
value: model.id,
label: model.name,
info: model
}))}
bind:value={selectedModelId} bind:value={selectedModelId}
> />
<option class=" text-gray-800" value="" selected disabled </div>
>{$i18n.t('Select a model')}</option </div>
> </div>
{#each $models as model}
{#if model.name === 'hr'}
<hr />
{:else}
<option value={model.id} class="text-gray-800 text-lg"
>{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
>
{/if}
{/each}
</select>
<!-- <button <!-- <button
class=" self-center dark:hover:text-gray-300" class=" self-center dark:hover:text-gray-300"

View file

@ -11,7 +11,7 @@
import '../tailwind.css'; import '../tailwind.css';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import i18n from '$lib/i18n'; import i18n, { initI18n } from '$lib/i18n';
setContext('i18n', i18n); setContext('i18n', i18n);
@ -25,6 +25,11 @@
if (backendConfig) { if (backendConfig) {
// Save Backend Status to Store // Save Backend Status to Store
await config.set(backendConfig); await config.set(backendConfig);
if ($config.default_locale) {
initI18n($config.default_locale);
} else {
initI18n();
}
await WEBUI_NAME.set(backendConfig.name); await WEBUI_NAME.set(backendConfig.name);
console.log(backendConfig); console.log(backendConfig);

View file

@ -1,6 +1,7 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { userSignIn, userSignUp } from '$lib/apis/auths'; import { userSignIn, userSignUp } from '$lib/apis/auths';
import Spinner from '$lib/components/common/Spinner.svelte';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
@ -56,6 +57,9 @@
await goto('/'); await goto('/');
} }
loaded = true; loaded = true;
if ($config?.trusted_header_auth ?? false) {
await signInHandler();
}
}); });
</script> </script>
@ -90,6 +94,23 @@
</div> --> </div> -->
<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col"> <div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
{#if $config?.trusted_header_auth ?? false}
<div class=" my-auto pb-10 w-full">
<div
class="flex items-center justify-center gap-3 text-xl sm:text-2xl text-center font-bold dark:text-gray-200"
>
<div>
{$i18n.t('Signing in')}
{$i18n.t('to')}
{$WEBUI_NAME}
</div>
<div>
<Spinner />
</div>
</div>
</div>
{:else}
<div class=" my-auto pb-10 w-full"> <div class=" my-auto pb-10 w-full">
<form <form
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl" class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
@ -184,6 +205,7 @@
</div> </div>
</form> </form>
</div> </div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -0,0 +1,180 @@
<script lang="ts">
import { onMount, tick, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import dayjs from 'dayjs';
import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores';
import { convertMessagesToHistory } from '$lib/utils';
import { getChatByShareId } from '$lib/apis/chats';
import Messages from '$lib/components/chat/Messages.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
const i18n = getContext('i18n');
let loaded = false;
let autoScroll = true;
let processing = '';
let messagesContainerElement: HTMLDivElement;
// let chatId = $page.params.id;
let showModelSelector = false;
let selectedModels = [''];
let selectedModelfiles = {};
$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
const modelfile =
$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;
return {
...a,
...(modelfile && { [tagName]: modelfile })
};
}, {});
let chat = null;
let title = '';
let files = [];
let messages = [];
let history = {
messages: {},
currentId: null
};
$: if (history.currentId !== null) {
let _messages = [];
let currentMessage = history.messages[history.currentId];
while (currentMessage !== null) {
_messages.unshift({ ...currentMessage });
currentMessage =
currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
}
messages = _messages;
} else {
messages = [];
}
$: if ($page.params.id) {
(async () => {
if (await loadSharedChat()) {
await tick();
loaded = true;
window.setTimeout(() => scrollToBottom(), 0);
const chatInput = document.getElementById('chat-textarea');
chatInput?.focus();
} else {
await goto('/');
}
})();
}
//////////////////////////
// Web functions
//////////////////////////
const loadSharedChat = async () => {
await chatId.set($page.params.id);
chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => {
await goto('/');
return null;
});
if (chat) {
const chatContent = chat.chat;
if (chatContent) {
console.log(chatContent);
selectedModels =
(chatContent?.models ?? undefined) !== undefined
? chatContent.models
: [chatContent.models ?? ''];
history =
(chatContent?.history ?? undefined) !== undefined
? chatContent.history
: convertMessagesToHistory(chatContent.messages);
title = chatContent.title;
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
await settings.set({
..._settings,
system: chatContent.system ?? _settings.system,
options: chatContent.options ?? _settings.options
});
autoScroll = true;
await tick();
if (messages.length > 0) {
history.messages[messages.at(-1).id].done = true;
}
await tick();
return true;
} else {
return null;
}
}
};
</script>
<svelte:head>
<title>
{title
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
: `${$WEBUI_NAME}`}
</title>
</svelte:head>
{#if loaded}
<div
class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900"
>
<div class="flex flex-col flex-auto justify-center py-8">
<div class="px-3 w-full max-w-3xl mx-auto">
<div>
<div class=" text-3xl font-semibold line-clamp-1">
{title}
</div>
<div class=" mt-1 text-gray-400">
{dayjs(chat.chat.timestamp).format('MMMM D, YYYY')}
</div>
</div>
<hr class=" dark:border-gray-800 mt-6 mb-2" />
</div>
<div
class=" flex flex-col justify-center w-full flex-auto overflow-auto h-0"
id="messages-container"
>
<div class=" h-full w-full flex flex-col py-4">
<div class="py-2">
<Messages
chatId={$chatId}
readOnly={true}
{selectedModels}
{selectedModelfiles}
{processing}
bind:history
bind:messages
bind:autoScroll
bottomPadding={files.length > 0}
sendPrompt={() => {}}
continueGeneration={() => {}}
regenerateResponse={() => {}}
/>
</div>
</div>
</div>
</div>
</div>
{/if}

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