This commit is contained in:
Michael Poluektov 2024-07-09 11:13:42 +01:00
commit f9e3c47d4a
149 changed files with 4645 additions and 2403 deletions

View File

@ -35,6 +35,10 @@ jobs:
done done
echo "Service is up!" echo "Service is up!"
- name: Delete Docker build cache
run: |
docker builder prune --all --force
- name: Preload Ollama model - name: Preload Ollama model
run: | run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
@ -43,7 +47,7 @@ jobs:
uses: cypress-io/github-action@v6 uses: cypress-io/github-action@v6
with: with:
browser: chrome browser: chrome
wait-on: "http://localhost:3000" wait-on: 'http://localhost:3000'
config: baseUrl=http://localhost:3000 config: baseUrl=http://localhost:3000
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -67,6 +71,28 @@ jobs:
path: compose-logs.txt path: compose-logs.txt
if-no-files-found: ignore if-no-files-found: ignore
# pytest:
# name: Run Backend Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r backend/requirements.txt
# - name: pytest run
# run: |
# ls -al
# cd backend
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
migration_test: migration_test:
name: Run Migration Tests name: Run Migration Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -126,11 +152,11 @@ jobs:
cd backend cd backend
uvicorn main:app --port "8080" --forwarded-allow-ips '*' & uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$! UVICORN_PID=$!
# Wait up to 20 seconds for the server to start # Wait up to 40 seconds for the server to start
for i in {1..20}; do for i in {1..40}; do
curl -s http://localhost:8080/api/config > /dev/null && break curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1 sleep 1
if [ $i -eq 20 ]; then if [ $i -eq 40 ]; then
echo "Server failed to start" echo "Server failed to start"
kill -9 $UVICORN_PID kill -9 $UVICORN_PID
exit 1 exit 1
@ -171,7 +197,7 @@ jobs:
fi fi
# Check that service will reconnect to postgres when connection will be closed # Check that service will reconnect to postgres when connection will be closed
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health) status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then if [[ "$status_code" -ne 200 ]] ; then
echo "Server has failed before postgres reconnect check" echo "Server has failed before postgres reconnect check"
exit 1 exit 1
@ -183,7 +209,7 @@ jobs:
cur = conn.cursor(); \ cur = conn.cursor(); \
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')" cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health) status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then if [[ "$status_code" -ne 200 ]] ; then
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code" echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
exit 1 exit 1

114
backend/alembic.ini Normal file
View File

@ -0,0 +1,114 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1004,10 +1004,11 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
return True return True
except Exception as e: except Exception as e:
log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError": if e.__class__.__name__ == "UniqueConstraintError":
return True return True
log.exception(e)
return False return False

View File

@ -1,18 +1,39 @@
import os import os
import logging import logging
import json import json
from contextlib import contextmanager
from peewee import *
from peewee_migrate import Router from peewee_migrate import Router
from apps.webui.internal.wrappers import register_connection from apps.webui.internal.wrappers import register_connection
from typing import Optional, Any
from typing_extensions import Self
from sqlalchemy import create_engine, types, Dialect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.sql.type_api import _T
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"]) log.setLevel(SRC_LOG_LEVELS["DB"])
class JSONField(TextField): class JSONField(types.TypeDecorator):
impl = types.Text
cache_ok = True
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
return json.dumps(value)
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
if value is not None:
return json.loads(value)
def copy(self, **kw: Any) -> Self:
return JSONField(self.impl.length)
def db_value(self, value): def db_value(self, value):
return json.dumps(value) return json.dumps(value)
@ -30,25 +51,60 @@ else:
pass pass
# The `register_connection` function encapsulates the logic for setting up # Workaround to handle the peewee migration
# the database connection based on the connection string, while `connect` # This is required to ensure the peewee migration is handled before the alembic migration
# is a Peewee-specific method to manage the connection state and avoid errors def handle_peewee_migration(DATABASE_URL):
# when a connection is already open. try:
try: # Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
DB = register_connection(DATABASE_URL) db = register_connection(
log.info(f"Connected to a {DB.__class__.__name__} database.") DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
except Exception as e: )
log.error(f"Failed to initialize the database connection: {e}") migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
raise router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
router = Router( # check if db connection has been closed
DB,
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations", except Exception as e:
logger=log, log.error(f"Failed to initialize the database connection: {e}")
raise
finally:
# Properly closing the database connection
if db and not db.is_closed():
db.close()
# Assert if db connection has been closed
assert db.is_closed(), "Database connection is still open."
handle_peewee_migration(DATABASE_URL)
SQLALCHEMY_DATABASE_URL = DATABASE_URL
if "sqlite" in SQLALCHEMY_DATABASE_URL:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
) )
router.run() Base = declarative_base()
try: Session = scoped_session(SessionLocal)
DB.connect(reuse_if_open=True)
except OperationalError as e:
log.info(f"Failed to connect to database again due to: {e}") # Dependency
pass def get_session():
db = SessionLocal()
try:
yield db
finally:
db.close()
get_db = contextmanager(get_session)

View File

