Compare commits

..
102 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek eb51ad14e4 Merge pull request #997 from open-webui/dev
0.1.108
2024-03-02 22:25:17 -05:00
Timothy J. Baek 65804e657a Update CHANGELOG.md 2024-03-02 19:24:02 -08:00
Timothy J. Baek 25c0316fc6 doc: changelog 2024-03-02 19:19:39 -08:00
Timothy J. Baek 4a1e506a35 fix: input autofocus on mobile issue 2024-03-02 19:05:16 -08:00
Timothy J. Baek 47a05a47b4 feat: add rag top k value setting 2024-03-02 18:56:57 -08:00
Timothy Jaeryang Baek 9694c6569f Merge pull request #1002 from open-webui/playground
feat: playground
2024-03-02 21:36:00 -05:00
Timothy J. Baek 9201769ba5 refac: unused style 2024-03-02 18:35:33 -08:00
Timothy J. Baek 79d7e135ec refac 2024-03-02 18:35:19 -08:00
Timothy J. Baek bf3f3c580e fix: text completion 2024-03-02 18:23:27 -08:00
Timothy J. Baek 901e7a33fa feat: chat playground backend integration 2024-03-02 18:16:02 -08:00
Timothy J. Baek 656f8dab05 feat: playground chat mode 2024-03-02 17:11:48 -08:00
Timothy J. Baek 06ce5aed4a fix: text completion replaced with chat completion 2024-03-02 13:14:14 -08:00
Timothy J. Baek 2379e94cbf feat: text completion playground 2024-03-02 03:01:44 -08:00
Timothy J. Baek 9b86e0bb41 Update Sidebar.svelte 2024-03-02 01:27:34 -08:00
Timothy J. Baek b9444d00b3 Update Tooltip.svelte 2024-03-02 01:27:01 -08:00
Timothy J. Baek 0bc3e0a302 feat: tooltip added to sidebar 2024-03-02 01:21:00 -08:00
Timothy J. Baek f9482275f2 fix: tooltip 2024-03-02 01:20:50 -08:00
Timothy J. BaekandJannik S. 63c0927022 feat: tooltip
Co-Authored-By: Jannik S. <69747628+jannikstdl@users.noreply.github.com>
2024-03-02 01:00:20 -08:00
Timothy J. Baek edb63c2280 feat: download db from admin settings 2024-03-02 00:33:20 -08:00
Timothy J. Baek 6d854125cc refac: ollama.db renamed to webui.db 2024-03-02 00:19:24 -08:00
Timothy J. Baek 74809e7330 fix: user chat delete loophole 2024-03-02 00:07:50 -08:00
Timothy J. Baek a4c6a8d5a4 fix: sonner toast styling issue 2024-03-02 00:05:25 -08:00
Timothy Jaeryang Baek 9caf6c26ff Merge pull request #998 from chamburr/patch-1
Fix enable signup env
2024-03-02 02:58:41 -05:00
Han Cen a12b5ffab3 Fix enable signup env 2024-03-02 15:38:14 +08:00
Timothy J. Baek b85c6ef667 fix: ol display issue
#963
2024-03-01 23:32:06 -08:00
Timothy Jaeryang Baek 6c70d0f770 Merge pull request #996 from open-webui/dev
0.1.107
2024-03-02 02:05:50 -05:00
Timothy J. Baek 96e2ee48f7 Update CHANGELOG.md 2024-03-01 23:04:20 -08:00
Timothy J. Baek 63e7246c97 doc: changelog 2024-03-01 23:02:47 -08:00
Timothy J. Baek 6f4a863562 feat: dark mode auth page 2024-03-01 22:52:21 -08:00
Timothy Jaeryang Baek a7ae141301 Merge pull request #982 from jannikstdl/svelte-sonner
ui: svelte-french-toast -> svelte-sonner
2024-03-02 01:41:49 -05:00
Timothy J. Baek 2564b154fc fix: toast position 2024-03-01 22:40:55 -08:00
Timothy Jaeryang Baek 8310392c9d Merge pull request #992 from jannikstdl/bottom-scroll
show chat in browser title + scroll to bottom
2024-03-02 01:39:00 -05:00
Timothy J. Baek 78d9c469a2 feat: title for all routes 2024-03-01 22:38:04 -08:00
Timothy J. Baek e5c812f1d2 refac 2024-03-01 22:35:08 -08:00
Jannik Streidl 8461b92f08 show chat in browser title + scroll to bottom 2024-03-01 21:18:17 +01:00
Timothy Jaeryang Baek 3396723057 Merge pull request #989 from jmtatsch/patch-1
Update run-compose.sh
2024-03-01 14:51:20 -05:00
jmtatsch 4fe47f43be Update run-compose.sh
fix webui port not being effective
2024-03-01 17:45:32 +01:00
Jannik Streidl 705f5aecd7 svelte-sonner migration 2024-03-01 10:18:07 +01:00
Timothy Jaeryang Baek 19d49df76f Merge pull request #968 from Carlos-err406/fix/double-loop
fix: unnecesary double loop
2024-02-29 15:15:19 -05:00
Carlos Daniel Vilaseca 30c94ff10f fix: unnecesary double loop 2024-02-29 12:37:46 -05:00
Timothy Jaeryang Baek 941ab5d6cf Merge pull request #848 from Patrice-Gaudicheau/feat-makefile-llm-update 2024-02-28 16:38:54 -05:00
Timothy Jaeryang Baek 5a81ca7e39 Merge pull request #960 from cocktailpeanut/main 2024-02-28 16:36:09 -05:00
cocktailpeanut f4122fd0ce update 2024-02-28 14:27:12 -05:00
Timothy Jaeryang Baek 5745b9c5db Merge pull request #955 from open-webui/dev
fix: openai convo
2024-02-28 05:19:20 -05:00
Timothy J. Baek 3fd55ebc32 fix: openai convo 2024-02-28 02:18:58 -08:00
Timothy Jaeryang Baek 776b463ff3 Merge pull request #953 from open-webui/dev
fix: title edit issue
2024-02-28 01:36:32 -05:00
Timothy J. Baek 5640b38694 fix: title edit issue 2024-02-27 22:35:57 -08:00
Timothy Jaeryang Baek a181b2b63b Merge pull request #951 from open-webui/dev
0.1.106
2024-02-27 23:11:43 -05:00
Timothy J. Baek 32361dc3da Update CHANGELOG.md 2024-02-27 20:10:29 -08:00
Timothy J. Baek 59ae5b6d34 fix: openai filtering 2024-02-27 20:08:57 -08:00
Timothy J. Baek a36627cbce doc: changelog 2024-02-27 20:07:23 -08:00
Timothy Jaeryang Baek 8a63d22000 Merge pull request #927 from sakkus-ai/fix-openai-integration
Fix OpenAI integration: allow OpenAI to make chat completion calls with a valid API key
2024-02-27 23:03:10 -05:00
Timothy J. Baek 781be2779b refac: unnecessary max_token value removed 2024-02-27 20:02:23 -08:00
Timothy Jaeryang Baek f347d664ac Merge pull request #941 from jannikstdl/autofocus-chat
fix: added focus selector
2024-02-27 22:59:10 -05:00
Timothy J. Baek 260a8b8b83 feat: chat input focus on new chat 2024-02-27 19:56:52 -08:00
Timothy J. Baek 1d4d1ddbea revert 2024-02-27 19:06:32 -08:00
Timothy Jaeryang Baek 8f2d580991 Merge pull request #940 from jannikstdl/link-fix
fix: mark text when editing chat titles
2024-02-27 22:05:32 -05:00
Timothy Jaeryang Baek 6bbc3721b4 Merge pull request #937 from jannikstdl/patch-3
Added star-history
2024-02-27 22:04:24 -05:00
Jannik Streidl 7fe412374c more readable 2024-02-27 16:00:58 +01:00
Jannik Streidl 03210c9204 keep the functionality to drag a chat 2024-02-27 15:48:12 +01:00
Jannik Streidl d24b6fa9af link fix 2024-02-27 15:48:12 +01:00
Jannik Streidl 7c7e16c382 added focus selector 2024-02-27 13:30:03 +01:00
Jannik S 70f807e8c9 Added star-history
Added https://star-history.com/#open-webui/open-webui&Date
2024-02-27 11:51:12 +01:00
Timothy Jaeryang Baek 210ae331cc Merge pull request #924 from eltociear/patch-1
chore: update Models.svelte
2024-02-26 17:29:30 -05:00
Sakkus bbeaa92904 Fix OpenAI integration:
from docker on mac get errors since num_ctx not recognized;
remove num_ctx from, and max_tokens to, body in API calls.
2024-02-26 12:25:08 +00:00
Ikko Eltociear Ashimine a7a817d12b chore: update Models.svelte
HuggingFace -> Hugging Face
2024-02-26 19:34:43 +09:00
Timothy Jaeryang Baek 6df2505bf0 Merge pull request #919 from open-webui/dev
0.1.105
2024-02-26 00:53:39 -05:00
Timothy J. Baek 3a7ded3f80 Update CHANGELOG.md 2024-02-25 21:46:43 -08:00
Timothy J. Baek 6d8b1385bb doc: changelog 2024-02-25 21:45:38 -08:00
Timothy J. Baek 92c922b061 feat: batch doc tagging 2024-02-25 21:39:10 -08:00
Timothy J. Baek 63628a70a6 feat: document select 2024-02-25 21:16:11 -08:00
Timothy Jaeryang Baek 95dd618a62 Merge pull request #914 from buroa/main
fix: add semver to container builds
2024-02-25 19:23:13 -05:00
Timothy J. Baek f8cf43c0f7 refac: console logs removed 2024-02-25 16:13:34 -08:00
Timothy Jaeryang Baek 334d5246ba Merge pull request #916 from open-webui/dev
fix: default model load issue
2024-02-25 19:09:27 -05:00
Timothy J. Baek 42251c232a fix: default model load issue 2024-02-25 16:09:05 -08:00
Timothy Jaeryang Baek 6b0eae9fad Merge pull request #915 from open-webui/dev
fix: continue generation
2024-02-25 18:48:40 -05:00
Timothy J. Baek 1ff0c9a95d fix: continue generation 2024-02-25 15:46:12 -08:00
Steven Kreitzer 0a2ee691db fix: add semver to container builds
Signed-off-by: Steven Kreitzer <skre@skre.me>
2024-02-25 15:57:46 -06:00
Timothy J. Baek 6245146661 refac: messages 2024-02-25 13:55:04 -08:00
Timothy Jaeryang Baek 588daace91 Merge pull request #913 from open-webui/dev
refac: styling
2024-02-25 16:45:55 -05:00
Timothy J. Baek 8f4a12b479 refac: styling 2024-02-25 13:45:24 -08:00
Timothy Jaeryang Baek 81a50c5104 Merge pull request #912 from open-webui/dev
refac: styling
2024-02-25 16:27:46 -05:00
Timothy J. Baek d1656d5da8 refac: styling 2024-02-25 13:27:19 -08:00
Timothy Jaeryang Baek 3c10c3b928 Merge pull request #910 from open-webui/dev
0.1.104
2024-02-25 16:05:21 -05:00
Timothy J. Baek 1d937ec2f2 fix: cleanup unused tags 2024-02-25 13:03:26 -08:00
Timothy J. Baek 7c1b8e396a Update CHANGELOG.md 2024-02-25 12:51:08 -08:00
Timothy J. Baek ce891dfd5a fix: styling 2024-02-25 12:45:23 -08:00
Timothy J. Baek c5009907ff Update CHANGELOG.md 2024-02-25 12:37:23 -08:00
Timothy J. Baek 6780b1b19e fix: styling 2024-02-25 12:36:33 -08:00
Timothy J. Baek 8dce6b4b49 feat: personalised greeting 2024-02-25 12:34:32 -08:00
Timothy J. Baek 0d9e38a0ce refac: styling 2024-02-25 12:31:31 -08:00
Timothy Jaeryang Baek 129bb21a0a Merge pull request #911 from open-webui/check-updates
fix: check version loading progress
2024-02-25 15:11:06 -05:00
Timothy J. Baek fb3804ac95 fix: check version loading progress 2024-02-25 12:10:14 -08:00
Timothy Jaeryang Baek 13db1d5063 Merge pull request #909 from open-webui/check-updates
Check updates
2024-02-25 14:55:56 -05:00
Timothy J. Baek 759883a4c8 feat: check for updates 2024-02-25 11:55:15 -08:00
Timothy J. Baek 6bfe2a6306 feat: latest release check api 2024-02-25 11:26:58 -08:00
Timothy Jaeryang Baek b82ac66c70 Merge pull request #908 from open-webui/main
dev
2024-02-25 14:12:48 -05:00
Patrice-Gaudicheau 0f6bdaa602 feat: implemented user confirmation for safe removal of containers and volumes in Makefile 2024-02-24 09:02:18 +01:00
Patrice-Gaudicheau a016171573 fix: update Makefile and rename script for open-webui integration 2024-02-24 07:51:27 +01:00
Patrice Gaudicheau 7d0d504390 Updated code comments to English. 2024-02-23 15:53:47 +01:00
Patrice Gaudicheau 6bc627bbfb Updated code comments to English. 2024-02-23 15:52:55 +01:00
Patrice-Gaudicheau 5adff7767f Adding Makefile and LLM update script 2024-02-22 22:00:59 +01:00
75 changed files with 2183 additions and 669 deletions
+1
View File
@@ -52,6 +52,7 @@ jobs:
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
+71
View File
@@ -5,6 +5,77 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.108] - 2024-03-02
### Added
- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter.
- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
### Fixed
- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
- Corrected numbered list display issue in Safari (#963).
- Restricted user ability to delete chats without proper permissions (#993).
### Changed
- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added.
- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
## [0.1.107] - 2024-03-01
### Added
- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
### Fixed
- Corrected issue where links in the settings modal didn't appear clickable (#960).
- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
### Changed
- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
- Enhanced accessibility with the addition of dark mode on the authentication page.
## [0.1.106] - 2024-02-27
### Added
- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
### Fixed
- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930)
## [0.1.105] - 2024-02-25
### Added
- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
### Changed
- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization.
## [0.1.104] - 2024-02-25
### Added
- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
### Changed
- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
## [0.1.103] - 2024-02-25
### Added
+25
View File
@@ -0,0 +1,25 @@
install:
@docker-compose up -d
remove:
@chmod +x confirm_remove.sh
@./confirm_remove.sh
start:
@docker-compose start
stop:
@docker-compose stop
update:
# Calls the LLM update script
chmod +x update_ollama_models.sh
@./update_ollama_models.sh
@git pull
@docker-compose down
# Make sure the ollama-webui container is stopped before rebuilding
@docker stop open-webui || true
@docker-compose up --build -d
@docker-compose start
+10
View File
@@ -160,6 +160,16 @@ This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LI
If you have any questions, suggestions, or need assistance, please open an issue or join our
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
## Star History
<a href="https://star-history.com/#open-webui/open-webui&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
</picture>
</a>
---
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open Web UI even more amazing together! 💪
+15 -8
View File
@@ -11,7 +11,7 @@ from pydantic import BaseModel
from apps.web.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import decode_token, get_current_user, get_admin_user
from config import OLLAMA_API_BASE_URL, WEBUI_AUTH
from config import OLLAMA_BASE_URL, WEBUI_AUTH
app = FastAPI()
app.add_middleware(
@@ -22,7 +22,7 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL
app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL
# TARGET_SERVER_URL = OLLAMA_API_BASE_URL
@@ -32,7 +32,7 @@ REQUEST_POOL = []
@app.get("/url")
async def get_ollama_api_url(user=Depends(get_admin_user)):
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
return {"OLLAMA_BASE_URL": app.state.OLLAMA_BASE_URL}
class UrlUpdateForm(BaseModel):
@@ -41,8 +41,8 @@ class UrlUpdateForm(BaseModel):
@app.post("/url/update")
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
app.state.OLLAMA_API_BASE_URL = form_data.url
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
app.state.OLLAMA_BASE_URL = form_data.url
return {"OLLAMA_BASE_URL": app.state.OLLAMA_BASE_URL}
@app.get("/cancel/{request_id}")
@@ -57,7 +57,7 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user))
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_current_user)):
target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}"
target_url = f"{app.state.OLLAMA_BASE_URL}/{path}"
body = await request.body()
headers = dict(request.headers)
@@ -91,7 +91,13 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)):
def stream_content():
try:
if path in ["chat"]:
if path == "generate":
data = json.loads(body.decode("utf-8"))
if not ("stream" in data and data["stream"] == False):
yield json.dumps({"id": request_id, "done": False}) + "\n"
elif path == "chat":
yield json.dumps({"id": request_id, "done": False}) + "\n"
for chunk in r.iter_content(chunk_size=8192):
@@ -103,7 +109,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)):
finally:
if hasattr(r, "close"):
r.close()
REQUEST_POOL.remove(request_id)
if request_id in REQUEST_POOL:
REQUEST_POOL.remove(request_id)
r = requests.request(
method=request.method,
+8 -1
View File
@@ -146,6 +146,13 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
body["max_tokens"] = 4000
print("Modified body_dict:", body)
# Fix for ChatGPT calls failing because the num_ctx key is in body
if "num_ctx" in body:
# If 'num_ctx' is in the dictionary, delete it
# Leaving it there generates an error with the
# OpenAI API (Feb 2024)
del body["num_ctx"]
# Convert the modified body back to JSON
body = json.dumps(body)
except json.JSONDecodeError as e:
@@ -184,7 +191,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
response_data = r.json()
if "openai" in app.state.OPENAI_API_BASE_URL and path == "models":
if "api.openai.com" in app.state.OPENAI_API_BASE_URL and path == "models":
response_data["data"] = list(
filter(lambda model: "gpt" in model["id"], response_data["data"])
)
+34 -14
View File
@@ -79,6 +79,8 @@ app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.TOP_K = 4
app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=app.state.RAG_EMBEDDING_MODEL,
@@ -210,23 +212,33 @@ async def get_rag_template(user=Depends(get_current_user)):
}
class RAGTemplateForm(BaseModel):
template: str
@app.get("/query/settings")
async def get_query_settings(user=Depends(get_admin_user)):
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
}
@app.post("/template/update")
async def update_rag_template(form_data: RAGTemplateForm, user=Depends(get_admin_user)):
# TODO: check template requirements
app.state.RAG_TEMPLATE = (
form_data.template if form_data.template != "" else RAG_TEMPLATE
)
class QuerySettingsForm(BaseModel):
k: Optional[int] = None
template: Optional[str] = None
@app.post("/query/settings/update")
async def update_query_settings(
form_data: QuerySettingsForm, user=Depends(get_admin_user)
):
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
app.state.TOP_K = form_data.k if form_data.k else 4
return {"status": True, "template": app.state.RAG_TEMPLATE}
class QueryDocForm(BaseModel):
collection_name: str
query: str
k: Optional[int] = 4
k: Optional[int] = None
@app.post("/query/doc")
@@ -240,7 +252,10 @@ def query_doc(
name=form_data.collection_name,
embedding_function=app.state.sentence_transformer_ef,
)
result = collection.query(query_texts=[form_data.query], n_results=form_data.k)
result = collection.query(
query_texts=[form_data.query],
n_results=form_data.k if form_data.k else app.state.TOP_K,
)
return result
except Exception as e:
print(e)
@@ -253,7 +268,7 @@ def query_doc(
class QueryCollectionsForm(BaseModel):
collection_names: List[str]
query: str
k: Optional[int] = 4
k: Optional[int] = None
def merge_and_sort_query_results(query_results, k):
@@ -317,13 +332,16 @@ def query_collection(
)
result = collection.query(
query_texts=[form_data.query], n_results=form_data.k
query_texts=[form_data.query],
n_results=form_data.k if form_data.k else app.state.TOP_K,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, form_data.k)
return merge_and_sort_query_results(
results, form_data.k if form_data.k else app.state.TOP_K
)
@app.post("/web")
@@ -423,7 +441,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0):
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path)
else:
loader = TextLoader(file_path)
+11 -1
View File
@@ -1,6 +1,16 @@
from peewee import *
from config import DATA_DIR
import os
DB = SqliteDatabase(f"{DATA_DIR}/ollama.db")
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
print("File renamed successfully.")
else:
pass
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
DB.connect()
+21
View File
@@ -167,6 +167,27 @@ class TagTable:
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
print(res)
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
)
query.execute() # Remove the rows, return number of rows removed.
return True
except Exception as e:
print("delete_tag", e)
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id(
self, tag_name: str, chat_id: str, user_id: str
) -> bool:
+16 -3
View File
@@ -115,9 +115,12 @@ async def get_user_chats_by_tag_name(
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
]
print(chat_ids)
chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
return Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
return chats
############################
@@ -268,6 +271,16 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
@router.delete("/", response_model=bool)
async def delete_all_user_chats(user=Depends(get_current_user)):
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
if (
user.role == "user"
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Chats.delete_chats_by_user_id(user.id)
return result
+14 -1
View File
@@ -1,6 +1,7 @@
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
from fastapi import Depends, HTTPException, status
from starlette.responses import StreamingResponse
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
@@ -9,6 +10,8 @@ import os
import aiohttp
import json
from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR
@@ -172,3 +175,13 @@ async def get_gravatar(
email: str,
):
return get_gravatar_url(email)
@router.get("/db/download")
async def download_db(user=Depends(get_admin_user)):
return FileResponse(
f"{DATA_DIR}/webui.db",
media_type="application/octet-stream",
filename="webui.db",
)
+12 -1
View File
@@ -211,6 +211,17 @@ if ENV == "prod":
if OLLAMA_API_BASE_URL == "/ollama/api":
OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api"
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
if OLLAMA_BASE_URL == "":
OLLAMA_BASE_URL = (
OLLAMA_API_BASE_URL[:-4]
if OLLAMA_API_BASE_URL.endswith("/api")
else OLLAMA_API_BASE_URL
)
####################################
# OPENAI_API
####################################
@@ -226,7 +237,7 @@ if OPENAI_API_BASE_URL == "":
# WEBUI
####################################
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True)
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
+1
View File
@@ -47,3 +47,4 @@ class ERROR_MESSAGES(str, Enum):
INCORRECT_FORMAT = (
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
)
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
+21 -1
View File
@@ -4,8 +4,9 @@ import markdown
import time
import os
import sys
import requests
from fastapi import FastAPI, Request, Depends
from fastapi import FastAPI, Request, Depends, status
from fastapi.staticfiles import StaticFiles
from fastapi import HTTPException
from fastapi.responses import JSONResponse
@@ -26,6 +27,8 @@ from apps.web.main import app as webui_app
from config import WEBUI_NAME, ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR
from constants import ERROR_MESSAGES
from utils.utils import get_http_authorization_cred, get_current_user
@@ -127,6 +130,23 @@ async def get_app_changelog():
return CHANGELOG
@app.get("/api/version/updates")
async def get_app_latest_release_version():
try:
response = requests.get(
f"https://api.github.com/repos/open-webui/open-webui/releases/latest"
)
response.raise_for_status()
latest_version = response.json()["tag_name"]
return {"current": VERSION, "latest": latest_version[1:]}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
)
app.mount("/static", StaticFiles(directory="static"), name="static")
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
read ans
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
docker-compose down -v
else
echo "Operation cancelled."
fi
+12 -34
View File
@@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "v1.0.0-alpha.101",
"version": "0.1.106",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "v1.0.0-alpha.101",
"version": "0.1.106",
"dependencies": {
"@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5",
@@ -17,7 +17,7 @@
"js-sha256": "^0.10.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"svelte-french-toast": "^1.2.0",
"svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1"
},
@@ -3211,17 +3211,6 @@
}
}
},
"node_modules/svelte-french-toast": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz",
"integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==",
"dependencies": {
"svelte-writable-derived": "^3.1.0"
},
"peerDependencies": {
"svelte": "^3.57.0 || ^4.0.0"
}
},
"node_modules/svelte-hmr": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
@@ -3307,15 +3296,12 @@
"node": ">=12"
}
},
"node_modules/svelte-writable-derived": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
"integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==",
"funding": {
"url": "https://ko-fi.com/pixievoltno1"
},
"node_modules/svelte-sonner": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
"peerDependencies": {
"svelte": "^3.2.1 || ^4.0.0-next.1"
"svelte": ">=3 <5"
}
},
"node_modules/tailwindcss": {
@@ -5882,14 +5868,6 @@
"postcss-scss": "^4.0.8"
}
},
"svelte-french-toast": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz",
"integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==",
"requires": {
"svelte-writable-derived": "^3.1.0"
}
},
"svelte-hmr": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
@@ -5920,10 +5898,10 @@
}
}
},
"svelte-writable-derived": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
"integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==",
"svelte-sonner": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
"requires": {}
},
"tailwindcss": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.1.103",
"version": "0.1.108",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -49,7 +49,7 @@
"js-sha256": "^0.10.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"svelte-french-toast": "^1.2.0",
"svelte-sonner": "^0.3.19",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1"
}
+1 -1
View File
@@ -182,7 +182,7 @@ else
export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
fi
if [[ -n $webui_port ]]; then
export OLLAMA_WEBUI_PORT=$webui_port # Set OLLAMA_WEBUI_PORT environment variable
export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
fi
DEFAULT_COMPOSE_COMMAND+=" up -d"
DEFAULT_COMPOSE_COMMAND+=" --remove-orphans"
+15
View File
@@ -28,6 +28,21 @@ math {
@apply rounded-lg;
}
ol > li {
counter-increment: list-number;
display: block;
margin-bottom: 0;
margin-top: 0;
min-height: 28px;
}
.prose ol > li::before {
content: counters(list-number, '.') '.';
padding-right: 0.5rem;
color: var(--tw-prose-counters);
font-weight: 400;
}
::-webkit-scrollbar-thumb {
--tw-border-opacity: 1;
background-color: rgba(217, 217, 227, 0.8);
+1 -1
View File
@@ -439,7 +439,7 @@ export const deleteAllChats = async (token: string) => {
return json;
})
.catch((err) => {
error = err;
error = err.detail;
console.log(err);
return null;
+4 -2
View File
@@ -5,7 +5,8 @@ export const createNewDoc = async (
collection_name: string,
filename: string,
name: string,
title: string
title: string,
content: object | null = null
) => {
let error = null;
@@ -20,7 +21,8 @@ export const createNewDoc = async (
collection_name: collection_name,
filename: filename,
name: name,
title: title
title: title,
...(content ? { content: JSON.stringify(content) } : {})
})
})
.then(async (res) => {
+34
View File
@@ -19,6 +19,10 @@ export const getBackendConfig = async () => {
return null;
});
if (error) {
throw error;
}
return res;
};
@@ -41,5 +45,35 @@ export const getChangelog = async () => {
return null;
});
if (error) {
throw error;
}
return res;
};
export const getVersionUpdates = async () => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
+36 -10
View File
@@ -29,7 +29,7 @@ export const getOllamaAPIUrl = async (token: string = '') => {
throw error;
}
return res.OLLAMA_API_BASE_URL;
return res.OLLAMA_BASE_URL;
};
export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
@@ -64,13 +64,13 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
throw error;
}
return res.OLLAMA_API_BASE_URL;
return res.OLLAMA_BASE_URL;
};
export const getOllamaVersion = async (token: string = '') => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -102,7 +102,7 @@ export const getOllamaVersion = async (token: string = '') => {
export const getOllamaModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -148,7 +148,7 @@ export const generateTitle = async (
console.log(template);
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
@@ -186,7 +186,7 @@ export const generatePrompt = async (token: string = '', model: string, conversa
conversation = '[no existing conversation]';
}
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
@@ -217,11 +217,37 @@ export const generatePrompt = async (token: string = '', model: string, conversa
return res;
};
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: text,
stream: true
})
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const generateChatCompletion = async (token: string = '', body: object) => {
let controller = new AbortController();
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, {
signal: controller.signal,
method: 'POST',
headers: {
@@ -265,7 +291,7 @@ export const cancelChatCompletion = async (token: string = '', requestId: string
export const createModel = async (token: string, tagName: string, content: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
@@ -290,7 +316,7 @@ export const createModel = async (token: string, tagName: string, content: strin
export const deleteModel = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/delete`, {
method: 'DELETE',
headers: {
'Content-Type': 'text/event-stream',
@@ -324,7 +350,7 @@ export const deleteModel = async (token: string, tagName: string) => {
export const pullModel = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
-1
View File
@@ -150,7 +150,6 @@ export const getOpenAIModels = async (token: string = '') => {
return res.json();
})
.catch((err) => {
console.log(err);
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
+36 -4
View File
@@ -85,17 +85,49 @@ export const getRAGTemplate = async (token: string) => {
return res?.template ?? '';
};
export const updateRAGTemplate = async (token: string, template: string) => {
export const getQuerySettings = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/template/update`, {
const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, {
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;
};
type QuerySettings = {
k: number | null;
template: string | null;
};
export const updateQuerySettings = async (token: string, settings: QuerySettings) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
template: template
...settings
})
})
.then(async (res) => {
@@ -183,7 +215,7 @@ export const queryDoc = async (
token: string,
collection_name: string,
query: string,
k: number
k: number | null = null
) => {
let error = null;
+32
View File
@@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => {
return res;
};
export const downloadDatabase = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.blob();
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'webui.db';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
};
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
@@ -0,0 +1,63 @@
<script lang="ts">
import { downloadDatabase } from '$lib/apis/utils';
import { onMount } from 'svelte';
export let saveHandler: Function;
onMount(async () => {
// permissions = await getUserPermissions(localStorage.token);
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div class=" mb-2 text-sm font-medium">Database</div>
<div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> -->
<button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
type="button"
on:click={() => {
// exportAllUserChats();
downloadDatabase(localStorage.token);
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">Download Database</div>
</button>
</div>
</div>
</div>
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div> -->
</form>
@@ -1,5 +1,6 @@
<script>
import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte';
@@ -86,6 +87,34 @@
</div>
<div class=" self-center">Users</div>
</button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'db'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'db';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
<path
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
/>
<path
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
/>
</svg>
</div>
<div class=" self-center">Database</div>
</button>
</div>
<div class="flex-1 md:min-h-[380px]">
{#if selectedTab === 'general'}
@@ -100,6 +129,12 @@
show = false;
}}
/>
{:else if selectedTab === 'db'}
<Database
saveHandler={() => {
show = false;
}}
/>
{/if}
</div>
</div>
+117 -103
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte';
import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
@@ -12,6 +12,7 @@
import Documents from './MessageInput/Documents.svelte';
import Models from './MessageInput/Models.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte';
export let submitPrompt: Function;
export let stopResponse: Function;
@@ -293,6 +294,9 @@
};
onMount(() => {
const chatInput = document.getElementById('chat-textarea');
window.setTimeout(() => chatInput?.focus(), 0);
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
@@ -490,7 +494,7 @@
}}
/>
<form
class=" flex flex-col relative w-full rounded-xl border dark:border-gray-700 bg-white dark:bg-gray-900 dark:text-gray-100"
class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100"
on:submit|preventDefault={() => {
submitPrompt(prompt, user);
}}
@@ -633,33 +637,33 @@
<div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1.5">
<button
class=" text-gray-600 dark:text-gray-200 transition rounded-lg p-1 ml-1"
type="button"
on:click={() => {
filesInputElement.click();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
<div class=" self-end mb-2 ml-1">
<Tooltip content="Upload files">
<button
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button"
on:click={() => {
filesInputElement.click();
}}
>
<path
fill-rule="evenodd"
d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
clip-rule="evenodd"
/>
</svg>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-[1.2rem] h-[1.2rem]"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
<textarea
id="chat-textarea"
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? ''
: ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== ''
@@ -803,92 +807,102 @@
}}
/>
<div class="self-end mb-2 flex space-x-0.5 mr-2">
<div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true}
{#if speechRecognitionEnabled}
<Tooltip content="Record voice">
{#if speechRecognitionEnabled}
<button
id="voice-input-button"
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
type="button"
on:click={() => {
speechRecognitionHandler();
}}
>
{#if isRecording}
<svg
class=" w-5 h-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle
class="spinner_qM83 spinner_ZTLf"
cx="20"
cy="12"
r="2.5"
/></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 translate-y-[0.5px]"
>
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
<path
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
/>
</svg>
{/if}
</button>
{/if}
</Tooltip>
<Tooltip content="Send message">
<button
id="voice-input-button"
class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1.5 mr-0.5 self-center"
type="button"
on:click={() => {
speechRecognitionHandler();
}}
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
>
{#if isRecording}
<svg
class=" w-5 h-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 translate-y-[0.5px]"
>
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
<path
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
/>
</svg>
{/if}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</button>
{/if}
<button
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-lg p-1 mr-0.5 w-7 h-7 self-center"
type="submit"
disabled={prompt === ''}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4.5 h-4.5 mx-auto"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-lg p-1.5"
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={stopResponse}
>
<svg
@@ -4,7 +4,7 @@
import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let prompt = '';
@@ -89,16 +89,16 @@
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">#</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredItems as doc, docIdx}
<button
class=" px-3 py-1.5 rounded-lg w-full text-left {docIdx === selectedIdx
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
@@ -132,7 +132,7 @@
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<button
class="px-3 py-1.5 rounded-lg w-full text-left bg-gray-100 selected-command-option-button"
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
on:click={() => {
const url = prompt.split(' ')?.at(0)?.substring(1);
@@ -3,7 +3,7 @@
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { tick } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let prompt = '';
export let user = null;
@@ -79,14 +79,18 @@
throw data;
}
if (data.done == false) {
if (prompt == '' && data.response == '\n') {
continue;
} else {
prompt += data.response;
console.log(data.response);
chatInputElement.scrollTop = chatInputElement.scrollHeight;
await tick();
if ('id' in data) {
console.log(data);
} else {
if (data.done == false) {
if (prompt == '' && data.response == '\n') {
continue;
} else {
prompt += data.response;
console.log(data.response);
chatInputElement.scrollTop = chatInputElement.scrollHeight;
await tick();
}
}
}
}
@@ -121,16 +125,16 @@
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-lg w-full text-left {modelIdx === selectedIdx
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
@@ -2,7 +2,7 @@
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let prompt = '';
let selectedCommandIdx = 0;
@@ -60,16 +60,16 @@
{#if filteredPromptCommands.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">/</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredPromptCommands as command, commandIdx}
<button
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
@@ -93,7 +93,7 @@
</div>
<div
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1"
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-xl flex items-center space-x-1"
>
<div>
<svg
@@ -10,45 +10,47 @@
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
</script>
<div class=" flex flex-wrap-reverse mb-3 md:p-1 text-left w-full">
{#each prompts as prompt, promptIdx}
<div
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-2"
>
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
<div class=" mb-3 md:p-1 text-left w-full">
<div class=" flex flex-wrap-reverse px-2 text-left">
{#each prompts as prompt, promptIdx}
<div
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
>
<div class="flex flex-col text-left self-center">
{#if prompt.title && prompt.title[0] !== ''}
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-white group-hover:bg-gray-100 group-hover:text-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-100 dark:text-gray-900 transition"
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
<div class="flex flex-col text-left self-center">
{#if prompt.title && prompt.title[0] !== ''}
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
{/each}
<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="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
{/each}
</div>
</div>
+1 -47
View File
@@ -4,7 +4,7 @@
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
import UserMessage from './Messages/UserMessage.svelte';
@@ -281,52 +281,6 @@
{showNextMessage}
{copyToClipboard}
/>
{#if messages.length - 1 === messageIdx && processing !== ''}
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
<div class=" dark:text-blue-100">
<svg
class=" w-4 h-4 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
</div>
<div class=" text-sm font-medium">
{processing}
</div>
</div>
{/if}
{:else}
<ResponseMessage
{message}
@@ -1,5 +1,6 @@
<script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores';
import { onMount } from 'svelte';
export let models = [];
@@ -47,7 +48,7 @@
{/each}
</div>
</div>
<div class=" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
{#if modelfile}
<span class=" capitalize">
{modelfile.title}
@@ -63,7 +64,9 @@
</div>
{/if}
{:else}
How can I help you today?
<div class=" line-clamp-1">Hello, {$user.name}</div>
<div>How can I help you today?</div>
{/if}
</div>
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { marked } from 'marked';
import tippy from 'tippy.js';
@@ -334,7 +334,7 @@
{/if}
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
<div>
{#if edit === true}
+1 -1
View File
@@ -2,7 +2,7 @@
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let selectedModels = [''];
export let disabled = false;
+48 -2
View File
@@ -1,14 +1,40 @@
<script lang="ts">
import { getVersionUpdates } from '$lib/apis';
import { getOllamaVersion } from '$lib/apis/ollama';
import { WEBUI_VERSION } from '$lib/constants';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte';
let ollamaVersion = '';
let updateAvailable = null;
let version = {
current: '',
latest: ''
};
const checkForVersionUpdates = async () => {
updateAvailable = null;
version = await getVersionUpdates(localStorage.token).catch((error) => {
return {
current: WEBUI_VERSION,
latest: WEBUI_VERSION
};
});
console.log(version);
updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable);
};
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
return '';
});
checkForVersionUpdates();
});
</script>
@@ -20,10 +46,21 @@
{$WEBUI_NAME} Version
</div>
</div>
<div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200 flex space-x-1.5 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>
v{WEBUI_VERSION}
<a
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
target="_blank"
>
{updateAvailable === null
? 'Checking for updates...'
: updateAvailable
? `(v${version.latest} available!)`
: '(latest)'}
</a>
</div>
<button
@@ -35,6 +72,15 @@
<div>See what's new</div>
</button>
</div>
<button
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
on:click={() => {
checkForVersionUpdates();
}}
>
Check for updates
</button>
</div>
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { onMount } from 'svelte';
import { user } from '$lib/stores';
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { updateUserPassword } from '$lib/apis/auths';
let show = false;
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
export let saveSettings: Function;
@@ -15,7 +15,7 @@
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let saveSettings: Function;
// Chats
@@ -75,7 +75,9 @@
const deleteChats = async () => {
await goto('/');
await deleteAllChats(localStorage.token);
await deleteAllChats(localStorage.token).catch((error) => {
toast.error(error);
});
await chats.set(await getChatList(localStorage.token));
};
@@ -5,7 +5,7 @@
import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama';
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let getModels: Function;
@@ -114,12 +114,12 @@
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter URL (e.g. http://localhost:11434/api)"
placeholder="Enter URL (e.g. http://localhost:11434)"
bind:value={API_BASE_URL}
/>
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
import { config, user } from '$lib/stores';
@@ -3,7 +3,7 @@
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, user } from '$lib/stores';
import { createEventDispatcher, onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
export let saveSettings: Function;
@@ -191,10 +191,12 @@
placeholder="Select a model"
>
<option value="" selected>Current Model</option>
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{#each $models as model}
{#if model.size != null}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
</option>
{/if}
{/each}
</select>
</div>
@@ -1,6 +1,6 @@
<script lang="ts">
import queue from 'async/queue';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { createModel, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
@@ -432,7 +432,7 @@
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://ollama.com/library"
target="_blank">click here.</a
>
@@ -580,7 +580,7 @@
type="url"
required
bind:value={modelFileUrl}
placeholder="Type HuggingFace Resolve (Download) URL"
placeholder="Type Hugging Face Resolve (Download) URL"
/>
</div>
{/if}
@@ -651,7 +651,7 @@
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
@@ -790,7 +790,7 @@
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add?
<a
class=" text-gray-300 font-medium"
class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
@@ -913,7 +913,7 @@
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add?
<a
class=" text-gray-300 font-medium"
class=" text-gray-300 font-medium underline"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores';
import { getOllamaModels } from '$lib/apis/ollama';
+70
View File
@@ -0,0 +1,70 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let state = 'unchecked';
export let indeterminate = false;
let _state = 'unchecked';
$: _state = state;
</script>
<button
class=" outline -outline-offset-1 outline-[1.5px] outline-gray-200 dark:outline-gray-600 {state !==
'unchecked'
? 'bg-black outline-black '
: 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded inline-block w-3.5 h-3.5 relative"
on:click={() => {
if (_state === 'unchecked') {
_state = 'checked';
dispatch('change', _state);
} else if (_state === 'checked') {
_state = 'unchecked';
if (!indeterminate) {
dispatch('change', _state);
}
} else if (indeterminate) {
_state = 'checked';
dispatch('change', _state);
}
}}
>
<div class="top-0 left-0 absolute w-full flex justify-center">
{#if _state === 'checked'}
<svg
class="w-3.5 h-3.5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="m5 12 4.7 4.5 9.3-9"
/>
</svg>
{:else if indeterminate}
<svg
class="w-3 h-3.5 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M5 12h14"
/>
</svg>
{/if}
</div>
<!-- {checked} -->
</button>
+1 -1
View File
@@ -41,7 +41,7 @@
}}
>
<div
class=" modal-content m-auto rounded-xl max-w-full {sizeToWidth(
class=" modal-content m-auto rounded-2xl max-w-full {sizeToWidth(
size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
in:fade={{ duration: 10 }}
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import tippy from 'tippy.js';
export let placement = 'top';
export let content = `I'm a tooltip!`;
export let touch = true;
let tooltipElement;
let tooltipInstance;
$: if (tooltipElement && content) {
if (tooltipInstance) {
tooltipInstance.setContent(content);
} else {
tooltipInstance = tippy(tooltipElement, {
content: content,
placement: placement,
allowHTML: true,
touch: touch
});
}
}
onDestroy(() => {
if (tooltipInstance) {
tooltipInstance.destroy();
}
});
</script>
<div bind:this={tooltipElement}>
<slot />
</div>
@@ -0,0 +1,188 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount } from 'svelte';
import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents';
import Modal from '../common/Modal.svelte';
import { documents } from '$lib/stores';
import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte';
import { addTagById } from '$lib/apis/chats';
import { uploadDocToVectorDB } from '$lib/apis/rag';
import { transformFileName } from '$lib/utils';
import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants';
export let show = false;
export let selectedDoc;
let inputFiles;
let tags = [];
let doc = {
name: '',
title: '',
content: null
};
const uploadDoc = async (file) => {
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
await createNewDoc(
localStorage.token,
res.collection_name,
res.filename,
transformFileName(res.filename),
res.filename,
tags.length > 0
? {
tags: tags
}
: null
).catch((error) => {
toast.error(error);
return null;
});
await documents.set(await getDocs(localStorage.token));
}
};
const submitHandler = async () => {
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file);
}
}
inputFiles = null;
document.getElementById('upload-doc-input').value = '';
} else {
toast.error(`File not found.`);
}
show = false;
documents.set(await getDocs(localStorage.token));
};
const addTagHandler = async (tagName) => {
if (!tags.find((tag) => tag.name === tagName) && tagName !== '') {
tags = [...tags, { name: tagName }];
} else {
console.log('tag already exists');
}
};
const deleteTagHandler = async (tagName) => {
tags = tags.filter((tag) => tag.name !== tagName);
};
onMount(() => {});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Add Docs</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="mb-3 w-full">
<input id="upload-doc-input" hidden bind:files={inputFiles} type="file" multiple />
<button
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
type="button"
on:click={() => {
document.getElementById('upload-doc-input')?.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
Click here to select documents.
{/if}
</button>
</div>
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">Tags</div>
<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} />
</div>
</div>
<div class="flex justify-end pt-5 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount } from 'svelte';
@@ -2,14 +2,14 @@
import { getDocs } from '$lib/apis/documents';
import {
getChunkParams,
getRAGTemplate,
getQuerySettings,
scanDocs,
updateChunkParams,
updateRAGTemplate
updateQuerySettings
} from '$lib/apis/rag';
import { documents } from '$lib/stores';
import { onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
export let saveHandler: Function;
@@ -18,7 +18,10 @@
let chunkSize = 0;
let chunkOverlap = 0;
let template = '';
let querySettings = {
template: '',
k: 4
};
const scanHandler = async () => {
loading = true;
@@ -33,7 +36,7 @@
const submitHandler = async () => {
const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap);
await updateRAGTemplate(localStorage.token, template);
querySettings = await updateQuerySettings(localStorage.token, querySettings);
};
onMount(async () => {
@@ -44,7 +47,7 @@
chunkOverlap = res.chunk_overlap;
}
template = await getRAGTemplate(localStorage.token);
querySettings = await getQuerySettings(localStorage.token);
});
</script>
@@ -156,10 +159,44 @@
</div>
</div>
<div class=" text-sm font-medium">Query Params</div>
<div class=" flex">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium flex-1">Top K</div>
<div class="self-center p-3">
<input
class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number"
placeholder="Enter Top K"
bind:value={querySettings.k}
autocomplete="off"
min="0"
/>
</div>
</div>
<!-- <div class="flex w-full">
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
<div class="self-center p-3">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="number"
placeholder="Enter Chunk Overlap"
bind:value={chunkOverlap}
autocomplete="off"
min="0"
/>
</div>
</div> -->
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">RAG Template</div>
<textarea
bind:value={template}
bind:value={querySettings.template}
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
rows="4"
/>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
+118 -102
View File
@@ -13,11 +13,13 @@
getChatList,
getChatById,
getChatListByTagName,
updateChatById
updateChatById,
getAllChatTags
} from '$lib/apis/chats';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte';
let show = false;
let navElement;
@@ -30,9 +32,10 @@
let chatTitle = '';
let showDropdown = false;
let isEditing = false;
onMount(async () => {
if (window.innerWidth > 1280) {
if (window.innerWidth > 1024) {
show = true;
}
await chats.set(await getChatList(localStorage.token));
@@ -99,18 +102,17 @@
? ''
: 'invisible'}"
>
<div class="px-2.5 flex justify-center space-x-2">
<button
<div class="px-2 flex justify-center space-x-2">
<a
id="sidebar-new-chat-button"
class="flex-grow flex justify-between rounded-md px-3 py-2 hover:bg-gray-900 transition"
class="flex-grow flex justify-between rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
href="/"
on:click={async () => {
goto('/');
await goto('/');
const newChatButton = document.getElementById('new-chat-button');
if (newChatButton) {
newChatButton.click();
}
setTimeout(() => {
newChatButton?.click();
}, 0);
}}
>
<div class="flex self-center">
@@ -140,13 +142,13 @@
/>
</svg>
</div>
</button>
</a>
</div>
{#if $user?.role === 'admin'}
<div class="px-2.5 flex justify-center mt-0.5">
<div class="px-2 flex justify-center mt-0.5">
<a
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
href="/modelfiles"
>
<div class="self-center">
@@ -172,9 +174,9 @@
</a>
</div>
<div class="px-2.5 flex justify-center">
<div class="px-2 flex justify-center">
<a
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
href="/prompts"
>
<div class="self-center">
@@ -200,9 +202,9 @@
</a>
</div>
<div class="px-2.5 flex justify-center mb-1">
<div class="px-2 flex justify-center mb-1">
<a
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
href="/documents"
>
<div class="self-center">
@@ -271,9 +273,9 @@
</div>
{/if}
<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l bg-gray-950">
<div class="self-center pl-3 py-2 rounded-l-xl bg-gray-950">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -289,7 +291,7 @@
</div>
<input
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
placeholder="Search"
bind:value={search}
on:focus={() => {
@@ -330,7 +332,12 @@
<button
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
on:click={async () => {
await chats.set(await getChatListByTagName(localStorage.token, tag.name));
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
await tags.set(await getAllChatTags(localStorage.token));
chatIds = await getChatList(localStorage.token);
}
await chats.set(chatIds);
}}
>
{tag.name}
@@ -339,7 +346,7 @@
</div>
{/if}
<div class="pl-2.5 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
{#each $chats.filter((chat) => {
if (search === '') {
return true;
@@ -360,27 +367,39 @@
}
}) as chat, i}
<div class=" w-full pr-2 relative">
<a
class=" w-full flex justify-between rounded-md px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
href="/c/{chat.id}"
>
<div class=" flex self-center flex-1 w-full">
<div
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[160px]'
: 'w-full'} "
>
{#if chatTitleEditId === chat.id}
<input bind:value={chatTitle} class=" bg-transparent w-full" />
{:else}
{chat.title}
{/if}
</div>
{#if chatTitleEditId === chat.id}
<div
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
>
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
</div>
</a>
{:else}
<a
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
href="/c/{chat.id}"
on:click={() => {
if (window.innerWidth < 1024) {
show = false;
}
}}
>
<div class=" flex self-center flex-1 w-full">
<div
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[160px]'
: 'w-full'} "
>
{chat.title}
</div>
</div>
</a>
{/if}
{#if chat.id === $chatId}
<div class=" absolute right-[22px] top-[10px]">
@@ -479,7 +498,6 @@
on:click={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
// editChatTitle(chat.id, 'a');
}}
>
<svg
@@ -528,12 +546,12 @@
</div>
<div class="px-2.5">
<hr class=" border-gray-900 mb-1 w-full" />
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
<div class="flex flex-col">
{#if $user !== undefined}
<button
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={() => {
showDropdown = !showDropdown;
}}
@@ -551,7 +569,7 @@
{#if showDropdown}
<div
id="dropdownDots"
class="absolute z-40 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-gray-900"
in:slide={{ duration: 150 }}
>
<div class="py-2 w-full">
@@ -581,6 +599,32 @@
</div>
<div class=" self-center font-medium">Admin Panel</div>
</button>
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
on:click={() => {
goto('/playground');
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<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="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</div>
<div class=" self-center font-medium">Playground</div>
</button>
{/if}
<button
@@ -650,36 +694,6 @@
</div>
</div>
{/if}
{:else}
<button
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
on:click={async () => {
await showSettings.set(true);
}}
>
<div class=" self-center mr-3">
<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="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>
</div>
<div class=" self-center font-medium">Settings</div>
</button>
{/if}
</div>
</div>
@@ -688,30 +702,32 @@
<div
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
<button
id="sidebar-toggle-button"
class=" group"
on:click={() => {
show = !show;
}}
><span class="" data-state="closed"
><div
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
>
<div class="flex h-6 w-6 flex-col items-center">
<div
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {show
? 'group-hover:rotate-[15deg]'
: 'group-hover:rotate-[-15deg]'}"
/>
<div
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {show
? 'group-hover:rotate-[-15deg]'
: 'group-hover:rotate-[15deg]'}"
/>
<Tooltip placement="right" content={`${show ? 'Close' : 'Open'} sidebar`} touch={false}>
<button
id="sidebar-toggle-button"
class=" group"
on:click={() => {
show = !show;
}}
><span class="" data-state="closed"
><div
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
>
<div class="flex h-6 w-6 flex-col items-center">
<div
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {show
? 'group-hover:rotate-[15deg]'
: 'group-hover:rotate-[-15deg]'}"
/>
<div
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {show
? 'group-hover:rotate-[-15deg]'
: 'group-hover:rotate-[15deg]'}"
/>
</div>
</div>
</div>
</span>
</button>
</span>
</button>
</Tooltip>
</div>
</div>
@@ -0,0 +1,105 @@
<script lang="ts">
import { onMount } from 'svelte';
export let messages = [];
onMount(() => {
messages.forEach((message, idx) => {
let textareaElement = document.getElementById(`${message.role}-${idx}-textarea`);
textareaElement.style.height = '';
textareaElement.style.height = textareaElement.scrollHeight + 'px';
});
});
</script>
<div class="py-3 space-y-3">
{#each messages as message, idx}
<div class="flex gap-2 group">
<div class="flex items-start pt-1">
<button
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition"
on:click={() => {
message.role = message.role === 'user' ? 'assistant' : 'user';
}}>{message.role}</button
>
</div>
<div class="flex-1">
<textarea
id="{message.role}-{idx}-textarea"
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here"
rows="1"
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px';
}}
on:focus={(e) => {
e.target.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px';
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}}
bind:value={message.content}
/>
</div>
<div class=" pt-1">
<button
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
on:click={() => {
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</button>
</div>
</div>
<hr class=" dark:border-gray-800" />
{/each}
<button
class="flex items-center gap-2 px-2 py-1"
on:click={() => {
console.log(messages.at(-1));
messages.push({
role: (messages.at(-1)?.role ?? 'assistant') === 'user' ? 'assistant' : 'user',
content: ''
});
messages = messages;
}}
>
<div>
<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="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
</div>
<div class=" text-sm font-medium">Add message</div>
</button>
</div>
+2 -3
View File
@@ -101,11 +101,10 @@ export const copyToClipboard = (text) => {
);
};
export const checkVersion = (required, current) => {
// Returns true when current version is below required
export const compareVersion = (latest, current) => {
return current === '0.0.0'
? false
: current.localeCompare(required, undefined, {
: current.localeCompare(latest, undefined, {
numeric: true,
sensitivity: 'case',
caseFirst: 'upper'
+7 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { openDB, deleteDB } from 'idb';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
@@ -28,7 +28,7 @@
config
} from '$lib/stores';
import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
import { checkVersion } from '$lib/utils';
import { compareVersion } from '$lib/utils';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
@@ -63,9 +63,6 @@
.filter((models) => models)
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
return models;
};
@@ -79,7 +76,7 @@
ollamaVersion = version;
console.log(ollamaVersion);
if (checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
if (compareVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
}
};
@@ -110,6 +107,10 @@
}
console.log();
await models.set(await getModels());
await tick();
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await modelfiles.set(await getModelfiles(localStorage.token));
+36 -33
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -14,6 +14,7 @@
chats,
chatId,
config,
WEBUI_NAME,
tags as _tags
} from '$lib/stores';
import { copyToClipboard, splitStream } from '$lib/utils';
@@ -38,7 +39,6 @@
import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
let stopResponseFlag = false;
let autoScroll = true;
let processing = '';
@@ -104,13 +104,8 @@
await cancelChatCompletion(localStorage.token, currentRequestId);
currentRequestId = null;
}
window.history.replaceState(history.state, '', `/`);
console.log('initNewChat');
await chatId.set('');
console.log($chatId);
autoScroll = true;
@@ -121,8 +116,6 @@
currentId: null
};
console.log($config);
if ($page.url.searchParams.get('models')) {
selectedModels = $page.url.searchParams.get('models')?.split(',');
} else if ($settings?.models) {
@@ -141,6 +134,9 @@
settings.set({
..._settings
});
const chatInput = document.getElementById('chat-textarea');
setTimeout(() => chatInput?.focus(), 0);
};
const scrollToBottom = () => {
@@ -252,19 +248,17 @@
let relevantContexts = await Promise.all(
docs.map(async (doc) => {
if (doc.type === 'collection') {
return await queryCollection(localStorage.token, doc.collection_names, query, 4).catch(
return await queryCollection(localStorage.token, doc.collection_names, query).catch(
(error) => {
console.log(error);
return null;
}
);
} else {
return await queryDoc(localStorage.token, doc.collection_name, query, 4).catch(
(error) => {
console.log(error);
return null;
}
);
return await queryDoc(localStorage.token, doc.collection_name, query).catch((error) => {
console.log(error);
return null;
});
}
})
);
@@ -732,25 +726,26 @@
responseMessage.done = false;
await tick();
const modelTag = $models.filter((m) => m.name === responseMessage.model).at(0);
const model = $models.filter((m) => m.id === responseMessage.model).at(0);
if (modelTag?.external) {
await sendPromptOpenAI(
responseMessage.model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else if (modelTag) {
await sendPromptOllama(
responseMessage.model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else {
toast.error(`Model ${model} not found`);
if (model) {
if (model?.external) {
await sendPromptOpenAI(
model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else
await sendPromptOllama(
model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
}
} else {
toast.error(`Model ${modelId} not found`);
}
};
@@ -812,6 +807,14 @@
};
</script>
<svelte:head>
<title>
{title
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
: `${$WEBUI_NAME}`}
</title>
</svelte:head>
<div class="h-screen max-h-[100dvh] w-full flex flex-col">
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} />
<div class="flex flex-col flex-auto">
+12 -8
View File
@@ -1,10 +1,10 @@
<script>
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { config, user } from '$lib/stores';
import { WEBUI_NAME, config, user } from '$lib/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
@@ -61,6 +61,12 @@
});
</script>
<svelte:head>
<title>
{`Admin Panel | ${$WEBUI_NAME}`}
</title>
</svelte:head>
{#key selectedUser}
<EditUserModal
bind:show={showEditUserModal}
@@ -74,13 +80,11 @@
<SettingsModal bind:show={showSettingsModal} />
<div
class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white bg-white dark:bg-gray-900 font-mona"
>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white font-mona">
{#if loaded}
<div class="overflow-y-auto w-full flex justify-center">
<div class="w-full max-w-3xl px-6 md:px-16 flex flex-col">
<div class="py-10 w-full">
<div class=" flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
<div class="w-full">
<div class=" flex flex-col justify-center">
<div class=" flex justify-between items-center">
<div class="flex items-center text-2xl font-semibold">
+119 -104
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
@@ -14,6 +14,7 @@
chats,
chatId,
config,
WEBUI_NAME,
tags as _tags
} from '$lib/stores';
import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
@@ -37,7 +38,7 @@
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag';
import { WEBUI_BASE_URL } from '$lib/constants';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
let loaded = false;
@@ -99,6 +100,10 @@
if (await loadChat()) {
await tick();
loaded = true;
window.setTimeout(() => scrollToBottom(), 0);
const chatInput = document.getElementById('chat-textarea');
chatInput?.focus();
} else {
await goto('/');
}
@@ -238,7 +243,6 @@
await sendPrompt(userPrompt, userMessageId);
}
};
const sendPrompt = async (prompt, parentId) => {
const _chatId = JSON.parse(JSON.stringify($chatId));
@@ -257,19 +261,17 @@
let relevantContexts = await Promise.all(
docs.map(async (doc) => {
if (doc.type === 'collection') {
return await queryCollection(localStorage.token, doc.collection_names, query, 4).catch(
return await queryCollection(localStorage.token, doc.collection_names, query).catch(
(error) => {
console.log(error);
return null;
}
);
} else {
return await queryDoc(localStorage.token, doc.collection_name, query, 4).catch(
(error) => {
console.log(error);
return null;
}
);
return await queryDoc(localStorage.token, doc.collection_name, query).catch((error) => {
console.log(error);
return null;
});
}
})
);
@@ -292,40 +294,41 @@
}
await Promise.all(
selectedModels.map(async (model) => {
console.log(model);
const modelTag = $models.filter((m) => m.name === model).at(0);
selectedModels.map(async (modelId) => {
const model = $models.filter((m) => m.id === modelId).at(0);
// Create response message
let responseMessageId = uuidv4();
let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant',
content: '',
model: model,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
if (model) {
// Create response message
let responseMessageId = uuidv4();
let responseMessage = {
parentId: parentId,
id: responseMessageId,
childrenIds: [],
role: 'assistant',
content: '',
model: model.id,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
// Add message to history and Set currentId to messageId
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
// Add message to history and Set currentId to messageId
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
// Append messageId to childrenIds of parent message
if (parentId !== null) {
history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds,
responseMessageId
];
}
if (modelTag?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (modelTag) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else {
toast.error(`Model ${model} not found`);
toast.error(`Model ${modelId} not found`);
}
})
);
@@ -334,6 +337,7 @@
};
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
model = model.id;
const responseMessage = history.messages[responseMessageId];
// Wait until history/message have been updated
@@ -543,57 +547,60 @@
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
const responseMessage = history.messages[responseMessageId];
scrollToBottom();
const res = await generateOpenAIChatCompletion(localStorage.token, {
model: model,
stream: true,
messages: [
$settings.system
? {
role: 'system',
content: $settings.system
}
: undefined,
...messages.filter((message) => !message.deleted)
]
.filter((message) => message)
.map((message, idx, arr) => ({
role: message.role,
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
const res = await generateOpenAIChatCompletion(
localStorage.token,
{
model: model.id,
stream: true,
messages: [
$settings.system
? {
content: [
{
type: 'text',
text:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
},
...message.files
.filter((file) => file.type === 'image')
.map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
]
role: 'system',
content: $settings.system
}
: {
content:
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
})
})),
seed: $settings?.options?.seed ?? undefined,
stop: $settings?.options?.stop ?? undefined,
temperature: $settings?.options?.temperature ?? undefined,
top_p: $settings?.options?.top_p ?? undefined,
num_ctx: $settings?.options?.num_ctx ?? undefined,
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
max_tokens: $settings?.options?.num_predict ?? undefined
});
: undefined,
...messages.filter((message) => !message.deleted)
]
.filter((message) => message)
.map((message, idx, arr) => ({
role: message.role,
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
? {
content: [
{
type: 'text',
text:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
},
...message.files
.filter((file) => file.type === 'image')
.map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
]
}
: {
content:
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
})
})),
seed: $settings?.options?.seed ?? undefined,
stop: $settings?.options?.stop ?? undefined,
temperature: $settings?.options?.temperature ?? undefined,
top_p: $settings?.options?.top_p ?? undefined,
num_ctx: $settings?.options?.num_ctx ?? undefined,
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
max_tokens: $settings?.options?.num_predict ?? undefined
},
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}`
);
if (res && res.ok) {
const reader = res.body
@@ -704,7 +711,6 @@
await setChatTitle(_chatId, userPrompt);
}
};
const stopResponse = () => {
stopResponseFlag = true;
console.log('stopResponse');
@@ -719,25 +725,26 @@
responseMessage.done = false;
await tick();
const modelTag = $models.filter((m) => m.name === responseMessage.model).at(0);
const model = $models.filter((m) => m.id === responseMessage.model).at(0);
if (modelTag?.external) {
await sendPromptOpenAI(
responseMessage.model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else if (modelTag) {
await sendPromptOllama(
responseMessage.model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else {
toast.error(`Model ${model} not found`);
if (model) {
if (model?.external) {
await sendPromptOpenAI(
model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
} else
await sendPromptOllama(
model,
history.messages[responseMessage.parentId].content,
responseMessage.id,
_chatId
);
}
} else {
toast.error(`Model ${modelId} not found`);
}
};
@@ -816,6 +823,14 @@
});
</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">
<Navbar
+118 -57
View File
@@ -1,19 +1,22 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount } from 'svelte';
import { documents } from '$lib/stores';
import { WEBUI_NAME, documents } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import { uploadDocToVectorDB } from '$lib/apis/rag';
import { transformFileName } from '$lib/utils';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
let importFiles = '';
let inputFiles = '';
@@ -22,6 +25,7 @@
let tags = [];
let showSettingsModal = false;
let showAddDocModal = false;
let showEditDocModal = false;
let selectedDoc;
let selectedTag = '';
@@ -33,6 +37,16 @@
await documents.set(await getDocs(localStorage.token));
};
const deleteDocs = async (docs) => {
const res = await Promise.all(
docs.map(async (doc) => {
return await deleteDocByName(localStorage.token, doc.name);
})
);
await documents.set(await getDocs(localStorage.token));
};
const uploadDoc = async (file) => {
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
toast.error(error);
@@ -123,8 +137,23 @@
dropZone?.removeEventListener('dragleave', onDragLeave);
};
});
let filteredDocs;
$: filteredDocs = $documents.filter(
(doc) =>
(selectedTag === '' ||
(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
(query === '' || doc.name.includes(query))
);
</script>
<svelte:head>
<title>
{`Documents | ${$WEBUI_NAME}`}
</title>
</svelte:head>
{#if dragged}
<div
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
@@ -150,36 +179,7 @@
<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
{/key}
<input
id="upload-doc-input"
bind:files={inputFiles}
type="file"
multiple
hidden
on:change={async (e) => {
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file);
}
}
inputFiles = null;
e.target.value = '';
} else {
toast.error(`File not found.`);
}
}}
/>
<AddDocModal bind:show={showAddDocModal} />
<SettingsModal bind:show={showSettingsModal} />
@@ -247,7 +247,7 @@
<button
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
on:click={() => {
document.getElementById('upload-doc-input')?.click();
showAddDocModal = true;
}}
>
<svg
@@ -287,38 +287,96 @@
{#if tags.length > 0}
<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">all</div>
</button>
{#each tags as tag}
<div class="ml-0.5 pr-3 my-auto flex items-center">
<Checkbox
state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
filteredDocs.length
? 'checked'
: 'unchecked'}
indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
filteredDocs.filter((doc) => doc?.selected === 'checked').length !==
filteredDocs.length}
on:change={(e) => {
if (e.detail === 'checked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
} else if (e.detail === 'unchecked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
}
}}
/>
</div>
{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = tag;
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
#{tag}
</div>
<div class=" text-xs font-medium self-center line-clamp-1">all</div>
</button>
{/each}
{#each tags as tag}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = tag;
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
#{tag}
</div>
</button>
{/each}
{:else}
<div class="flex-1 flex w-full justify-between items-center">
<div class="text-xs font-medium py-0.5 self-center mr-1">
{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
</div>
<div class="flex gap-1">
<!-- <button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
</button> -->
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">delete</div>
</button>
</div>
</div>
{/if}
</div>
{/if}
<div class="my-3 mb-5">
{#each $documents.filter((doc) => (selectedTag === '' || (doc?.content?.tags ?? [])
.map((tag) => tag.name)
.includes(selectedTag)) && (query === '' || doc.name.includes(query))) as doc}
<div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
{#each filteredDocs as doc}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
on:click={() => {
if (doc?.selected === 'checked') {
doc.selected = 'unchecked';
} else {
doc.selected = 'checked';
}
}}
>
<div class="my-auto flex items-center">
<Checkbox state={doc?.selected ?? 'unchecked'} />
</div>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<div class=" flex items-center space-x-3">
<div class="p-2.5 bg-red-400 text-white rounded-lg">
@@ -387,9 +445,10 @@
</div>
<div class="flex flex-row space-x-1 self-center">
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={async () => {
on:click={async (e) => {
e.stopPropagation();
showEditDocModal = !showEditDocModal;
selectedDoc = doc;
}}
@@ -435,7 +494,9 @@
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
on:click={(e) => {
e.stopPropagation();
deleteDoc(doc.name);
}}
>
@@ -455,7 +516,7 @@
</svg>
</button>
</div>
</div>
</button>
{/each}
</div>
+8 -2
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount } from 'svelte';
import { modelfiles, settings, user } from '$lib/stores';
import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
import { createModel, deleteModel } from '$lib/apis/ollama';
import {
createNewModelfile,
@@ -69,6 +69,12 @@
});
</script>
<svelte:head>
<title>
{`Modelfiles | ${$WEBUI_NAME}`}
</title>
</svelte:head>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class="flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
@@ -1,6 +1,6 @@
<script>
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { settings, user, config, modelfiles, models } from '$lib/stores';
@@ -1,6 +1,6 @@
<script>
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
+472
View File
@@ -0,0 +1,472 @@
<script>
import { goto } from '$app/navigation';
import { onMount, tick } from 'svelte';
import { toast } from 'svelte-sonner';
import {
LITELLM_API_BASE_URL,
OLLAMA_API_BASE_URL,
OPENAI_API_BASE_URL,
WEBUI_API_BASE_URL
} from '$lib/constants';
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
import { cancelChatCompletion, generateChatCompletion } from '$lib/apis/ollama';
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
import { splitStream } from '$lib/utils';
import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte';
let mode = 'chat';
let loaded = false;
let text = '';
let selectedModelId = '';
let loading = false;
let currentRequestId;
let stopResponseFlag = false;
let system = '';
let messages = [
{
role: 'user',
content: ''
}
];
const scrollToBottom = () => {
let element;
if (mode === 'chat') {
element = document.getElementById('messages-container');
} else {
element = document.getElementById('text-completion-textarea');
}
if (element) {
element.scrollTop = element?.scrollHeight;
}
};
// const cancelHandler = async () => {
// if (currentRequestId) {
// const res = await cancelChatCompletion(localStorage.token, currentRequestId);
// currentRequestId = null;
// loading = false;
// }
// };
const stopResponse = () => {
stopResponseFlag = true;
console.log('stopResponse');
};
const textCompletionHandler = async () => {
const model = $models.find((model) => model.id === selectedModelId);
const res = await generateOpenAIChatCompletion(
localStorage.token,
{
model: model.id,
stream: true,
messages: [
{
role: 'assistant',
content: text
}
]
},
model.external
? model.source === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
);
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done || stopResponseFlag) {
currentRequestId = null;
break;
}
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
if (line === 'data: [DONE]') {
// responseMessage.done = true;
console.log('done');
} else {
let data = JSON.parse(line.replace(/^data: /, ''));
console.log(data);
text += data.choices[0].delta.content ?? '';
}
}
}
} catch (error) {
console.log(error);
}
scrollToBottom();
}
}
};
const chatCompletionHandler = async () => {
const model = $models.find((model) => model.id === selectedModelId);
const res = await generateOpenAIChatCompletion(
localStorage.token,
{
model: model.id,
stream: true,
messages: [
system
? {
role: 'system',
content: system
}
: undefined,
...messages
].filter((message) => message)
},
model.external
? model.source === 'litellm'
? `${LITELLM_API_BASE_URL}/v1`
: `${OPENAI_API_BASE_URL}`
: `${OLLAMA_API_BASE_URL}/v1`
);
// const [res, controller] = await generateChatCompletion(localStorage.token, {
// model: selectedModelId,
// messages: [
// {
// role: 'assistant',
// content: text
// }
// ]
// });
let responseMessage;
if (messages.at(-1)?.role === 'assistant') {
responseMessage = messages.at(-1);
} else {
responseMessage = {
role: 'assistant',
content: ''
};
messages.push(responseMessage);
messages = messages;
}
await tick();
const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`);
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done || stopResponseFlag) {
break;
}
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
if (line === 'data: [DONE]') {
// responseMessage.done = true;
messages = messages;
} else {
let data = JSON.parse(line.replace(/^data: /, ''));
console.log(data);
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
continue;
} else {
textareaElement.style.height = textareaElement.scrollHeight + 'px';
responseMessage.content += data.choices[0].delta.content ?? '';
messages = messages;
textareaElement.style.height = textareaElement.scrollHeight + 'px';
await tick();
}
}
}
}
} catch (error) {
console.log(error);
}
scrollToBottom();
}
// while (true) {
// const { value, done } = await reader.read();
// if (done || stopResponseFlag) {
// if (stopResponseFlag) {
// await cancelChatCompletion(localStorage.token, currentRequestId);
// }
// currentRequestId = null;
// break;
// }
// try {
// let lines = value.split('\n');
// for (const line of lines) {
// if (line !== '') {
// console.log(line);
// let data = JSON.parse(line);
// if ('detail' in data) {
// throw data;
// }
// if ('id' in data) {
// console.log(data);
// currentRequestId = data.id;
// } else {
// if (data.done == false) {
// text += data.message.content;
// } else {
// console.log('done');
// }
// }
// }
// }
// } catch (error) {
// console.log(error);
// }
// scrollToBottom();
// }
}
};
const submitHandler = async () => {
if (selectedModelId) {
loading = true;
if (mode === 'complete') {
await textCompletionHandler();
} else if (mode === 'chat') {
await chatCompletionHandler();
}
loading = false;
stopResponseFlag = false;
currentRequestId = null;
}
};
onMount(async () => {
if ($user?.role !== 'admin') {
await goto('/');
}
if ($settings?.models) {
selectedModelId = $settings?.models[0];
} else if ($config?.default_models) {
selectedModelId = $config?.default_models.split(',')[0];
} else {
selectedModelId = '';
}
loaded = true;
});
</script>
<svelte:head>
<title>
{`Playground | ${$WEBUI_NAME}`}
</title>
</svelte:head>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class=" flex flex-col justify-between w-full overflow-y-auto h-[100dvh]">
<div class="max-w-2xl mx-auto w-full px-3 p-3 md:px-0 h-full">
<div class=" flex flex-col h-full">
<div class="flex flex-col justify-between mb-2.5 gap-1">
<div class="flex justify-between items-center gap-2">
<div class=" text-2xl font-semibold self-center flex">
Playground <span class=" text-xs text-gray-500 self-center ml-1">(Beta)</span>
</div>
<div>
<button
class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode ===
'chat' && 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' &&
'text-green-600 dark:text-green-200 bg-green-200/30'} "
on:click={() => {
if (mode === 'complete') {
mode = 'chat';
} else {
mode = 'complete';
}
}}
>
{#if mode === 'complete'}
Text Completion
{:else if mode === 'chat'}
Chat
{/if}
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3 h-3"
>
<path
fill-rule="evenodd"
d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div>
<div class=" flex gap-1 px-1">
<select
id="models"
class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400"
bind:value={selectedModelId}
>
<option class=" text-gray-800" value="" selected disabled>Select a model</option>
{#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
class=" self-center dark:hover:text-gray-300"
id="open-settings-button"
on:click={async () => {}}
>
<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> -->
</div>
</div>
{#if mode === 'chat'}
<div class="p-1">
<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
<div class=" text-sm font-medium">System</div>
<textarea
id="system-textarea"
class="w-full h-full bg-transparent resize-none outline-none text-sm"
bind:value={system}
placeholder="You're a helpful assistant."
rows="4"
/>
</div>
</div>
{/if}
<div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
id="messages-container"
>
<div class=" h-full w-full flex flex-col">
<div class="flex-1 p-1">
{#if mode === 'complete'}
<textarea
id="text-completion-textarea"
class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
bind:value={text}
placeholder="You're a helpful assistant."
/>
{:else if mode === 'chat'}
<ChatCompletion bind:messages />
{/if}
</div>
</div>
</div>
<div class="pb-2">
{#if !loading}
<button
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
on:click={() => {
submitHandler();
}}
>
Submit
</button>
{:else}
<button
class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg"
on:click={() => {
stopResponse();
}}
>
Cancel
</button>
{/if}
</div>
</div>
</div>
</div>
</div>
<style>
.scrollbar-hidden::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.scrollbar-hidden {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
+8 -2
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount } from 'svelte';
import { prompts } from '$lib/stores';
import { WEBUI_NAME, prompts } from '$lib/stores';
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
import { error } from '@sveltejs/kit';
import { goto } from '$app/navigation';
@@ -36,6 +36,12 @@
};
</script>
<svelte:head>
<title>
{`Prompts | ${$WEBUI_NAME}`}
</title>
</svelte:head>
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
<div class="flex flex-col justify-between w-full overflow-y-auto">
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
+1 -1
View File
@@ -1,5 +1,5 @@
<script>
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { prompts } from '$lib/stores';
+1 -1
View File
@@ -1,5 +1,5 @@
<script>
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { prompts } from '$lib/stores';
+2 -2
View File
@@ -2,7 +2,7 @@
import { onMount, tick } from 'svelte';
import { config, user, theme, WEBUI_NAME } from '$lib/stores';
import { goto } from '$app/navigation';
import toast, { Toaster } from 'svelte-french-toast';
import { Toaster, toast } from 'svelte-sonner';
import { getBackendConfig } from '$lib/apis';
import { getSessionUser } from '$lib/apis/auths';
@@ -68,4 +68,4 @@
<slot />
{/if}
<Toaster />
<Toaster richColors position="top-center" />
+11 -5
View File
@@ -4,7 +4,7 @@
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
let loaded = false;
let mode = 'signin';
@@ -57,6 +57,12 @@
});
</script>
<svelte:head>
<title>
{`${$WEBUI_NAME}`}
</title>
</svelte:head>
{#if loaded}
<div class="fixed m-10 z-50">
<div class="flex space-x-2">
@@ -66,7 +72,7 @@
</div>
</div>
<div class=" bg-white min-h-screen w-full flex justify-center font-mona">
<div class=" bg-white dark:bg-gray-900 min-h-screen w-full flex justify-center font-mona">
<!-- <div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center">
<div class=" my-auto pb-16 text-left">
<div>
@@ -81,15 +87,15 @@
</div>
</div> -->
<div class="w-full max-w-lg px-10 md:px-16 bg-white min-h-screen flex flex-col">
<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
<div class=" my-auto pb-10 w-full">
<form
class=" flex flex-col justify-center"
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class=" text-xl md:text-2xl font-bold">
<div class=" text-xl sm:text-2xl font-bold">
{mode === 'signin' ? 'Sign in' : 'Sign up'} to {$WEBUI_NAME}
</div>
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# update_llm.sh
# Retrieves the list of LLMs installed in the Docker container
llm_list=$(docker exec ollama ollama list | tail -n +2 | awk '{print $1}')
# Loop over each LLM to update it
for llm in $llm_list; do
docker exec ollama ollama pull $llm
done