mirror of
https://git.mirrors.martin98.com/https://github.com/open-webui/open-webui
synced 2025-08-18 21:26:03 +08:00
rebase
This commit is contained in:
commit
f9e3c47d4a
38
.github/workflows/integration-test.yml
vendored
38
.github/workflows/integration-test.yml
vendored
@ -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
114
backend/alembic.ini
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
|
@ -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://")
|
||||||
|
@ -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"
|
||||||
|
119
backend/main.py
119
backend/main.py
@ -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")
|
||||||
|
|
||||||
|
4
backend/migrations/README
Normal file
4
backend/migrations/README
Normal 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
96
backend/migrations/env.py
Normal 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()
|
27
backend/migrations/script.py.mako
Normal file
27
backend/migrations/script.py.mako
Normal 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"}
|
9
backend/migrations/util.py
Normal file
9
backend/migrations/util.py
Normal 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
|
202
backend/migrations/versions/7e5b5dc7342b_init.py
Normal file
202
backend/migrations/versions/7e5b5dc7342b_init.py
Normal 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 ###
|
@ -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
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
0
backend/test/__init__.py
Normal file
202
backend/test/apps/webui/routers/test_auths.py
Normal file
202
backend/test/apps/webui/routers/test_auths.py
Normal 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"}
|
238
backend/test/apps/webui/routers/test_chats.py
Normal file
238
backend/test/apps/webui/routers/test_chats.py
Normal 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
|
106
backend/test/apps/webui/routers/test_documents.py
Normal file
106
backend/test/apps/webui/routers/test_documents.py
Normal 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
|
62
backend/test/apps/webui/routers/test_models.py
Normal file
62
backend/test/apps/webui/routers/test_models.py
Normal 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
|
92
backend/test/apps/webui/routers/test_prompts.py
Normal file
92
backend/test/apps/webui/routers/test_prompts.py
Normal 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
|
168
backend/test/apps/webui/routers/test_users.py
Normal file
168
backend/test/apps/webui/routers/test_users.py
Normal 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")
|
161
backend/test/util/abstract_integration_test.py
Normal file
161
backend/test/util/abstract_integration_test.py
Normal 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()
|
45
backend/test/util/mock_user.py
Normal file
45
backend/test/util/mock_user.py
Normal 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 = {}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
18
src/app.css
18
src/app.css
@ -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;
|
||||||
|
}
|
||||||
|
12
src/app.html
12
src/app.html
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -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('What’s New in')}
|
{$i18n.t('What’s 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'
|
||||||
|
@ -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';
|
||||||
}}
|
}}
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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]}
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
63
src/lib/components/chat/ChatControls.svelte
Normal file
63
src/lib/components/chat/ChatControls.svelte
Normal 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}
|
49
src/lib/components/chat/Controls/Controls.svelte
Normal file
49
src/lib/components/chat/Controls/Controls.svelte
Normal 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>
|
@ -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">
|
||||||
|
@ -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}');`
|
||||||
: ''}
|
: ''}
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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
|
||||||
|
@ -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">×</button
|
class=" -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white"
|
||||||
|
>×</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
18
src/lib/components/common/Collapsible.svelte
Normal file
18
src/lib/components/common/Collapsible.svelte
Normal 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>
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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();
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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 === '') {
|
||||||
|
@ -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>
|
||||||
|
17
src/lib/components/icons/AdjustmentsHorizontal.svelte
Normal file
17
src/lib/components/icons/AdjustmentsHorizontal.svelte
Normal 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>
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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": "تسجيل",
|
||||||
|
@ -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": "Регистрация",
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user