@ -1,10 +1,7 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py. """Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name):: Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name > Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name > Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL > migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args > migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator) > migrator.create_model(Model) # Create a model (could be used as decorator)
@ -21,7 +18,6 @@ Some examples (model - class or model name)::
> migrator.drop_index(model, *col_names) > migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names) > migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints) > migrator.drop_constraints(model, *constraints)
""" """
from contextlib import suppress from contextlib import suppress

View File

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

View File

@ -3,7 +3,7 @@ from fastapi.routing import APIRoute
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.orm import Session
from apps.webui.routers import ( from apps.webui.routers import (
auths, auths,
users, users,
@ -19,8 +19,13 @@ from apps.webui.routers import (
functions, functions,
) )
from apps.webui.models.functions import Functions from apps.webui.models.functions import Functions
from apps.webui.models.models import Models
from apps.webui.utils import load_function_module_by_id from apps.webui.utils import load_function_module_by_id
from utils.misc import stream_message_template from utils.misc import stream_message_template
from utils.task import prompt_template
from config import ( from config import (
WEBUI_BUILD_HASH, WEBUI_BUILD_HASH,
@ -39,6 +44,8 @@ from config import (
WEBUI_BANNERS, WEBUI_BANNERS,
ENABLE_COMMUNITY_SHARING, ENABLE_COMMUNITY_SHARING,
AppConfig, AppConfig,
OAUTH_USERNAME_CLAIM,
OAUTH_PICTURE_CLAIM,
) )
import inspect import inspect
@ -74,6 +81,9 @@ app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
app.state.MODELS = {} app.state.MODELS = {}
app.state.TOOLS = {} app.state.TOOLS = {}
app.state.FUNCTIONS = {} app.state.FUNCTIONS = {}
@ -129,7 +139,6 @@ async def get_pipe_models():
function_module = app.state.FUNCTIONS[pipe.id] function_module = app.state.FUNCTIONS[pipe.id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
print(f"Getting valves for {pipe.id}")
valves = Functions.get_function_valves_by_id(pipe.id) valves = Functions.get_function_valves_by_id(pipe.id)
function_module.valves = function_module.Valves( function_module.valves = function_module.Valves(
**(valves if valves else {}) **(valves if valves else {})
@ -181,6 +190,77 @@ async def get_pipe_models():
async def generate_function_chat_completion(form_data, user): async def generate_function_chat_completion(form_data, user):
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
if model_info.params:
if model_info.params.get("temperature", None) is not None:
form_data["temperature"] = float(model_info.params.get("temperature"))
if model_info.params.get("top_p", None):
form_data["top_p"] = int(model_info.params.get("top_p", None))
if model_info.params.get("max_tokens", None):
form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
if model_info.params.get("frequency_penalty", None):
form_data["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
form_data["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
form_data["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
system = model_info.params.get("system", None)
if system:
system = prompt_template(
system,
**(
{
"user_name": user.name,
"user_location": (
user.info.get("location") if user.info else None
),
}
if user
else {}
),
)
# Check if the payload already has a system message
# If not, add a system message to the payload
if form_data.get("messages"):
for message in form_data["messages"]:
if message.get("role") == "system":
message["content"] = system + message["content"]
break
else:
form_data["messages"].insert(
0,
{
"role": "system",
"content": system,
},
)
else:
pass
async def job(): async def job():
pipe_id = form_data["model"] pipe_id = form_data["model"]
if "." in pipe_id: if "." in pipe_id:

View File

@ -1,14 +1,13 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Union, Optional from typing import Optional
import time
import uuid import uuid
import logging import logging
from peewee import * from sqlalchemy import String, Column, Boolean, Text
from apps.webui.models.users import UserModel, Users from apps.webui.models.users import UserModel, Users
from utils.utils import verify_password from utils.utils import verify_password
from apps.webui.internal.db import DB from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
@ -20,14 +19,13 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class Auth(Model): class Auth(Base):
id = CharField(unique=True) __tablename__ = "auth"
email = CharField()
password = TextField()
active = BooleanField()
class Meta: id = Column(String, primary_key=True)
database = DB email = Column(String)
password = Column(Text)
active = Column(Boolean)
class AuthModel(BaseModel): class AuthModel(BaseModel):
@ -94,9 +92,6 @@ class AddUserForm(SignupForm):
class AuthsTable: class AuthsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Auth])
def insert_new_auth( def insert_new_auth(
self, self,
@ -107,36 +102,44 @@ class AuthsTable:
role: str = "pending", role: str = "pending",
oauth_sub: Optional[str] = None, oauth_sub: Optional[str] = None,
) -> Optional[UserModel]: ) -> Optional[UserModel]:
log.info("insert_new_auth") with get_db() as db:
id = str(uuid.uuid4()) log.info("insert_new_auth")
auth = AuthModel( id = str(uuid.uuid4())
**{"id": id, "email": email, "password": password, "active": True}
)
result = Auth.create(**auth.model_dump())
user = Users.insert_new_user( auth = AuthModel(
id, name, email, profile_image_url, role, oauth_sub **{"id": id, "email": email, "password": password, "active": True}
) )
result = Auth(**auth.model_dump())
db.add(result)
if result and user: user = Users.insert_new_user(
return user id, name, email, profile_image_url, role, oauth_sub
else: )
return None
db.commit()
db.refresh(result)
if result and user:
return user
else:
return None
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
log.info(f"authenticate_user: {email}") log.info(f"authenticate_user: {email}")
try: try:
auth = Auth.get(Auth.email == email, Auth.active == True) with get_db() as db:
if auth:
if verify_password(password, auth.password): auth = db.query(Auth).filter_by(email=email, active=True).first()
user = Users.get_user_by_id(auth.id) if auth:
return user if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
else:
return None
else: else:
return None return None
else:
return None
except: except:
return None return None
@ -155,46 +158,50 @@ class AuthsTable:
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]: def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}") log.info(f"authenticate_user_by_trusted_header: {email}")
try: try:
auth = Auth.get(Auth.email == email, Auth.active == True) with get_db() as db:
if auth: auth = db.query(Auth).filter(email=email, active=True).first()
user = Users.get_user_by_id(auth.id) if auth:
return user user = Users.get_user_by_id(auth.id)
return user
except: except:
return None return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool: def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try: try:
query = Auth.update(password=new_password).where(Auth.id == id) with get_db() as db:
result = query.execute() result = (
db.query(Auth).filter_by(id=id).update({"password": new_password})
return True if result == 1 else False )
db.commit()
return True if result == 1 else False
except: except:
return False return False
def update_email_by_id(self, id: str, email: str) -> bool: def update_email_by_id(self, id: str, email: str) -> bool:
try: try:
query = Auth.update(email=email).where(Auth.id == id) with get_db() as db:
result = query.execute() result = db.query(Auth).filter_by(id=id).update({"email": email})
db.commit()
return True if result == 1 else False return True if result == 1 else False
except: except:
return False return False
def delete_auth_by_id(self, id: str) -> bool: def delete_auth_by_id(self, id: str) -> bool:
try: try:
# Delete User with get_db() as db:
result = Users.delete_user_by_id(id)
if result: # Delete User
# Delete Auth result = Users.delete_user_by_id(id)
query = Auth.delete().where(Auth.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True if result:
else: db.query(Auth).filter_by(id=id).delete()
return False db.commit()
return True
else:
return False
except: except:
return False return False
Auths = AuthsTable(DB) Auths = AuthsTable()

View File

@ -1,36 +1,38 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional from typing import List, Union, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
import json import json
import uuid import uuid
import time import time
from apps.webui.internal.db import DB from sqlalchemy import Column, String, BigInteger, Boolean, Text
from apps.webui.internal.db import Base, get_db
#################### ####################
# Chat DB Schema # Chat DB Schema
#################### ####################
class Chat(Model): class Chat(Base):
id = CharField(unique=True) __tablename__ = "chat"
user_id = CharField()
title = TextField()
chat = TextField() # Save Chat JSON as Text
created_at = BigIntegerField() id = Column(String, primary_key=True)
updated_at = BigIntegerField() user_id = Column(String)
title = Column(Text)
chat = Column(Text) # Save Chat JSON as Text
share_id = CharField(null=True, unique=True) created_at = Column(BigInteger)
archived = BooleanField(default=False) updated_at = Column(BigInteger)
class Meta: share_id = Column(Text, unique=True, nullable=True)
database = DB archived = Column(Boolean, default=False)
class ChatModel(BaseModel): class ChatModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str id: str
user_id: str user_id: str
title: str title: str
@ -75,91 +77,104 @@ class ChatTitleIdResponse(BaseModel):
class ChatTable: class ChatTable:
def __init__(self, db):
self.db = db
db.create_tables([Chat])
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]: def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
id = str(uuid.uuid4()) with get_db() as db:
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat.create(**chat.model_dump()) id = str(uuid.uuid4())
return chat if result else None chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"]
if "title" in form_data.chat
else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat(**chat.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return ChatModel.model_validate(result) if result else None
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try: try:
query = Chat.update( with get_db() as db:
chat=json.dumps(chat),
title=chat["title"] if "title" in chat else "New Chat",
updated_at=int(time.time()),
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id) chat_obj = db.get(Chat, id)
return ChatModel(**model_to_dict(chat)) chat_obj.chat = json.dumps(chat)
except: chat_obj.title = chat["title"] if "title" in chat else "New Chat"
chat_obj.updated_at = int(time.time())
db.commit()
db.refresh(chat_obj)
return ChatModel.model_validate(chat_obj)
except Exception as e:
return None return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share with get_db() as db:
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
return shared_chat if (shared_result and result) else None # Get the existing chat to share
chat = db.get(Chat, chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat(**shared_chat.model_dump())
db.add(shared_result)
db.commit()
db.refresh(shared_result)
# Update the original chat with the share_id
result = (
db.query(Chat)
.filter_by(id=chat_id)
.update({"share_id": shared_chat.id})
)
db.commit()
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try: try:
print("update_shared_chat_by_id") with get_db() as db:
chat = Chat.get(Chat.id == chat_id)
print(chat)
query = Chat.update( print("update_shared_chat_by_id")
title=chat.title, chat = db.get(Chat, chat_id)
chat=chat.chat, print(chat)
).where(Chat.id == chat.share_id) chat.title = chat.title
chat.chat = chat.chat
db.commit()
db.refresh(chat)
query.execute() return self.get_chat_by_id(chat.share_id)
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except: except:
return None return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool: def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try: try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}") with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
return True
except: except:
return False return False
@ -167,56 +182,50 @@ class ChatTable:
self, id: str, share_id: Optional[str] self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]: ) -> Optional[ChatModel]:
try: try:
query = Chat.update( with get_db() as db:
share_id=share_id,
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id) chat = db.get(Chat, id)
return ChatModel(**model_to_dict(chat)) chat.share_id = share_id
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except: except:
return None return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]: def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try: try:
chat = self.get_chat_by_id(id) with get_db() as db:
query = Chat.update(
archived=(not chat.archived),
).where(Chat.id == id)
query.execute() chat = db.get(Chat, id)
chat.archived = not chat.archived
chat = Chat.get(Chat.id == id) db.commit()
return ChatModel(**model_to_dict(chat)) db.refresh(chat)
return ChatModel.model_validate(chat)
except: except:
return None return None
def archive_all_chats_by_user_id(self, user_id: str) -> bool: def archive_all_chats_by_user_id(self, user_id: str) -> bool:
try: try:
chats = self.get_chats_by_user_id(user_id) with get_db() as db:
for chat in chats: db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
query = Chat.update( db.commit()
archived=True, return True
).where(Chat.id == chat.id)
query.execute()
return True
except: except:
return False return False
def get_archived_chat_list_by_user_id( def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50 self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]: ) -> List[ChatModel]:
return [ with get_db() as db:
ChatModel(**model_to_dict(chat))
for chat in Chat.select() all_chats = (
.where(Chat.archived == True) db.query(Chat)
.where(Chat.user_id == user_id) .filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc()) .order_by(Chat.updated_at.desc())
# .limit(limit) # .limit(limit).offset(skip)
# .offset(skip) .all()
] )
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_list_by_user_id( def get_chat_list_by_user_id(
self, self,
@ -225,131 +234,141 @@ class ChatTable:
skip: int = 0, skip: int = 0,
limit: int = 50, limit: int = 50,
) -> List[ChatModel]: ) -> List[ChatModel]:
if include_archived: with get_db() as db:
return [ query = db.query(Chat).filter_by(user_id=user_id)
ChatModel(**model_to_dict(chat)) if not include_archived:
for chat in Chat.select() query = query.filter_by(archived=False)
.where(Chat.user_id == user_id) all_chats = (
.order_by(Chat.updated_at.desc()) query.order_by(Chat.updated_at.desc())
# .limit(limit) # .limit(limit).offset(skip)
# .offset(skip) .all()
] )
else: return [ChatModel.model_validate(chat) for chat in all_chats]
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
def get_chat_list_by_chat_ids( def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50 self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]: ) -> List[ChatModel]:
return [ with get_db() as db:
ChatModel(**model_to_dict(chat)) all_chats = (
for chat in Chat.select() db.query(Chat)
.where(Chat.archived == False) .filter(Chat.id.in_(chat_ids))
.where(Chat.id.in_(chat_ids)) .filter_by(archived=False)
.order_by(Chat.updated_at.desc()) .order_by(Chat.updated_at.desc())
] .all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]: def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id) with get_db() as db:
return ChatModel(**model_to_dict(chat))
chat = db.get(Chat, id)
return ChatModel.model_validate(chat)
except: except:
return None return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]: def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.share_id == id) with get_db() as db:
if chat: chat = db.query(Chat).filter_by(share_id=id).first()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat)) if chat:
else: return self.get_chat_by_id(id)
return None else:
except: return None
except Exception as e:
return None return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try: try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id) with get_db() as db:
return ChatModel(**model_to_dict(chat))
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
return ChatModel.model_validate(chat)
except: except:
return None return None
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]: def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [ with get_db() as db:
ChatModel(**model_to_dict(chat))
for chat in Chat.select().order_by(Chat.updated_at.desc()) all_chats = (
# .limit(limit).offset(skip) db.query(Chat)
] # .limit(limit).offset(skip)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]: def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [ with get_db() as db:
ChatModel(**model_to_dict(chat))
for chat in Chat.select() all_chats = (
.where(Chat.user_id == user_id) db.query(Chat)
.order_by(Chat.updated_at.desc()) .filter_by(user_id=user_id)
# .limit(limit).offset(skip) .order_by(Chat.updated_at.desc())
] )
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]: def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [ with get_db() as db:
ChatModel(**model_to_dict(chat))
for chat in Chat.select() all_chats = (
.where(Chat.archived == True) db.query(Chat)
.where(Chat.user_id == user_id) .filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc()) .order_by(Chat.updated_at.desc())
] )
return [ChatModel.model_validate(chat) for chat in all_chats]
def delete_chat_by_id(self, id: str) -> bool: def delete_chat_by_id(self, id: str) -> bool:
try: try:
query = Chat.delete().where((Chat.id == id)) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id) db.query(Chat).filter_by(id=id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except: except:
return False return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool: def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try: try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id) db.query(Chat).filter_by(id=id, user_id=user_id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except: except:
return False return False
def delete_chats_by_user_id(self, user_id: str) -> bool: def delete_chats_by_user_id(self, user_id: str) -> bool:
try: try:
self.delete_shared_chats_by_user_id(user_id) with get_db() as db:
query = Chat.delete().where(Chat.user_id == user_id) self.delete_shared_chats_by_user_id(user_id)
query.execute() # Remove the rows, return number of rows removed.
return True db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
return True
except: except:
return False return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool: def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try: try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
db.commit()
return True
except: except:
return False return False
Chats = ChatTable(DB) Chats = ChatTable()

View File

@ -1,14 +1,11 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import * from typing import List, Optional
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time import time
import logging import logging
from utils.utils import decode_token from sqlalchemy import String, Column, BigInteger, Text
from utils.misc import get_gravatar_url
from apps.webui.internal.db import DB from apps.webui.internal.db import Base, get_db
import json import json
@ -22,20 +19,21 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class Document(Model): class Document(Base):
collection_name = CharField(unique=True) __tablename__ = "document"
name = CharField(unique=True)
title = TextField()
filename = TextField()
content = TextField(null=True)
user_id = CharField()
timestamp = BigIntegerField()
class Meta: collection_name = Column(String, primary_key=True)
database = DB name = Column(String, unique=True)
title = Column(Text)
filename = Column(Text)
content = Column(Text, nullable=True)
user_id = Column(String)
timestamp = Column(BigInteger)
class DocumentModel(BaseModel): class DocumentModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
collection_name: str collection_name: str
name: str name: str
title: str title: str
@ -72,57 +70,63 @@ class DocumentForm(DocumentUpdateForm):
class DocumentsTable: class DocumentsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Document])
def insert_new_doc( def insert_new_doc(
self, user_id: str, form_data: DocumentForm self, user_id: str, form_data: DocumentForm
) -> Optional[DocumentModel]: ) -> Optional[DocumentModel]:
document = DocumentModel( with get_db() as db:
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
try: document = DocumentModel(
result = Document.create(**document.model_dump()) **{
if result: **form_data.model_dump(),
return document "user_id": user_id,
else: "timestamp": int(time.time()),
}
)
try:
result = Document(**document.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return DocumentModel.model_validate(result)
else:
return None
except:
return None return None
except:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]: def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
try: try:
document = Document.get(Document.name == name) with get_db() as db:
return DocumentModel(**model_to_dict(document))
document = db.query(Document).filter_by(name=name).first()
return DocumentModel.model_validate(document) if document else None
except: except:
return None return None
def get_docs(self) -> List[DocumentModel]: def get_docs(self) -> List[DocumentModel]:
return [ with get_db() as db:
DocumentModel(**model_to_dict(doc))
for doc in Document.select() return [
# .limit(limit).offset(skip) DocumentModel.model_validate(doc) for doc in db.query(Document).all()
] ]
def update_doc_by_name( def update_doc_by_name(
self, name: str, form_data: DocumentUpdateForm self, name: str, form_data: DocumentUpdateForm
) -> Optional[DocumentModel]: ) -> Optional[DocumentModel]:
try: try:
query = Document.update( with get_db() as db:
title=form_data.title,
name=form_data.name,
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
doc = Document.get(Document.name == form_data.name) db.query(Document).filter_by(name=name).update(
return DocumentModel(**model_to_dict(doc)) {
"title": form_data.title,
"name": form_data.name,
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(form_data.name)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
return None return None
@ -135,26 +139,29 @@ class DocumentsTable:
doc_content = json.loads(doc.content if doc.content else "{}") doc_content = json.loads(doc.content if doc.content else "{}")
doc_content = {**doc_content, **updated} doc_content = {**doc_content, **updated}
query = Document.update( with get_db() as db:
content=json.dumps(doc_content),
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
doc = Document.get(Document.name == name) db.query(Document).filter_by(name=name).update(
return DocumentModel(**model_to_dict(doc)) {
"content": json.dumps(doc_content),
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(name)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
return None return None
def delete_doc_by_name(self, name: str) -> bool: def delete_doc_by_name(self, name: str) -> bool:
try: try:
query = Document.delete().where((Document.name == name)) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True db.query(Document).filter_by(name=name).delete()
db.commit()
return True
except: except:
return False return False
Documents = DocumentsTable(DB) Documents = DocumentsTable()

View File

@ -1,10 +1,11 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional from typing import List, Union, Optional
import time import time
import logging import logging
from apps.webui.internal.db import DB, JSONField
from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.internal.db import JSONField, Base, get_db
import json import json
@ -18,15 +19,14 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class File(Model): class File(Base):
id = CharField(unique=True) __tablename__ = "file"
user_id = CharField()
filename = TextField()
meta = JSONField()
created_at = BigIntegerField()
class Meta: id = Column(String, primary_key=True)
database = DB user_id = Column(String)
filename = Column(Text)
meta = Column(JSONField)
created_at = Column(BigInteger)
class FileModel(BaseModel): class FileModel(BaseModel):
@ -36,6 +36,8 @@ class FileModel(BaseModel):
meta: dict meta: dict
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -57,56 +59,68 @@ class FileForm(BaseModel):
class FilesTable: class FilesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([File])
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]: def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
file = FileModel( with get_db() as db:
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
}
)
try: file = FileModel(
result = File.create(**file.model_dump()) **{
if result: **form_data.model_dump(),
return file "user_id": user_id,
else: "created_at": int(time.time()),
}
)
try:
result = File(**file.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FileModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_file_by_id(self, id: str) -> Optional[FileModel]: def get_file_by_id(self, id: str) -> Optional[FileModel]:
try: with get_db() as db:
file = File.get(File.id == id)
return FileModel(**model_to_dict(file)) try:
except: file = db.get(File, id)
return None return FileModel.model_validate(file)
except:
return None
def get_files(self) -> List[FileModel]: def get_files(self) -> List[FileModel]:
return [FileModel(**model_to_dict(file)) for file in File.select()] with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def delete_file_by_id(self, id: str) -> bool: def delete_file_by_id(self, id: str) -> bool:
try:
query = File.delete().where((File.id == id))
query.execute() # Remove the rows, return number of rows removed.
return True with get_db() as db:
except:
return False try:
db.query(File).filter_by(id=id).delete()
db.commit()
return True
except:
return False
def delete_all_files(self) -> bool: def delete_all_files(self) -> bool:
try:
query = File.delete()
query.execute() # Remove the rows, return number of rows removed.
return True with get_db() as db:
except:
return False try:
db.query(File).delete()
db.commit()
return True
except:
return False
Files = FilesTable(DB) Files = FilesTable()

View File

@ -1,10 +1,11 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional from typing import List, Union, Optional
import time import time
import logging import logging
from apps.webui.internal.db import DB, JSONField
from sqlalchemy import Column, String, Text, BigInteger, Boolean
from apps.webui.internal.db import JSONField, Base, get_db
from apps.webui.models.users import Users from apps.webui.models.users import Users
import json import json
@ -21,21 +22,20 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class Function(Model): class Function(Base):
id = CharField(unique=True) __tablename__ = "function"
user_id = CharField()
name = TextField()
type = TextField()
content = TextField()
meta = JSONField()
valves = JSONField()
is_active = BooleanField(default=False)
is_global = BooleanField(default=False)
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta: id = Column(String, primary_key=True)
database = DB user_id = Column(String)
name = Column(Text)
type = Column(Text)
content = Column(Text)
meta = Column(JSONField)
valves = Column(JSONField)
is_active = Column(Boolean)
is_global = Column(Boolean)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class FunctionMeta(BaseModel): class FunctionMeta(BaseModel):
@ -55,6 +55,8 @@ class FunctionModel(BaseModel):
updated_at: int # timestamp in epoch updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -85,13 +87,11 @@ class FunctionValves(BaseModel):
class FunctionsTable: class FunctionsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Function])
def insert_new_function( def insert_new_function(
self, user_id: str, type: str, form_data: FunctionForm self, user_id: str, type: str, form_data: FunctionForm
) -> Optional[FunctionModel]: ) -> Optional[FunctionModel]:
function = FunctionModel( function = FunctionModel(
**{ **{
**form_data.model_dump(), **form_data.model_dump(),
@ -103,89 +103,102 @@ class FunctionsTable:
) )
try: try:
result = Function.create(**function.model_dump()) with get_db() as db:
if result: result = Function(**function.model_dump())
return function db.add(result)
else: db.commit()
return None db.refresh(result)
if result:
return FunctionModel.model_validate(result)
else:
return None
except Exception as e: except Exception as e:
print(f"Error creating tool: {e}") print(f"Error creating tool: {e}")
return None return None
def get_function_by_id(self, id: str) -> Optional[FunctionModel]: def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
try: try:
function = Function.get(Function.id == id) with get_db() as db:
return FunctionModel(**model_to_dict(function))
function = db.get(Function, id)
return FunctionModel.model_validate(function)
except: except:
return None return None
def get_functions(self, active_only=False) -> List[FunctionModel]: def get_functions(self, active_only=False) -> List[FunctionModel]:
if active_only: with get_db() as db:
return [
FunctionModel(**model_to_dict(function)) if active_only:
for function in Function.select().where(Function.is_active == True) return [
] FunctionModel.model_validate(function)
else: for function in db.query(Function).filter_by(is_active=True).all()
return [ ]
FunctionModel(**model_to_dict(function)) else:
for function in Function.select() return [
] FunctionModel.model_validate(function)
for function in db.query(Function).all()
]
def get_functions_by_type( def get_functions_by_type(
self, type: str, active_only=False self, type: str, active_only=False
) -> List[FunctionModel]: ) -> List[FunctionModel]:
if active_only: with get_db() as db:
return [
FunctionModel(**model_to_dict(function)) if active_only:
for function in Function.select().where( return [
Function.type == type, Function.is_active == True FunctionModel.model_validate(function)
) for function in db.query(Function)
] .filter_by(type=type, is_active=True)
else: .all()
return [ ]
FunctionModel(**model_to_dict(function)) else:
for function in Function.select().where(Function.type == type) return [
] FunctionModel.model_validate(function)
for function in db.query(Function).filter_by(type=type).all()
]
def get_global_filter_functions(self) -> List[FunctionModel]: def get_global_filter_functions(self) -> List[FunctionModel]:
return [ with get_db() as db:
FunctionModel(**model_to_dict(function))
for function in Function.select().where( return [
Function.type == "filter", FunctionModel.model_validate(function)
Function.is_active == True, for function in db.query(Function)
Function.is_global == True, .filter_by(type="filter", is_active=True, is_global=True)
) .all()
] ]
def get_function_valves_by_id(self, id: str) -> Optional[dict]: def get_function_valves_by_id(self, id: str) -> Optional[dict]:
try: with get_db() as db:
function = Function.get(Function.id == id)
return function.valves if function.valves else {} try:
except Exception as e: function = db.get(Function, id)
print(f"An error occurred: {e}") return function.valves if function.valves else {}
return None except Exception as e:
print(f"An error occurred: {e}")
return None
def update_function_valves_by_id( def update_function_valves_by_id(
self, id: str, valves: dict self, id: str, valves: dict
) -> Optional[FunctionValves]: ) -> Optional[FunctionValves]:
try: with get_db() as db:
query = Function.update(
**{"valves": valves},
updated_at=int(time.time()),
).where(Function.id == id)
query.execute()
function = Function.get(Function.id == id) try:
return FunctionValves(**model_to_dict(function)) function = db.get(Function, id)
except: function.valves = valves
return None function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
except:
return None
def get_user_valves_by_id_and_user_id( def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str self, id: str, user_id: str
) -> Optional[dict]: ) -> Optional[dict]:
try: try:
user = Users.get_user_by_id(user_id) user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings # Check if user has "functions" and "valves" settings
if "functions" not in user_settings: if "functions" not in user_settings:
@ -201,9 +214,10 @@ class FunctionsTable:
def update_user_valves_by_id_and_user_id( def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict self, id: str, user_id: str, valves: dict
) -> Optional[dict]: ) -> Optional[dict]:
try: try:
user = Users.get_user_by_id(user_id) user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings # Check if user has "functions" and "valves" settings
if "functions" not in user_settings: if "functions" not in user_settings:
@ -222,39 +236,44 @@ class FunctionsTable:
return None return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]: def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
try: with get_db() as db:
query = Function.update(
**updated,
updated_at=int(time.time()),
).where(Function.id == id)
query.execute()
function = Function.get(Function.id == id) try:
return FunctionModel(**model_to_dict(function)) db.query(Function).filter_by(id=id).update(
except: {
return None **updated,
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_function_by_id(id)
except:
return None
def deactivate_all_functions(self) -> Optional[bool]: def deactivate_all_functions(self) -> Optional[bool]:
try: with get_db() as db:
query = Function.update(
**{"is_active": False},
updated_at=int(time.time()),
)
query.execute() try:
db.query(Function).update(
return True {
except: "is_active": False,
return None "updated_at": int(time.time()),
}
)
db.commit()
return True
except:
return None
def delete_function_by_id(self, id: str) -> bool: def delete_function_by_id(self, id: str) -> bool:
try: with get_db() as db:
query = Function.delete().where((Function.id == id)) try:
query.execute() # Remove the rows, return number of rows removed. db.query(Function).filter_by(id=id).delete()
db.commit()
return True return True
except: except:
return False return False
Functions = FunctionsTable(DB) Functions = FunctionsTable()

View File

@ -1,10 +1,9 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional from typing import List, Union, Optional
from apps.webui.internal.db import DB from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.models.chats import Chats
from apps.webui.internal.db import Base, get_db
import time import time
import uuid import uuid
@ -14,15 +13,14 @@ import uuid
#################### ####################
class Memory(Model): class Memory(Base):
id = CharField(unique=True) __tablename__ = "memory"
user_id = CharField()
content = TextField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta: id = Column(String, primary_key=True)
database = DB user_id = Column(String)
content = Column(Text)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class MemoryModel(BaseModel): class MemoryModel(BaseModel):
@ -32,6 +30,8 @@ class MemoryModel(BaseModel):
updated_at: int # timestamp in epoch updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -39,94 +39,110 @@ class MemoryModel(BaseModel):
class MemoriesTable: class MemoriesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Memory])
def insert_new_memory( def insert_new_memory(
self, self,
user_id: str, user_id: str,
content: str, content: str,
) -> Optional[MemoryModel]: ) -> Optional[MemoryModel]:
id = str(uuid.uuid4())
memory = MemoryModel( with get_db() as db:
**{ id = str(uuid.uuid4())
"id": id,
"user_id": user_id, memory = MemoryModel(
"content": content, **{
"created_at": int(time.time()), "id": id,
"updated_at": int(time.time()), "user_id": user_id,
} "content": content,
) "created_at": int(time.time()),
result = Memory.create(**memory.model_dump()) "updated_at": int(time.time()),
if result: }
return memory )
else: result = Memory(**memory.model_dump())
return None db.add(result)
db.commit()
db.refresh(result)
if result:
return MemoryModel.model_validate(result)
else:
return None
def update_memory_by_id( def update_memory_by_id(
self, self,
id: str, id: str,
content: str, content: str,
) -> Optional[MemoryModel]: ) -> Optional[MemoryModel]:
try: with get_db() as db:
memory = Memory.get(Memory.id == id)
memory.content = content try:
memory.updated_at = int(time.time()) db.query(Memory).filter_by(id=id).update(
memory.save() {"content": content, "updated_at": int(time.time())}
return MemoryModel(**model_to_dict(memory)) )
except: db.commit()
return None return self.get_memory_by_id(id)
except:
return None
def get_memories(self) -> List[MemoryModel]: def get_memories(self) -> List[MemoryModel]:
try: with get_db() as db:
memories = Memory.select()
return [MemoryModel(**model_to_dict(memory)) for memory in memories] try:
except: memories = db.query(Memory).all()
return None return [MemoryModel.model_validate(memory) for memory in memories]
except:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]: def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
try: with get_db() as db:
memories = Memory.select().where(Memory.user_id == user_id)
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memory_by_id(self, id) -> Optional[MemoryModel]: try:
try: memories = db.query(Memory).filter_by(user_id=user_id).all()
memory = Memory.get(Memory.id == id) return [MemoryModel.model_validate(memory) for memory in memories]
return MemoryModel(**model_to_dict(memory)) except:
except: return None
return None
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
with get_db() as db:
try:
memory = db.get(Memory, id)
return MemoryModel.model_validate(memory)
except:
return None
def delete_memory_by_id(self, id: str) -> bool: def delete_memory_by_id(self, id: str) -> bool:
try: with get_db() as db:
query = Memory.delete().where(Memory.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True try:
db.query(Memory).filter_by(id=id).delete()
db.commit()
except: return True
return False
except:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool: def delete_memories_by_user_id(self, user_id: str) -> bool:
try: with get_db() as db:
query = Memory.delete().where(Memory.user_id == user_id)
query.execute()
return True try:
except: db.query(Memory).filter_by(user_id=user_id).delete()
return False db.commit()
return True
except:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool: def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try: with get_db() as db:
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
query.execute()
return True try:
except: db.query(Memory).filter_by(id=id, user_id=user_id).delete()
return False db.commit()
return True
except:
return False
Memories = MemoriesTable(DB) Memories = MemoriesTable()

View File

@ -2,13 +2,10 @@ import json
import logging import logging
from typing import Optional from typing import Optional
import peewee as pw
from peewee import *
from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import DB, JSONField from apps.webui.internal.db import Base, JSONField, get_db
from typing import List, Union, Optional from typing import List, Union, Optional
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
@ -32,7 +29,7 @@ class ModelParams(BaseModel):
# ModelMeta is a model for the data stored in the meta field of the Model table # ModelMeta is a model for the data stored in the meta field of the Model table
class ModelMeta(BaseModel): class ModelMeta(BaseModel):
profile_image_url: Optional[str] = "/favicon.png" profile_image_url: Optional[str] = "/static/favicon.png"
description: Optional[str] = None description: Optional[str] = None
""" """
@ -46,38 +43,37 @@ class ModelMeta(BaseModel):
pass pass
class Model(pw.Model): class Model(Base):
id = pw.TextField(unique=True) __tablename__ = "model"
id = Column(Text, primary_key=True)
""" """
The model's id as used in the API. If set to an existing model, it will override the model. The model's id as used in the API. If set to an existing model, it will override the model.
""" """
user_id = pw.TextField() user_id = Column(Text)
base_model_id = pw.TextField(null=True) base_model_id = Column(Text, nullable=True)
""" """
An optional pointer to the actual model that should be used when proxying requests. An optional pointer to the actual model that should be used when proxying requests.
""" """
name = pw.TextField() name = Column(Text)
""" """
The human-readable display name of the model. The human-readable display name of the model.
""" """
params = JSONField() params = Column(JSONField)
""" """
Holds a JSON encoded blob of parameters, see `ModelParams`. Holds a JSON encoded blob of parameters, see `ModelParams`.
""" """
meta = JSONField() meta = Column(JSONField)
""" """
Holds a JSON encoded blob of metadata, see `ModelMeta`. Holds a JSON encoded blob of metadata, see `ModelMeta`.
""" """
updated_at = BigIntegerField() updated_at = Column(BigInteger)
created_at = BigIntegerField() created_at = Column(BigInteger)
class Meta:
database = DB
class ModelModel(BaseModel): class ModelModel(BaseModel):
@ -92,6 +88,8 @@ class ModelModel(BaseModel):
updated_at: int # timestamp in epoch updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -115,12 +113,6 @@ class ModelForm(BaseModel):
class ModelsTable: class ModelsTable:
def __init__(
self,
db: pw.SqliteDatabase | pw.PostgresqlDatabase,
):
self.db = db
self.db.create_tables([Model])
def insert_new_model( def insert_new_model(
self, form_data: ModelForm, user_id: str self, form_data: ModelForm, user_id: str
@ -134,34 +126,50 @@ class ModelsTable:
} }
) )
try: try:
result = Model.create(**model.model_dump())
if result: with get_db() as db:
return model
else: result = Model(**model.model_dump())
return None db.add(result)
db.commit()
db.refresh(result)
if result:
return ModelModel.model_validate(result)
else:
return None
except Exception as e: except Exception as e:
print(e) print(e)
return None return None
def get_all_models(self) -> List[ModelModel]: def get_all_models(self) -> List[ModelModel]:
return [ModelModel(**model_to_dict(model)) for model in Model.select()] with get_db() as db:
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
def get_model_by_id(self, id: str) -> Optional[ModelModel]: def get_model_by_id(self, id: str) -> Optional[ModelModel]:
try: try:
model = Model.get(Model.id == id) with get_db() as db:
return ModelModel(**model_to_dict(model))
model = db.get(Model, id)
return ModelModel.model_validate(model)
except: except:
return None return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]: def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
try: try:
# update only the fields that are present in the model with get_db() as db:
query = Model.update(**model.model_dump()).where(Model.id == id) # update only the fields that are present in the model
query.execute() result = (
db.query(Model)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}, exclude_none=True))
)
db.commit()
model = Model.get(Model.id == id) model = db.get(Model, id)
return ModelModel(**model_to_dict(model)) db.refresh(model)
return ModelModel.model_validate(model)
except Exception as e: except Exception as e:
print(e) print(e)
@ -169,11 +177,14 @@ class ModelsTable:
def delete_model_by_id(self, id: str) -> bool: def delete_model_by_id(self, id: str) -> bool:
try: try:
query = Model.delete().where(Model.id == id) with get_db() as db:
query.execute()
return True db.query(Model).filter_by(id=id).delete()
db.commit()
return True
except: except:
return False return False
Models = ModelsTable(DB) Models = ModelsTable()

View File

@ -1,13 +1,10 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import * from typing import List, Optional
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time import time
from utils.utils import decode_token from sqlalchemy import String, Column, BigInteger, Text
from utils.misc import get_gravatar_url
from apps.webui.internal.db import DB from apps.webui.internal.db import Base, get_db
import json import json
@ -16,15 +13,14 @@ import json
#################### ####################
class Prompt(Model): class Prompt(Base):
command = CharField(unique=True) __tablename__ = "prompt"
user_id = CharField()
title = TextField()
content = TextField()
timestamp = BigIntegerField()
class Meta: command = Column(String, primary_key=True)
database = DB user_id = Column(String)
title = Column(Text)
content = Column(Text)
timestamp = Column(BigInteger)
class PromptModel(BaseModel): class PromptModel(BaseModel):
@ -34,6 +30,8 @@ class PromptModel(BaseModel):
content: str content: str
timestamp: int # timestamp in epoch timestamp: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -48,10 +46,6 @@ class PromptForm(BaseModel):
class PromptsTable: class PromptsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Prompt])
def insert_new_prompt( def insert_new_prompt(
self, user_id: str, form_data: PromptForm self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
@ -66,53 +60,60 @@ class PromptsTable:
) )
try: try:
result = Prompt.create(**prompt.model_dump()) with get_db() as db:
if result:
return prompt result = Prompt(**prompt.dict())
else: db.add(result)
return None db.commit()
except: db.refresh(result)
if result:
return PromptModel.model_validate(result)
else:
return None
except Exception as e:
return None return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]: def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
try: try:
prompt = Prompt.get(Prompt.command == command) with get_db() as db:
return PromptModel(**model_to_dict(prompt))
prompt = db.query(Prompt).filter_by(command=command).first()
return PromptModel.model_validate(prompt)
except: except:
return None return None
def get_prompts(self) -> List[PromptModel]: def get_prompts(self) -> List[PromptModel]:
return [ with get_db() as db:
PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select() return [
# .limit(limit).offset(skip) PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
] ]
def update_prompt_by_command( def update_prompt_by_command(
self, command: str, form_data: PromptForm self, command: str, form_data: PromptForm
) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
try: try:
query = Prompt.update( with get_db() as db:
title=form_data.title,
content=form_data.content,
timestamp=int(time.time()),
).where(Prompt.command == command)
query.execute() prompt = db.query(Prompt).filter_by(command=command).first()
prompt.title = form_data.title
prompt = Prompt.get(Prompt.command == command) prompt.content = form_data.content
return PromptModel(**model_to_dict(prompt)) prompt.timestamp = int(time.time())
db.commit()
return PromptModel.model_validate(prompt)
except: except:
return None return None
def delete_prompt_by_command(self, command: str) -> bool: def delete_prompt_by_command(self, command: str) -> bool:
try: try:
query = Prompt.delete().where((Prompt.command == command)) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed.
return True db.query(Prompt).filter_by(command=command).delete()
db.commit()
return True
except: except:
return False return False
Prompts = PromptsTable(DB) Prompts = PromptsTable()

View File

@ -1,14 +1,14 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional from typing import List, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
import json import json
import uuid import uuid
import time import time
import logging import logging
from apps.webui.internal.db import DB from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS from config import SRC_LOG_LEVELS
@ -20,25 +20,23 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class Tag(Model): class Tag(Base):
id = CharField(unique=True) __tablename__ = "tag"
name = CharField()
user_id = CharField()
data = TextField(null=True)
class Meta: id = Column(String, primary_key=True)
database = DB name = Column(String)
user_id = Column(String)
data = Column(Text, nullable=True)
class ChatIdTag(Model): class ChatIdTag(Base):
id = CharField(unique=True) __tablename__ = "chatidtag"
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
timestamp = BigIntegerField()
class Meta: id = Column(String, primary_key=True)
database = DB tag_name = Column(String)
chat_id = Column(String)
user_id = Column(String)
timestamp = Column(BigInteger)
class TagModel(BaseModel): class TagModel(BaseModel):
@ -47,6 +45,8 @@ class TagModel(BaseModel):
user_id: str user_id: str
data: Optional[str] = None data: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ChatIdTagModel(BaseModel): class ChatIdTagModel(BaseModel):
id: str id: str
@ -55,6 +55,8 @@ class ChatIdTagModel(BaseModel):
user_id: str user_id: str
timestamp: int timestamp: int
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -75,28 +77,31 @@ class ChatTagsResponse(BaseModel):
class TagTable: class TagTable:
def __init__(self, db):
self.db = db
db.create_tables([Tag, ChatIdTag])
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]: def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
id = str(uuid.uuid4()) with get_db() as db:
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try: id = str(uuid.uuid4())
result = Tag.create(**tag.model_dump()) tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
if result: try:
return tag result = Tag(**tag.model_dump())
else: db.add(result)
db.commit()
db.refresh(result)
if result:
return TagModel.model_validate(result)
else:
return None
except Exception as e:
return None return None
except Exception as e:
return None
def get_tag_by_name_and_user_id( def get_tag_by_name_and_user_id(
self, name: str, user_id: str self, name: str, user_id: str
) -> Optional[TagModel]: ) -> Optional[TagModel]:
try: try:
tag = Tag.get(Tag.name == name, Tag.user_id == user_id) with get_db() as db:
return TagModel(**model_to_dict(tag)) tag = db.query(Tag).filter(name=name, user_id=user_id).first()
return TagModel.model_validate(tag)
except Exception as e: except Exception as e:
return None return None
@ -118,82 +123,110 @@ class TagTable:
} }
) )
try: try:
result = ChatIdTag.create(**chatIdTag.model_dump()) with get_db() as db:
if result: result = ChatIdTag(**chatIdTag.model_dump())
return chatIdTag db.add(result)
else: db.commit()
return None db.refresh(result)
if result:
return ChatIdTagModel.model_validate(result)
else:
return None
except: except:
return None return None
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]: def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
tag_names = [ with get_db() as db:
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name tag_names = [
for chat_id_tag in ChatIdTag.select() chat_id_tag.tag_name
.where(ChatIdTag.user_id == user_id) for chat_id_tag in (
.order_by(ChatIdTag.timestamp.desc()) db.query(ChatIdTag)
] .filter_by(user_id=user_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [ return [
TagModel(**model_to_dict(tag)) TagModel.model_validate(tag)
for tag in Tag.select() for tag in (
.where(Tag.user_id == user_id) db.query(Tag)
.where(Tag.name.in_(tag_names)) .filter_by(user_id=user_id)
] .filter(Tag.name.in_(tag_names))
.all()
)
]
def get_tags_by_chat_id_and_user_id( def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str self, chat_id: str, user_id: str
) -> List[TagModel]: ) -> List[TagModel]:
tag_names = [ with get_db() as db:
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
for chat_id_tag in ChatIdTag.select()
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id))
.order_by(ChatIdTag.timestamp.desc())
]
return [ tag_names = [
TagModel(**model_to_dict(tag)) chat_id_tag.tag_name
for tag in Tag.select() for chat_id_tag in (
.where(Tag.user_id == user_id) db.query(ChatIdTag)
.where(Tag.name.in_(tag_names)) .filter_by(user_id=user_id, chat_id=chat_id)
] .order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
]
def get_chat_ids_by_tag_name_and_user_id( def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str self, tag_name: str, user_id: str
) -> Optional[ChatIdTagModel]: ) -> List[ChatIdTagModel]:
return [ with get_db() as db:
ChatIdTagModel(**model_to_dict(chat_id_tag))
for chat_id_tag in ChatIdTag.select() return [
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name)) ChatIdTagModel.model_validate(chat_id_tag)
.order_by(ChatIdTag.timestamp.desc()) for chat_id_tag in (
] db.query(ChatIdTag)
.filter_by(user_id=user_id, tag_name=tag_name)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
def count_chat_ids_by_tag_name_and_user_id( def count_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str self, tag_name: str, user_id: str
) -> int: ) -> int:
return ( with get_db() as db:
ChatIdTag.select()
.where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)) return (
.count() db.query(ChatIdTag)
) .filter_by(tag_name=tag_name, user_id=user_id)
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool: def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try: try:
query = ChatIdTag.delete().where( with get_db() as db:
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id) res = (
) db.query(ChatIdTag)
res = query.execute() # Remove the rows, return number of rows removed. .filter_by(tag_name=tag_name, user_id=user_id)
log.debug(f"res: {res}") .delete()
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. log.debug(f"res: {res}")
db.commit()
return True 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
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e: except Exception as e:
log.error(f"delete_tag: {e}") log.error(f"delete_tag: {e}")
return False return False
@ -202,23 +235,25 @@ class TagTable:
self, tag_name: str, chat_id: str, user_id: str self, tag_name: str, chat_id: str, user_id: str
) -> bool: ) -> bool:
try: try:
query = ChatIdTag.delete().where( with get_db() as db:
(ChatIdTag.tag_name == tag_name)
& (ChatIdTag.chat_id == chat_id)
& (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) res = (
if tag_count == 0: db.query(ChatIdTag)
# Remove tag item from Tag col as well .filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
query = Tag.delete().where( .delete()
(Tag.name == tag_name) & (Tag.user_id == user_id)
) )
query.execute() # Remove the rows, return number of rows removed. log.debug(f"res: {res}")
db.commit()
return True 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
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e: except Exception as e:
log.error(f"delete_tag: {e}") log.error(f"delete_tag: {e}")
return False return False
@ -234,4 +269,4 @@ class TagTable:
return True return True
Tags = TagTable(DB) Tags = TagTable()

View File

@ -1,10 +1,10 @@
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import * from typing import List, Optional
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time import time
import logging import logging
from apps.webui.internal.db import DB, JSONField from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from apps.webui.models.users import Users from apps.webui.models.users import Users
import json import json
@ -21,19 +21,18 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
#################### ####################
class Tool(Model): class Tool(Base):
id = CharField(unique=True) __tablename__ = "tool"
user_id = CharField()
name = TextField()
content = TextField()
specs = JSONField()
meta = JSONField()
valves = JSONField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta: id = Column(String, primary_key=True)
database = DB user_id = Column(String)
name = Column(Text)
content = Column(Text)
specs = Column(JSONField)
meta = Column(JSONField)
valves = Column(JSONField)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class ToolMeta(BaseModel): class ToolMeta(BaseModel):
@ -51,6 +50,8 @@ class ToolModel(BaseModel):
updated_at: int # timestamp in epoch updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -78,61 +79,68 @@ class ToolValves(BaseModel):
class ToolsTable: class ToolsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Tool])
def insert_new_tool( def insert_new_tool(
self, user_id: str, form_data: ToolForm, specs: List[dict] self, user_id: str, form_data: ToolForm, specs: List[dict]
) -> Optional[ToolModel]: ) -> Optional[ToolModel]:
tool = ToolModel(
**{
**form_data.model_dump(),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try: with get_db() as db:
result = Tool.create(**tool.model_dump())
if result: tool = ToolModel(
return tool **{
else: **form_data.model_dump(),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try:
result = Tool(**tool.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ToolModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]: def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
try: try:
tool = Tool.get(Tool.id == id) with get_db() as db:
return ToolModel(**model_to_dict(tool))
tool = db.get(Tool, id)
return ToolModel.model_validate(tool)
except: except:
return None return None
def get_tools(self) -> List[ToolModel]: def get_tools(self) -> List[ToolModel]:
return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()] with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]: def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
try: try:
tool = Tool.get(Tool.id == id) with get_db() as db:
return tool.valves if tool.valves else {}
tool = db.get(Tool, id)
return tool.valves if tool.valves else {}
except Exception as e: except Exception as e:
print(f"An error occurred: {e}") print(f"An error occurred: {e}")
return None return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]: def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
try: try:
query = Tool.update( with get_db() as db:
**{"valves": valves},
updated_at=int(time.time()),
).where(Tool.id == id)
query.execute()
tool = Tool.get(Tool.id == id) db.query(Tool).filter_by(id=id).update(
return ToolValves(**model_to_dict(tool)) {"valves": valves, "updated_at": int(time.time())}
)
db.commit()
return self.get_tool_by_id(id)
except: except:
return None return None
@ -141,7 +149,7 @@ class ToolsTable:
) -> Optional[dict]: ) -> Optional[dict]:
try: try:
user = Users.get_user_by_id(user_id) user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings # Check if user has "tools" and "valves" settings
if "tools" not in user_settings: if "tools" not in user_settings:
@ -159,7 +167,7 @@ class ToolsTable:
) -> Optional[dict]: ) -> Optional[dict]:
try: try:
user = Users.get_user_by_id(user_id) user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings # Check if user has "tools" and "valves" settings
if "tools" not in user_settings: if "tools" not in user_settings:
@ -179,25 +187,27 @@ class ToolsTable:
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]: def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
try: try:
query = Tool.update( with get_db() as db:
**updated, db.query(Tool).filter_by(id=id).update(
updated_at=int(time.time()), {**updated, "updated_at": int(time.time())}
).where(Tool.id == id) )
query.execute() db.commit()
tool = Tool.get(Tool.id == id) tool = db.query(Tool).get(id)
return ToolModel(**model_to_dict(tool)) db.refresh(tool)
return ToolModel.model_validate(tool)
except: except:
return None return None
def delete_tool_by_id(self, id: str) -> bool: def delete_tool_by_id(self, id: str) -> bool:
try: try:
query = Tool.delete().where((Tool.id == id)) with get_db() as db:
query.execute() # Remove the rows, return number of rows removed. db.query(Tool).filter_by(id=id).delete()
db.commit()
return True return True
except: except:
return False return False
Tools = ToolsTable(DB) Tools = ToolsTable()

View File

@ -1,11 +1,12 @@
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict, parse_obj_as
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional from typing import List, Union, Optional
import time import time
from sqlalchemy import String, Column, BigInteger, Text
from utils.misc import get_gravatar_url from utils.misc import get_gravatar_url
from apps.webui.internal.db import DB, JSONField from apps.webui.internal.db import Base, JSONField, Session, get_db
from apps.webui.models.chats import Chats from apps.webui.models.chats import Chats
#################### ####################
@ -13,25 +14,24 @@ from apps.webui.models.chats import Chats
#################### ####################
class User(Model): class User(Base):
id = CharField(unique=True) __tablename__ = "user"
name = CharField()
email = CharField()
role = CharField()
profile_image_url = TextField()
last_active_at = BigIntegerField() id = Column(String, primary_key=True)
updated_at = BigIntegerField() name = Column(String)
created_at = BigIntegerField() email = Column(String)
role = Column(String)
profile_image_url = Column(Text)
api_key = CharField(null=True, unique=True) last_active_at = Column(BigInteger)
settings = JSONField(null=True) updated_at = Column(BigInteger)
info = JSONField(null=True) created_at = Column(BigInteger)
oauth_sub = TextField(null=True, unique=True) api_key = Column(String, nullable=True, unique=True)
settings = Column(JSONField, nullable=True)
info = Column(JSONField, nullable=True)
class Meta: oauth_sub = Column(Text, unique=True)
database = DB
class UserSettings(BaseModel): class UserSettings(BaseModel):
@ -57,6 +57,8 @@ class UserModel(BaseModel):
oauth_sub: Optional[str] = None oauth_sub: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
#################### ####################
# Forms # Forms
@ -76,9 +78,6 @@ class UserUpdateForm(BaseModel):
class UsersTable: class UsersTable:
def __init__(self, db):
self.db = db
self.db.create_tables([User])
def insert_new_user( def insert_new_user(
self, self,
@ -89,77 +88,92 @@ class UsersTable:
role: str = "pending", role: str = "pending",
oauth_sub: Optional[str] = None, oauth_sub: Optional[str] = None,
) -> Optional[UserModel]: ) -> Optional[UserModel]:
user = UserModel( with get_db() as db:
**{ user = UserModel(
"id": id, **{
"name": name, "id": id,
"email": email, "name": name,
"role": role, "email": email,
"profile_image_url": profile_image_url, "role": role,
"last_active_at": int(time.time()), "profile_image_url": profile_image_url,
"created_at": int(time.time()), "last_active_at": int(time.time()),
"updated_at": int(time.time()), "created_at": int(time.time()),
"oauth_sub": oauth_sub, "updated_at": int(time.time()),
} "oauth_sub": oauth_sub,
) }
result = User.create(**user.model_dump()) )
if result: result = User(**user.model_dump())
return user db.add(result)
else: db.commit()
return None db.refresh(result)
if result:
return user
else:
return None
def get_user_by_id(self, id: str) -> Optional[UserModel]: def get_user_by_id(self, id: str) -> Optional[UserModel]:
try: try:
user = User.get(User.id == id) with get_db() as db:
return UserModel(**model_to_dict(user)) user = db.query(User).filter_by(id=id).first()
except: return UserModel.model_validate(user)
except Exception as e:
return None return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]: def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try: try:
user = User.get(User.api_key == api_key) with get_db() as db:
return UserModel(**model_to_dict(user))
user = db.query(User).filter_by(api_key=api_key).first()
return UserModel.model_validate(user)
except: except:
return None return None
def get_user_by_email(self, email: str) -> Optional[UserModel]: def get_user_by_email(self, email: str) -> Optional[UserModel]:
try: try:
user = User.get(User.email == email) with get_db() as db:
return UserModel(**model_to_dict(user))
user = db.query(User).filter_by(email=email).first()
return UserModel.model_validate(user)
except: except:
return None return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]: def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try: try:
user = User.get(User.oauth_sub == sub) with get_db() as db:
return UserModel(**model_to_dict(user))
user = db.query(User).filter_by(oauth_sub=sub).first()
return UserModel.model_validate(user)
except: except:
return None return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]: def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
return [ with get_db() as db:
UserModel(**model_to_dict(user)) users = (
for user in User.select() db.query(User)
# .limit(limit).offset(skip) # .offset(skip).limit(limit)
] .all()
)
return [UserModel.model_validate(user) for user in users]
def get_num_users(self) -> Optional[int]: def get_num_users(self) -> Optional[int]:
return User.select().count() with get_db() as db:
return db.query(User).count()
def get_first_user(self) -> UserModel: def get_first_user(self) -> UserModel:
try: try:
user = User.select().order_by(User.created_at).first() with get_db() as db:
return UserModel(**model_to_dict(user)) user = db.query(User).order_by(User.created_at).first()
return UserModel.model_validate(user)
except: except:
return None return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]: def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try: try:
query = User.update(role=role).where(User.id == id) with get_db() as db:
query.execute() db.query(User).filter_by(id=id).update({"role": role})
db.commit()
user = User.get(User.id == id) user = db.query(User).filter_by(id=id).first()
return UserModel(**model_to_dict(user)) return UserModel.model_validate(user)
except: except:
return None return None
@ -167,23 +181,28 @@ class UsersTable:
self, id: str, profile_image_url: str self, id: str, profile_image_url: str
) -> Optional[UserModel]: ) -> Optional[UserModel]:
try: try:
query = User.update(profile_image_url=profile_image_url).where( with get_db() as db:
User.id == id db.query(User).filter_by(id=id).update(
) {"profile_image_url": profile_image_url}
query.execute() )
db.commit()
user = User.get(User.id == id) user = db.query(User).filter_by(id=id).first()
return UserModel(**model_to_dict(user)) return UserModel.model_validate(user)
except: except:
return None return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]: def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try: try:
query = User.update(last_active_at=int(time.time())).where(User.id == id) with get_db() as db:
query.execute()
user = User.get(User.id == id) db.query(User).filter_by(id=id).update(
return UserModel(**model_to_dict(user)) {"last_active_at": int(time.time())}
)
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except: except:
return None return None
@ -191,22 +210,25 @@ class UsersTable:
self, id: str, oauth_sub: str self, id: str, oauth_sub: str
) -> Optional[UserModel]: ) -> Optional[UserModel]:
try: try:
query = User.update(oauth_sub=oauth_sub).where(User.id == id) with get_db() as db:
query.execute() db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
db.commit()
user = User.get(User.id == id) user = db.query(User).filter_by(id=id).first()
return UserModel(**model_to_dict(user)) return UserModel.model_validate(user)
except: except:
return None return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try: try:
query = User.update(**updated).where(User.id == id) with get_db() as db:
query.execute() db.query(User).filter_by(id=id).update(updated)
db.commit()
user = User.get(User.id == id) user = db.query(User).filter_by(id=id).first()
return UserModel(**model_to_dict(user)) return UserModel.model_validate(user)
except: # return UserModel(**user.dict())
except Exception as e:
return None return None
def delete_user_by_id(self, id: str) -> bool: def delete_user_by_id(self, id: str) -> bool:
@ -215,9 +237,10 @@ class UsersTable:
result = Chats.delete_chats_by_user_id(id) result = Chats.delete_chats_by_user_id(id)
if result: if result:
# Delete User with get_db() as db:
query = User.delete().where(User.id == id) # Delete User
query.execute() # Remove the rows, return number of rows removed. db.query(User).filter_by(id=id).delete()
db.commit()
return True return True
else: else:
@ -227,19 +250,20 @@ class UsersTable:
def update_user_api_key_by_id(self, id: str, api_key: str) -> str: def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try: try:
query = User.update(api_key=api_key).where(User.id == id) with get_db() as db:
result = query.execute() result = db.query(User).filter_by(id=id).update({"api_key": api_key})
db.commit()
return True if result == 1 else False return True if result == 1 else False
except: except:
return False return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]: def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try: try:
user = User.get(User.id == id) with get_db() as db:
return user.api_key user = db.query(User).filter_by(id=id).first()
except: return user.api_key
except Exception as e:
return None return None
Users = UsersTable(DB) Users = UsersTable()

View File

@ -76,7 +76,10 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse]) @router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_user_id( async def get_user_chat_list_by_user_id(
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50 user_id: str,
user=Depends(get_admin_user),
skip: int = 0,
limit: int = 50,
): ):
return Chats.get_chat_list_by_user_id( return Chats.get_chat_list_by_user_id(
user_id, include_archived=True, skip=skip, limit=limit user_id, include_archived=True, skip=skip, limit=limit
@ -119,7 +122,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
@router.get("/all/archived", response_model=List[ChatResponse]) @router.get("/all/archived", response_model=List[ChatResponse])
async def get_user_chats(user=Depends(get_verified_user)): async def get_user_archived_chats(user=Depends(get_verified_user)):
return [ return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_archived_chats_by_user_id(user.id) for chat in Chats.get_archived_chats_by_user_id(user.id)
@ -207,7 +210,6 @@ async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_verified_user) form_data: TagNameForm, user=Depends(get_verified_user)
): ):
print(form_data)
chat_ids = [ chat_ids = [
chat_id_tag.chat_id chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id( for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(

View File

@ -130,7 +130,9 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_
@router.post("/doc/update", response_model=Optional[DocumentResponse]) @router.post("/doc/update", response_model=Optional[DocumentResponse])
async def update_doc_by_name( async def update_doc_by_name(
name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user) name: str,
form_data: DocumentUpdateForm,
user=Depends(get_admin_user),
): ):
doc = Documents.update_doc_by_name(name, form_data) doc = Documents.update_doc_by_name(name, form_data)
if doc: if doc:

View File

@ -50,10 +50,7 @@ router = APIRouter()
@router.post("/") @router.post("/")
def upload_file( def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
file: UploadFile = File(...),
user=Depends(get_verified_user),
):
log.info(f"file.content_type: {file.content_type}") log.info(f"file.content_type: {file.content_type}")
try: try:
unsanitized_filename = file.filename unsanitized_filename = file.filename

View File

@ -233,7 +233,10 @@ async def delete_function_by_id(
# delete the function file # delete the function file
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
os.remove(function_path) try:
os.remove(function_path)
except:
pass
return result return result

View File

@ -50,7 +50,9 @@ class MemoryUpdateModel(BaseModel):
@router.post("/add", response_model=Optional[MemoryModel]) @router.post("/add", response_model=Optional[MemoryModel])
async def add_memory( async def add_memory(
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user) request: Request,
form_data: AddMemoryForm,
user=Depends(get_verified_user),
): ):
memory = Memories.insert_new_memory(user.id, form_data.content) memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content) memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)

View File

@ -5,6 +5,7 @@ from typing import List, Union, Optional
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import json import json
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
from utils.utils import get_verified_user, get_admin_user from utils.utils import get_verified_user, get_admin_user
@ -29,7 +30,9 @@ async def get_models(user=Depends(get_verified_user)):
@router.post("/add", response_model=Optional[ModelModel]) @router.post("/add", response_model=Optional[ModelModel])
async def add_new_model( async def add_new_model(
request: Request, form_data: ModelForm, user=Depends(get_admin_user) request: Request,
form_data: ModelForm,
user=Depends(get_admin_user),
): ):
if form_data.id in request.app.state.MODELS: if form_data.id in request.app.state.MODELS:
raise HTTPException( raise HTTPException(
@ -73,7 +76,10 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/update", response_model=Optional[ModelModel]) @router.post("/update", response_model=Optional[ModelModel])
async def update_model_by_id( async def update_model_by_id(
request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user) request: Request,
id: str,
form_data: ModelForm,
user=Depends(get_admin_user),
): ):
model = Models.get_model_by_id(id) model = Models.get_model_by_id(id)
if model: if model:

View File

@ -71,7 +71,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
@router.post("/command/{command}/update", response_model=Optional[PromptModel]) @router.post("/command/{command}/update", response_model=Optional[PromptModel])
async def update_prompt_by_command( async def update_prompt_by_command(
command: str, form_data: PromptForm, user=Depends(get_admin_user) command: str,
form_data: PromptForm,
user=Depends(get_admin_user),
): ):
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data) prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt: if prompt:

View File

@ -6,7 +6,6 @@ from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import json import json
from apps.webui.models.users import Users from apps.webui.models.users import Users
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
from apps.webui.utils import load_toolkit_module_by_id from apps.webui.utils import load_toolkit_module_by_id
@ -57,7 +56,9 @@ async def get_toolkits(user=Depends(get_admin_user)):
@router.post("/create", response_model=Optional[ToolResponse]) @router.post("/create", response_model=Optional[ToolResponse])
async def create_new_toolkit( async def create_new_toolkit(
request: Request, form_data: ToolForm, user=Depends(get_admin_user) request: Request,
form_data: ToolForm,
user=Depends(get_admin_user),
): ):
if not form_data.id.isidentifier(): if not form_data.id.isidentifier():
raise HTTPException( raise HTTPException(
@ -131,7 +132,10 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
@router.post("/id/{id}/update", response_model=Optional[ToolModel]) @router.post("/id/{id}/update", response_model=Optional[ToolModel])
async def update_toolkit_by_id( async def update_toolkit_by_id(
request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user) request: Request,
id: str,
form_data: ToolForm,
user=Depends(get_admin_user),
): ):
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")

View File

@ -138,7 +138,7 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user)):
@router.post("/user/info/update", response_model=Optional[dict]) @router.post("/user/info/update", response_model=Optional[dict])
async def update_user_settings_by_session_user( async def update_user_info_by_session_user(
form_data: dict, user=Depends(get_verified_user) form_data: dict, user=Depends(get_verified_user)
): ):
user = Users.get_user_by_id(user.id) user = Users.get_user_by_id(user.id)
@ -205,7 +205,9 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
@router.post("/{user_id}/update", response_model=Optional[UserModel]) @router.post("/{user_id}/update", response_model=Optional[UserModel])
async def update_user_by_id( async def update_user_by_id(
user_id: str, form_data: UserUpdateForm, session_user=Depends(get_admin_user) user_id: str,
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
): ):
user = Users.get_user_by_id(user_id) user = Users.get_user_by_id(user_id)

View File

@ -1,6 +1,5 @@
from fastapi import APIRouter, UploadFile, File, Response from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel from pydantic import BaseModel
@ -10,7 +9,6 @@ import markdown
import black import black
from apps.webui.internal.db import DB
from utils.utils import get_admin_user from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url from utils.misc import calculate_sha256, get_gravatar_url
@ -114,13 +112,15 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
) )
if not isinstance(DB, SqliteDatabase): from apps.webui.internal.db import engine
if engine.name != "sqlite":
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE, detail=ERROR_MESSAGES.DB_NOT_SQLITE,
) )
return FileResponse( return FileResponse(
DB.database, engine.url.database,
media_type="application/octet-stream", media_type="application/octet-stream",
filename="webui.db", filename="webui.db",
) )

View File

@ -393,6 +393,18 @@ OAUTH_PROVIDER_NAME = PersistentConfig(
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"), os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
) )
OAUTH_USERNAME_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.username_claim",
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
)
OAUTH_PICTURE_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.avatar_claim",
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
)
def load_oauth_providers(): def load_oauth_providers():
OAUTH_PROVIDERS.clear() OAUTH_PROVIDERS.clear()
@ -438,16 +450,27 @@ load_oauth_providers()
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve() STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png" frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists(): if frontend_favicon.exists():
try: try:
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
except Exception as e: except Exception as e:
logging.error(f"An error occurred: {e}") logging.error(f"An error occurred: {e}")
else: else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}") logging.warning(f"Frontend favicon not found at {frontend_favicon}")
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
if frontend_splash.exists():
try:
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
except Exception as e:
logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend splash not found at {frontend_splash}")
#################################### ####################################
# CUSTOM_NAME # CUSTOM_NAME
#################################### ####################################
@ -472,6 +495,19 @@ if CUSTOM_NAME:
r.raw.decode_content = True r.raw.decode_content = True
shutil.copyfileobj(r.raw, f) shutil.copyfileobj(r.raw, f)
if "splash" in data:
url = (
f"https://api.openwebui.com{data['splash']}"
if data["splash"][0] == "/"
else data["splash"]
)
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(f"{STATIC_DIR}/splash.png", "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
WEBUI_NAME = data["name"] WEBUI_NAME = data["name"]
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
@ -766,6 +802,7 @@ class BannerModel(BaseModel):
dismissible: bool dismissible: bool
timestamp: int timestamp: int
try: try:
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]")) banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
banners = [BannerModel(**banner) for banner in banners] banners = [BannerModel(**banner) for banner in banners]
@ -1318,3 +1355,7 @@ AUDIO_TTS_VOICE = PersistentConfig(
#################################### ####################################
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db") DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")

View File

@ -89,3 +89,14 @@ class ERROR_MESSAGES(str, Enum):
OLLAMA_API_DISABLED = ( OLLAMA_API_DISABLED = (
"The Ollama API is disabled. Please enable it to use this feature." "The Ollama API is disabled. Please enable it to use this feature."
) )
class TASKS(str, Enum):
def __str__(self) -> str:
return super().__str__()
DEFAULT = lambda task="": f"{task if task else 'default'}"
TITLE_GENERATION = "Title Generation"
EMOJI_GENERATION = "Emoji Generation"
QUERY_GENERATION = "Query Generation"
FUNCTION_CALLING = "Function Calling"

View File

@ -1,5 +1,6 @@
import base64 import base64
import uuid import uuid
import subprocess
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from authlib.integrations.starlette_client import OAuth from authlib.integrations.starlette_client import OAuth
@ -27,6 +28,7 @@ from fastapi.responses import JSONResponse
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
@ -54,6 +56,7 @@ from apps.webui.main import (
get_pipe_models, get_pipe_models,
generate_function_chat_completion, generate_function_chat_completion,
) )
from apps.webui.internal.db import Session, SessionLocal
from pydantic import BaseModel from pydantic import BaseModel
@ -125,8 +128,10 @@ from config import (
WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE, WEBUI_SESSION_COOKIE_SECURE,
AppConfig, AppConfig,
BACKEND_DIR,
DATABASE_URL,
) )
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES, TASKS
from utils.webhook import post_webhook from utils.webhook import post_webhook
if SAFE_MODE: if SAFE_MODE:
@ -167,8 +172,20 @@ https://github.com/open-webui/open-webui
) )
def run_migrations():
try:
from alembic.config import Config
from alembic import command
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
except Exception as e:
print(f"Error: {e}")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
run_migrations()
yield yield
@ -285,6 +302,7 @@ async def get_function_call_response(
user, user,
model, model,
__event_emitter__=None, __event_emitter__=None,
__event_call__=None,
): ):
tool = Tools.get_tool_by_id(tool_id) tool = Tools.get_tool_by_id(tool_id)
tools_specs = json.dumps(tool.specs, indent=2) tools_specs = json.dumps(tool.specs, indent=2)
@ -311,7 +329,7 @@ async def get_function_call_response(
{"role": "user", "content": f"Query: {prompt}"}, {"role": "user", "content": f"Query: {prompt}"},
], ],
"stream": False, "stream": False,
"function": True, "task": TASKS.FUNCTION_CALLING,
} }
try: try:
@ -324,7 +342,6 @@ async def get_function_call_response(
response = None response = None
try: try:
response = await generate_chat_completions(form_data=payload, user=user) response = await generate_chat_completions(form_data=payload, user=user)
content = None content = None
if hasattr(response, "body_iterator"): if hasattr(response, "body_iterator"):
@ -429,6 +446,13 @@ async def get_function_call_response(
"__event_emitter__": __event_emitter__, "__event_emitter__": __event_emitter__,
} }
if "__event_call__" in sig.parameters:
# Call the function with the '__event_call__' parameter included
params = {
**params,
"__event_call__": __event_call__,
}
if inspect.iscoroutinefunction(function): if inspect.iscoroutinefunction(function):
function_result = await function(**params) function_result = await function(**params)
else: else:
@ -452,7 +476,9 @@ async def get_function_call_response(
return None, None, False return None, None, False
async def chat_completion_functions_handler(body, model, user, __event_emitter__): async def chat_completion_functions_handler(
body, model, user, __event_emitter__, __event_call__
):
skip_files = None skip_files = None
filter_ids = get_filter_function_ids(model) filter_ids = get_filter_function_ids(model)
@ -518,12 +544,19 @@ async def chat_completion_functions_handler(body, model, user, __event_emitter__
**params, **params,
"__model__": model, "__model__": model,
} }
if "__event_emitter__" in sig.parameters: if "__event_emitter__" in sig.parameters:
params = { params = {
**params, **params,
"__event_emitter__": __event_emitter__, "__event_emitter__": __event_emitter__,
} }
if "__event_call__" in sig.parameters:
params = {
**params,
"__event_call__": __event_call__,
}
if inspect.iscoroutinefunction(inlet): if inspect.iscoroutinefunction(inlet):
body = await inlet(**params) body = await inlet(**params)
else: else:
@ -540,7 +573,9 @@ async def chat_completion_functions_handler(body, model, user, __event_emitter__
return body, {} return body, {}
async def chat_completion_tools_handler(body, model, user, __event_emitter__): async def chat_completion_tools_handler(
body, model, user, __event_emitter__, __event_call__
):
skip_files = None skip_files = None
contexts = [] contexts = []
@ -563,6 +598,7 @@ async def chat_completion_tools_handler(body, model, user, __event_emitter__):
user=user, user=user,
model=model, model=model,
__event_emitter__=__event_emitter__, __event_emitter__=__event_emitter__,
__event_call__=__event_call__,
) )
print(file_handler) print(file_handler)
@ -660,6 +696,14 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
to=session_id, to=session_id,
) )
async def __event_call__(data):
response = await sio.call(
"chat-events",
{"chat_id": chat_id, "message_id": message_id, "data": data},
to=session_id,
)
return response
# Initialize data_items to store additional data to be sent to the client # Initialize data_items to store additional data to be sent to the client
data_items = [] data_items = []
@ -669,7 +713,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
try: try:
body, flags = await chat_completion_functions_handler( body, flags = await chat_completion_functions_handler(
body, model, user, __event_emitter__ body, model, user, __event_emitter__, __event_call__
) )
except Exception as e: except Exception as e:
return JSONResponse( return JSONResponse(
@ -679,7 +723,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
try: try:
body, flags = await chat_completion_tools_handler( body, flags = await chat_completion_tools_handler(
body, model, user, __event_emitter__ body, model, user, __event_emitter__, __event_call__
) )
contexts.extend(flags.get("contexts", [])) contexts.extend(flags.get("contexts", []))
@ -834,9 +878,8 @@ def filter_pipeline(payload, user):
pass pass
if "pipeline" not in app.state.MODELS[model_id]: if "pipeline" not in app.state.MODELS[model_id]:
for key in ["title", "task", "function"]: if "task" in payload:
if key in payload: del payload["task"]
del payload[key]
return payload return payload
@ -901,6 +944,14 @@ app.add_middleware(
) )
@app.middleware("http")
async def commit_session_after_request(request: Request, call_next):
response = await call_next(request)
log.debug("Commit session after request")
Session.commit()
return response
@app.middleware("http") @app.middleware("http")
async def check_url(request: Request, call_next): async def check_url(request: Request, call_next):
if len(app.state.MODELS) == 0: if len(app.state.MODELS) == 0:
@ -977,12 +1028,16 @@ async def get_all_models():
model["info"] = custom_model.model_dump() model["info"] = custom_model.model_dump()
else: else:
owned_by = "openai" owned_by = "openai"
pipe = None
for model in models: for model in models:
if ( if (
custom_model.base_model_id == model["id"] custom_model.base_model_id == model["id"]
or custom_model.base_model_id == model["id"].split(":")[0] or custom_model.base_model_id == model["id"].split(":")[0]
): ):
owned_by = model["owned_by"] owned_by = model["owned_by"]
if "pipe" in model:
pipe = model["pipe"]
break break
models.append( models.append(
@ -994,11 +1049,11 @@ async def get_all_models():
"owned_by": owned_by, "owned_by": owned_by,
"info": custom_model.model_dump(), "info": custom_model.model_dump(),
"preset": True, "preset": True,
**({"pipe": pipe} if pipe is not None else {}),
} }
) )
app.state.MODELS = {model["id"]: model for model in models} app.state.MODELS = {model["id"]: model for model in models}
webui_app.state.MODELS = app.state.MODELS webui_app.state.MODELS = app.state.MODELS
return models return models
@ -1133,6 +1188,14 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
to=data["session_id"], to=data["session_id"],
) )
async def __event_call__(data):
response = await sio.call(
"chat-events",
{"chat_id": data["chat_id"], "message_id": data["id"], "data": data},
to=data["session_id"],
)
return response
def get_priority(function_id): def get_priority(function_id):
function = Functions.get_function_by_id(function_id) function = Functions.get_function_by_id(function_id)
if function is not None and hasattr(function, "valves"): if function is not None and hasattr(function, "valves"):
@ -1220,6 +1283,12 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
"__event_emitter__": __event_emitter__, "__event_emitter__": __event_emitter__,
} }
if "__event_call__" in sig.parameters:
params = {
**params,
"__event_call__": __event_call__,
}
if inspect.iscoroutinefunction(outlet): if inspect.iscoroutinefunction(outlet):
data = await outlet(**params) data = await outlet(**params)
else: else:
@ -1337,7 +1406,7 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
"stream": False, "stream": False,
"max_tokens": 50, "max_tokens": 50,
"chat_id": form_data.get("chat_id", None), "chat_id": form_data.get("chat_id", None),
"title": True, "task": TASKS.TITLE_GENERATION,
} }
log.debug(payload) log.debug(payload)
@ -1400,7 +1469,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
"messages": [{"role": "user", "content": content}], "messages": [{"role": "user", "content": content}],
"stream": False, "stream": False,
"max_tokens": 30, "max_tokens": 30,
"task": True, "task": TASKS.QUERY_GENERATION,
} }
print(payload) print(payload)
@ -1467,7 +1536,7 @@ Message: """{{prompt}}"""
"stream": False, "stream": False,
"max_tokens": 4, "max_tokens": 4,
"chat_id": form_data.get("chat_id", None), "chat_id": form_data.get("chat_id", None),
"task": True, "task": TASKS.EMOJI_GENERATION,
} }
log.debug(payload) log.debug(payload)
@ -1742,7 +1811,9 @@ async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_use
@app.get("/api/pipelines/{pipeline_id}/valves") @app.get("/api/pipelines/{pipeline_id}/valves")
async def get_pipeline_valves( async def get_pipeline_valves(
urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user) urlIdx: Optional[int],
pipeline_id: str,
user=Depends(get_admin_user),
): ):
models = await get_all_models() models = await get_all_models()
r = None r = None
@ -1780,7 +1851,9 @@ async def get_pipeline_valves(
@app.get("/api/pipelines/{pipeline_id}/valves/spec") @app.get("/api/pipelines/{pipeline_id}/valves/spec")
async def get_pipeline_valves_spec( async def get_pipeline_valves_spec(
urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user) urlIdx: Optional[int],
pipeline_id: str,
user=Depends(get_admin_user),
): ):
models = await get_all_models() models = await get_all_models()
@ -2066,7 +2139,8 @@ async def oauth_callback(provider: str, request: Request, response: Response):
if existing_user: if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_url = user_data.get("picture", "") picture_claim = webui_app.state.config.OAUTH_PICTURE_CLAIM
picture_url = user_data.get(picture_claim, "")
if picture_url: if picture_url:
# Download the profile image into a base64 string # Download the profile image into a base64 string
try: try:
@ -2086,6 +2160,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
picture_url = "" picture_url = ""
if not picture_url: if not picture_url:
picture_url = "/user.png" picture_url = "/user.png"
username_claim = webui_app.state.config.OAUTH_USERNAME_CLAIM
role = ( role = (
"admin" "admin"
if Users.get_num_users() == 0 if Users.get_num_users() == 0
@ -2096,7 +2171,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
password=get_password_hash( password=get_password_hash(
str(uuid.uuid4()) str(uuid.uuid4())
), # Random password, not used ), # Random password, not used
name=user_data.get("name", "User"), name=user_data.get(username_claim, "User"),
profile_image_url=picture_url, profile_image_url=picture_url,
role=role, role=role,
oauth_sub=provider_sub, oauth_sub=provider_sub,
@ -2154,7 +2229,7 @@ async def get_opensearch_xml():
<ShortName>{WEBUI_NAME}</ShortName> <ShortName>{WEBUI_NAME}</ShortName>
<Description>Search {WEBUI_NAME}</Description> <Description>Search {WEBUI_NAME}</Description>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image> <Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/static/favicon.png</Image>
<Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/> <Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
<moz:SearchForm>{WEBUI_URL}</moz:SearchForm> <moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
</OpenSearchDescription> </OpenSearchDescription>
@ -2167,6 +2242,12 @@ async def healthcheck():
return {"status": True} return {"status": True}
@app.get("/health/db")
async def healthcheck_with_db():
Session.execute(text("SELECT 1;")).all()
return {"status": True}
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache") app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")

View File

@ -0,0 +1,4 @@
Generic single-database configuration.
Create new migrations with
DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description"

96
backend/migrations/env.py Normal file
View File

@ -0,0 +1,96 @@
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from apps.webui.models.auths import Auth
from apps.webui.models.chats import Chat
from apps.webui.models.documents import Document
from apps.webui.models.memories import Memory
from apps.webui.models.models import Model
from apps.webui.models.prompts import Prompt
from apps.webui.models.tags import Tag, ChatIdTag
from apps.webui.models.tools import Tool
from apps.webui.models.users import User
from apps.webui.models.files import File
from apps.webui.models.functions import Function
from config import DATABASE_URL
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Auth.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
DB_URL = DATABASE_URL
if DB_URL:
config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%"))
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import apps.webui.internal.db
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,9 @@
from alembic import op
from sqlalchemy import Inspector
def get_existing_tables():
con = op.get_bind()
inspector = Inspector.from_engine(con)
tables = set(inspector.get_table_names())
return tables

View File

@ -0,0 +1,202 @@
"""init
Revision ID: 7e5b5dc7342b
Revises:
Create Date: 2024-06-24 13:15:33.808998
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import apps.webui.internal.db
from migrations.util import get_existing_tables
# revision identifiers, used by Alembic.
revision: str = "7e5b5dc7342b"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
existing_tables = set(get_existing_tables())
# ### commands auto generated by Alembic - please adjust! ###
if "auth" not in existing_tables:
op.create_table(
"auth",
sa.Column("id", sa.String(), nullable=False),
sa.Column("email", sa.String(), nullable=True),
sa.Column("password", sa.Text(), nullable=True),
sa.Column("active", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "chat" not in existing_tables:
op.create_table(
"chat",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("chat", sa.Text(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("share_id", sa.Text(), nullable=True),
sa.Column("archived", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("share_id"),
)
if "chatidtag" not in existing_tables:
op.create_table(
"chatidtag",
sa.Column("id", sa.String(), nullable=False),
sa.Column("tag_name", sa.String(), nullable=True),
sa.Column("chat_id", sa.String(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "document" not in existing_tables:
op.create_table(
"document",
sa.Column("collection_name", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("filename", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("collection_name"),
sa.UniqueConstraint("name"),
)
if "file" not in existing_tables:
op.create_table(
"file",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("filename", sa.Text(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "function" not in existing_tables:
op.create_table(
"function",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("type", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("is_global", sa.Boolean(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "memory" not in existing_tables:
op.create_table(
"memory",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "model" not in existing_tables:
op.create_table(
"model",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("base_model_id", sa.Text(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "prompt" not in existing_tables:
op.create_table(
"prompt",
sa.Column("command", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("command"),
)
if "tag" not in existing_tables:
op.create_table(
"tag",
sa.Column("id", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("data", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "tool" not in existing_tables:
op.create_table(
"tool",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "user" not in existing_tables:
op.create_table(
"user",
sa.Column("id", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("email", sa.String(), nullable=True),
sa.Column("role", sa.String(), nullable=True),
sa.Column("profile_image_url", sa.Text(), nullable=True),
sa.Column("last_active_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("api_key", sa.String(), nullable=True),
sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("oauth_sub", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("api_key"),
sa.UniqueConstraint("oauth_sub"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("user")
op.drop_table("tool")
op.drop_table("tag")
op.drop_table("prompt")
op.drop_table("model")
op.drop_table("memory")
op.drop_table("function")
op.drop_table("file")
op.drop_table("document")
op.drop_table("chatidtag")
op.drop_table("chat")
op.drop_table("auth")
# ### end Alembic commands ###

View File

@ -12,7 +12,9 @@ passlib[bcrypt]==1.7.4
requests==2.32.3 requests==2.32.3
aiohttp==3.9.5 aiohttp==3.9.5
peewee==3.17.5 sqlalchemy==2.0.30
alembic==1.13.2
peewee==3.17.6
peewee-migrate==1.12.2 peewee-migrate==1.12.2
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
PyMySQL==1.1.1 PyMySQL==1.1.1
@ -49,7 +51,7 @@ pyxlsb==1.0.10
xlrd==2.0.1 xlrd==2.0.1
validators==0.28.1 validators==0.28.1
opencv-python-headless==4.9.0.80 opencv-python-headless==4.10.0.84
rapidocr-onnxruntime==1.3.22 rapidocr-onnxruntime==1.3.22
fpdf2==2.7.9 fpdf2==2.7.9
@ -61,10 +63,15 @@ PyJWT[crypto]==2.8.0
authlib==1.3.1 authlib==1.3.1
black==24.4.2 black==24.4.2
langfuse==2.36.2 langfuse==2.38.0
youtube-transcript-api==0.6.2 youtube-transcript-api==0.6.2
pytube==15.0.0 pytube==15.0.0
extract_msg extract_msg
pydub pydub
duckduckgo-search~=6.1.7 duckduckgo-search~=6.1.7
## Tests
docker~=7.1.0
pytest~=8.2.2
pytest-docker~=3.1.1

BIN
backend/static/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

0
backend/test/__init__.py Normal file
View File

View File

@ -0,0 +1,202 @@
import pytest
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestAuths(AbstractPostgresTest):
BASE_PATH = "/api/v1/auths"
def setup_class(cls):
super().setup_class()
from apps.webui.models.users import Users
from apps.webui.models.auths import Auths
cls.users = Users
cls.auths = Auths
def test_get_session_user(self):
with mock_webui_user():
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert response.json() == {
"id": "1",
"name": "John Doe",
"email": "john.doe@openwebui.com",
"role": "user",
"profile_image_url": "/user.png",
}
def test_update_profile(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("old_password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(
self.create_url("/update/profile"),
json={"name": "John Doe 2", "profile_image_url": "/user2.png"},
)
assert response.status_code == 200
db_user = self.users.get_user_by_id(user.id)
assert db_user.name == "John Doe 2"
assert db_user.profile_image_url == "/user2.png"
def test_update_password(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("old_password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(
self.create_url("/update/password"),
json={"password": "old_password", "new_password": "new_password"},
)
assert response.status_code == 200
old_auth = self.auths.authenticate_user(
"john.doe@openwebui.com", "old_password"
)
assert old_auth is None
new_auth = self.auths.authenticate_user(
"john.doe@openwebui.com", "new_password"
)
assert new_auth is not None
def test_signin(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
response = self.fast_api_client.post(
self.create_url("/signin"),
json={"email": "john.doe@openwebui.com", "password": "password"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == user.id
assert data["name"] == "John Doe"
assert data["email"] == "john.doe@openwebui.com"
assert data["role"] == "user"
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_signup(self):
response = self.fast_api_client.post(
self.create_url("/signup"),
json={
"name": "John Doe",
"email": "john.doe@openwebui.com",
"password": "password",
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] is not None and len(data["id"]) > 0
assert data["name"] == "John Doe"
assert data["email"] == "john.doe@openwebui.com"
assert data["role"] in ["admin", "user", "pending"]
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_add_user(self):
with mock_webui_user():
response = self.fast_api_client.post(
self.create_url("/add"),
json={
"name": "John Doe 2",
"email": "john.doe2@openwebui.com",
"password": "password2",
"role": "admin",
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] is not None and len(data["id"]) > 0
assert data["name"] == "John Doe 2"
assert data["email"] == "john.doe2@openwebui.com"
assert data["role"] == "admin"
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_get_admin_details(self):
self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
with mock_webui_user():
response = self.fast_api_client.get(self.create_url("/admin/details"))
assert response.status_code == 200
assert response.json() == {
"name": "John Doe",
"email": "john.doe@openwebui.com",
}
def test_create_api_key_(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(self.create_url("/api_key"))
assert response.status_code == 200
data = response.json()
assert data["api_key"] is not None
assert len(data["api_key"]) > 0
def test_delete_api_key(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
self.users.update_user_api_key_by_id(user.id, "abc")
with mock_webui_user(id=user.id):
response = self.fast_api_client.delete(self.create_url("/api_key"))
assert response.status_code == 200
assert response.json() == True
db_user = self.users.get_user_by_id(user.id)
assert db_user.api_key is None
def test_get_api_key(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
self.users.update_user_api_key_by_id(user.id, "abc")
with mock_webui_user(id=user.id):
response = self.fast_api_client.get(self.create_url("/api_key"))
assert response.status_code == 200
assert response.json() == {"api_key": "abc"}

View File

@ -0,0 +1,238 @@
import uuid
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestChats(AbstractPostgresTest):
BASE_PATH = "/api/v1/chats"
def setup_class(cls):
super().setup_class()
def setup_method(self):
super().setup_method()
from apps.webui.models.chats import ChatForm
from apps.webui.models.chats import Chats
self.chats = Chats
self.chats.insert_new_chat(
"2",
ChatForm(
**{
"chat": {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
}
),
)
def test_get_session_user_chat_list(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_delete_all_user_chats(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url("/"))
assert response.status_code == 200
assert len(self.chats.get_chats()) == 0
def test_get_user_chat_list_by_user_id(self):
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url("/list/user/2"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_create_new_chat(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/new"),
json={
"chat": {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag1", "tag2"],
}
},
)
assert response.status_code == 200
data = response.json()
assert data["archived"] is False
assert data["chat"] == {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag1", "tag2"],
}
assert data["user_id"] == "2"
assert data["id"] is not None
assert data["share_id"] is None
assert data["title"] == "New Chat"
assert data["updated_at"] is not None
assert data["created_at"] is not None
assert len(self.chats.get_chats()) == 2
def test_get_user_chats(self):
self.test_get_session_user_chat_list()
def test_get_user_archived_chats(self):
self.chats.archive_all_chats_by_user_id("2")
from apps.webui.internal.db import Session
Session.commit()
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/all/archived"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_get_all_user_chats_in_db(self):
with mock_webui_user(id="4"):
response = self.fast_api_client.get(self.create_url("/all/db"))
assert response.status_code == 200
assert len(response.json()) == 1
def test_get_archived_session_user_chat_list(self):
self.test_get_user_archived_chats()
def test_archive_all_chats(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.post(self.create_url("/archive/all"))
assert response.status_code == 200
assert len(self.chats.get_archived_chats_by_user_id("2")) == 1
def test_get_shared_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
self.chats.update_chat_share_id_by_id(chat_id, chat_id)
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}"))
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
assert data["id"] == chat_id
assert data["share_id"] == chat_id
assert data["title"] == "New Chat"
def test_get_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}"))
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
assert data["share_id"] is None
assert data["title"] == "New Chat"
assert data["user_id"] == "2"
def test_update_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url(f"/{chat_id}"),
json={
"chat": {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag2", "tag4"],
"title": "Just another title",
}
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat2",
"title": "Just another title",
"description": "chat2 description",
"tags": ["tag2", "tag4"],
"history": {"currentId": "1", "messages": []},
}
assert data["share_id"] is None
assert data["title"] == "Just another title"
assert data["user_id"] == "2"
def test_delete_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}"))
assert response.status_code == 200
assert response.json() is True
def test_clone_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone"))
assert response.status_code == 200
data = response.json()
assert data["id"] != chat_id
assert data["chat"] == {
"branchPointMessageId": "1",
"description": "chat1 description",
"history": {"currentId": "1", "messages": []},
"name": "chat1",
"originalChatId": chat_id,
"tags": ["tag1", "tag2"],
"title": "Clone of New Chat",
}
assert data["share_id"] is None
assert data["title"] == "Clone of New Chat"
assert data["user_id"] == "2"
def test_archive_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive"))
assert response.status_code == 200
chat = self.chats.get_chat_by_id(chat_id)
assert chat.archived is True
def test_share_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share"))
assert response.status_code == 200
chat = self.chats.get_chat_by_id(chat_id)
assert chat.share_id is not None
def test_delete_shared_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
share_id = str(uuid.uuid4())
self.chats.update_chat_share_id_by_id(chat_id, share_id)
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share"))
assert response.status_code
chat = self.chats.get_chat_by_id(chat_id)
assert chat.share_id is None

View File

@ -0,0 +1,106 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestDocuments(AbstractPostgresTest):
BASE_PATH = "/api/v1/documents"
def setup_class(cls):
super().setup_class()
from apps.webui.models.documents import Documents
cls.documents = Documents
def test_documents(self):
# Empty database
assert len(self.documents.get_docs()) == 0
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
# Create a new document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"name": "doc_name",
"title": "doc title",
"collection_name": "custom collection",
"filename": "doc_name.pdf",
"content": "",
},
)
assert response.status_code == 200
assert response.json()["name"] == "doc_name"
assert len(self.documents.get_docs()) == 1
# Get the document
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/doc?name=doc_name"))
assert response.status_code == 200
data = response.json()
assert data["collection_name"] == "custom collection"
assert data["name"] == "doc_name"
assert data["title"] == "doc title"
assert data["filename"] == "doc_name.pdf"
assert data["content"] == {}
# Create another document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"name": "doc_name 2",
"title": "doc title 2",
"collection_name": "custom collection 2",
"filename": "doc_name2.pdf",
"content": "",
},
)
assert response.status_code == 200
assert response.json()["name"] == "doc_name 2"
assert len(self.documents.get_docs()) == 2
# Get all documents
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 2
# Update the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/doc/update?name=doc_name"),
json={"name": "doc_name rework", "title": "updated title"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "doc_name rework"
assert data["title"] == "updated title"
# Tag the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/doc/tags"),
json={
"name": "doc_name rework",
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}],
},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "doc_name rework"
assert data["content"] == {
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}]
}
assert len(self.documents.get_docs()) == 2
# Delete the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/doc/delete?name=doc_name rework")
)
assert response.status_code == 200
assert len(self.documents.get_docs()) == 1

View File

@ -0,0 +1,62 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestModels(AbstractPostgresTest):
BASE_PATH = "/api/v1/models"
def setup_class(cls):
super().setup_class()
from apps.webui.models.models import Model
cls.models = Model
def test_models(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/add"),
json={
"id": "my-model",
"base_model_id": "base-model-id",
"name": "Hello World",
"meta": {
"profile_image_url": "/static/favicon.png",
"description": "description",
"capabilities": None,
"model_config": {},
},
"params": {},
},
)
assert response.status_code == 200
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 1
with mock_webui_user(id="2"):
response = self.fast_api_client.get(
self.create_url(query_params={"id": "my-model"})
)
assert response.status_code == 200
data = response.json()[0]
assert data["id"] == "my-model"
assert data["name"] == "Hello World"
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/delete?id=my-model")
)
assert response.status_code == 200
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0

View File

@ -0,0 +1,92 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestPrompts(AbstractPostgresTest):
BASE_PATH = "/api/v1/prompts"
def test_prompts(self):
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
# Create a two new prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"command": "/my-command",
"title": "Hello World",
"content": "description",
},
)
assert response.status_code == 200
with mock_webui_user(id="3"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"command": "/my-command2",
"title": "Hello World 2",
"content": "description 2",
},
)
assert response.status_code == 200
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 2
# Get prompt by command
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/command/my-command"))
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command"
assert data["title"] == "Hello World"
assert data["content"] == "description"
assert data["user_id"] == "2"
# Update prompt
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/command/my-command2/update"),
json={
"command": "irrelevant for request",
"title": "Hello World Updated",
"content": "description Updated",
},
)
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command2"
assert data["title"] == "Hello World Updated"
assert data["content"] == "description Updated"
assert data["user_id"] == "3"
# Get prompt by command
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/command/my-command2"))
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command2"
assert data["title"] == "Hello World Updated"
assert data["content"] == "description Updated"
assert data["user_id"] == "3"
# Delete prompt
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/command/my-command/delete")
)
assert response.status_code == 200
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 1

View File

@ -0,0 +1,168 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
def _get_user_by_id(data, param):
return next((item for item in data if item["id"] == param), None)
def _assert_user(data, id, **kwargs):
user = _get_user_by_id(data, id)
assert user is not None
comparison_data = {
"name": f"user {id}",
"email": f"user{id}@openwebui.com",
"profile_image_url": f"/user{id}.png",
"role": "user",
**kwargs,
}
for key, value in comparison_data.items():
assert user[key] == value
class TestUsers(AbstractPostgresTest):
BASE_PATH = "/api/v1/users"
def setup_class(cls):
super().setup_class()
from apps.webui.models.users import Users
cls.users = Users
def setup_method(self):
super().setup_method()
self.users.insert_new_user(
id="1",
name="user 1",
email="user1@openwebui.com",
profile_image_url="/user1.png",
role="user",
)
self.users.insert_new_user(
id="2",
name="user 2",
email="user2@openwebui.com",
profile_image_url="/user2.png",
role="user",
)
def test_users(self):
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(data, "2")
# update role
with mock_webui_user(id="3"):
response = self.fast_api_client.post(
self.create_url("/update/role"), json={"id": "2", "role": "admin"}
)
assert response.status_code == 200
_assert_user([response.json()], "2", role="admin")
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(data, "2", role="admin")
# Get (empty) user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/user/settings"))
assert response.status_code == 200
assert response.json() is None
# Update user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/user/settings/update"),
json={
"ui": {"attr1": "value1", "attr2": "value2"},
"model_config": {"attr3": "value3", "attr4": "value4"},
},
)
assert response.status_code == 200
# Get user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/user/settings"))
assert response.status_code == 200
assert response.json() == {
"ui": {"attr1": "value1", "attr2": "value2"},
"model_config": {"attr3": "value3", "attr4": "value4"},
}
# Get (empty) user info
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/user/info"))
assert response.status_code == 200
assert response.json() is None
# Update user info
with mock_webui_user(id="1"):
response = self.fast_api_client.post(
self.create_url("/user/info/update"),
json={"attr1": "value1", "attr2": "value2"},
)
assert response.status_code == 200
# Get user info
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/user/info"))
assert response.status_code == 200
assert response.json() == {"attr1": "value1", "attr2": "value2"}
# Get user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/2"))
assert response.status_code == 200
assert response.json() == {"name": "user 2", "profile_image_url": "/user2.png"}
# Update user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.post(
self.create_url("/2/update"),
json={
"name": "user 2 updated",
"email": "user2-updated@openwebui.com",
"profile_image_url": "/user2-updated.png",
},
)
assert response.status_code == 200
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(
data,
"2",
role="admin",
name="user 2 updated",
email="user2-updated@openwebui.com",
profile_image_url="/user2-updated.png",
)
# Delete user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.delete(self.create_url("/2"))
assert response.status_code == 200
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 1
data = response.json()
_assert_user(data, "1")

View File

@ -0,0 +1,161 @@
import logging
import os
import time
import docker
import pytest
from docker import DockerClient
from pytest_docker.plugin import get_docker_ip
from fastapi.testclient import TestClient
from sqlalchemy import text, create_engine
log = logging.getLogger(__name__)
def get_fast_api_client():
from main import app
with TestClient(app) as c:
return c
class AbstractIntegrationTest:
BASE_PATH = None
def create_url(self, path="", query_params=None):
if self.BASE_PATH is None:
raise Exception("BASE_PATH is not set")
parts = self.BASE_PATH.split("/")
parts = [part.strip() for part in parts if part.strip() != ""]
path_parts = path.split("/")
path_parts = [part.strip() for part in path_parts if part.strip() != ""]
query_parts = ""
if query_params:
query_parts = "&".join(
[f"{key}={value}" for key, value in query_params.items()]
)
query_parts = f"?{query_parts}"
return "/".join(parts + path_parts) + query_parts
@classmethod
def setup_class(cls):
pass
def setup_method(self):
pass
@classmethod
def teardown_class(cls):
pass
def teardown_method(self):
pass
class AbstractPostgresTest(AbstractIntegrationTest):
DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted"
docker_client: DockerClient
@classmethod
def _create_db_url(cls, env_vars_postgres: dict) -> str:
host = get_docker_ip()
user = env_vars_postgres["POSTGRES_USER"]
pw = env_vars_postgres["POSTGRES_PASSWORD"]
port = 8081
db = env_vars_postgres["POSTGRES_DB"]
return f"postgresql://{user}:{pw}@{host}:{port}/{db}"
@classmethod
def setup_class(cls):
super().setup_class()
try:
env_vars_postgres = {
"POSTGRES_USER": "user",
"POSTGRES_PASSWORD": "example",
"POSTGRES_DB": "openwebui",
}
cls.docker_client = docker.from_env()
cls.docker_client.containers.run(
"postgres:16.2",
detach=True,
environment=env_vars_postgres,
name=cls.DOCKER_CONTAINER_NAME,
ports={5432: ("0.0.0.0", 8081)},
command="postgres -c log_statement=all",
)
time.sleep(0.5)
database_url = cls._create_db_url(env_vars_postgres)
os.environ["DATABASE_URL"] = database_url
retries = 10
db = None
while retries > 0:
try:
from config import BACKEND_DIR
db = create_engine(database_url, pool_pre_ping=True)
db = db.connect()
log.info("postgres is ready!")
break
except Exception as e:
log.warning(e)
time.sleep(3)
retries -= 1
if db:
# import must be after setting env!
cls.fast_api_client = get_fast_api_client()
db.close()
else:
raise Exception("Could not connect to Postgres")
except Exception as ex:
log.error(ex)
cls.teardown_class()
pytest.fail(f"Could not setup test environment: {ex}")
def _check_db_connection(self):
from apps.webui.internal.db import Session
retries = 10
while retries > 0:
try:
Session.execute(text("SELECT 1"))
Session.commit()
break
except Exception as e:
Session.rollback()
log.warning(e)
time.sleep(3)
retries -= 1
def setup_method(self):
super().setup_method()
self._check_db_connection()
@classmethod
def teardown_class(cls) -> None:
super().teardown_class()
cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True)
def teardown_method(self):
from apps.webui.internal.db import Session
# rollback everything not yet committed
Session.commit()
# truncate all tables
tables = [
"auth",
"chat",
"chatidtag",
"document",
"memory",
"model",
"prompt",
"tag",
'"user"',
]
for table in tables:
Session.execute(text(f"TRUNCATE TABLE {table}"))
Session.commit()

View File

@ -0,0 +1,45 @@
from contextlib import contextmanager
from fastapi import FastAPI
@contextmanager
def mock_webui_user(**kwargs):
from apps.webui.main import app
with mock_user(app, **kwargs):
yield
@contextmanager
def mock_user(app: FastAPI, **kwargs):
from utils.utils import (
get_current_user,
get_verified_user,
get_admin_user,
get_current_user_by_api_key,
)
from apps.webui.models.users import User
def create_user():
user_parameters = {
"id": "1",
"name": "John Doe",
"email": "john.doe@openwebui.com",
"role": "user",
"profile_image_url": "/user.png",
"last_active_at": 1627351200,
"updated_at": 1627351200,
"created_at": 162735120,
**kwargs,
}
return User(**user_parameters)
app.dependency_overrides = {
get_current_user: create_user,
get_verified_user: create_user,
get_admin_user: create_user,
get_current_user_by_api_key: create_user,
}
yield
app.dependency_overrides = {}

View File

@ -59,7 +59,10 @@ def get_tools_specs(tools) -> List[dict]:
for param_name, param_annotation in get_type_hints( for param_name, param_annotation in get_type_hints(
function function
).items() ).items()
if param_name != "return" and param_name != "__user__" if param_name != "return"
and not (
param_name.startswith("__") and param_name.endswith("__")
)
}, },
"required": [ "required": [
name name

View File

@ -1,5 +1,6 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends, Request from fastapi import HTTPException, status, Depends, Request
from sqlalchemy.orm import Session
from apps.webui.models.users import Users from apps.webui.models.users import Users

View File

@ -1,6 +1,12 @@
@font-face { @font-face {
font-family: 'Arimo'; font-family: 'Inter';
src: url('/assets/fonts/Arimo-Variable.ttf'); src: url('/assets/fonts/Inter-Variable.ttf');
font-display: swap;
}
@font-face {
font-family: 'Archivo';
src: url('/assets/fonts/Archivo-Variable.ttf');
font-display: swap; font-display: swap;
} }
@ -32,6 +38,10 @@ math {
@apply underline; @apply underline;
} }
.font-primary {
font-family: 'Archivo', sans-serif;
}
iframe { iframe {
@apply rounded-lg; @apply rounded-lg;
} }
@ -140,3 +150,7 @@ input[type='number'] {
.cm-editor.cm-focused { .cm-editor.cm-focused {
outline: none; outline: none;
} }
.tippy-box[data-theme~='dark'] {
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
}

View File

@ -23,6 +23,8 @@
// On page load or when changing themes, best to add inline in `head` to avoid FOUC // On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => { (() => {
if (localStorage?.theme && localStorage?.theme.includes('oled')) { if (localStorage?.theme && localStorage?.theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-800', '#101010');
document.documentElement.style.setProperty('--color-gray-850', '#050505');
document.documentElement.style.setProperty('--color-gray-900', '#000000'); document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000'); document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
@ -80,13 +82,13 @@
id="logo" id="logo"
style=" style="
position: absolute; position: absolute;
width: 6rem; width: auto;
height: 6rem; height: 6rem;
top: 41%; top: 44%;
left: 50%; left: 50%;
margin-left: -3rem; margin-left: -3rem;
" "
src="/logo.svg" src="/static/splash.png"
/> />
<div <div
@ -105,8 +107,8 @@
> >
<img <img
id="logo-her" id="logo-her"
style="width: 13rem; height: 13rem" style="width: auto; height: 13rem"
src="/logo.svg" src="/static/splash.png"
class="animate-pulse-fast" class="animate-pulse-fast"
/> />

View File

@ -24,7 +24,7 @@
<Modal bind:show> <Modal bind:show>
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700"> <div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="text-xl font-bold"> <div class="text-xl font-semibold">
{$i18n.t('Whats New in')} {$i18n.t('Whats New in')}
{$WEBUI_NAME} {$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} /> <Confetti x={[-1, -0.25]} y={[0, 0.5]} />
@ -63,7 +63,7 @@
{#if changelog} {#if changelog}
{#each Object.keys(changelog) as version} {#each Object.keys(changelog) as version}
<div class=" mb-3 pr-2"> <div class=" mb-3 pr-2">
<div class="font-bold text-xl mb-1 dark:text-white"> <div class="font-semibold text-xl mb-1 dark:text-white">
v{version} - {changelog[version].date} v{version} - {changelog[version].date}
</div> </div>
@ -72,7 +72,7 @@
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section} {#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
<div class=""> <div class="">
<div <div
class="font-bold uppercase text-xs {section === 'added' class="font-semibold uppercase text-xs {section === 'added'
? 'text-white bg-blue-600' ? 'text-white bg-blue-600'
: section === 'fixed' : section === 'fixed'
? 'text-white bg-green-600' ? 'text-white bg-green-600'

View File

@ -1,5 +1,5 @@
<script> <script>
import { getContext, tick } from 'svelte'; import { getContext, tick, onMount } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Database from './Settings/Database.svelte'; import Database from './Settings/Database.svelte';
@ -21,17 +21,31 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let selectedTab = 'general'; let selectedTab = 'general';
onMount(() => {
const containerElement = document.getElementById('admin-settings-tabs-container');
if (containerElement) {
containerElement.addEventListener('wheel', function (event) {
if (event.deltaY !== 0) {
// Adjust horizontal scroll position based on vertical scroll
containerElement.scrollLeft += event.deltaY;
}
});
}
});
</script> </script>
<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4"> <div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
<div <div
id="admin-settings-tabs-container"
class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none" class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
> >
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
'general' 'general'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'general'; selectedTab = 'general';
}} }}
@ -56,8 +70,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'users' 'users'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'users'; selectedTab = 'users';
}} }}
@ -80,8 +94,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'connections' 'connections'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'connections'; selectedTab = 'connections';
}} }}
@ -104,8 +118,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'models' 'models'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'models'; selectedTab = 'models';
}} }}
@ -130,8 +144,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'documents' 'documents'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'documents'; selectedTab = 'documents';
}} }}
@ -160,8 +174,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'web' 'web'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'web'; selectedTab = 'web';
}} }}
@ -184,8 +198,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'interface' 'interface'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'interface'; selectedTab = 'interface';
}} }}
@ -210,8 +224,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'audio' 'audio'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'audio'; selectedTab = 'audio';
}} }}
@ -237,8 +251,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'images' 'images'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'images'; selectedTab = 'images';
}} }}
@ -263,8 +277,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'pipelines' 'pipelines'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'pipelines'; selectedTab = 'pipelines';
}} }}
@ -293,8 +307,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'db' 'db'
? 'bg-gray-200 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}" : ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'db'; selectedTab = 'db';
}} }}

