Compare commits

...

52 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek eb51ad14e4 Merge pull request #997 from open-webui/dev
Create and publish a Docker image / build-and-push-image (push) Failing after 48s
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. Baek 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
Create and publish a Docker image / build-and-push-image (push) Failing after 49s
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
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
60 changed files with 1391 additions and 308 deletions
+37
View File
@@ -5,6 +5,43 @@ 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
+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
+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,
+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()
+11 -1
View File
@@ -271,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)
+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.106",
"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;
+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',
+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>
+109 -96
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;
@@ -637,24 +638,26 @@
<div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1">
<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();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-[1.2rem] h-[1.2rem]"
<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
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>
<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}
@@ -806,87 +809,97 @@
<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 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();
}}
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-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
>
<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>
</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-full p-1.5"
@@ -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 = '';
@@ -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();
}
}
}
}
@@ -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;
+1 -1
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';
@@ -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;
@@ -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
>
@@ -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';
+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>
@@ -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';
@@ -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;
+87 -49
View File
@@ -16,9 +16,10 @@
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;
@@ -34,7 +35,7 @@
let isEditing = false;
onMount(async () => {
if (window.innerWidth > 1280) {
if (window.innerWidth > 1024) {
show = true;
}
await chats.set(await getChatList(localStorage.token));
@@ -366,28 +367,39 @@
}
}) as chat, i}
<div class=" w-full pr-2 relative">
<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}"
draggable={isEditing ? 'false' : 'true'}
>
<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]">
@@ -399,7 +411,6 @@
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitle = '';
isEditing = false;
}}
>
<svg
@@ -487,7 +498,6 @@
on:click={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
isEditing = true;
}}
>
<svg
@@ -589,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
@@ -666,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>
+1 -1
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;
+15 -9
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 = '';
@@ -248,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;
});
}
})
);
@@ -809,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">
+8 -2
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}
+17 -9
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;
@@ -100,6 +101,7 @@
await tick();
loaded = true;
window.setTimeout(() => scrollToBottom(), 0);
const chatInput = document.getElementById('chat-textarea');
chatInput?.focus();
} else {
@@ -259,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;
});
}
})
);
@@ -823,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
+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 { 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';
@@ -148,6 +148,12 @@
);
</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"
+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