View File

@ -138,7 +138,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL} bind:value={STT_OPENAI_API_BASE_URL}
required required
@ -156,7 +156,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />
@ -203,7 +203,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL} bind:value={TTS_OPENAI_API_BASE_URL}
required required
@ -222,7 +222,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
> >
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option> <option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@ -245,7 +245,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="voice-list" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
placeholder="Select a voice" placeholder="Select a voice"
/> />
@ -264,7 +264,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />

View File

@ -200,7 +200,7 @@
<input <input
class="w-full rounded-lg py-2 px-4 {pipelineUrls[url] class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
? 'pr-8' ? 'pr-8'
: ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" : ''} text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={url} bind:value={url}
autocomplete="off" autocomplete="off"
@ -338,7 +338,7 @@
{#each OLLAMA_BASE_URLS as url, idx} {#each OLLAMA_BASE_URLS as url, idx}
<div class="flex gap-1.5"> <div class="flex gap-1.5">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')} placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url} bind:value={url}
/> />

View File

@ -279,7 +279,7 @@
</div> </div>
<button <button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading class=" self-center text-xs p-1 px-3 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed' ? ' cursor-not-allowed'
: ''}" : ''}"
on:click={() => { on:click={() => {
@ -352,7 +352,7 @@
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2"> <div class="my-0.5 flex gap-2">
<input <input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
@ -415,7 +415,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={embeddingModel} bind:value={embeddingModel}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
required required
@ -424,7 +424,7 @@
<option value="" disabled selected>{$i18n.t('Select a model')}</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model} {#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> <option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -433,7 +433,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
@ -443,7 +443,7 @@
{#if embeddingEngine === ''} {#if embeddingEngine === ''}
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
embeddingModelUpdateHandler(); embeddingModelUpdateHandler();
}} }}
@ -512,7 +512,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@ -520,7 +520,7 @@
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
rerankingModelUpdateHandler(); rerankingModelUpdateHandler();
}} }}
@ -602,7 +602,7 @@
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={tikaServerUrl} bind:value={tikaServerUrl}
/> />
@ -621,7 +621,7 @@
<div class="self-center p-3"> <div class="self-center p-3">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number" type="number"
placeholder={$i18n.t('Enter Top K')} placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k} bind:value={querySettings.k}
@ -639,7 +639,7 @@
<div class="self-center p-3"> <div class="self-center p-3">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number" type="number"
step="0.01" step="0.01"
placeholder={$i18n.t('Enter Score')} placeholder={$i18n.t('Enter Score')}
@ -667,7 +667,7 @@
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
<textarea <textarea
bind:value={querySettings.template} bind:value={querySettings.template}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="4" rows="4"
/> />
</div> </div>
@ -683,7 +683,7 @@
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div> <div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
<div class="self-center"> <div class="self-center">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Size')} placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize} bind:value={chunkSize}
@ -700,7 +700,7 @@
<div class="self-center"> <div class="self-center">
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Overlap')} placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap} bind:value={chunkOverlap}

View File

@ -107,7 +107,7 @@
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={`e.g.) "30m","1h", "10d". `} placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={adminConfig.JWT_EXPIRES_IN} bind:value={adminConfig.JWT_EXPIRES_IN}
@ -131,7 +131,7 @@
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={`https://example.com/webhook`} placeholder={`https://example.com/webhook`}
bind:value={webhookUrl} bind:value={webhookUrl}

View File

@ -240,13 +240,13 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={AUTOMATIC1111_BASE_URL} bind:value={AUTOMATIC1111_BASE_URL}
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button" type="button"
on:click={() => { on:click={() => {
updateUrlHandler(); updateUrlHandler();
@ -299,13 +299,13 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={COMFYUI_BASE_URL} bind:value={COMFYUI_BASE_URL}
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button" type="button"
on:click={() => { on:click={() => {
updateUrlHandler(); updateUrlHandler();
@ -331,7 +331,7 @@
<div class="flex gap-2 mb-1"> <div class="flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OPENAI_API_BASE_URL} bind:value={OPENAI_API_BASE_URL}
required required
@ -354,7 +354,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel} bind:value={selectedModel}
placeholder="Select a model" placeholder="Select a model"
/> />
@ -368,7 +368,7 @@
</div> </div>
{:else} {:else}
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel} bind:value={selectedModel}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
required required
@ -391,7 +391,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')} placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={imageSize} bind:value={imageSize}
/> />
@ -404,7 +404,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')} placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={steps} bind:value={steps}
/> />

View File

@ -88,7 +88,7 @@
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div> <div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={taskConfig.TASK_MODEL} bind:value={taskConfig.TASK_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -104,7 +104,7 @@
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div> <div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={taskConfig.TASK_MODEL_EXTERNAL} bind:value={taskConfig.TASK_MODEL_EXTERNAL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -122,7 +122,7 @@
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
<textarea <textarea
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="6" rows="6"
/> />
</div> </div>
@ -131,7 +131,7 @@
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
<textarea <textarea
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE} bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
rows="6" rows="6"
/> />
</div> </div>
@ -142,7 +142,7 @@
</div> </div>
<input <input
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
type="number" type="number"
/> />
</div> </div>
@ -273,24 +273,26 @@
</div> </div>
<div class="grid lg:grid-cols-2 flex-col gap-1.5"> <div class="grid lg:grid-cols-2 flex-col gap-1.5">
{#each promptSuggestions as prompt, promptIdx} {#each promptSuggestions as prompt, promptIdx}
<div class=" flex dark:bg-gray-850 rounded-xl py-1.5"> <div
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
>
<div class="flex flex-col flex-1 pl-1"> <div class="flex flex-col flex-1 pl-1">
<div class="flex border-b dark:border-gray-800 w-full"> <div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')} placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
bind:value={prompt.title[0]} bind:value={prompt.title[0]}
/> />
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')} placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
bind:value={prompt.title[1]} bind:value={prompt.title[1]}
/> />
</div> </div>
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')} placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
bind:value={prompt.content} bind:value={prompt.content}
/> />

View File

@ -158,12 +158,14 @@
return; return;
} }
const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch( const [res, controller] = await pullModel(
(error) => { localStorage.token,
toast.error(error); sanitizedModelTag,
return null; selectedOllamaUrlIdx
} ).catch((error) => {
); toast.error(error);
return null;
});
if (res) { if (res) {
const reader = res.body const reader = res.body
@ -570,12 +572,12 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1 pb-1"> <div class="flex-1 pb-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedOllamaUrlIdx} bind:value={selectedOllamaUrlIdx}
placeholder={$i18n.t('Select an Ollama instance')} placeholder={$i18n.t('Select an Ollama instance')}
> >
{#each OLLAMA_URLS as url, idx} {#each OLLAMA_URLS as url, idx}
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> <option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -584,7 +586,7 @@
<div class="flex w-full justify-end"> <div class="flex w-full justify-end">
<Tooltip content="Update All Models" placement="top"> <Tooltip content="Update All Models" placement="top">
<button <button
class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="p-2.5 flex gap-2 items-center bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
updateModelsHandler(); updateModelsHandler();
}} }}
@ -619,7 +621,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'mistral:7b' modelTag: 'mistral:7b'
})} })}
@ -627,7 +629,7 @@
/> />
</div> </div>
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
pullModelHandler(); pullModelHandler();
}} }}
@ -753,7 +755,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteModelTag} bind:value={deleteModelTag}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -761,7 +763,7 @@
<option value="" disabled selected>{$i18n.t('Select a model')}</option> <option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if} {/if}
{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700" <option value={model.name} class="bg-gray-50 dark:bg-gray-700"
>{model.name + >{model.name +
' (' + ' (' +
(model.ollama.size / 1024 ** 3).toFixed(1) + (model.ollama.size / 1024 ** 3).toFixed(1) +
@ -771,7 +773,7 @@
</select> </select>
</div> </div>
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
showModelDeleteConfirm = true; showModelDeleteConfirm = true;
}} }}
@ -797,7 +799,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2 flex flex-col gap-2"> <div class="flex-1 mr-2 flex flex-col gap-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'my-modelfile' modelTag: 'my-modelfile'
})} })}
@ -807,7 +809,7 @@
<textarea <textarea
bind:value={createModelContent} bind:value={createModelContent}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
rows="6" rows="6"
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`} placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
disabled={createModelLoading} disabled={createModelLoading}
@ -816,7 +818,7 @@
<div class="flex self-start"> <div class="flex self-start">
<button <button
class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed" class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
on:click={() => { on:click={() => {
createModelHandler(); createModelHandler();
}} }}
@ -925,7 +927,7 @@
<button <button
type="button" type="button"
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850" class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
on:click={() => { on:click={() => {
modelUploadInputElement.click(); modelUploadInputElement.click();
}} }}
@ -940,7 +942,7 @@
{:else} {:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input <input
class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
'' ''
? 'mr-2' ? 'mr-2'
: ''}" : ''}"
@ -955,7 +957,7 @@
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button <button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition" class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
type="submit" type="submit"
disabled={modelTransferring} disabled={modelTransferring}
> >
@ -1014,7 +1016,7 @@
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div> <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
<textarea <textarea
bind:value={modelFileContent} bind:value={modelFileContent}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
rows="6" rows="6"
/> />
</div> </div>

View File

@ -214,7 +214,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedPipelinesUrlIdx} bind:value={selectedPipelinesUrlIdx}
placeholder={$i18n.t('Select a pipeline url')} placeholder={$i18n.t('Select a pipeline url')}
on:change={async () => { on:change={async () => {
@ -328,7 +328,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Github Raw URL')} placeholder={$i18n.t('Enter Github Raw URL')}
bind:value={pipelineDownloadUrl} bind:value={pipelineDownloadUrl}
/> />
@ -412,7 +412,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedPipelineIdx} bind:value={selectedPipelineIdx}
placeholder={$i18n.t('Select a pipeline')} placeholder={$i18n.t('Select a pipeline')}
on:change={async () => { on:change={async () => {
@ -482,7 +482,7 @@
<div class=" flex-1"> <div class=" flex-1">
{#if valves_spec.properties[property]?.enum ?? null} {#if valves_spec.properties[property]?.enum ?? null}
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={valves[property]} bind:value={valves[property]}
> >
{#each valves_spec.properties[property].enum as option} {#each valves_spec.properties[property].enum as option}
@ -503,7 +503,7 @@
</div> </div>
{:else} {:else}
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={valves_spec.properties[property].title} placeholder={valves_spec.properties[property].title}
bind:value={valves[property]} bind:value={valves[property]}

View File

@ -112,7 +112,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={defaultModelId} bind:value={defaultModelId}
placeholder="Select a model" placeholder="Select a model"
> >
@ -140,7 +140,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={modelId} bind:value={modelId}
placeholder="Select a model" placeholder="Select a model"
> >

View File

@ -101,7 +101,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={$i18n.t('Enter Searxng Query URL')} placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url} bind:value={webConfig.search.searxng_query_url}
@ -129,7 +129,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')} placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id} bind:value={webConfig.search.google_pse_engine_id}
@ -205,7 +205,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Search Result Count')} placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count} bind:value={webConfig.search.result_count}
required required
@ -218,7 +218,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Concurrent Requests')} placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests} bind:value={webConfig.search.concurrent_requests}
required required
@ -267,7 +267,7 @@
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div> <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
placeholder={$i18n.t('Enter language codes')} placeholder={$i18n.t('Enter language codes')}
bind:value={youtubeLanguage} bind:value={youtubeLanguage}

View File

@ -60,19 +60,26 @@
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte'; import CallOverlay from './MessageInput/CallOverlay.svelte';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import ChatControls from './ChatControls.svelte';
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
const i18n: Writable<i18nType> = getContext('i18n'); const i18n: Writable<i18nType> = getContext('i18n');
export let chatIdProp = ''; export let chatIdProp = '';
let loaded = false; let loaded = false;
const eventTarget = new EventTarget(); const eventTarget = new EventTarget();
let showControls = false;
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let processing = ''; let processing = '';
let messagesContainerElement: HTMLDivElement; let messagesContainerElement: HTMLDivElement;
let showEventConfirmation = false;
let eventConfirmationTitle = '';
let eventConfirmationMessage = '';
let eventCallback = null;
let showModelSelector = true; let showModelSelector = true;
let selectedModels = ['']; let selectedModels = [''];
@ -96,6 +103,8 @@
currentId: null currentId: null
}; };
let params = {};
$: if (history.currentId !== null) { $: if (history.currentId !== null) {
let _messages = []; let _messages = [];
@ -126,21 +135,35 @@
})(); })();
} }
const chatEventHandler = async (data) => { const chatEventHandler = async (event, cb) => {
if (data.chat_id === $chatId) { if (event.chat_id === $chatId) {
await tick(); await tick();
console.log(data); console.log(event);
let message = history.messages[data.message_id]; let message = history.messages[event.message_id];
const status = { const type = event?.data?.type ?? null;
done: data?.data?.done ?? null, const data = event?.data?.data ?? null;
description: data?.data?.status ?? null
};
if (message.statusHistory) { if (type === 'status') {
message.statusHistory.push(status); if (message?.statusHistory) {
message.statusHistory.push(data);
} else {
message.statusHistory = [data];
}
} else if (type === 'citation') {
if (message?.citations) {
message.citations.push(data);
} else {
message.citations = [data];
}
} else if (type === 'confirmation') {
eventCallback = cb;
showEventConfirmation = true;
eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message;
} else { } else {
message.statusHistory = [status]; console.log('Unknown message type', data);
} }
messages = messages; messages = messages;
@ -221,6 +244,7 @@
messages: {}, messages: {},
currentId: null currentId: null
}; };
params = {};
if ($page.url.searchParams.get('models')) { if ($page.url.searchParams.get('models')) {
selectedModels = $page.url.searchParams.get('models')?.split(','); selectedModels = $page.url.searchParams.get('models')?.split(',');
@ -290,11 +314,7 @@
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
} }
await settings.set({ params = chatContent?.params ?? {};
...$settings,
system: chatContent.system ?? $settings.system,
params: chatContent.options ?? $settings.params
});
autoScroll = true; autoScroll = true;
await tick(); await tick();
@ -507,9 +527,7 @@
title: $i18n.t('New Chat'), title: $i18n.t('New Chat'),
models: selectedModels, models: selectedModels,
system: $settings.system ?? undefined, system: $settings.system ?? undefined,
options: { params: params,
...($settings.params ?? {})
},
messages: messages, messages: messages,
history: history, history: history,
tags: [], tags: [],
@ -607,11 +625,11 @@
scrollToBottom(); scrollToBottom();
const messagesBody = [ const messagesBody = [
$settings.system || (responseMessage?.userContext ?? null) params?.system || $settings.system || (responseMessage?.userContext ?? null)
? { ? {
role: 'system', role: 'system',
content: `${promptTemplate( content: `${promptTemplate(
$settings?.system ?? '', params?.system ?? $settings?.system ?? '',
$user.name, $user.name,
$settings?.userLocation $settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token) ? await getAndUpdateUserLocation(localStorage.token)
@ -696,15 +714,16 @@
model: model.id, model: model.id,
messages: messagesBody, messages: messagesBody,
options: { options: {
...($settings.params ?? {}), ...(params ?? $settings.params ?? {}),
stop: stop:
$settings?.params?.stop ?? undefined params?.stop ?? $settings?.params?.stop ?? undefined
? $settings.params.stop.map((str) => ? (params?.stop ?? $settings.params.stop).map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined, : undefined,
num_predict: $settings?.params?.max_tokens ?? undefined, num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
repeat_penalty: $settings?.params?.frequency_penalty ?? undefined repeat_penalty:
params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined
}, },
format: $settings.requestFormat ?? undefined, format: $settings.requestFormat ?? undefined,
keep_alive: $settings.keepAlive ?? undefined, keep_alive: $settings.keepAlive ?? undefined,
@ -840,7 +859,8 @@
chat = await updateChatById(localStorage.token, _chatId, { chat = await updateChatById(localStorage.token, _chatId, {
messages: messages, messages: messages,
history: history, history: history,
models: selectedModels models: selectedModels,
params: params
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -950,11 +970,11 @@
} }
: undefined, : undefined,
messages: [ messages: [
$settings.system || (responseMessage?.userContext ?? null) params?.system || $settings.system || (responseMessage?.userContext ?? null)
? { ? {
role: 'system', role: 'system',
content: `${promptTemplate( content: `${promptTemplate(
$settings?.system ?? '', params?.system ?? $settings?.system ?? '',
$user.name, $user.name,
$settings?.userLocation $settings?.userLocation
? await getAndUpdateUserLocation(localStorage.token) ? await getAndUpdateUserLocation(localStorage.token)
@ -999,17 +1019,18 @@
: message?.raContent ?? message.content : message?.raContent ?? message.content
}) })
})), })),
seed: $settings?.params?.seed ?? undefined, seed: params?.seed ?? $settings?.params?.seed ?? undefined,
stop: stop:
$settings?.params?.stop ?? undefined params?.stop ?? $settings?.params?.stop ?? undefined
? $settings.params.stop.map((str) => ? (params?.stop ?? $settings.params.stop).map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined, : undefined,
temperature: $settings?.params?.temperature ?? undefined, temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
top_p: $settings?.params?.top_p ?? undefined, top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
frequency_penalty: $settings?.params?.frequency_penalty ?? undefined, frequency_penalty:
max_tokens: $settings?.params?.max_tokens ?? undefined, params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined,
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined, files: files.length > 0 ? files : undefined,
session_id: $socket?.id, session_id: $socket?.id,
@ -1115,7 +1136,8 @@
chat = await updateChatById(localStorage.token, _chatId, { chat = await updateChatById(localStorage.token, _chatId, {
models: selectedModels, models: selectedModels,
messages: messages, messages: messages,
history: history history: history,
params: params
}); });
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
} }
@ -1382,6 +1404,18 @@
<audio id="audioElement" src="" style="display: none;" /> <audio id="audioElement" src="" style="display: none;" />
<EventConfirmDialog
bind:show={showEventConfirmation}
title={eventConfirmationTitle}
message={eventConfirmationMessage}
on:confirm={(e) => {
eventCallback(true);
}}
on:cancel={() => {
eventCallback(false);
}}
/>
{#if $showCallOverlay} {#if $showCallOverlay}
<CallOverlay <CallOverlay
{submitPrompt} {submitPrompt}
@ -1416,6 +1450,7 @@
{title} {title}
bind:selectedModels bind:selectedModels
bind:showModelSelector bind:showModelSelector
bind:showControls
shareEnabled={messages.length > 0} shareEnabled={messages.length > 0}
{chat} {chat}
{initNewChat} {initNewChat}
@ -1425,7 +1460,7 @@
<div <div
class="absolute top-[4.25rem] w-full {$showSidebar class="absolute top-[4.25rem] w-full {$showSidebar
? 'md:max-w-[calc(100%-260px)]' ? 'md:max-w-[calc(100%-260px)]'
: ''} z-20" : ''} {showControls ? 'lg:pr-[24rem]' : ''} z-20"
> >
<div class=" flex flex-col gap-1 w-full"> <div class=" flex flex-col gap-1 w-full">
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner} {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
@ -1452,7 +1487,9 @@
<div class="flex flex-col flex-auto z-10"> <div class="flex flex-col flex-auto z-10">
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {showControls
? 'lg:pr-[24rem]'
: ''}"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement} bind:this={messagesContainerElement}
on:scroll={(e) => { on:scroll={(e) => {
@ -1477,26 +1514,31 @@
/> />
</div> </div>
</div> </div>
<MessageInput
bind:files <div class={showControls ? 'lg:pr-[24rem]' : ''}>
bind:prompt <MessageInput
bind:autoScroll bind:files
bind:selectedToolIds bind:prompt
bind:webSearchEnabled bind:autoScroll
bind:atSelectedModel bind:selectedToolIds
availableToolIds={selectedModelIds.reduce((a, e, i, arr) => { bind:webSearchEnabled
const model = $models.find((m) => m.id === e); bind:atSelectedModel
if (model?.info?.meta?.toolIds ?? false) { availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
return [...new Set([...a, ...model.info.meta.toolIds])]; const model = $models.find((m) => m.id === e);
} if (model?.info?.meta?.toolIds ?? false) {
return a; return [...new Set([...a, ...model.info.meta.toolIds])];
}, [])} }
transparentBackground={$settings?.backgroundImageUrl ?? false} return a;
{selectedModels} }, [])}
{messages} transparentBackground={$settings?.backgroundImageUrl ?? false}
{submitPrompt} {selectedModels}
{stopResponse} {messages}
/> {submitPrompt}
{stopResponse}
/>
</div>
</div> </div>
<ChatControls bind:show={showControls} bind:params />
</div> </div>
{/if} {/if}

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import { onMount } from 'svelte';
export let show = false;
export let chatId = null;
export let params = {};
let largeScreen = false;
onMount(() => {
// listen to resize 1024px
const mediaQuery = window.matchMedia('(min-width: 1024px)');
const handleMediaQuery = (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
return () => {
mediaQuery.removeEventListener('change', handleMediaQuery);
};
});
</script>
{#if largeScreen}
{#if show}
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
<div
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
<Controls
on:close={() => {
show = false;
}}
bind:params
/>
</div>
</div>
</div>
{/if}
{:else}
<Modal bind:show>
<div class=" px-6 py-4 h-full">
<Controls
on:close={() => {
show = false;
}}
bind:params
/>
</div>
</Modal>
{/if}

View File

@ -0,0 +1,49 @@
<script>
import { createEventDispatcher, getContext } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
import XMark from '$lib/components/icons/XMark.svelte';
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
export let params = {};
</script>
<div class=" dark:text-white">
<div class=" flex justify-between dark:text-gray-100 mb-2">
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div>
<button
class="self-center"
on:click={() => {
dispatch('close');
}}
>
<XMark className="size-4" />
</button>
</div>
<div class=" dark:text-gray-200 text-sm font-primary">
<div>
<div class="mb-1.5 font-medium">System Prompt</div>
<div>
<textarea
bind:value={params.system}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
rows="3"
placeholder="Enter system prompt"
/>
</div>
</div>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
<div>
<div class="mb-1.5 font-medium">Advanced Params</div>
<div>
<AdvancedParams bind:params />
</div>
</div>
</div>
</div>

View File

@ -316,7 +316,7 @@
</div> </div>
{/if} {/if}
<div class="w-full"> <div class="w-full font-primary">
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> <div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full"> <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
<div class="relative"> <div class="relative">

View File

@ -662,10 +662,11 @@
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-14' ? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta : 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png' ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat' ? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white" : 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png' style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');` ? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''} : ''}
/> />
@ -743,10 +744,11 @@
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-[11.5rem]' ? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta : 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/favicon.png') !== '/favicon.png' ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat' ? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} " : 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png' style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');` ? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''} : ''}
/> />

View File

@ -250,7 +250,8 @@ __builtins__.input = input`);
stderr || stderr ||
result) && result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code 'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code class="language-{lang} rounded-t-none whitespace-pre"
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
></pre> ></pre>
<div <div

View File

@ -1,3 +1,3 @@
<div class=" self-center font-bold mb-0.5 line-clamp-1 contents"> <div class=" self-center font-semibold mb-0.5 line-clamp-1 contents">
<slot /> <slot />
</div> </div>

View File

@ -65,7 +65,7 @@
</div> </div>
<div <div
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4" class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4 font-primary"
> >
<div> <div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}> <div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
@ -102,7 +102,7 @@
</div> </div>
{/if} {/if}
{:else} {:else}
<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1"> <div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1 font-p">
{$i18n.t('How can I help you today?')} {$i18n.t('How can I help you today?')}
</div> </div>
{/if} {/if}
@ -110,7 +110,7 @@
</div> </div>
</div> </div>
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}> <div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions <Suggestions
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ?? suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config.default_prompt_suggestions} $config.default_prompt_suggestions}

View File

@ -152,7 +152,10 @@
} }
tooltipInstance = tippy(`#info-${message.id}`, { tooltipInstance = tippy(`#info-${message.id}`, {
content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`, content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
allowHTML: true allowHTML: true,
theme: 'dark',
arrow: false,
offset: [0, 4]
}); });
} }
}; };

View File

@ -2,32 +2,25 @@
import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte'; import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
import { Collapsible } from 'bits-ui'; import Collapsible from '$lib/components/common/Collapsible.svelte';
import { slide } from 'svelte/transition';
export let status = { urls: [], query: '' }; export let status = { urls: [], query: '' };
let state = false; let state = false;
</script> </script>
<Collapsible.Root class="w-full space-y-1" bind:open={state}> <Collapsible bind:open={state} className="w-full space-y-1">
<Collapsible.Trigger> <div
<div class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
>
<slot />
{#if state}
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
{:else}
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
{/if}
</div>
</Collapsible.Trigger>
<Collapsible.Content
class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
transition={slide}
> >
<slot />
{#if state}
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
{:else}
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
{/if}
</div>
<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content">
{#if status?.query} {#if status?.query}
<a <a
href="https://www.google.com/search?q={status.query}" href="https://www.google.com/search?q={status.query}"
@ -93,5 +86,5 @@
</div> </div>
</a> </a>
{/each} {/each}
</Collapsible.Content> </div>
</Collapsible.Root> </Collapsible>

View File

@ -34,7 +34,7 @@
} }
</script> </script>
<div class="flex flex-col w-full items-center md:items-start"> <div class="flex flex-col w-full items-start">
{#each selectedModels as selectedModel, selectedModelIdx} {#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full max-w-fit"> <div class="flex w-full max-w-fit">
<div class="overflow-hidden w-full"> <div class="overflow-hidden w-full">
@ -103,7 +103,7 @@
</div> </div>
{#if showSetDefault && !$mobile} {#if showSetDefault && !$mobile}
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500"> <div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500 font-primary">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>
{/if} {/if}

View File

@ -206,7 +206,7 @@
}} }}
closeFocus={false} closeFocus={false}
> >
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}> <DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}>
<div <div
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none" class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
> >
@ -222,7 +222,7 @@
<DropdownMenu.Content <DropdownMenu.Content
class=" z-40 {$mobile class=" z-40 {$mobile
? `w-full` ? `w-full`
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none " : `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none"
transition={flyAndScale} transition={flyAndScale}
side={$mobile ? 'bottom' : 'bottom-start'} side={$mobile ? 'bottom' : 'bottom-start'}
sideOffset={4} sideOffset={4}
@ -260,7 +260,7 @@
<div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1"> <div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
{#each item.model?.info?.meta.tags as tag} {#each item.model?.info?.meta.tags as tag}
<div <div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{tag.name} {tag.name}
</div> </div>
@ -299,7 +299,7 @@
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]"> <div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag} {#each item.model?.info?.meta.tags as tag}
<div <div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{tag.name} {tag.name}
</div> </div>

View File

@ -44,7 +44,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.seed = (params?.seed ?? null) === null ? 0 : null; params.seed = (params?.seed ?? null) === null ? 0 : null;
@ -79,7 +79,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.stop = (params?.stop ?? null) === null ? '' : null; params.stop = (params?.stop ?? null) === null ? '' : null;
@ -113,7 +113,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.temperature = (params?.temperature ?? null) === null ? 0.8 : null; params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
@ -159,7 +159,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat = (params?.mirostat ?? null) === null ? 0 : null; params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
@ -205,7 +205,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null; params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
@ -251,7 +251,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null; params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
@ -297,7 +297,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.top_k = (params?.top_k ?? null) === null ? 40 : null; params.top_k = (params?.top_k ?? null) === null ? 40 : null;
@ -343,7 +343,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.top_p = (params?.top_p ?? null) === null ? 0.9 : null; params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
@ -389,7 +389,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null; params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
@ -435,7 +435,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null; params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
@ -481,7 +481,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null; params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
@ -527,7 +527,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null; params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
@ -572,7 +572,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Batch Size (num_batch)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Batch Size (num_batch)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.num_batch = (params?.num_batch ?? null) === null ? 512 : null; params.num_batch = (params?.num_batch ?? null) === null ? 512 : null;
@ -619,7 +619,7 @@
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.num_keep = (params?.num_keep ?? null) === null ? 24 : null; params.num_keep = (params?.num_keep ?? null) === null ? 24 : null;
@ -664,7 +664,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null; params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
@ -711,7 +711,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.use_mmap = (params?.use_mmap ?? null) === null ? true : null; params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
@ -731,7 +731,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.use_mlock = (params?.use_mlock ?? null) === null ? true : null; params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
@ -751,7 +751,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.num_thread = (params?.num_thread ?? null) === null ? 2 : null; params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
@ -797,7 +797,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button" type="button"
on:click={() => { on:click={() => {
params.template = (params?.template ?? null) === null ? '' : null; params.template = (params?.template ?? null) === null ? '' : null;

View File

@ -95,6 +95,8 @@
} }
if (themeToApply === 'dark' && !_theme.includes('oled')) { if (themeToApply === 'dark' && !_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-800', '#333');
document.documentElement.style.setProperty('--color-gray-850', '#262626');
document.documentElement.style.setProperty('--color-gray-900', '#171717'); document.documentElement.style.setProperty('--color-gray-900', '#171717');
document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d'); document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
} }
@ -118,6 +120,8 @@
theme.set(_theme); theme.set(_theme);
localStorage.setItem('theme', _theme); localStorage.setItem('theme', _theme);
if (_theme.includes('oled')) { if (_theme.includes('oled')) {
document.documentElement.style.setProperty('--color-gray-800', '#101010');
document.documentElement.style.setProperty('--color-gray-850', '#050505');
document.documentElement.style.setProperty('--color-gray-900', '#000000'); document.documentElement.style.setProperty('--color-gray-900', '#000000');
document.documentElement.style.setProperty('--color-gray-950', '#000000'); document.documentElement.style.setProperty('--color-gray-950', '#000000');
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');

View File

@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext, tick } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores'; import { models, settings, user } from '$lib/stores';
import { updateUserSettings } from '$lib/apis/users';
import { getModels as _getModels } from '$lib/apis'; import { getModels as _getModels } from '$lib/apis';
import { goto } from '$app/navigation';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte'; import Account from './Settings/Account.svelte';
@ -14,8 +15,6 @@
import Chats from './Settings/Chats.svelte'; import Chats from './Settings/Chats.svelte';
import User from '../icons/User.svelte'; import User from '../icons/User.svelte';
import Personalization from './Settings/Personalization.svelte'; import Personalization from './Settings/Personalization.svelte';
import { updateUserSettings } from '$lib/apis/users';
import { goto } from '$app/navigation';
import Valves from './Settings/Valves.svelte'; import Valves from './Settings/Valves.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -34,6 +33,37 @@
}; };
let selectedTab = 'general'; let selectedTab = 'general';
// Function to handle sideways scrolling
const scrollHandler = (event) => {
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
event.preventDefault(); // Prevent default vertical scrolling
settingsTabsContainer.scrollLeft += event.deltaY; // Scroll sideways
}
};
const addScrollListener = async () => {
await tick();
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
settingsTabsContainer.addEventListener('wheel', scrollHandler);
}
};
const removeScrollListener = async () => {
await tick();
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
settingsTabsContainer.removeEventListener('wheel', scrollHandler);
}
};
$: if (show) {
addScrollListener();
} else {
removeScrollListener();
}
</script> </script>
<Modal bind:show> <Modal bind:show>
@ -61,6 +91,7 @@
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4"> <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div <div
id="settings-tabs-container"
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0" class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
> >
<button <button

View File

@ -45,7 +45,7 @@
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5"> <div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
<div class="flex justify-between self-start"> <div class="flex justify-between self-start">
<div <div
class=" text-xs font-black {classNames[banner.type] ?? class=" text-xs font-bold {classNames[banner.type] ??
classNames['info']} w-fit px-2 rounded uppercase line-clamp-1 mr-0.5" classNames['info']} w-fit px-2 rounded uppercase line-clamp-1 mr-0.5"
> >
{banner.type} {banner.type}
@ -54,7 +54,7 @@
{#if banner.url} {#if banner.url}
<div class="flex md:hidden group w-fit md:items-center"> <div class="flex md:hidden group w-fit md:items-center">
<a <a
class="text-gray-700 dark:text-white text-xs font-bold underline" class="text-gray-700 dark:text-white text-xs font-semibold underline"
href="/assets/files/whitepaper.pdf" href="/assets/files/whitepaper.pdf"
target="_blank">Learn More</a target="_blank">Learn More</a
> >
@ -88,7 +88,7 @@
{#if banner.url} {#if banner.url}
<div class="hidden md:flex group w-fit md:items-center"> <div class="hidden md:flex group w-fit md:items-center">
<a <a
class="text-gray-700 dark:text-white text-xs font-bold underline" class="text-gray-700 dark:text-white text-xs font-semibold underline"
href="/" href="/"
target="_blank">Learn More</a target="_blank">Learn More</a
> >
@ -116,7 +116,8 @@
on:click={() => { on:click={() => {
dismiss(banner.id); dismiss(banner.id);
}} }}
class=" -mt-[3px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white h-1">&times;</button class=" -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white"
>&times;</button
> >
{/if} {/if}
</div> </div>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
export let open = false;
export let className = '';
</script>
<div class={className}>
<button on:click={() => (open = !open)}>
<slot />
</button>
{#if open}
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
<slot name="content" />
</div>
{/if}
</div>

View File

@ -7,8 +7,8 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let title = $i18n.t('Confirm your action'); export let title = '';
export let message = $i18n.t('This action cannot be undone. Do you wish to continue?'); export let message = '';
export let cancelLabel = $i18n.t('Cancel'); export let cancelLabel = $i18n.t('Cancel');
export let confirmLabel = $i18n.t('Confirm'); export let confirmLabel = $i18n.t('Confirm');
@ -58,11 +58,21 @@
}} }}
> >
<div class="px-[1.75rem] py-6"> <div class="px-[1.75rem] py-6">
<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">{title}</div> <div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">
{#if title !== ''}
{title}
{:else}
{$i18n.t('Confirm your action')}
{/if}
</div>
<slot> <slot>
<div class=" text-sm text-gray-500"> <div class=" text-sm text-gray-500">
{message} {#if message !== ''}
{message}
{:else}
{$i18n.t('This action cannot be undone. Do you wish to continue?')}
{/if}
</div> </div>
</slot> </slot>
@ -71,6 +81,7 @@
class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition" class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition"
on:click={() => { on:click={() => {
show = false; show = false;
dispatch('cancel');
}} }}
type="button" type="button"
> >

View File

@ -63,7 +63,7 @@
<div <div
class=" m-auto rounded-2xl max-w-full {sizeToWidth( class=" m-auto rounded-2xl max-w-full {sizeToWidth(
size size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl" )} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden"
in:flyAndScale in:flyAndScale
on:mousedown={(e) => { on:mousedown={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@ -48,7 +48,7 @@
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> <ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger> </Select.Trigger>
<Select.Content <Select.Content
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none" class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none"
transition={flyAndScale} transition={flyAndScale}
sideOffset={4} sideOffset={4}
> >

View File

@ -4,8 +4,8 @@
export let readOnly = false; export let readOnly = false;
export let outerClassName = 'flex flex-1'; export let outerClassName = 'flex flex-1';
export let inputClassName = export let inputClassName =
'w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none'; 'w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none';
export let showButtonClassName = 'px-2 transition rounded-r-lg bg-white dark:bg-gray-850'; export let showButtonClassName = 'px-2 transition rounded-r-lg bg-gray-50 dark:bg-gray-850';
let show = false; let show = false;
</script> </script>

View File

@ -22,7 +22,7 @@
}; };
</script> </script>
<div class="flex {showTagInput ? 'flex-row-reverse' : ''}"> <div class="px-0.5 flex {showTagInput ? 'flex-row-reverse' : ''}">
{#if showTagInput} {#if showTagInput}
<div class="flex items-center"> <div class="flex items-center">
<input <input

View File

@ -3,11 +3,13 @@
import { marked } from 'marked'; import { marked } from 'marked';
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import { roundArrow } from 'tippy.js';
export let placement = 'top'; export let placement = 'top';
export let content = `I'm a tooltip!`; export let content = `I'm a tooltip!`;
export let touch = true; export let touch = true;
export let className = 'flex'; export let className = 'flex';
export let theme = '';
let tooltipElement; let tooltipElement;
let tooltipInstance; let tooltipInstance;
@ -20,7 +22,10 @@
content: content, content: content,
placement: placement, placement: placement,
allowHTML: true, allowHTML: true,
touch: touch touch: touch,
...(theme !== '' ? { theme } : { theme: 'dark' }),
arrow: false,
offset: [0, 4]
}); });
} }
} else if (tooltipInstance && content === '') { } else if (tooltipInstance && content === '') {

View File

@ -109,7 +109,7 @@
<div class="flex flex-1"> <div class="flex flex-1">
<div <div
class="bg-gray-200 dark:bg-gray-800 font-bold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center" class="bg-gray-200 dark:bg-gray-800 font-semibold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
> >
# #
</div> </div>

View File

@ -0,0 +1,17 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
fill="currentColor"
class={className}
stroke-width={strokeWidth}
>
<path
d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
/>
</svg>

View File

@ -21,6 +21,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import UserMenu from './Sidebar/UserMenu.svelte'; import UserMenu from './Sidebar/UserMenu.svelte';
import MenuLines from '../icons/MenuLines.svelte'; import MenuLines from '../icons/MenuLines.svelte';
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -32,6 +33,7 @@
export let selectedModels; export let selectedModels;
export let showModelSelector = true; export let showModelSelector = true;
export let showControls = false;
let showShareChatModal = false; let showShareChatModal = false;
let showDownloadChatModal = false; let showDownloadChatModal = false;
@ -48,7 +50,7 @@
> >
<button <button
id="sidebar-toggle-button" id="sidebar-toggle-button"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => { on:click={() => {
showSidebar.set(!$showSidebar); showSidebar.set(!$showSidebar);
}} }}
@ -58,6 +60,7 @@
</div> </div>
</button> </button>
</div> </div>
<div class="flex-1 overflow-hidden max-w-full"> <div class="flex-1 overflow-hidden max-w-full">
{#if showModelSelector} {#if showModelSelector}
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} /> <ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
@ -79,7 +82,7 @@
}} }}
> >
<button <button
class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
id="chat-context-menu-button" id="chat-context-menu-button"
> >
<div class=" m-auto self-center"> <div class=" m-auto self-center">
@ -101,12 +104,26 @@
</button> </button>
</Menu> </Menu>
{/if} {/if}
<Tooltip content={$i18n.t('Controls')}>
<button
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
showControls = !showControls;
}}
>
<div class=" m-auto self-center">
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
</div>
</button>
</Tooltip>
<Tooltip content={$i18n.t('New Chat')}> <Tooltip content={$i18n.t('New Chat')}>
<button <button
id="new-chat-button" id="new-chat-button"
class=" flex {$showSidebar class=" flex {$showSidebar
? 'md:hidden' ? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" : ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => { on:click={() => {
initNewChat(); initNewChat();
}} }}
@ -140,7 +157,7 @@
}} }}
> >
<button <button
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition" class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
aria-label="User Menu" aria-label="User Menu"
> >
<div class=" self-center"> <div class=" self-center">

View File

@ -186,6 +186,7 @@
goto('/'); goto('/');
} }
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
} }
}; };
</script> </script>
@ -260,7 +261,7 @@
alt="logo" alt="logo"
/> />
</div> </div>
<div class=" self-center font-medium text-sm text-gray-850 dark:text-white"> <div class=" self-center font-medium text-sm text-gray-850 dark:text-white font-primary">
{$i18n.t('New Chat')} {$i18n.t('New Chat')}
</div> </div>
<div class="self-center ml-auto"> <div class="self-center ml-auto">
@ -338,7 +339,7 @@
</div> </div>
<div class="flex self-center"> <div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Workspace')}</div> <div class=" self-center font-medium text-sm font-primary">{$i18n.t('Workspace')}</div>
</div> </div>
</a> </a>
</div> </div>
@ -532,7 +533,7 @@
<div class="px-2.5"> <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"> <div class="flex flex-col font-primary">
{#if $user !== undefined} {#if $user !== undefined}
<UserMenu <UserMenu
role={$user.role} role={$user.role}
@ -555,50 +556,13 @@
alt="User profile" alt="User profile"
/> />
</div> </div>
<div class=" self-center font-semibold">{$user.name}</div> <div class=" self-center font-medium">{$user.name}</div>
</button> </button>
</UserMenu> </UserMenu>
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
<!-- <div
id="sidebar-handle"
class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
<Tooltip
placement="right"
content={`${$showSidebar ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
touch={false}
>
<button
id="sidebar-toggle-button"
class=" group"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
><span class="" data-state="closed"
><div
class="flex h-[72px] w-8 items-center justify-center opacity-50 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] {$showSidebar
? '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] {$showSidebar
? 'group-hover:rotate-[-15deg]'
: 'group-hover:rotate-[15deg]'}"
/>
</div>
</div>
</span>
</button>
</Tooltip>
</div> -->
</div> </div>
<style> <style>

View File

@ -30,7 +30,7 @@
<slot name="content"> <slot name="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow font-primary"
sideOffset={8} sideOffset={8}
side="bottom" side="bottom"
align="start" align="start"

View File

@ -407,7 +407,7 @@
{/if} {/if}
</div> </div>
<div class=" self-center flex-1"> <div class=" self-center flex-1">
<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div> <div class=" font-semibold line-clamp-1">#{doc.name} ({doc.filename})</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{doc.title} {doc.title}
</div> </div>

View File

@ -200,14 +200,14 @@
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <div class=" font-semibold flex items-center gap-1.5">
<div <div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{func.type} {func.type}
</div> </div>
{#if func?.meta?.manifest?.version} {#if func?.meta?.manifest?.version}
<div <div
class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
v{func?.meta?.manifest?.version ?? ''} v{func?.meta?.manifest?.version ?? ''}
</div> </div>
@ -430,7 +430,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a function')}</div> <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
<div class=" text-sm line-clamp-1"> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom functions')} {$i18n.t('Discover, download, and explore custom functions')}
</div> </div>

View File

@ -271,7 +271,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Create a model')}</div> <div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div>
<div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div> <div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div>
</div> </div>
</a> </a>
@ -297,7 +297,7 @@
: ''} " : ''} "
> >
<img <img
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'} src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="modelfile profile" alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover" class=" rounded-full w-full h-auto object-cover"
/> />
@ -307,7 +307,7 @@
<div <div
class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}" class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}"
> >
<div class=" font-bold line-clamp-1">{model.name}</div> <div class=" font-semibold line-clamp-1">{model.name}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id} {!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
</div> </div>
@ -518,7 +518,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a model')}</div> <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
<div class=" text-sm line-clamp-1"> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore model presets')} {$i18n.t('Discover, download, and explore model presets')}
</div> </div>

View File

@ -119,7 +119,7 @@
<div class="flex items-center"> <div class="flex items-center">
<div class="flex flex-col"> <div class="flex flex-col">
<div <div
class=" w-fit text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" w-fit text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{item?.type ?? 'Document'} {item?.type ?? 'Document'}
</div> </div>

View File

@ -119,7 +119,7 @@
<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> <div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}> <a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
<div class=" flex-1 self-center pl-5"> <div class=" flex-1 self-center pl-5">
<div class=" font-bold line-clamp-1">{prompt.command}</div> <div class=" font-semibold line-clamp-1">{prompt.command}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{prompt.title} {prompt.title}
</div> </div>
@ -299,7 +299,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a prompt')}</div> <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
<div class=" text-sm line-clamp-1"> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom prompts')} {$i18n.t('Discover, download, and explore custom prompts')}
</div> </div>

View File

@ -177,14 +177,14 @@
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <div class=" font-semibold flex items-center gap-1.5">
<div <div
class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
TOOL TOOL
</div> </div>
{#if tool?.meta?.manifest?.version} {#if tool?.meta?.manifest?.version}
<div <div
class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
v{tool?.meta?.manifest?.version ?? ''} v{tool?.meta?.manifest?.version ?? ''}
</div> </div>
@ -389,7 +389,7 @@
</div> </div>
<div class=" self-center"> <div class=" self-center">
<div class=" font-bold line-clamp-1">{$i18n.t('Discover a tool')}</div> <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
<div class=" text-sm line-clamp-1"> <div class=" text-sm line-clamp-1">
{$i18n.t('Discover, download, and explore custom tools')} {$i18n.t('Discover, download, and explore custom tools')}
</div> </div>

View File

@ -88,6 +88,7 @@
"Chat": "المحادثة", "Chat": "المحادثة",
"Chat Background Image": "", "Chat Background Image": "",
"Chat Bubble UI": "UI الدردشة", "Chat Bubble UI": "UI الدردشة",
"Chat Controls": "",
"Chat direction": "اتجاه المحادثة", "Chat direction": "اتجاه المحادثة",
"Chat History": "تاريخ المحادثة", "Chat History": "تاريخ المحادثة",
"Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح", "Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح",
@ -130,6 +131,7 @@
"Context Length": "طول السياق", "Context Length": "طول السياق",
"Continue Response": "متابعة الرد", "Continue Response": "متابعة الرد",
"Continue with {{provider}}": "", "Continue with {{provider}}": "",
"Controls": "",
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة", "Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
"Copy": "نسخ", "Copy": "نسخ",
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة", "Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
@ -409,7 +411,6 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "", "Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
"Open": "فتح",
"Open AI (Dall-E)": "AI (Dall-E) فتح", "Open AI (Dall-E)": "AI (Dall-E) فتح",
"Open new chat": "فتح محادثة جديده", "Open new chat": "فتح محادثة جديده",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@ -546,7 +547,6 @@
"Show shortcuts": "إظهار الاختصارات", "Show shortcuts": "إظهار الاختصارات",
"Show your support!": "", "Show your support!": "",
"Showcased creativity": "أظهر الإبداع", "Showcased creativity": "أظهر الإبداع",
"sidebar": "الشريط الجانبي",
"Sign in": "تسجيل الدخول", "Sign in": "تسجيل الدخول",
"Sign Out": "تسجيل الخروج", "Sign Out": "تسجيل الخروج",
"Sign up": "تسجيل", "Sign up": "تسجيل",

View File

@ -88,6 +88,7 @@
"Chat": "Чат", "Chat": "Чат",
"Chat Background Image": "", "Chat Background Image": "",
"Chat Bubble UI": "UI за чат бублон", "Chat Bubble UI": "UI за чат бублон",
"Chat Controls": "",
"Chat direction": "Направление на чата", "Chat direction": "Направление на чата",
"Chat History": "Чат История", "Chat History": "Чат История",
"Chat History is off for this browser.": "Чат История е изключен за този браузър.", "Chat History is off for this browser.": "Чат История е изключен за този браузър.",
@ -130,6 +131,7 @@
"Context Length": "Дължина на Контекста", "Context Length": "Дължина на Контекста",
"Continue Response": "Продължи отговора", "Continue Response": "Продължи отговора",
"Continue with {{provider}}": "", "Continue with {{provider}}": "",
"Controls": "",
"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!", "Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
"Copy": "Копирай", "Copy": "Копирай",
"Copy last code block": "Копиране на последен код блок", "Copy last code block": "Копиране на последен код блок",
@ -409,7 +411,6 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.", "Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
"Oops! There was an error in the previous response. Please try again or contact admin.": "", "Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
"Open": "Отвори",
"Open AI (Dall-E)": "Open AI (Dall-E)", "Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Отвори нов чат", "Open new chat": "Отвори нов чат",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@ -542,7 +543,6 @@
"Show shortcuts": "Покажи", "Show shortcuts": "Покажи",
"Show your support!": "", "Show your support!": "",
"Showcased creativity": "Показана креативност", "Showcased creativity": "Показана креативност",
"sidebar": "sidebar",
"Sign in": "Вписване", "Sign in": "Вписване",
"Sign Out": "Изход", "Sign Out": "Изход",
"Sign up": "Регистрация", "Sign up": "Регистрация",

View File

@ -88,6 +88,7 @@
"Chat": "চ্যাট", "Chat": "চ্যাট",
"Chat Background Image": "", "Chat Background Image": "",
"Chat Bubble UI": "চ্যাট বাবল UI", "Chat Bubble UI": "চ্যাট বাবল UI",
"Chat Controls": "",
"Chat direction": "চ্যাট দিকনির্দেশ", "Chat direction": "চ্যাট দিকনির্দেশ",
"Chat History": "চ্যাট হিস্টোরি", "Chat History": "চ্যাট হিস্টোরি",
"Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে", "Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে",
@ -130,6 +131,7 @@
"Context Length": "কনটেক্সটের দৈর্ঘ্য", "Context Length": "কনটেক্সটের দৈর্ঘ্য",
"Continue Response": "যাচাই করুন", "Continue Response": "যাচাই করুন",
"Continue with {{provider}}": "", "Continue with {{provider}}": "",
"Controls": "",
"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!", "Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
"Copy": "অনুলিপি", "Copy": "অনুলিপি",
"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন", "Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
@ -409,7 +411,6 @@
"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।", "Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
"Oops! There was an error in the previous response. Please try again or contact admin.": "", "Oops! There was an error in the previous response. Please try again or contact admin.": "",
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
"Open": "খোলা",
"Open AI (Dall-E)": "Open AI (Dall-E)", "Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "নতুন চ্যাট খুলুন", "Open new chat": "নতুন চ্যাট খুলুন",
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@ -542,7 +543,6 @@
"Show shortcuts": "শর্টকাটগুলো দেখান", "Show shortcuts": "শর্টকাটগুলো দেখান",
"Show your support!": "", "Show your support!": "",
"Showcased creativity": "সৃজনশীলতা প্রদর্শন", "Showcased creativity": "সৃজনশীলতা প্রদর্শন",
"sidebar": "সাইডবার",
"Sign in": "সাইন ইন", "Sign in": "সাইন ইন",
"Sign Out": "সাইন আউট", "Sign Out": "সাইন আউট",
"Sign up": "সাইন আপ", "Sign up": "সাইন আপ",

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