mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
Merge remote-tracking branch 'origin/dev' into Anush008/main
Signed-off-by: Anush008 <anushshetty90@gmail.com>
This commit is contained in:
commit
7c734d3fea
216 changed files with 2166 additions and 1816 deletions
|
|
@ -19,6 +19,7 @@ from open_webui.env import (
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
ENV,
|
ENV,
|
||||||
REDIS_URL,
|
REDIS_URL,
|
||||||
|
REDIS_KEY_PREFIX,
|
||||||
REDIS_SENTINEL_HOSTS,
|
REDIS_SENTINEL_HOSTS,
|
||||||
REDIS_SENTINEL_PORT,
|
REDIS_SENTINEL_PORT,
|
||||||
FRONTEND_BUILD_DIR,
|
FRONTEND_BUILD_DIR,
|
||||||
|
|
@ -211,11 +212,16 @@ class PersistentConfig(Generic[T]):
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
_state: dict[str, PersistentConfig]
|
_state: dict[str, PersistentConfig]
|
||||||
_redis: Optional[redis.Redis] = None
|
_redis: Optional[redis.Redis] = None
|
||||||
|
_redis_key_prefix: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, redis_url: Optional[str] = None, redis_sentinels: Optional[list] = []
|
self,
|
||||||
|
redis_url: Optional[str] = None,
|
||||||
|
redis_sentinels: Optional[list] = [],
|
||||||
|
redis_key_prefix: str = "open-webui",
|
||||||
):
|
):
|
||||||
super().__setattr__("_state", {})
|
super().__setattr__("_state", {})
|
||||||
|
super().__setattr__("_redis_key_prefix", redis_key_prefix)
|
||||||
if redis_url:
|
if redis_url:
|
||||||
super().__setattr__(
|
super().__setattr__(
|
||||||
"_redis",
|
"_redis",
|
||||||
|
|
@ -230,7 +236,7 @@ class AppConfig:
|
||||||
self._state[key].save()
|
self._state[key].save()
|
||||||
|
|
||||||
if self._redis:
|
if self._redis:
|
||||||
redis_key = f"open-webui:config:{key}"
|
redis_key = f"{self._redis_key_prefix}:config:{key}"
|
||||||
self._redis.set(redis_key, json.dumps(self._state[key].value))
|
self._redis.set(redis_key, json.dumps(self._state[key].value))
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
|
|
@ -239,7 +245,7 @@ class AppConfig:
|
||||||
|
|
||||||
# If Redis is available, check for an updated value
|
# If Redis is available, check for an updated value
|
||||||
if self._redis:
|
if self._redis:
|
||||||
redis_key = f"open-webui:config:{key}"
|
redis_key = f"{self._redis_key_prefix}:config:{key}"
|
||||||
redis_value = self._redis.get(redis_key)
|
redis_value = self._redis.get(redis_key)
|
||||||
|
|
||||||
if redis_value is not None:
|
if redis_value is not None:
|
||||||
|
|
@ -431,6 +437,12 @@ OAUTH_SCOPES = PersistentConfig(
|
||||||
os.environ.get("OAUTH_SCOPES", "openid email profile"),
|
os.environ.get("OAUTH_SCOPES", "openid email profile"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OAUTH_TIMEOUT = PersistentConfig(
|
||||||
|
"OAUTH_TIMEOUT",
|
||||||
|
"oauth.oidc.oauth_timeout",
|
||||||
|
os.environ.get("OAUTH_TIMEOUT", ""),
|
||||||
|
)
|
||||||
|
|
||||||
OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
|
OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
|
||||||
"OAUTH_CODE_CHALLENGE_METHOD",
|
"OAUTH_CODE_CHALLENGE_METHOD",
|
||||||
"oauth.oidc.code_challenge_method",
|
"oauth.oidc.code_challenge_method",
|
||||||
|
|
@ -540,7 +552,14 @@ def load_oauth_providers():
|
||||||
client_id=GOOGLE_CLIENT_ID.value,
|
client_id=GOOGLE_CLIENT_ID.value,
|
||||||
client_secret=GOOGLE_CLIENT_SECRET.value,
|
client_secret=GOOGLE_CLIENT_SECRET.value,
|
||||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||||
client_kwargs={"scope": GOOGLE_OAUTH_SCOPE.value},
|
client_kwargs={
|
||||||
|
"scope": GOOGLE_OAUTH_SCOPE.value,
|
||||||
|
**(
|
||||||
|
{"timeout": int(OAUTH_TIMEOUT.value)}
|
||||||
|
if OAUTH_TIMEOUT.value
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
redirect_uri=GOOGLE_REDIRECT_URI.value,
|
redirect_uri=GOOGLE_REDIRECT_URI.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -563,6 +582,11 @@ def load_oauth_providers():
|
||||||
server_metadata_url=f"{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
|
server_metadata_url=f"{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
|
||||||
client_kwargs={
|
client_kwargs={
|
||||||
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
||||||
|
**(
|
||||||
|
{"timeout": int(OAUTH_TIMEOUT.value)}
|
||||||
|
if OAUTH_TIMEOUT.value
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
redirect_uri=MICROSOFT_REDIRECT_URI.value,
|
redirect_uri=MICROSOFT_REDIRECT_URI.value,
|
||||||
)
|
)
|
||||||
|
|
@ -584,7 +608,14 @@ def load_oauth_providers():
|
||||||
authorize_url="https://github.com/login/oauth/authorize",
|
authorize_url="https://github.com/login/oauth/authorize",
|
||||||
api_base_url="https://api.github.com",
|
api_base_url="https://api.github.com",
|
||||||
userinfo_endpoint="https://api.github.com/user",
|
userinfo_endpoint="https://api.github.com/user",
|
||||||
client_kwargs={"scope": GITHUB_CLIENT_SCOPE.value},
|
client_kwargs={
|
||||||
|
"scope": GITHUB_CLIENT_SCOPE.value,
|
||||||
|
**(
|
||||||
|
{"timeout": int(OAUTH_TIMEOUT.value)}
|
||||||
|
if OAUTH_TIMEOUT.value
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
|
redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -603,6 +634,9 @@ def load_oauth_providers():
|
||||||
def oidc_oauth_register(client):
|
def oidc_oauth_register(client):
|
||||||
client_kwargs = {
|
client_kwargs = {
|
||||||
"scope": OAUTH_SCOPES.value,
|
"scope": OAUTH_SCOPES.value,
|
||||||
|
**(
|
||||||
|
{"timeout": int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -895,6 +929,18 @@ except Exception:
|
||||||
pass
|
pass
|
||||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# MODELS
|
||||||
|
####################################
|
||||||
|
|
||||||
|
ENABLE_BASE_MODELS_CACHE = PersistentConfig(
|
||||||
|
"ENABLE_BASE_MODELS_CACHE",
|
||||||
|
"models.base_models_cache",
|
||||||
|
os.environ.get("ENABLE_BASE_MODELS_CACHE", "False").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# TOOL_SERVERS
|
# TOOL_SERVERS
|
||||||
####################################
|
####################################
|
||||||
|
|
@ -1799,6 +1845,7 @@ QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
|
||||||
ENABLE_QDRANT_MULTITENANCY_MODE = (
|
ENABLE_QDRANT_MULTITENANCY_MODE = (
|
||||||
os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "true").lower() == "true"
|
os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "true").lower() == "true"
|
||||||
)
|
)
|
||||||
|
QDRANT_COLLECTION_PREFIX = os.environ.get("QDRANT_COLLECTION_PREFIX", "open-webui")
|
||||||
|
|
||||||
# OpenSearch
|
# OpenSearch
|
||||||
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
|
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,30 @@ else:
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
DATABASE_TYPE = os.environ.get("DATABASE_TYPE")
|
||||||
|
DATABASE_USER = os.environ.get("DATABASE_USER")
|
||||||
|
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
|
||||||
|
|
||||||
|
DATABASE_CRED = ""
|
||||||
|
if DATABASE_USER:
|
||||||
|
DATABASE_CRED += f"{DATABASE_USER}"
|
||||||
|
if DATABASE_PASSWORD:
|
||||||
|
DATABASE_CRED += f":{DATABASE_PASSWORD}"
|
||||||
|
if DATABASE_CRED:
|
||||||
|
DATABASE_CRED += "@"
|
||||||
|
|
||||||
|
|
||||||
|
DB_VARS = {
|
||||||
|
"db_type": DATABASE_TYPE,
|
||||||
|
"db_cred": DATABASE_CRED,
|
||||||
|
"db_host": os.environ.get("DATABASE_HOST"),
|
||||||
|
"db_port": os.environ.get("DATABASE_PORT"),
|
||||||
|
"db_name": os.environ.get("DATABASE_NAME"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if all(DB_VARS.values()):
|
||||||
|
DATABASE_URL = f"{DB_VARS['db_type']}://{DB_VARS['db_cred']}@{DB_VARS['db_host']}:{DB_VARS['db_port']}/{DB_VARS['db_name']}"
|
||||||
|
|
||||||
# Replace the postgres:// with postgresql://
|
# Replace the postgres:// with postgresql://
|
||||||
if "postgres://" in DATABASE_URL:
|
if "postgres://" in DATABASE_URL:
|
||||||
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
||||||
|
|
@ -324,6 +348,7 @@ ENABLE_REALTIME_CHAT_SAVE = (
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
REDIS_URL = os.environ.get("REDIS_URL", "")
|
REDIS_URL = os.environ.get("REDIS_URL", "")
|
||||||
|
REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
|
||||||
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
|
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
|
||||||
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
|
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
|
||||||
|
|
||||||
|
|
@ -399,10 +424,29 @@ ENABLE_COMPRESSION_MIDDLEWARE = (
|
||||||
os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
|
os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# MODELS
|
||||||
|
####################################
|
||||||
|
|
||||||
|
MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
|
||||||
|
if MODELS_CACHE_TTL == "":
|
||||||
|
MODELS_CACHE_TTL = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
|
||||||
|
except Exception:
|
||||||
|
MODELS_CACHE_TTL = 1
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# WEBSOCKET SUPPORT
|
||||||
|
####################################
|
||||||
|
|
||||||
ENABLE_WEBSOCKET_SUPPORT = (
|
ENABLE_WEBSOCKET_SUPPORT = (
|
||||||
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
|
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
||||||
|
|
||||||
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
||||||
|
|
@ -509,11 +553,14 @@ else:
|
||||||
# OFFLINE_MODE
|
# OFFLINE_MODE
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
ENABLE_VERSION_UPDATE_CHECK = (
|
||||||
|
os.environ.get("ENABLE_VERSION_UPDATE_CHECK", "true").lower() == "true"
|
||||||
|
)
|
||||||
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
|
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
|
||||||
|
|
||||||
if OFFLINE_MODE:
|
if OFFLINE_MODE:
|
||||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||||
|
ENABLE_VERSION_UPDATE_CHECK = False
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# AUDIT LOGGING
|
# AUDIT LOGGING
|
||||||
|
|
@ -522,6 +569,14 @@ if OFFLINE_MODE:
|
||||||
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
|
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
|
||||||
# Maximum size of a file before rotating into a new log file
|
# Maximum size of a file before rotating into a new log file
|
||||||
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
|
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
|
||||||
|
|
||||||
|
# Comma separated list of logger names to use for audit logging
|
||||||
|
# Default is "uvicorn.access" which is the access log for Uvicorn
|
||||||
|
# You can add more logger names to this list if you want to capture more logs
|
||||||
|
AUDIT_UVICORN_LOGGER_NAMES = os.getenv(
|
||||||
|
"AUDIT_UVICORN_LOGGER_NAMES", "uvicorn.access"
|
||||||
|
).split(",")
|
||||||
|
|
||||||
# METADATA | REQUEST | REQUEST_RESPONSE
|
# METADATA | REQUEST | REQUEST_RESPONSE
|
||||||
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
|
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper()
|
||||||
try:
|
try:
|
||||||
|
|
@ -559,6 +614,12 @@ OTEL_TRACES_SAMPLER = os.environ.get(
|
||||||
OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
|
OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
|
||||||
OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
|
OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
|
||||||
|
|
||||||
|
|
||||||
|
OTEL_OTLP_SPAN_EXPORTER = os.environ.get(
|
||||||
|
"OTEL_OTLP_SPAN_EXPORTER", "grpc"
|
||||||
|
).lower() # grpc or http
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# TOOLS/FUNCTIONS PIP OPTIONS
|
# TOOLS/FUNCTIONS PIP OPTIONS
|
||||||
####################################
|
####################################
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ from fastapi import (
|
||||||
applications,
|
applications,
|
||||||
BackgroundTasks,
|
BackgroundTasks,
|
||||||
)
|
)
|
||||||
|
|
||||||
from fastapi.openapi.docs import get_swagger_ui_html
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
@ -49,6 +48,7 @@ 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
|
||||||
from starlette.responses import Response, StreamingResponse
|
from starlette.responses import Response, StreamingResponse
|
||||||
|
from starlette.datastructures import Headers
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils import logger
|
from open_webui.utils import logger
|
||||||
|
|
@ -116,6 +116,8 @@ from open_webui.config import (
|
||||||
OPENAI_API_CONFIGS,
|
OPENAI_API_CONFIGS,
|
||||||
# Direct Connections
|
# Direct Connections
|
||||||
ENABLE_DIRECT_CONNECTIONS,
|
ENABLE_DIRECT_CONNECTIONS,
|
||||||
|
# Model list
|
||||||
|
ENABLE_BASE_MODELS_CACHE,
|
||||||
# Thread pool size for FastAPI/AnyIO
|
# Thread pool size for FastAPI/AnyIO
|
||||||
THREAD_POOL_SIZE,
|
THREAD_POOL_SIZE,
|
||||||
# Tool Server Configs
|
# Tool Server Configs
|
||||||
|
|
@ -396,6 +398,7 @@ from open_webui.env import (
|
||||||
AUDIT_LOG_LEVEL,
|
AUDIT_LOG_LEVEL,
|
||||||
CHANGELOG,
|
CHANGELOG,
|
||||||
REDIS_URL,
|
REDIS_URL,
|
||||||
|
REDIS_KEY_PREFIX,
|
||||||
REDIS_SENTINEL_HOSTS,
|
REDIS_SENTINEL_HOSTS,
|
||||||
REDIS_SENTINEL_PORT,
|
REDIS_SENTINEL_PORT,
|
||||||
GLOBAL_LOG_LEVEL,
|
GLOBAL_LOG_LEVEL,
|
||||||
|
|
@ -415,7 +418,7 @@ from open_webui.env import (
|
||||||
ENABLE_WEBSOCKET_SUPPORT,
|
ENABLE_WEBSOCKET_SUPPORT,
|
||||||
BYPASS_MODEL_ACCESS_CONTROL,
|
BYPASS_MODEL_ACCESS_CONTROL,
|
||||||
RESET_CONFIG_ON_START,
|
RESET_CONFIG_ON_START,
|
||||||
OFFLINE_MODE,
|
ENABLE_VERSION_UPDATE_CHECK,
|
||||||
ENABLE_OTEL,
|
ENABLE_OTEL,
|
||||||
EXTERNAL_PWA_MANIFEST_URL,
|
EXTERNAL_PWA_MANIFEST_URL,
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
|
@ -534,6 +537,27 @@ async def lifespan(app: FastAPI):
|
||||||
|
|
||||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||||
|
|
||||||
|
if app.state.config.ENABLE_BASE_MODELS_CACHE:
|
||||||
|
await get_all_models(
|
||||||
|
Request(
|
||||||
|
# Creating a mock request object to pass to get_all_models
|
||||||
|
{
|
||||||
|
"type": "http",
|
||||||
|
"asgi.version": "3.0",
|
||||||
|
"asgi.spec_version": "2.0",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/internal",
|
||||||
|
"query_string": b"",
|
||||||
|
"headers": Headers({}).raw,
|
||||||
|
"client": ("127.0.0.1", 12345),
|
||||||
|
"server": ("127.0.0.1", 80),
|
||||||
|
"scheme": "http",
|
||||||
|
"app": app,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
if hasattr(app.state, "redis_task_command_listener"):
|
if hasattr(app.state, "redis_task_command_listener"):
|
||||||
|
|
@ -554,6 +578,7 @@ app.state.instance_id = None
|
||||||
app.state.config = AppConfig(
|
app.state.config = AppConfig(
|
||||||
redis_url=REDIS_URL,
|
redis_url=REDIS_URL,
|
||||||
redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
|
redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
|
||||||
|
redis_key_prefix=REDIS_KEY_PREFIX,
|
||||||
)
|
)
|
||||||
app.state.redis = None
|
app.state.redis = None
|
||||||
|
|
||||||
|
|
@ -616,6 +641,15 @@ app.state.TOOL_SERVERS = []
|
||||||
|
|
||||||
app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
|
app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
|
||||||
|
|
||||||
|
########################################
|
||||||
|
#
|
||||||
|
# MODELS
|
||||||
|
#
|
||||||
|
########################################
|
||||||
|
|
||||||
|
app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE
|
||||||
|
app.state.BASE_MODELS = []
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
# WEBUI
|
# WEBUI
|
||||||
|
|
@ -1191,7 +1225,9 @@ if audit_level != AuditLevel.NONE:
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/models")
|
@app.get("/api/models")
|
||||||
async def get_models(request: Request, user=Depends(get_verified_user)):
|
async def get_models(
|
||||||
|
request: Request, refresh: bool = False, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
def get_filtered_models(models, user):
|
def get_filtered_models(models, user):
|
||||||
filtered_models = []
|
filtered_models = []
|
||||||
for model in models:
|
for model in models:
|
||||||
|
|
@ -1215,7 +1251,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
return filtered_models
|
return filtered_models
|
||||||
|
|
||||||
all_models = await get_all_models(request, user=user)
|
all_models = await get_all_models(request, refresh=refresh, user=user)
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
for model in all_models:
|
for model in all_models:
|
||||||
|
|
@ -1471,7 +1507,7 @@ async def list_tasks_by_chat_id_endpoint(
|
||||||
|
|
||||||
task_ids = await list_task_ids_by_chat_id(request, chat_id)
|
task_ids = await list_task_ids_by_chat_id(request, chat_id)
|
||||||
|
|
||||||
print(f"Task IDs for chat {chat_id}: {task_ids}")
|
log.debug(f"Task IDs for chat {chat_id}: {task_ids}")
|
||||||
return {"task_ids": task_ids}
|
return {"task_ids": task_ids}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1524,6 +1560,7 @@ async def get_app_config(request: Request):
|
||||||
"enable_signup": app.state.config.ENABLE_SIGNUP,
|
"enable_signup": app.state.config.ENABLE_SIGNUP,
|
||||||
"enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
|
"enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
|
||||||
"enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
|
"enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
|
||||||
|
"enable_version_update_check": ENABLE_VERSION_UPDATE_CHECK,
|
||||||
**(
|
**(
|
||||||
{
|
{
|
||||||
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||||
|
|
@ -1629,9 +1666,9 @@ async def get_app_version():
|
||||||
|
|
||||||
@app.get("/api/version/updates")
|
@app.get("/api/version/updates")
|
||||||
async def get_app_latest_release_version(user=Depends(get_verified_user)):
|
async def get_app_latest_release_version(user=Depends(get_verified_user)):
|
||||||
if OFFLINE_MODE:
|
if not ENABLE_VERSION_UPDATE_CHECK:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Offline mode is enabled, returning current version as latest version"
|
f"Version update check is disabled, returning current version as latest version"
|
||||||
)
|
)
|
||||||
return {"current": VERSION, "latest": VERSION}
|
return {"current": VERSION, "latest": VERSION}
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
|
||||||
from sqlalchemy import or_, func, select, and_, text
|
from sqlalchemy import or_, func, select, and_, text
|
||||||
from sqlalchemy.sql import exists
|
from sqlalchemy.sql import exists
|
||||||
|
from sqlalchemy.sql.expression import bindparam
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Chat DB Schema
|
# Chat DB Schema
|
||||||
|
|
@ -72,6 +73,8 @@ class ChatImportForm(ChatForm):
|
||||||
meta: Optional[dict] = {}
|
meta: Optional[dict] = {}
|
||||||
pinned: Optional[bool] = False
|
pinned: Optional[bool] = False
|
||||||
folder_id: Optional[str] = None
|
folder_id: Optional[str] = None
|
||||||
|
created_at: Optional[int] = None
|
||||||
|
updated_at: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ChatTitleMessagesForm(BaseModel):
|
class ChatTitleMessagesForm(BaseModel):
|
||||||
|
|
@ -147,8 +150,16 @@ class ChatTable:
|
||||||
"meta": form_data.meta,
|
"meta": form_data.meta,
|
||||||
"pinned": form_data.pinned,
|
"pinned": form_data.pinned,
|
||||||
"folder_id": form_data.folder_id,
|
"folder_id": form_data.folder_id,
|
||||||
"created_at": int(time.time()),
|
"created_at": (
|
||||||
"updated_at": int(time.time()),
|
form_data.created_at
|
||||||
|
if form_data.created_at
|
||||||
|
else int(time.time())
|
||||||
|
),
|
||||||
|
"updated_at": (
|
||||||
|
form_data.updated_at
|
||||||
|
if form_data.updated_at
|
||||||
|
else int(time.time())
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -232,6 +243,10 @@ class ChatTable:
|
||||||
if chat is None:
|
if chat is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Sanitize message content for null characters before upserting
|
||||||
|
if isinstance(message.get("content"), str):
|
||||||
|
message["content"] = message["content"].replace("\x00", "")
|
||||||
|
|
||||||
chat = chat.chat
|
chat = chat.chat
|
||||||
history = chat.get("history", {})
|
history = chat.get("history", {})
|
||||||
|
|
||||||
|
|
@ -580,7 +595,7 @@ class ChatTable:
|
||||||
"""
|
"""
|
||||||
Filters chats based on a search query using Python, allowing pagination using skip and limit.
|
Filters chats based on a search query using Python, allowing pagination using skip and limit.
|
||||||
"""
|
"""
|
||||||
search_text = search_text.lower().strip()
|
search_text = search_text.replace("\u0000", "").lower().strip()
|
||||||
|
|
||||||
if not search_text:
|
if not search_text:
|
||||||
return self.get_chat_list_by_user_id(
|
return self.get_chat_list_by_user_id(
|
||||||
|
|
@ -614,21 +629,19 @@ class ChatTable:
|
||||||
dialect_name = db.bind.dialect.name
|
dialect_name = db.bind.dialect.name
|
||||||
if dialect_name == "sqlite":
|
if dialect_name == "sqlite":
|
||||||
# SQLite case: using JSON1 extension for JSON searching
|
# SQLite case: using JSON1 extension for JSON searching
|
||||||
|
sqlite_content_sql = (
|
||||||
|
"EXISTS ("
|
||||||
|
" SELECT 1 "
|
||||||
|
" FROM json_each(Chat.chat, '$.messages') AS message "
|
||||||
|
" WHERE LOWER(message.value->>'content') LIKE '%' || :content_key || '%'"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
sqlite_content_clause = text(sqlite_content_sql)
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
(
|
or_(
|
||||||
Chat.title.ilike(
|
Chat.title.ilike(bindparam('title_key')),
|
||||||
f"%{search_text}%"
|
sqlite_content_clause
|
||||||
) # Case-insensitive search in title
|
).params(title_key=f"%{search_text}%", content_key=search_text)
|
||||||
| text(
|
|
||||||
"""
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM json_each(Chat.chat, '$.messages') AS message
|
|
||||||
WHERE LOWER(message.value->>'content') LIKE '%' || :search_text || '%'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
).params(search_text=search_text)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if there are any tags to filter, it should have all the tags
|
# Check if there are any tags to filter, it should have all the tags
|
||||||
|
|
@ -663,21 +676,19 @@ class ChatTable:
|
||||||
|
|
||||||
elif dialect_name == "postgresql":
|
elif dialect_name == "postgresql":
|
||||||
# PostgreSQL relies on proper JSON query for search
|
# PostgreSQL relies on proper JSON query for search
|
||||||
|
postgres_content_sql = (
|
||||||
|
"EXISTS ("
|
||||||
|
" SELECT 1 "
|
||||||
|
" FROM json_array_elements(Chat.chat->'messages') AS message "
|
||||||
|
" WHERE LOWER(message->>'content') LIKE '%' || :content_key || '%'"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
postgres_content_clause = text(postgres_content_sql)
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
(
|
or_(
|
||||||
Chat.title.ilike(
|
Chat.title.ilike(bindparam('title_key')),
|
||||||
f"%{search_text}%"
|
postgres_content_clause
|
||||||
) # Case-insensitive search in title
|
).params(title_key=f"%{search_text}%", content_key=search_text)
|
||||||
| text(
|
|
||||||
"""
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM json_array_elements(Chat.chat->'messages') AS message
|
|
||||||
WHERE LOWER(message->>'content') LIKE '%' || :search_text || '%'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
).params(search_text=search_text)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if there are any tags to filter, it should have all the tags
|
# Check if there are any tags to filter, it should have all the tags
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,7 @@ class MistralLoader:
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
headers={"User-Agent": "OpenWebUI-MistralLoader/2.0"},
|
headers={"User-Agent": "OpenWebUI-MistralLoader/2.0"},
|
||||||
raise_for_status=False, # We handle status codes manually
|
raise_for_status=False, # We handle status codes manually
|
||||||
|
trust_env=True,
|
||||||
) as session:
|
) as session:
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -460,20 +460,19 @@ def get_sources_from_files(
|
||||||
)
|
)
|
||||||
|
|
||||||
extracted_collections = []
|
extracted_collections = []
|
||||||
relevant_contexts = []
|
query_results = []
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
|
query_result = None
|
||||||
context = None
|
|
||||||
if file.get("docs"):
|
if file.get("docs"):
|
||||||
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
context = {
|
query_result = {
|
||||||
"documents": [[doc.get("content") for doc in file.get("docs")]],
|
"documents": [[doc.get("content") for doc in file.get("docs")]],
|
||||||
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
|
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
|
||||||
}
|
}
|
||||||
elif file.get("context") == "full":
|
elif file.get("context") == "full":
|
||||||
# Manual Full Mode Toggle
|
# Manual Full Mode Toggle
|
||||||
context = {
|
query_result = {
|
||||||
"documents": [[file.get("file").get("data", {}).get("content")]],
|
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||||
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
|
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
|
||||||
}
|
}
|
||||||
|
|
@ -500,7 +499,7 @@ def get_sources_from_files(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {
|
query_result = {
|
||||||
"documents": [documents],
|
"documents": [documents],
|
||||||
"metadatas": [metadatas],
|
"metadatas": [metadatas],
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +507,7 @@ def get_sources_from_files(
|
||||||
elif file.get("id"):
|
elif file.get("id"):
|
||||||
file_object = Files.get_file_by_id(file.get("id"))
|
file_object = Files.get_file_by_id(file.get("id"))
|
||||||
if file_object:
|
if file_object:
|
||||||
context = {
|
query_result = {
|
||||||
"documents": [[file_object.data.get("content", "")]],
|
"documents": [[file_object.data.get("content", "")]],
|
||||||
"metadatas": [
|
"metadatas": [
|
||||||
[
|
[
|
||||||
|
|
@ -521,7 +520,7 @@ def get_sources_from_files(
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
elif file.get("file").get("data"):
|
elif file.get("file").get("data"):
|
||||||
context = {
|
query_result = {
|
||||||
"documents": [[file.get("file").get("data", {}).get("content")]],
|
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||||
"metadatas": [
|
"metadatas": [
|
||||||
[file.get("file").get("data", {}).get("metadata", {})]
|
[file.get("file").get("data", {}).get("metadata", {})]
|
||||||
|
|
@ -549,19 +548,27 @@ def get_sources_from_files(
|
||||||
|
|
||||||
if full_context:
|
if full_context:
|
||||||
try:
|
try:
|
||||||
context = get_all_items_from_collections(collection_names)
|
query_result = get_all_items_from_collections(collection_names)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
context = None
|
query_result = None
|
||||||
if file.get("type") == "text":
|
if file.get("type") == "text":
|
||||||
context = file["content"]
|
# Not sure when this is used, but it seems to be a fallback
|
||||||
|
query_result = {
|
||||||
|
"documents": [
|
||||||
|
[file.get("file").get("data", {}).get("content")]
|
||||||
|
],
|
||||||
|
"metadatas": [
|
||||||
|
[file.get("file").get("data", {}).get("meta", {})]
|
||||||
|
],
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
if hybrid_search:
|
if hybrid_search:
|
||||||
try:
|
try:
|
||||||
context = query_collection_with_hybrid_search(
|
query_result = query_collection_with_hybrid_search(
|
||||||
collection_names=collection_names,
|
collection_names=collection_names,
|
||||||
queries=queries,
|
queries=queries,
|
||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
|
|
@ -577,8 +584,8 @@ def get_sources_from_files(
|
||||||
" non hybrid search as fallback."
|
" non hybrid search as fallback."
|
||||||
)
|
)
|
||||||
|
|
||||||
if (not hybrid_search) or (context is None):
|
if (not hybrid_search) or (query_result is None):
|
||||||
context = query_collection(
|
query_result = query_collection(
|
||||||
collection_names=collection_names,
|
collection_names=collection_names,
|
||||||
queries=queries,
|
queries=queries,
|
||||||
embedding_function=embedding_function,
|
embedding_function=embedding_function,
|
||||||
|
|
@ -589,24 +596,24 @@ def get_sources_from_files(
|
||||||
|
|
||||||
extracted_collections.extend(collection_names)
|
extracted_collections.extend(collection_names)
|
||||||
|
|
||||||
if context:
|
if query_result:
|
||||||
if "data" in file:
|
if "data" in file:
|
||||||
del file["data"]
|
del file["data"]
|
||||||
|
|
||||||
relevant_contexts.append({**context, "file": file})
|
query_results.append({**query_result, "file": file})
|
||||||
|
|
||||||
sources = []
|
sources = []
|
||||||
for context in relevant_contexts:
|
for query_result in query_results:
|
||||||
try:
|
try:
|
||||||
if "documents" in context:
|
if "documents" in query_result:
|
||||||
if "metadatas" in context:
|
if "metadatas" in query_result:
|
||||||
source = {
|
source = {
|
||||||
"source": context["file"],
|
"source": query_result["file"],
|
||||||
"document": context["documents"][0],
|
"document": query_result["documents"][0],
|
||||||
"metadata": context["metadatas"][0],
|
"metadata": query_result["metadatas"][0],
|
||||||
}
|
}
|
||||||
if "distances" in context and context["distances"]:
|
if "distances" in query_result and query_result["distances"]:
|
||||||
source["distances"] = context["distances"][0]
|
source["distances"] = query_result["distances"][0]
|
||||||
|
|
||||||
sources.append(source)
|
sources.append(source)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -157,10 +157,10 @@ class OpenSearchClient(VectorDBBase):
|
||||||
|
|
||||||
for field, value in filter.items():
|
for field, value in filter.items():
|
||||||
query_body["query"]["bool"]["filter"].append(
|
query_body["query"]["bool"]["filter"].append(
|
||||||
{"match": {"metadata." + str(field): value}}
|
{"term": {"metadata." + str(field) + ".keyword": value}}
|
||||||
)
|
)
|
||||||
|
|
||||||
size = limit if limit else 10
|
size = limit if limit else 10000
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.client.search(
|
result = self.client.search(
|
||||||
|
|
@ -206,6 +206,7 @@ class OpenSearchClient(VectorDBBase):
|
||||||
for item in batch
|
for item in batch
|
||||||
]
|
]
|
||||||
bulk(self.client, actions)
|
bulk(self.client, actions)
|
||||||
|
self.client.indices.refresh(self._get_index_name(collection_name))
|
||||||
|
|
||||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||||
self._create_index_if_not_exists(
|
self._create_index_if_not_exists(
|
||||||
|
|
@ -228,6 +229,7 @@ class OpenSearchClient(VectorDBBase):
|
||||||
for item in batch
|
for item in batch
|
||||||
]
|
]
|
||||||
bulk(self.client, actions)
|
bulk(self.client, actions)
|
||||||
|
self.client.indices.refresh(self._get_index_name(collection_name))
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
self,
|
self,
|
||||||
|
|
@ -251,11 +253,12 @@ class OpenSearchClient(VectorDBBase):
|
||||||
}
|
}
|
||||||
for field, value in filter.items():
|
for field, value in filter.items():
|
||||||
query_body["query"]["bool"]["filter"].append(
|
query_body["query"]["bool"]["filter"].append(
|
||||||
{"match": {"metadata." + str(field): value}}
|
{"term": {"metadata." + str(field) + ".keyword": value}}
|
||||||
)
|
)
|
||||||
self.client.delete_by_query(
|
self.client.delete_by_query(
|
||||||
index=self._get_index_name(collection_name), body=query_body
|
index=self._get_index_name(collection_name), body=query_body
|
||||||
)
|
)
|
||||||
|
self.client.indices.refresh(self._get_index_name(collection_name))
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
|
indices = self.client.indices.get(index=f"{self.index_prefix}_*")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from open_webui.config import (
|
||||||
QDRANT_ON_DISK,
|
QDRANT_ON_DISK,
|
||||||
QDRANT_GRPC_PORT,
|
QDRANT_GRPC_PORT,
|
||||||
QDRANT_PREFER_GRPC,
|
QDRANT_PREFER_GRPC,
|
||||||
|
QDRANT_COLLECTION_PREFIX,
|
||||||
)
|
)
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
class QdrantClient(VectorDBBase):
|
class QdrantClient(VectorDBBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.collection_prefix = "open-webui"
|
self.collection_prefix = QDRANT_COLLECTION_PREFIX
|
||||||
self.QDRANT_URI = QDRANT_URI
|
self.QDRANT_URI = QDRANT_URI
|
||||||
self.QDRANT_API_KEY = QDRANT_API_KEY
|
self.QDRANT_API_KEY = QDRANT_API_KEY
|
||||||
self.QDRANT_ON_DISK = QDRANT_ON_DISK
|
self.QDRANT_ON_DISK = QDRANT_ON_DISK
|
||||||
|
|
@ -86,6 +87,25 @@ class QdrantClient(VectorDBBase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create payload indexes for efficient filtering
|
||||||
|
self.client.create_payload_index(
|
||||||
|
collection_name=collection_name_with_prefix,
|
||||||
|
field_name="metadata.hash",
|
||||||
|
field_schema=models.KeywordIndexParams(
|
||||||
|
type=models.KeywordIndexType.KEYWORD,
|
||||||
|
is_tenant=False,
|
||||||
|
on_disk=self.QDRANT_ON_DISK,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.client.create_payload_index(
|
||||||
|
collection_name=collection_name_with_prefix,
|
||||||
|
field_name="metadata.file_id",
|
||||||
|
field_schema=models.KeywordIndexParams(
|
||||||
|
type=models.KeywordIndexType.KEYWORD,
|
||||||
|
is_tenant=False,
|
||||||
|
on_disk=self.QDRANT_ON_DISK,
|
||||||
|
),
|
||||||
|
)
|
||||||
log.info(f"collection {collection_name_with_prefix} successfully created!")
|
log.info(f"collection {collection_name_with_prefix} successfully created!")
|
||||||
|
|
||||||
def _create_collection_if_not_exists(self, collection_name, dimension):
|
def _create_collection_if_not_exists(self, collection_name, dimension):
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from open_webui.config import (
|
||||||
QDRANT_ON_DISK,
|
QDRANT_ON_DISK,
|
||||||
QDRANT_PREFER_GRPC,
|
QDRANT_PREFER_GRPC,
|
||||||
QDRANT_URI,
|
QDRANT_URI,
|
||||||
|
QDRANT_COLLECTION_PREFIX,
|
||||||
)
|
)
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from open_webui.retrieval.vector.main import (
|
from open_webui.retrieval.vector.main import (
|
||||||
|
|
@ -31,7 +32,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
class QdrantClient(VectorDBBase):
|
class QdrantClient(VectorDBBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.collection_prefix = "open-webui"
|
self.collection_prefix = QDRANT_COLLECTION_PREFIX
|
||||||
self.QDRANT_URI = QDRANT_URI
|
self.QDRANT_URI = QDRANT_URI
|
||||||
self.QDRANT_API_KEY = QDRANT_API_KEY
|
self.QDRANT_API_KEY = QDRANT_API_KEY
|
||||||
self.QDRANT_ON_DISK = QDRANT_ON_DISK
|
self.QDRANT_ON_DISK = QDRANT_ON_DISK
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ def search_brave(
|
||||||
|
|
||||||
return [
|
return [
|
||||||
SearchResult(
|
SearchResult(
|
||||||
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
|
link=result["url"],
|
||||||
|
title=result.get("title"),
|
||||||
|
snippet=result.get("description"),
|
||||||
)
|
)
|
||||||
for result in results[:count]
|
for result in results[:count]
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -675,7 +675,7 @@ async def signout(request: Request, response: Response):
|
||||||
oauth_id_token = request.cookies.get("oauth_id_token")
|
oauth_id_token = request.cookies.get("oauth_id_token")
|
||||||
if oauth_id_token:
|
if oauth_id_token:
|
||||||
try:
|
try:
|
||||||
async with ClientSession() as session:
|
async with ClientSession(trust_env=True) as session:
|
||||||
async with session.get(OPENID_PROVIDER_URL.value) as resp:
|
async with session.get(OPENID_PROVIDER_URL.value) as resp:
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
openid_data = await resp.json()
|
openid_data = await resp.json()
|
||||||
|
|
@ -687,7 +687,7 @@ async def signout(request: Request, response: Response):
|
||||||
status_code=200,
|
status_code=200,
|
||||||
content={
|
content={
|
||||||
"status": True,
|
"status": True,
|
||||||
"redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}",
|
"redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}" + (f"&post_logout_redirect_uri={WEBUI_AUTH_SIGNOUT_REDIRECT_URL}" if WEBUI_AUTH_SIGNOUT_REDIRECT_URL else ""),
|
||||||
},
|
},
|
||||||
headers=response.headers,
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -684,8 +684,10 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
@router.post("/{id}/share", response_model=Optional[ChatResponse])
|
@router.post("/{id}/share", response_model=Optional[ChatResponse])
|
||||||
async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
|
async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
|
||||||
if not has_permission(
|
if (user.role != "admin") and (
|
||||||
|
not has_permission(
|
||||||
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
|
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
|
||||||
|
)
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
|
||||||
|
|
@ -39,32 +39,39 @@ async def export_config(user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Direct Connections Config
|
# Connections Config
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
class DirectConnectionsConfigForm(BaseModel):
|
class ConnectionsConfigForm(BaseModel):
|
||||||
ENABLE_DIRECT_CONNECTIONS: bool
|
ENABLE_DIRECT_CONNECTIONS: bool
|
||||||
|
ENABLE_BASE_MODELS_CACHE: bool
|
||||||
|
|
||||||
|
|
||||||
@router.get("/direct_connections", response_model=DirectConnectionsConfigForm)
|
@router.get("/connections", response_model=ConnectionsConfigForm)
|
||||||
async def get_direct_connections_config(request: Request, user=Depends(get_admin_user)):
|
async def get_connections_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||||
|
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/direct_connections", response_model=DirectConnectionsConfigForm)
|
@router.post("/connections", response_model=ConnectionsConfigForm)
|
||||||
async def set_direct_connections_config(
|
async def set_connections_config(
|
||||||
request: Request,
|
request: Request,
|
||||||
form_data: DirectConnectionsConfigForm,
|
form_data: ConnectionsConfigForm,
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
|
request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
|
||||||
form_data.ENABLE_DIRECT_CONNECTIONS
|
form_data.ENABLE_DIRECT_CONNECTIONS
|
||||||
)
|
)
|
||||||
|
request.app.state.config.ENABLE_BASE_MODELS_CACHE = (
|
||||||
|
form_data.ENABLE_BASE_MODELS_CACHE
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
"ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||||
|
"ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ async def load_function_from_url(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url, headers={"Content-Type": "application/json"}
|
url, headers={"Content-Type": "application/json"}
|
||||||
) as resp:
|
) as resp:
|
||||||
|
|
|
||||||
|
|
@ -303,10 +303,12 @@ async def update_image_config(
|
||||||
):
|
):
|
||||||
set_image_model(request, form_data.MODEL)
|
set_image_model(request, form_data.MODEL)
|
||||||
|
|
||||||
if (form_data.IMAGE_SIZE == "auto" and form_data.MODEL != 'gpt-image-1'):
|
if form_data.IMAGE_SIZE == "auto" and form_data.MODEL != "gpt-image-1":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (auto is only allowed with gpt-image-1).")
|
detail=ERROR_MESSAGES.INCORRECT_FORMAT(
|
||||||
|
" (auto is only allowed with gpt-image-1)."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
pattern = r"^\d+x\d+$"
|
pattern = r"^\d+x\d+$"
|
||||||
|
|
@ -483,7 +485,7 @@ async def image_generations(
|
||||||
# image model other than gpt-image-1, which is warned about on settings save
|
# image model other than gpt-image-1, which is warned about on settings save
|
||||||
width, height = (
|
width, height = (
|
||||||
tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
|
tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
|
||||||
if 'x' in request.app.state.config.IMAGE_SIZE
|
if "x" in request.app.state.config.IMAGE_SIZE
|
||||||
else (512, 512)
|
else (512, 512)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ from open_webui.config import (
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
ENV,
|
ENV,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
|
MODELS_CACHE_TTL,
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||||
|
|
@ -330,7 +331,7 @@ def merge_ollama_models_lists(model_lists):
|
||||||
return list(merged_models.values())
|
return list(merged_models.values())
|
||||||
|
|
||||||
|
|
||||||
@cached(ttl=1)
|
@cached(ttl=MODELS_CACHE_TTL)
|
||||||
async def get_all_models(request: Request, user: UserModel = None):
|
async def get_all_models(request: Request, user: UserModel = None):
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
if request.app.state.config.ENABLE_OLLAMA_API:
|
if request.app.state.config.ENABLE_OLLAMA_API:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from open_webui.config import (
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
)
|
)
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
MODELS_CACHE_TTL,
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||||
|
|
@ -386,7 +387,7 @@ async def get_filtered_models(models, user):
|
||||||
return filtered_models
|
return filtered_models
|
||||||
|
|
||||||
|
|
||||||
@cached(ttl=1)
|
@cached(ttl=MODELS_CACHE_TTL)
|
||||||
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1794,6 +1794,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
||||||
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||||
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
)
|
)
|
||||||
|
elif engine == "exa":
|
||||||
|
return search_exa(
|
||||||
|
request.app.state.config.EXA_API_KEY,
|
||||||
|
query,
|
||||||
|
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||||
|
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
)
|
||||||
elif engine == "perplexity":
|
elif engine == "perplexity":
|
||||||
return search_perplexity(
|
return search_perplexity(
|
||||||
request.app.state.config.PERPLEXITY_API_KEY,
|
request.app.state.config.PERPLEXITY_API_KEY,
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ async def load_tool_from_url(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url, headers={"Content-Type": "application/json"}
|
url, headers={"Content-Type": "application/json"}
|
||||||
) as resp:
|
) as resp:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import random
|
||||||
|
|
||||||
import socketio
|
import socketio
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -105,10 +107,26 @@ else:
|
||||||
|
|
||||||
|
|
||||||
async def periodic_usage_pool_cleanup():
|
async def periodic_usage_pool_cleanup():
|
||||||
if not aquire_func():
|
max_retries = 2
|
||||||
log.debug("Usage pool cleanup lock already exists. Not running it.")
|
retry_delay = random.uniform(
|
||||||
|
WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT
|
||||||
|
)
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
|
if aquire_func():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if attempt < max_retries:
|
||||||
|
log.debug(
|
||||||
|
f"Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(retry_delay)
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
"Failed to acquire cleanup lock after retries. Skipping cleanup."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
log.debug("Running periodic_usage_pool_cleanup")
|
|
||||||
|
log.debug("Running periodic_cleanup")
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
if not renew_func():
|
if not renew_func():
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,17 @@ import asyncio
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
# A dictionary to keep track of active tasks
|
# A dictionary to keep track of active tasks
|
||||||
tasks: Dict[str, asyncio.Task] = {}
|
tasks: Dict[str, asyncio.Task] = {}
|
||||||
chat_tasks = {}
|
chat_tasks = {}
|
||||||
|
|
@ -38,7 +45,7 @@ async def redis_task_command_listener(app):
|
||||||
if local_task:
|
if local_task:
|
||||||
local_task.cancel()
|
local_task.cancel()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error handling distributed task command: {e}")
|
log.exception(f"Error handling distributed task command: {e}")
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------
|
### ------------------------------
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
AUDIT_UVICORN_LOGGER_NAMES,
|
||||||
AUDIT_LOG_FILE_ROTATION_SIZE,
|
AUDIT_LOG_FILE_ROTATION_SIZE,
|
||||||
AUDIT_LOG_LEVEL,
|
AUDIT_LOG_LEVEL,
|
||||||
AUDIT_LOGS_FILE_PATH,
|
AUDIT_LOGS_FILE_PATH,
|
||||||
|
|
@ -128,11 +130,13 @@ def start_logger():
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
|
handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
|
||||||
)
|
)
|
||||||
|
|
||||||
for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
|
for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
|
||||||
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
||||||
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
||||||
uvicorn_logger.handlers = []
|
uvicorn_logger.handlers = []
|
||||||
for uvicorn_logger_name in ["uvicorn.access"]:
|
|
||||||
|
for uvicorn_logger_name in AUDIT_UVICORN_LOGGER_NAMES:
|
||||||
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
||||||
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
||||||
uvicorn_logger.handlers = [InterceptHandler()]
|
uvicorn_logger.handlers = [InterceptHandler()]
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ async def chat_completion_tools_handler(
|
||||||
if tool_id
|
if tool_id
|
||||||
else f"{tool_function_name}"
|
else f"{tool_function_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if tool.get("metadata", {}).get("citation", False) or tool.get(
|
if tool.get("metadata", {}).get("citation", False) or tool.get(
|
||||||
"direct", False
|
"direct", False
|
||||||
):
|
):
|
||||||
|
|
@ -718,6 +719,10 @@ def apply_params_to_form_data(form_data, model):
|
||||||
|
|
||||||
|
|
||||||
async def process_chat_payload(request, form_data, user, metadata, model):
|
async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
|
# Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation
|
||||||
|
# -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling
|
||||||
|
# -> Chat Files
|
||||||
|
|
||||||
form_data = apply_params_to_form_data(form_data, model)
|
form_data = apply_params_to_form_data(form_data, model)
|
||||||
log.debug(f"form_data: {form_data}")
|
log.debug(f"form_data: {form_data}")
|
||||||
|
|
||||||
|
|
@ -911,7 +916,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
request, form_data, extra_params, user, models, tools_dict
|
request, form_data, extra_params, user, models, tools_dict
|
||||||
)
|
)
|
||||||
sources.extend(flags.get("sources", []))
|
sources.extend(flags.get("sources", []))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
|
|
@ -924,24 +928,27 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
# If context is not empty, insert it into the messages
|
# If context is not empty, insert it into the messages
|
||||||
if len(sources) > 0:
|
if len(sources) > 0:
|
||||||
context_string = ""
|
context_string = ""
|
||||||
citation_idx = {}
|
citation_idx_map = {}
|
||||||
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
if "document" in source:
|
if "document" in source:
|
||||||
for doc_context, doc_meta in zip(
|
for document_text, document_metadata in zip(
|
||||||
source["document"], source["metadata"]
|
source["document"], source["metadata"]
|
||||||
):
|
):
|
||||||
source_name = source.get("source", {}).get("name", None)
|
source_name = source.get("source", {}).get("name", None)
|
||||||
citation_id = (
|
source_id = (
|
||||||
doc_meta.get("source", None)
|
document_metadata.get("source", None)
|
||||||
or source.get("source", {}).get("id", None)
|
or source.get("source", {}).get("id", None)
|
||||||
or "N/A"
|
or "N/A"
|
||||||
)
|
)
|
||||||
if citation_id not in citation_idx:
|
|
||||||
citation_idx[citation_id] = len(citation_idx) + 1
|
if source_id not in citation_idx_map:
|
||||||
|
citation_idx_map[source_id] = len(citation_idx_map) + 1
|
||||||
|
|
||||||
context_string += (
|
context_string += (
|
||||||
f'<source id="{citation_idx[citation_id]}"'
|
f'<source id="{citation_idx_map[source_id]}"'
|
||||||
+ (f' name="{source_name}"' if source_name else "")
|
+ (f' name="{source_name}"' if source_name else "")
|
||||||
+ f">{doc_context}</source>\n"
|
+ f">{document_text}</source>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
context_string = context_string.strip()
|
context_string = context_string.strip()
|
||||||
|
|
@ -1369,7 +1376,7 @@ async def process_chat_response(
|
||||||
return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
|
return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
|
||||||
|
|
||||||
# Handle as a background task
|
# Handle as a background task
|
||||||
async def post_response_handler(response, events):
|
async def response_handler(response, events):
|
||||||
def serialize_content_blocks(content_blocks, raw=False):
|
def serialize_content_blocks(content_blocks, raw=False):
|
||||||
content = ""
|
content = ""
|
||||||
|
|
||||||
|
|
@ -2428,9 +2435,9 @@ async def process_chat_response(
|
||||||
if response.background is not None:
|
if response.background is not None:
|
||||||
await response.background()
|
await response.background()
|
||||||
|
|
||||||
# background_tasks.add_task(post_response_handler, response, events)
|
# background_tasks.add_task(response_handler, response, events)
|
||||||
task_id, _ = await create_task(
|
task_id, _ = await create_task(
|
||||||
request, post_response_handler(response, events), id=metadata["chat_id"]
|
request, response_handler(response, events), id=metadata["chat_id"]
|
||||||
)
|
)
|
||||||
return {"status": True, "task_id": task_id}
|
return {"status": True, "task_id": task_id}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,16 @@ async def get_all_base_models(request: Request, user: UserModel = None):
|
||||||
return function_models + openai_models + ollama_models
|
return function_models + openai_models + ollama_models
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models(request, user: UserModel = None):
|
async def get_all_models(request, refresh: bool = False, user: UserModel = None):
|
||||||
|
if (
|
||||||
|
request.app.state.MODELS
|
||||||
|
and request.app.state.BASE_MODELS
|
||||||
|
and (request.app.state.config.ENABLE_BASE_MODELS_CACHE and not refresh)
|
||||||
|
):
|
||||||
|
models = request.app.state.BASE_MODELS
|
||||||
|
else:
|
||||||
models = await get_all_base_models(request, user=user)
|
models = await get_all_base_models(request, user=user)
|
||||||
|
request.app.state.BASE_MODELS = models
|
||||||
|
|
||||||
# If there are no models, return an empty list
|
# If there are no models, return an empty list
|
||||||
if len(models) == 0:
|
if len(models) == 0:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||||
|
OTLPSpanExporter as HttpOTLPSpanExporter,
|
||||||
|
)
|
||||||
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
||||||
from opentelemetry.sdk.trace import TracerProvider
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
from sqlalchemy import Engine
|
from sqlalchemy import Engine
|
||||||
|
|
@ -16,6 +19,7 @@ from open_webui.env import (
|
||||||
ENABLE_OTEL_METRICS,
|
ENABLE_OTEL_METRICS,
|
||||||
OTEL_BASIC_AUTH_USERNAME,
|
OTEL_BASIC_AUTH_USERNAME,
|
||||||
OTEL_BASIC_AUTH_PASSWORD,
|
OTEL_BASIC_AUTH_PASSWORD,
|
||||||
|
OTEL_OTLP_SPAN_EXPORTER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,6 +39,13 @@ def setup(app: FastAPI, db_engine: Engine):
|
||||||
headers = [("authorization", f"Basic {auth_header}")]
|
headers = [("authorization", f"Basic {auth_header}")]
|
||||||
|
|
||||||
# otlp export
|
# otlp export
|
||||||
|
if OTEL_OTLP_SPAN_EXPORTER == "http":
|
||||||
|
exporter = HttpOTLPSpanExporter(
|
||||||
|
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||||
|
insecure=OTEL_EXPORTER_OTLP_INSECURE,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
else:
|
||||||
exporter = OTLPSpanExporter(
|
exporter = OTLPSpanExporter(
|
||||||
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
|
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||||
insecure=OTEL_EXPORTER_OTLP_INSECURE,
|
insecure=OTEL_EXPORTER_OTLP_INSECURE,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
fastapi==0.115.7
|
fastapi==0.115.7
|
||||||
uvicorn[standard]==0.34.2
|
uvicorn[standard]==0.35.0
|
||||||
pydantic==2.10.6
|
pydantic==2.11.7
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
|
|
||||||
python-socketio==5.13.0
|
python-socketio==5.13.0
|
||||||
|
|
@ -42,8 +42,8 @@ google-genai==1.15.0
|
||||||
google-generativeai==0.8.5
|
google-generativeai==0.8.5
|
||||||
tiktoken
|
tiktoken
|
||||||
|
|
||||||
langchain==0.3.24
|
langchain==0.3.26
|
||||||
langchain-community==0.3.23
|
langchain-community==0.3.26
|
||||||
|
|
||||||
fake-useragent==2.1.0
|
fake-useragent==2.1.0
|
||||||
chromadb==0.6.3
|
chromadb==0.6.3
|
||||||
|
|
@ -98,7 +98,6 @@ langfuse==2.44.0
|
||||||
youtube-transcript-api==1.1.0
|
youtube-transcript-api==1.1.0
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
|
|
||||||
extract_msg
|
|
||||||
pydub
|
pydub
|
||||||
duckduckgo-search==8.0.2
|
duckduckgo-search==8.0.2
|
||||||
|
|
||||||
|
|
@ -115,7 +114,7 @@ pytest-docker~=3.1.1
|
||||||
googleapis-common-protos==1.63.2
|
googleapis-common-protos==1.63.2
|
||||||
google-cloud-storage==2.19.0
|
google-cloud-storage==2.19.0
|
||||||
|
|
||||||
azure-identity==1.21.0
|
azure-identity==1.23.0
|
||||||
azure-storage-blob==12.24.1
|
azure-storage-blob==12.24.1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
|
||||||
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
|
||||||
|
|
||||||
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
|
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
|
||||||
IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
|
IF "%WEBUI_SECRET_KEY% %WEBUI_JWT_SECRET_KEY%" == " " (
|
||||||
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
|
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
|
||||||
|
|
||||||
IF NOT EXIST "%KEY_FILE%" (
|
IF NOT EXIST "%KEY_FILE%" (
|
||||||
|
|
|
||||||
24
docker-compose.otel.yaml
Normal file
24
docker-compose.otel.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
services:
|
||||||
|
grafana:
|
||||||
|
image: grafana/otel-lgtm:latest
|
||||||
|
container_name: lgtm
|
||||||
|
ports:
|
||||||
|
- "3000:3000" # Grafana UI
|
||||||
|
- "4317:4317" # OTLP/gRPC
|
||||||
|
- "4318:4318" # OTLP/HTTP
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
open-webui:
|
||||||
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
|
container_name: open-webui
|
||||||
|
depends_on: [grafana]
|
||||||
|
environment:
|
||||||
|
- ENABLE_OTEL=true
|
||||||
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://grafana:4317
|
||||||
|
- OTEL_SERVICE_NAME=open-webui
|
||||||
|
ports:
|
||||||
|
- "8088:8080"
|
||||||
|
networks: [default]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
50
package-lock.json
generated
50
package-lock.json
generated
|
|
@ -32,6 +32,7 @@
|
||||||
"@xyflow/svelte": "^0.1.19",
|
"@xyflow/svelte": "^0.1.19",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.21.15",
|
"bits-ui": "^0.21.15",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"codemirror-lang-hcl": "^0.1.0",
|
"codemirror-lang-hcl": "^0.1.0",
|
||||||
|
|
@ -42,9 +43,10 @@
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"focus-trap": "^7.6.4",
|
"focus-trap": "^7.6.4",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"html-entities": "^2.5.3",
|
"html-entities": "^2.5.3",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.11",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
|
|
@ -53,6 +55,7 @@
|
||||||
"jspdf": "^3.0.0",
|
"jspdf": "^3.0.0",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"kokoro-js": "^1.1.1",
|
"kokoro-js": "^1.1.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
|
|
@ -70,7 +73,7 @@
|
||||||
"prosemirror-view": "^1.34.3",
|
"prosemirror-view": "^1.34.3",
|
||||||
"pyodide": "^0.27.3",
|
"pyodide": "^0.27.3",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.6",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
|
|
@ -1870,6 +1873,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@lezer/common": {
|
"node_modules/@lezer/common": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
|
||||||
|
|
@ -4723,6 +4732,18 @@
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
||||||
|
|
@ -7295,6 +7316,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/heic2any": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/heimdalljs": {
|
"node_modules/heimdalljs": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz",
|
||||||
|
|
@ -7379,9 +7406,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html2canvas-pro": {
|
"node_modules/html2canvas-pro": {
|
||||||
"version": "1.5.8",
|
"version": "1.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.11.tgz",
|
||||||
"integrity": "sha512-bVGAU7IvhBwBlRAmX6QhekX8lsaxmYoF6zIwf/HNlHscjx+KN8jw/U4PQRYqeEVm9+m13hcS1l5ChJB9/e29Lw==",
|
"integrity": "sha512-W4pEeKLG8+9a54RDOSiEKq7gRXXDzt0ORMaLXX+l6a3urSKbmnkmyzcRDCtgTOzmHLaZTLG2wiTQMJqKLlSh3w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-line-break": "^2.1.0",
|
"css-line-break": "^2.1.0",
|
||||||
|
|
@ -8046,6 +8073,12 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
|
|
@ -11138,9 +11171,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sortablejs": {
|
"node_modules/sortablejs": {
|
||||||
"version": "1.15.2",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
|
||||||
"integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
|
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@
|
||||||
"@xyflow/svelte": "^0.1.19",
|
"@xyflow/svelte": "^0.1.19",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.21.15",
|
"bits-ui": "^0.21.15",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"codemirror-lang-hcl": "^0.1.0",
|
"codemirror-lang-hcl": "^0.1.0",
|
||||||
|
|
@ -86,9 +87,10 @@
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"focus-trap": "^7.6.4",
|
"focus-trap": "^7.6.4",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"html-entities": "^2.5.3",
|
"html-entities": "^2.5.3",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.11",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-browser-languagedetector": "^7.2.0",
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
|
|
@ -97,6 +99,7 @@
|
||||||
"jspdf": "^3.0.0",
|
"jspdf": "^3.0.0",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"kokoro-js": "^1.1.1",
|
"kokoro-js": "^1.1.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
|
|
@ -114,7 +117,7 @@
|
||||||
"prosemirror-view": "^1.34.3",
|
"prosemirror-view": "^1.34.3",
|
||||||
"pyodide": "^0.27.3",
|
"pyodide": "^0.27.3",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.6",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ license = { file = "LICENSE" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi==0.115.7",
|
"fastapi==0.115.7",
|
||||||
"uvicorn[standard]==0.34.2",
|
"uvicorn[standard]==0.34.2",
|
||||||
"pydantic==2.10.6",
|
"pydantic==2.11.7",
|
||||||
"python-multipart==0.0.20",
|
"python-multipart==0.0.20",
|
||||||
|
|
||||||
"python-socketio==5.13.0",
|
"python-socketio==5.13.0",
|
||||||
|
|
@ -50,8 +50,8 @@ dependencies = [
|
||||||
"google-generativeai==0.8.5",
|
"google-generativeai==0.8.5",
|
||||||
"tiktoken",
|
"tiktoken",
|
||||||
|
|
||||||
"langchain==0.3.24",
|
"langchain==0.3.26",
|
||||||
"langchain-community==0.3.23",
|
"langchain-community==0.3.26",
|
||||||
|
|
||||||
"fake-useragent==2.1.0",
|
"fake-useragent==2.1.0",
|
||||||
"chromadb==0.6.3",
|
"chromadb==0.6.3",
|
||||||
|
|
@ -105,7 +105,6 @@ dependencies = [
|
||||||
"youtube-transcript-api==1.1.0",
|
"youtube-transcript-api==1.1.0",
|
||||||
"pytube==15.0.0",
|
"pytube==15.0.0",
|
||||||
|
|
||||||
"extract_msg",
|
|
||||||
"pydub",
|
"pydub",
|
||||||
"duckduckgo-search==8.0.2",
|
"duckduckgo-search==8.0.2",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ async function downloadPackages() {
|
||||||
console.log('Pyodide version mismatch, removing static/pyodide directory');
|
console.log('Pyodide version mismatch, removing static/pyodide directory');
|
||||||
await rmdir('static/pyodide', { recursive: true });
|
await rmdir('static/pyodide', { recursive: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.log('Pyodide package not found, proceeding with download.');
|
console.log('Pyodide package not found, proceeding with download.', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
41
src/app.html
41
src/app.html
|
|
@ -77,28 +77,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setSplashImage() {
|
|
||||||
const logo = document.getElementById('logo');
|
|
||||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
if (isDarkMode) {
|
const logo = document.createElement('img');
|
||||||
const darkImage = new Image();
|
logo.id = 'logo';
|
||||||
darkImage.src = '/static/splash-dark.png';
|
logo.style =
|
||||||
|
'position: absolute; width: auto; height: 6rem; top: 44%; left: 50%; transform: translateX(-50%); display:block;';
|
||||||
|
logo.src = isDarkMode ? '/static/splash-dark.png' : '/static/splash.png';
|
||||||
|
|
||||||
darkImage.onload = () => {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
logo.src = '/static/splash-dark.png';
|
const splash = document.getElementById('splash-screen');
|
||||||
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
if (splash) splash.prepend(logo);
|
||||||
};
|
});
|
||||||
|
|
||||||
darkImage.onerror = () => {
|
|
||||||
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs after classes are assigned
|
|
||||||
window.onload = setSplashImage;
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -120,19 +110,6 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<img
|
|
||||||
id="logo"
|
|
||||||
style="
|
|
||||||
position: absolute;
|
|
||||||
width: auto;
|
|
||||||
height: 6rem;
|
|
||||||
top: 44%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
"
|
|
||||||
src="/static/splash.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style="
|
style="
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ export const importChat = async (
|
||||||
chat: object,
|
chat: object,
|
||||||
meta: object | null,
|
meta: object | null,
|
||||||
pinned?: boolean,
|
pinned?: boolean,
|
||||||
folderId?: string | null
|
folderId?: string | null,
|
||||||
|
createdAt: number | null = null,
|
||||||
|
updatedAt: number | null = null
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
@ -52,7 +54,9 @@ export const importChat = async (
|
||||||
chat: chat,
|
chat: chat,
|
||||||
meta: meta ?? {},
|
meta: meta ?? {},
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
folder_id: folderId
|
folder_id: folderId,
|
||||||
|
created_at: createdAt ?? null,
|
||||||
|
updated_at: updatedAt ?? null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,10 @@ export const exportConfig = async (token: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDirectConnectionsConfig = async (token: string) => {
|
export const getConnectionsConfig = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -85,10 +85,10 @@ export const getDirectConnectionsConfig = async (token: string) => {
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setDirectConnectionsConfig = async (token: string, config: object) => {
|
export const setConnectionsConfig = async (token: string, config: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,26 @@ import { toast } from 'svelte-sonner';
|
||||||
export const getModels = async (
|
export const getModels = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
connections: object | null = null,
|
connections: object | null = null,
|
||||||
base: boolean = false
|
base: boolean = false,
|
||||||
|
refresh: boolean = false
|
||||||
) => {
|
) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (refresh) {
|
||||||
|
searchParams.append('refresh', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
|
const res = await fetch(
|
||||||
|
`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}?${searchParams.toString()}`,
|
||||||
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) throw await res.json();
|
if (!res.ok) throw await res.json();
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,7 @@ export const deleteUserById = async (token: string, userId: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserUpdateForm = {
|
type UserUpdateForm = {
|
||||||
|
role: string;
|
||||||
profile_image_url: string;
|
profile_image_url: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Tags from './common/Tags.svelte';
|
import Tags from './common/Tags.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let onSubmit: Function = () => {};
|
export let onSubmit: Function = () => {};
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
|
|
@ -208,17 +210,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -524,29 +516,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
import { getToolServerData } from '$lib/apis';
|
import { getToolServerData } from '$lib/apis';
|
||||||
import { verifyToolServerConnection } from '$lib/apis/configs';
|
import { verifyToolServerConnection } from '$lib/apis/configs';
|
||||||
import AccessControl from './workspace/common/AccessControl.svelte';
|
import AccessControl from './workspace/common/AccessControl.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let onSubmit: Function = () => {};
|
export let onSubmit: Function = () => {};
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
|
|
@ -168,17 +170,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -389,30 +381,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
aria-hidden="true"
|
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
import Modal from './common/Modal.svelte';
|
import Modal from './common/Modal.svelte';
|
||||||
import { updateUserSettings } from '$lib/apis/users';
|
import { updateUserSettings } from '$lib/apis/users';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -36,18 +37,11 @@
|
||||||
localStorage.version = $config.version;
|
localStorage.version = $config.version;
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
|
aria-label={$i18n.t('Close')}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<p class="sr-only">{$i18n.t('Close')}</p>
|
<p class="sr-only">{$i18n.t('Close')}</p>
|
||||||
<path
|
</XMark>
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1">
|
<div class="flex items-center mt-1">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import { extractFrontmatter } from '$lib/utils';
|
import { extractFrontmatter } from '$lib/utils';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
@ -69,16 +71,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -120,29 +113,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,42 @@
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import { getFeedbackById } from '$lib/apis/evaluations';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedFeedback = null;
|
export let selectedFeedback = null;
|
||||||
|
|
||||||
export let onClose: () => void = () => {};
|
export let onClose: () => void = () => {};
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
let feedbackData = null;
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
show = false;
|
show = false;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
loaded = false;
|
||||||
|
feedbackData = null;
|
||||||
|
if (selectedFeedback) {
|
||||||
|
feedbackData = await getFeedbackById(localStorage.token, selectedFeedback.id).catch((err) => {
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Feedback Data:', selectedFeedback, feedbackData);
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
|
|
@ -22,34 +48,38 @@
|
||||||
{$i18n.t('Feedback Details')}
|
{$i18n.t('Feedback Details')}
|
||||||
</div>
|
</div>
|
||||||
<button class="self-center" on:click={close} aria-label="Close">
|
<button class="self-center" on:click={close} aria-label="Close">
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
{#if loaded}
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="mb-2 -mx-1">
|
{#if feedbackData}
|
||||||
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
{@const messageId = feedbackData?.meta?.message_id}
|
||||||
<div class="flex flex-wrap gap-1 mt-1">
|
{@const messages = feedbackData?.snapshot?.chat?.chat?.history.messages}
|
||||||
{#each selectedFeedback?.data?.tags as tag}
|
|
||||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs"
|
{#if messages[messages[messageId]?.parentId]}
|
||||||
>{tag}</span
|
<div class="flex flex-col w-full mb-2">
|
||||||
>
|
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Prompt')}</div>
|
||||||
{/each}
|
|
||||||
|
<div class="flex-1 text-xs whitespace-pre-line break-words">
|
||||||
|
<span>{messages[messages[messageId]?.parentId]?.content || '-'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<span>-</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if messages[messageId]}
|
||||||
|
<div class="flex flex-col w-full mb-2">
|
||||||
|
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Response')}</div>
|
||||||
|
<div
|
||||||
|
class="flex-1 text-xs whitespace-pre-line break-words max-h-32 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<span>{messages[messageId]?.content || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col w-full mb-2">
|
<div class="flex flex-col w-full mb-2">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
||||||
|
|
@ -66,6 +96,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full mb-2">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Comment')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1 text-xs">
|
||||||
|
<span>{selectedFeedback?.data?.comment || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
||||||
|
<div class="mb-2 -mx-1">
|
||||||
|
<div class="flex flex-wrap gap-1 mt-1">
|
||||||
|
{#each selectedFeedback?.data?.tags as tag}
|
||||||
|
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-[9px]"
|
||||||
|
>{tag}</span
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end pt-2">
|
<div class="flex justify-end pt-2">
|
||||||
<button
|
<button
|
||||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||||
|
|
@ -76,6 +126,11 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center w-full h-32">
|
||||||
|
<Spinner className={'size-5'} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@
|
||||||
{dayjs(feedback.updated_at * 1000).fromNow()}
|
{dayjs(feedback.updated_at * 1000).fromNow()}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class=" px-3 py-1 text-right font-semibold">
|
<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
|
||||||
<FeedbackMenu
|
<FeedbackMenu
|
||||||
on:delete={(e) => {
|
on:delete={(e) => {
|
||||||
deleteFeedbackHandler(feedback.id);
|
deleteFeedbackHandler(feedback.id);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
let showLeaderboardModal = false;
|
let showLeaderboardModal = false;
|
||||||
let selectedModel = null;
|
let selectedModel = null;
|
||||||
|
|
||||||
const openFeedbackModal = (model) => {
|
const openLeaderboardModelModal = (model) => {
|
||||||
showLeaderboardModal = true;
|
showLeaderboardModal = true;
|
||||||
selectedModel = model;
|
selectedModel = model;
|
||||||
};
|
};
|
||||||
|
|
@ -350,7 +350,7 @@
|
||||||
<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
|
<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<MagnifyingGlass className="size-3" />
|
<Search className="size-3" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
|
|
@ -371,7 +371,7 @@
|
||||||
{#if loadingLeaderboard}
|
{#if loadingLeaderboard}
|
||||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -505,7 +505,7 @@
|
||||||
{#each sortedModels as model, modelIdx (model.id)}
|
{#each sortedModels as model, modelIdx (model.id)}
|
||||||
<tr
|
<tr
|
||||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||||
on:click={() => openFeedbackModal(model)}
|
on:click={() => openLeaderboardModelModal(model)}
|
||||||
>
|
>
|
||||||
<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
|
<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
|
||||||
<div class=" line-clamp-1">
|
<div class=" line-clamp-1">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
export let feedbacks = [];
|
export let feedbacks = [];
|
||||||
export let onClose: () => void = () => {};
|
export let onClose: () => void = () => {};
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
show = false;
|
show = false;
|
||||||
|
|
@ -37,16 +38,7 @@
|
||||||
{model.name}
|
{model.name}
|
||||||
</div>
|
</div>
|
||||||
<button class="self-center" on:click={close} aria-label="Close">
|
<button class="self-center" on:click={close} aria-label="Close">
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 pb-4 dark:text-gray-200">
|
<div class="px-5 pb-4 dark:text-gray-200">
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@
|
||||||
let showConfirm = false;
|
let showConfirm = false;
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|
||||||
|
let selectedType = 'all';
|
||||||
|
|
||||||
let showManifestModal = false;
|
let showManifestModal = false;
|
||||||
let showValvesModal = false;
|
let showValvesModal = false;
|
||||||
let selectedFunction = null;
|
let selectedFunction = null;
|
||||||
|
|
@ -59,9 +61,10 @@
|
||||||
$: filteredItems = $functions
|
$: filteredItems = $functions
|
||||||
.filter(
|
.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
query === '' ||
|
(selectedType !== 'all' ? f.type === selectedType : true) &&
|
||||||
|
(query === '' ||
|
||||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
f.id.toLowerCase().includes(query.toLowerCase())
|
f.id.toLowerCase().includes(query.toLowerCase()))
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
|
@ -135,7 +138,9 @@
|
||||||
models.set(
|
models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +166,9 @@
|
||||||
models.set(
|
models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -215,8 +222,8 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col mt-1.5 mb-0.5">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
|
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
|
||||||
{$i18n.t('Functions')}
|
{$i18n.t('Functions')}
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
|
|
@ -266,12 +273,54 @@
|
||||||
</AddFunctionMenu>
|
</AddFunctionMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full">
|
||||||
|
<div
|
||||||
|
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="min-w-fit p-1.5 {selectedType === 'all'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selectedType = 'all';
|
||||||
|
}}>{$i18n.t('All')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="min-w-fit p-1.5 {selectedType === 'pipe'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selectedType = 'pipe';
|
||||||
|
}}>{$i18n.t('Pipe')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="min-w-fit p-1.5 {selectedType === 'filter'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selectedType = 'filter';
|
||||||
|
}}>{$i18n.t('Filter')}</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="min-w-fit p-1.5 {selectedType === 'action'
|
||||||
|
? ''
|
||||||
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
|
on:click={() => {
|
||||||
|
selectedType = 'action';
|
||||||
|
}}>{$i18n.t('Action')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
{#each filteredItems as func (func.id)}
|
{#each filteredItems as func (func.id)}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||||
|
|
@ -413,7 +462,9 @@
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections &&
|
$config?.features?.enable_direct_connections &&
|
||||||
($settings?.directConnections ?? null)
|
($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
@ -559,7 +610,9 @@
|
||||||
models.set(
|
models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
@ -585,7 +638,9 @@
|
||||||
models.set(
|
models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
} from '$lib/apis/audio';
|
} from '$lib/apis/audio';
|
||||||
import { config, settings } from '$lib/stores';
|
import { config, settings } from '$lib/stores';
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
|
||||||
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
||||||
|
|
@ -373,33 +374,7 @@
|
||||||
>
|
>
|
||||||
{#if STT_WHISPER_MODEL_LOADING}
|
{#if STT_WHISPER_MODEL_LOADING}
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
|
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
|
||||||
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
|
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
|
||||||
import { getModels as _getModels } from '$lib/apis';
|
import { getModels as _getModels } from '$lib/apis';
|
||||||
import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
|
import { getConnectionsConfig, setConnectionsConfig } from '$lib/apis/configs';
|
||||||
|
|
||||||
import { config, models, settings, user } from '$lib/stores';
|
import { config, models, settings, user } from '$lib/stores';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,9 @@
|
||||||
const getModels = async () => {
|
const getModels = async () => {
|
||||||
const models = await _getModels(
|
const models = await _getModels(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||||
|
false,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
@ -41,7 +43,7 @@
|
||||||
let ENABLE_OPENAI_API: null | boolean = null;
|
let ENABLE_OPENAI_API: null | boolean = null;
|
||||||
let ENABLE_OLLAMA_API: null | boolean = null;
|
let ENABLE_OLLAMA_API: null | boolean = null;
|
||||||
|
|
||||||
let directConnectionsConfig = null;
|
let connectionsConfig = null;
|
||||||
|
|
||||||
let pipelineUrls = {};
|
let pipelineUrls = {};
|
||||||
let showAddOpenAIConnectionModal = false;
|
let showAddOpenAIConnectionModal = false;
|
||||||
|
|
@ -104,15 +106,13 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDirectConnectionsHandler = async () => {
|
const updateConnectionsHandler = async () => {
|
||||||
const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
|
const res = await setConnectionsConfig(localStorage.token, connectionsConfig).catch((error) => {
|
||||||
(error) => {
|
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Direct Connections settings updated'));
|
toast.success($i18n.t('Connections settings updated'));
|
||||||
await models.set(await getModels());
|
await models.set(await getModels());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
openaiConfig = await getOpenAIConfig(localStorage.token);
|
openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||||
})(),
|
})(),
|
||||||
(async () => {
|
(async () => {
|
||||||
directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
|
connectionsConfig = await getConnectionsConfig(localStorage.token);
|
||||||
})()
|
})()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -215,9 +215,14 @@
|
||||||
|
|
||||||
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
|
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
|
||||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
|
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
|
||||||
|
<div class="mb-3.5">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="mt-2 space-y-2 pr-1.5">
|
<div class="mt-2 space-y-2">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||||
|
|
||||||
|
|
@ -234,11 +239,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ENABLE_OPENAI_API}
|
{#if ENABLE_OPENAI_API}
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
|
<div class="font-medium text-xs">{$i18n.t('Manage OpenAI API Connections')}</div>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -271,7 +274,8 @@
|
||||||
|
|
||||||
let newConfig = {};
|
let newConfig = {};
|
||||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||||
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
newConfig[newIdx] =
|
||||||
|
OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||||
});
|
});
|
||||||
OPENAI_API_CONFIGS = newConfig;
|
OPENAI_API_CONFIGS = newConfig;
|
||||||
updateOpenAIHandler();
|
updateOpenAIHandler();
|
||||||
|
|
@ -284,9 +288,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<div class=" my-2">
|
||||||
|
|
||||||
<div class="pr-1.5 my-2">
|
|
||||||
<div class="flex justify-between items-center text-sm mb-2">
|
<div class="flex justify-between items-center text-sm mb-2">
|
||||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||||
|
|
||||||
|
|
@ -301,11 +303,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if ENABLE_OLLAMA_API}
|
{#if ENABLE_OLLAMA_API}
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
|
<div class="font-medium text-xs">{$i18n.t('Manage Ollama API Connections')}</div>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -335,7 +335,8 @@
|
||||||
|
|
||||||
let newConfig = {};
|
let newConfig = {};
|
||||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||||
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
newConfig[newIdx] =
|
||||||
|
OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||||
});
|
});
|
||||||
OLLAMA_API_CONFIGS = newConfig;
|
OLLAMA_API_CONFIGS = newConfig;
|
||||||
}}
|
}}
|
||||||
|
|
@ -358,31 +359,53 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<div class="my-2">
|
||||||
|
|
||||||
<div class="pr-1.5 my-2">
|
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="">
|
<div class="">
|
||||||
<Switch
|
<Switch
|
||||||
bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||||
on:change={async () => {
|
on:change={async () => {
|
||||||
updateDirectConnectionsHandler();
|
updateConnectionsHandler();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-1.5">
|
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{$i18n.t(
|
{$i18n.t(
|
||||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="my-2">
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<div class=" text-xs font-medium">{$i18n.t('Cache Base Model List')}</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="">
|
||||||
|
<Switch
|
||||||
|
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||||
|
on:change={async () => {
|
||||||
|
updateConnectionsHandler();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-full justify-center">
|
<div class="flex h-full justify-center">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import ManageOllama from '../Models/Manage/ManageOllama.svelte';
|
import ManageOllama from '../Models/Manage/ManageOllama.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let urlIdx: number | null = null;
|
export let urlIdx: number | null = null;
|
||||||
|
|
@ -26,16 +27,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embeddingEngine === 'openai' && (OpenAIKey === '' || OpenAIUrl === '')) {
|
|
||||||
toast.error($i18n.t('OpenAI URL/Key required.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
embeddingEngine === 'azure_openai' &&
|
embeddingEngine === 'azure_openai' &&
|
||||||
(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
|
(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
|
||||||
|
|
@ -731,7 +727,11 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('API Key')}
|
||||||
|
bind:value={OpenAIKey}
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if embeddingEngine === 'ollama'}
|
{:else if embeddingEngine === 'ollama'}
|
||||||
<div class="my-0.5 flex gap-2 pr-2">
|
<div class="my-0.5 flex gap-2 pr-2">
|
||||||
|
|
@ -808,33 +808,7 @@
|
||||||
>
|
>
|
||||||
{#if updateEmbeddingModelLoading}
|
{#if updateEmbeddingModelLoading}
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -1272,7 +1246,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex items-center justify-center h-full">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
@ -141,16 +143,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -406,29 +399,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
if ($config?.features?.enable_version_update_check) {
|
||||||
checkForVersionUpdates();
|
checkForVersionUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -137,6 +139,7 @@
|
||||||
v{WEBUI_VERSION}
|
v{WEBUI_VERSION}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_version_update_check}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -147,6 +150,7 @@
|
||||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||||
: $i18n.t('(latest)')}
|
: $i18n.t('(latest)')}
|
||||||
</a>
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -160,6 +164,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_version_update_check}
|
||||||
<button
|
<button
|
||||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -169,6 +174,7 @@
|
||||||
>
|
>
|
||||||
{$i18n.t('Check for updates')}
|
{$i18n.t('Check for updates')}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@
|
||||||
updateConfig,
|
updateConfig,
|
||||||
verifyConfigUrl
|
verifyConfigUrl
|
||||||
} from '$lib/apis/images';
|
} from '$lib/apis/images';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -504,7 +506,7 @@
|
||||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
|
||||||
|
|
||||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||||
<textarea
|
<Textarea
|
||||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
||||||
rows="10"
|
rows="10"
|
||||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||||
|
|
@ -533,7 +535,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-50 border border-dashed border-gray-50 dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||||
|
|
@ -555,10 +557,10 @@
|
||||||
|
|
||||||
<div class="text-xs flex flex-col gap-1.5">
|
<div class="text-xs flex flex-col gap-1.5">
|
||||||
{#each requiredWorkflowNodes as node}
|
{#each requiredWorkflowNodes as node}
|
||||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
<div class="flex w-full items-center">
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<div
|
<div
|
||||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center bg-green-500/10 text-green-700 dark:text-green-200"
|
||||||
>
|
>
|
||||||
{node.type}{node.type === 'prompt' ? '*' : ''}
|
{node.type}{node.type === 'prompt' ? '*' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -566,7 +568,7 @@
|
||||||
<div class="">
|
<div class="">
|
||||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||||
<input
|
<input
|
||||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
|
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r border-gray-50 dark:border-gray-850"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
bind:value={node.key}
|
bind:value={node.key}
|
||||||
required
|
required
|
||||||
|
|
@ -580,7 +582,7 @@
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
|
class="w-full py-1 px-4 text-xs bg-transparent outline-hidden"
|
||||||
placeholder="Node Ids"
|
placeholder="Node Ids"
|
||||||
bind:value={node.node_ids}
|
bind:value={node.node_ids}
|
||||||
/>
|
/>
|
||||||
|
|
@ -711,29 +713,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-sm">
|
<div class=" self-center text-xs">
|
||||||
{$i18n.t('Banners')}
|
{$i18n.t('Banners')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -432,7 +432,7 @@
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
<div class=" space-y-3">
|
<div class=" space-y-3">
|
||||||
<div class="flex w-full justify-between mb-2">
|
<div class="flex w-full justify-between mb-2">
|
||||||
<div class=" self-center text-sm">
|
<div class=" self-center text-xs">
|
||||||
{$i18n.t('Default Prompt Suggestions')}
|
{$i18n.t('Default Prompt Suggestions')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -636,6 +636,6 @@
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" h-full w-full flex justify-center items-center">
|
<div class=" h-full w-full flex justify-center items-center">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -23,6 +25,13 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const classNames: Record<string, string> = {
|
||||||
|
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
|
||||||
|
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
|
||||||
|
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
|
||||||
|
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||||
|
};
|
||||||
|
|
||||||
$: if (banners) {
|
$: if (banners) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
@ -44,14 +53,14 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" flex flex-col space-y-0.5" bind:this={bannerListElement}>
|
<div class=" flex flex-col gap-3 {banners?.length > 0 ? 'mt-2' : ''}" bind:this={bannerListElement}>
|
||||||
{#each banners as banner, bannerIdx (banner.id)}
|
{#each banners as banner, bannerIdx (banner.id)}
|
||||||
<div class=" flex justify-between items-center -ml-1" id="banner-item-{banner.id}">
|
<div class=" flex justify-between items-start -ml-1" id="banner-item-{banner.id}">
|
||||||
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
||||||
|
|
||||||
<div class="flex flex-row flex-1 gap-2 items-center">
|
<div class="flex flex-row flex-1 gap-2 items-start">
|
||||||
<select
|
<select
|
||||||
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden text-left pl-1 pr-2"
|
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden pl-1 pr-5"
|
||||||
bind:value={banner.type}
|
bind:value={banner.type}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
|
|
@ -64,10 +73,11 @@
|
||||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input
|
<Textarea
|
||||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
|
className="mr-2 text-xs w-full bg-transparent outline-hidden resize-none"
|
||||||
placeholder={$i18n.t('Content')}
|
placeholder={$i18n.t('Content')}
|
||||||
bind:value={banner.content}
|
bind:value={banner.content}
|
||||||
|
maxSize={100}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="relative -left-2">
|
<div class="relative -left-2">
|
||||||
|
|
@ -85,16 +95,7 @@
|
||||||
banners = banners;
|
banners = banners;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-4'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -563,6 +563,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" h-full w-full flex justify-center items-center">
|
<div class=" h-full w-full flex justify-center items-center">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let initHandler = () => {};
|
export let initHandler = () => {};
|
||||||
|
|
@ -129,16 +130,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -278,29 +270,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -308,7 +278,7 @@
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1057,6 +1057,6 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex justify-center items-center w-full h-full py-3">
|
<div class="flex justify-center items-center w-full h-full py-3">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
|
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import ManageOllama from './Manage/ManageOllama.svelte';
|
import ManageOllama from './Manage/ManageOllama.svelte';
|
||||||
import { getOllamaConfig } from '$lib/apis/ollama';
|
import { getOllamaConfig } from '$lib/apis/ollama';
|
||||||
|
|
@ -48,16 +49,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -75,7 +67,7 @@
|
||||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="min-w-fit rounded-full p-1.5 {selected === 'ollama'
|
class="min-w-fit p-1.5 {selected === 'ollama'
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -84,7 +76,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- <button
|
<!-- <button
|
||||||
class="min-w-fit rounded-full p-1.5 {selected === 'llamacpp'
|
class="min-w-fit p-1.5 {selected === 'llamacpp'
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@
|
||||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||||
bind:value={webConfig.SEARXNG_QUERY_URL}
|
bind:value={webConfig.SEARXNG_QUERY_URL}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -248,7 +249,6 @@
|
||||||
bind:value={webConfig.KAGI_SEARCH_API_KEY}
|
bind:value={webConfig.KAGI_SEARCH_API_KEY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
.
|
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
|
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
|
||||||
<div class="mb-2.5 flex w-full flex-col">
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
import User from '$lib/components/icons/User.svelte';
|
import User from '$lib/components/icons/User.svelte';
|
||||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||||
import GroupModal from './Groups/EditGroupModal.svelte';
|
import GroupModal from './Groups/EditGroupModal.svelte';
|
||||||
|
|
@ -159,18 +160,7 @@
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<svg
|
<Search />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
export let onSubmit: Function = () => {};
|
export let onSubmit: Function = () => {};
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
|
@ -45,16 +47,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -111,29 +104,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import Display from './Display.svelte';
|
import Display from './Display.svelte';
|
||||||
import Permissions from './Permissions.svelte';
|
import Permissions from './Permissions.svelte';
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let onSubmit: Function = () => {};
|
export let onSubmit: Function = () => {};
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
|
|
@ -124,16 +126,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -305,29 +298,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
|
||||||
export let users = [];
|
export let users = [];
|
||||||
export let userIds = [];
|
export let userIds = [];
|
||||||
|
|
@ -50,18 +51,7 @@
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
<svg
|
<Search />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
|
|
||||||
{#if users === null || total === null}
|
{#if users === null || total === null}
|
||||||
<div class="my-10">
|
<div class="my-10">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
|
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import { generateInitialsImage } from '$lib/utils';
|
import { generateInitialsImage } from '$lib/utils';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
@ -132,16 +134,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -157,7 +150,7 @@
|
||||||
class="flex -mt-2 mb-1.5 gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
class="flex -mt-2 mb-1.5 gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="min-w-fit rounded-full p-1.5 {tab === ''
|
class="min-w-fit p-1.5 {tab === ''
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -167,7 +160,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="min-w-fit rounded-full p-1.5 {tab === 'import'
|
class="min-w-fit p-1.5 {tab === 'import'
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -293,29 +286,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
@ -54,16 +55,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import heic2any from 'heic2any';
|
||||||
|
|
||||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
|
@ -78,7 +79,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputFilesHandler = async (inputFiles) => {
|
const inputFilesHandler = async (inputFiles) => {
|
||||||
inputFiles.forEach((file) => {
|
inputFiles.forEach(async (file) => {
|
||||||
console.info('Processing file:', {
|
console.info('Processing file:', {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
|
|
@ -102,43 +103,50 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (file['type'].startsWith('image/')) {
|
||||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||||
) {
|
// Quick shortcut so we don’t do unnecessary work.
|
||||||
|
const settingsCompression = settings?.imageCompression ?? false;
|
||||||
|
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||||
|
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||||
|
|
||||||
|
// If neither settings nor config wants compression, return original URL.
|
||||||
|
if (!settingsCompression && !configWidth && !configHeight) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to null (no compression unless set)
|
||||||
|
let width = null;
|
||||||
|
let height = null;
|
||||||
|
|
||||||
|
// If user/settings want compression, pick their preferred size.
|
||||||
|
if (settingsCompression) {
|
||||||
|
width = settings?.imageCompressionSize?.width ?? null;
|
||||||
|
height = settings?.imageCompressionSize?.height ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply config limits as an upper bound if any
|
||||||
|
if (configWidth && (width === null || width > configWidth)) {
|
||||||
|
width = configWidth;
|
||||||
|
}
|
||||||
|
if (configHeight && (height === null || height > configHeight)) {
|
||||||
|
height = configHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the compression if required
|
||||||
|
if (width || height) {
|
||||||
|
return await compressImage(imageUrl, width, height);
|
||||||
|
}
|
||||||
|
return imageUrl;
|
||||||
|
};
|
||||||
|
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
let imageUrl = event.target.result;
|
let imageUrl = event.target.result;
|
||||||
|
|
||||||
if (
|
// Compress the image if settings or config require it
|
||||||
($settings?.imageCompression ?? false) ||
|
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||||
($config?.file?.image_compression?.width ?? null) ||
|
|
||||||
($config?.file?.image_compression?.height ?? null)
|
|
||||||
) {
|
|
||||||
let width = null;
|
|
||||||
let height = null;
|
|
||||||
|
|
||||||
if ($settings?.imageCompression ?? false) {
|
|
||||||
width = $settings?.imageCompressionSize?.width ?? null;
|
|
||||||
height = $settings?.imageCompressionSize?.height ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
($config?.file?.image_compression?.width ?? null) ||
|
|
||||||
($config?.file?.image_compression?.height ?? null)
|
|
||||||
) {
|
|
||||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
|
||||||
width = $config?.file?.image_compression?.width ?? null;
|
|
||||||
}
|
|
||||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
|
||||||
height = $config?.file?.image_compression?.height ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width || height) {
|
|
||||||
imageUrl = await compressImage(imageUrl, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
...files,
|
...files,
|
||||||
|
|
@ -149,7 +157,11 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(
|
||||||
|
file['type'] === 'image/heic'
|
||||||
|
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||||
|
: file
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,7 @@
|
||||||
|
|
||||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||||
import {
|
import {
|
||||||
addTagById,
|
|
||||||
createNewChat,
|
createNewChat,
|
||||||
deleteTagById,
|
|
||||||
deleteTagsById,
|
|
||||||
getAllTags,
|
getAllTags,
|
||||||
getChatById,
|
getChatById,
|
||||||
getChatList,
|
getChatList,
|
||||||
|
|
@ -708,6 +705,10 @@
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
const initNewChat = async () => {
|
const initNewChat = async () => {
|
||||||
|
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
||||||
|
await temporaryChatEnabled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
const availableModels = $models
|
const availableModels = $models
|
||||||
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
||||||
.map((m) => m.id);
|
.map((m) => m.id);
|
||||||
|
|
@ -835,10 +836,12 @@
|
||||||
prompt = $page.url.searchParams.get('q') ?? '';
|
prompt = $page.url.searchParams.get('q') ?? '';
|
||||||
|
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
|
if (($page.url.searchParams.get('submit') ?? 'true') === 'true') {
|
||||||
await tick();
|
await tick();
|
||||||
submitPrompt(prompt);
|
submitPrompt(prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectedModels = selectedModels.map((modelId) =>
|
selectedModels = selectedModels.map((modelId) =>
|
||||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||||
|
|
@ -2227,7 +2230,7 @@
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<div class=" flex items-center justify-center h-full w-full">
|
<div class=" flex items-center justify-center h-full w-full">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
<Spinner />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import heic2any from 'heic2any';
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
|
@ -320,7 +321,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
inputFiles.forEach((file) => {
|
inputFiles.forEach(async (file) => {
|
||||||
console.log('Processing file:', {
|
console.log('Processing file:', {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
|
|
@ -344,46 +345,53 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (file['type'].startsWith('image/')) {
|
||||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
|
||||||
) {
|
|
||||||
if (visionCapableModels.length === 0) {
|
if (visionCapableModels.length === 0) {
|
||||||
toast.error($i18n.t('Selected model(s) do not support image inputs'));
|
toast.error($i18n.t('Selected model(s) do not support image inputs'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||||
|
// Quick shortcut so we don’t do unnecessary work.
|
||||||
|
const settingsCompression = settings?.imageCompression ?? false;
|
||||||
|
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||||
|
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||||
|
|
||||||
|
// If neither settings nor config wants compression, return original URL.
|
||||||
|
if (!settingsCompression && !configWidth && !configHeight) {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to null (no compression unless set)
|
||||||
|
let width = null;
|
||||||
|
let height = null;
|
||||||
|
|
||||||
|
// If user/settings want compression, pick their preferred size.
|
||||||
|
if (settingsCompression) {
|
||||||
|
width = settings?.imageCompressionSize?.width ?? null;
|
||||||
|
height = settings?.imageCompressionSize?.height ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply config limits as an upper bound if any
|
||||||
|
if (configWidth && (width === null || width > configWidth)) {
|
||||||
|
width = configWidth;
|
||||||
|
}
|
||||||
|
if (configHeight && (height === null || height > configHeight)) {
|
||||||
|
height = configHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the compression if required
|
||||||
|
if (width || height) {
|
||||||
|
return await compressImage(imageUrl, width, height);
|
||||||
|
}
|
||||||
|
return imageUrl;
|
||||||
|
};
|
||||||
|
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
let imageUrl = event.target.result;
|
let imageUrl = event.target.result;
|
||||||
|
|
||||||
if (
|
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||||
($settings?.imageCompression ?? false) ||
|
|
||||||
($config?.file?.image_compression?.width ?? null) ||
|
|
||||||
($config?.file?.image_compression?.height ?? null)
|
|
||||||
) {
|
|
||||||
let width = null;
|
|
||||||
let height = null;
|
|
||||||
|
|
||||||
if ($settings?.imageCompression ?? false) {
|
|
||||||
width = $settings?.imageCompressionSize?.width ?? null;
|
|
||||||
height = $settings?.imageCompressionSize?.height ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
($config?.file?.image_compression?.width ?? null) ||
|
|
||||||
($config?.file?.image_compression?.height ?? null)
|
|
||||||
) {
|
|
||||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
|
||||||
width = $config?.file?.image_compression?.width ?? null;
|
|
||||||
}
|
|
||||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
|
||||||
height = $config?.file?.image_compression?.height ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width || height) {
|
|
||||||
imageUrl = await compressImage(imageUrl, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
files = [
|
files = [
|
||||||
...files,
|
...files,
|
||||||
|
|
@ -393,7 +401,11 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(
|
||||||
|
file['type'] === 'image/heic'
|
||||||
|
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||||
|
: file
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
}
|
}
|
||||||
|
|
@ -659,7 +671,7 @@
|
||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src={file.url}
|
src={file.url}
|
||||||
alt="input"
|
alt=""
|
||||||
imageClassName=" size-14 rounded-xl object-cover"
|
imageClassName=" size-14 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
||||||
|
|
@ -677,6 +689,7 @@
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
class="size-4 fill-yellow-300"
|
class="size-4 fill-yellow-300"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -690,8 +703,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class=" absolute -top-1 -right-1">
|
<div class=" absolute -top-1 -right-1">
|
||||||
<button
|
<button
|
||||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
class=" bg-white text-black border border-white rounded-full {($settings?.highContrastMode ??
|
||||||
|
false)
|
||||||
|
? ''
|
||||||
|
: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={$i18n.t('Remove file')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
files.splice(fileIdx, 1);
|
files.splice(fileIdx, 1);
|
||||||
files = files;
|
files = files;
|
||||||
|
|
@ -701,6 +718,7 @@
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
class="size-4"
|
class="size-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|
@ -1253,11 +1271,12 @@
|
||||||
<button
|
<button
|
||||||
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="More"
|
aria-label={$i18n.t('More Available Tools')}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="size-5"
|
class="size-5"
|
||||||
>
|
>
|
||||||
|
|
@ -1379,6 +1398,10 @@
|
||||||
{#if showCodeInterpreterButton}
|
{#if showCodeInterpreterButton}
|
||||||
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
|
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
|
||||||
<button
|
<button
|
||||||
|
aria-label={codeInterpreterEnabled
|
||||||
|
? $i18n.t('Disable Code Interpreter')
|
||||||
|
: $i18n.t('Enable Code Interpreter')}
|
||||||
|
aria-pressed={codeInterpreterEnabled}
|
||||||
on:click|preventDefault={() =>
|
on:click|preventDefault={() =>
|
||||||
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1530,7 +1553,7 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label="Call"
|
aria-label={$i18n.t('Voice mode')}
|
||||||
>
|
>
|
||||||
<Headphone className="size-5" />
|
<Headphone className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -854,6 +854,7 @@
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
|
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
|
||||||
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
<video
|
<video
|
||||||
id="camera-feed"
|
id="camera-feed"
|
||||||
autoplay
|
autoplay
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
|
import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||||
import { knowledge } from '$lib/stores';
|
import { knowledge } from '$lib/stores';
|
||||||
|
|
||||||
|
|
@ -42,6 +42,24 @@
|
||||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let container;
|
||||||
|
let adjustHeightDebounce;
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
if (container) {
|
||||||
|
if (adjustHeightDebounce) {
|
||||||
|
clearTimeout(adjustHeightDebounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustHeightDebounce = setTimeout(() => {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Ensure the container is visible before adjusting height
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
const confirmSelect = async (item) => {
|
const confirmSelect = async (item) => {
|
||||||
dispatch('select', item);
|
dispatch('select', item);
|
||||||
|
|
||||||
|
|
@ -75,7 +93,18 @@
|
||||||
await tick();
|
await tick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const decodeString = (str: string) => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
window.addEventListener('resize', adjustHeight);
|
||||||
|
adjustHeight();
|
||||||
|
|
||||||
let legacy_documents = $knowledge
|
let legacy_documents = $knowledge
|
||||||
.filter((item) => item?.meta?.document)
|
.filter((item) => item?.meta?.document)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
|
|
@ -155,13 +184,9 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const decodeString = (str: string) => {
|
onDestroy(() => {
|
||||||
try {
|
window.removeEventListener('resize', adjustHeight);
|
||||||
return decodeURIComponent(str);
|
});
|
||||||
} catch (e) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||||
|
|
@ -174,6 +199,7 @@
|
||||||
<div
|
<div
|
||||||
class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
|
class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
|
||||||
id="command-options-container"
|
id="command-options-container"
|
||||||
|
bind:this={container}
|
||||||
>
|
>
|
||||||
{#each filteredItems as item, idx}
|
{#each filteredItems as item, idx}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick, getContext } from 'svelte';
|
||||||
|
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
|
@ -51,18 +51,44 @@
|
||||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let container;
|
||||||
|
let adjustHeightDebounce;
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
if (container) {
|
||||||
|
if (adjustHeightDebounce) {
|
||||||
|
clearTimeout(adjustHeightDebounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustHeightDebounce = setTimeout(() => {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Ensure the container is visible before adjusting height
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmSelect = async (model) => {
|
const confirmSelect = async (model) => {
|
||||||
command = '';
|
command = '';
|
||||||
dispatch('select', model);
|
dispatch('select', model);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
window.addEventListener('resize', adjustHeight);
|
||||||
|
adjustHeight();
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
const chatInputElement = document.getElementById('chat-input');
|
||||||
await tick();
|
await tick();
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
await tick();
|
await tick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('resize', adjustHeight);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0}
|
{#if filteredItems.length > 0}
|
||||||
|
|
@ -75,6 +101,7 @@
|
||||||
<div
|
<div
|
||||||
class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
|
class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
|
||||||
id="command-options-container"
|
id="command-options-container"
|
||||||
|
bind:this={container}
|
||||||
>
|
>
|
||||||
{#each filteredItems as model, modelIdx}
|
{#each filteredItems as model, modelIdx}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
getUserTimezone,
|
getUserTimezone,
|
||||||
getWeekday
|
getWeekday
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
import { tick, getContext } from 'svelte';
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -38,6 +38,25 @@
|
||||||
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
|
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let container;
|
||||||
|
let adjustHeightDebounce;
|
||||||
|
|
||||||
|
const adjustHeight = () => {
|
||||||
|
if (container) {
|
||||||
|
if (adjustHeightDebounce) {
|
||||||
|
clearTimeout(adjustHeightDebounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustHeightDebounce = setTimeout(() => {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Ensure the container is visible before adjusting height
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmPrompt = async (command) => {
|
const confirmPrompt = async (command) => {
|
||||||
let text = command.content;
|
let text = command.content;
|
||||||
|
|
||||||
|
|
@ -156,22 +175,30 @@
|
||||||
|
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
const word = words.at(0);
|
const word = words.at(0);
|
||||||
const fullPrompt = prompt;
|
|
||||||
|
|
||||||
prompt = prompt.substring(0, word?.endIndex + 1);
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
if (!($settings?.richTextInput ?? true)) {
|
||||||
|
chatInputElement.setSelectionRange(word.startIndex, word.endIndex + 1);
|
||||||
|
chatInputElement.focus();
|
||||||
|
|
||||||
prompt = fullPrompt;
|
// This is a workaround to ensure the cursor is placed correctly
|
||||||
await tick();
|
// after the text is inserted, especially for multiline inputs.
|
||||||
|
chatInputElement.setSelectionRange(word.startIndex, word.endIndex + 1);
|
||||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
}
|
||||||
} else {
|
} else {
|
||||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('resize', adjustHeight);
|
||||||
|
adjustHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('resize', adjustHeight);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredPrompts.length > 0}
|
{#if filteredPrompts.length > 0}
|
||||||
|
|
@ -184,6 +211,7 @@
|
||||||
<div
|
<div
|
||||||
class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
|
class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
|
||||||
id="command-options-container"
|
id="command-options-container"
|
||||||
|
bind:this={container}
|
||||||
>
|
>
|
||||||
{#each filteredPrompts as prompt, promptIdx}
|
{#each filteredPrompts as prompt, promptIdx}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
|
import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
|
||||||
|
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
|
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
|
@ -406,16 +407,7 @@
|
||||||
onCancel();
|
onCancel();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-4'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="3"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,7 @@
|
||||||
<Message
|
<Message
|
||||||
{chatId}
|
{chatId}
|
||||||
bind:history
|
bind:history
|
||||||
|
{selectedModels}
|
||||||
messageId={message.id}
|
messageId={message.id}
|
||||||
idx={messageIdx}
|
idx={messageIdx}
|
||||||
{user}
|
{user}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
console.log('sources', sources);
|
|
||||||
citations = sources.reduce((acc, source) => {
|
citations = sources.reduce((acc, source) => {
|
||||||
if (Object.keys(source).length === 0) {
|
if (Object.keys(source).length === 0) {
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
@ -67,16 +69,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
@ -49,16 +50,7 @@
|
||||||
codeExecution = null;
|
codeExecution = null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
export let id;
|
export let id;
|
||||||
export let content;
|
export let content;
|
||||||
export let history;
|
export let history;
|
||||||
|
export let selectedModels = [];
|
||||||
|
|
||||||
export let model = null;
|
export let model = null;
|
||||||
export let sources = null;
|
export let sources = null;
|
||||||
|
|
||||||
|
|
@ -25,11 +27,10 @@
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
export let floatingButtons = true;
|
export let floatingButtons = true;
|
||||||
|
|
||||||
export let onSave = () => {};
|
export let onSave = (e) => {};
|
||||||
export let onSourceClick = () => {};
|
export let onSourceClick = (e) => {};
|
||||||
export let onTaskClick = () => {};
|
export let onTaskClick = (e) => {};
|
||||||
|
export let onAddMessages = (e) => {};
|
||||||
export let onAddMessages = () => {};
|
|
||||||
|
|
||||||
let contentContainerElement;
|
let contentContainerElement;
|
||||||
|
|
||||||
|
|
@ -192,7 +193,11 @@
|
||||||
<FloatingButtons
|
<FloatingButtons
|
||||||
bind:this={floatingButtonsElement}
|
bind:this={floatingButtonsElement}
|
||||||
{id}
|
{id}
|
||||||
model={model?.id}
|
model={(selectedModels ?? []).includes(model?.id)
|
||||||
|
? model?.id
|
||||||
|
: (selectedModels ?? []).length > 0
|
||||||
|
? selectedModels.at(0)
|
||||||
|
: model?.id}
|
||||||
messages={createMessagesList(history, id)}
|
messages={createMessagesList(history, id)}
|
||||||
onAdd={({ modelId, parentId, messages }) => {
|
onAdd={({ modelId, parentId, messages }) => {
|
||||||
console.log(modelId, parentId, messages);
|
console.log(modelId, parentId, messages);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import UserMessage from './UserMessage.svelte';
|
import UserMessage from './UserMessage.svelte';
|
||||||
|
|
||||||
export let chatId;
|
export let chatId;
|
||||||
|
export let selectedModels = [];
|
||||||
export let idx = 0;
|
export let idx = 0;
|
||||||
|
|
||||||
export let history;
|
export let history;
|
||||||
|
|
@ -70,6 +71,7 @@
|
||||||
{chatId}
|
{chatId}
|
||||||
{history}
|
{history}
|
||||||
{messageId}
|
{messageId}
|
||||||
|
{selectedModels}
|
||||||
isLastMessage={messageId === history.currentId}
|
isLastMessage={messageId === history.currentId}
|
||||||
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
||||||
{gotoMessage}
|
{gotoMessage}
|
||||||
|
|
@ -92,6 +94,7 @@
|
||||||
bind:history
|
bind:history
|
||||||
{chatId}
|
{chatId}
|
||||||
{messageId}
|
{messageId}
|
||||||
|
{selectedModels}
|
||||||
isLastMessage={messageId === history?.currentId}
|
isLastMessage={messageId === history?.currentId}
|
||||||
{updateChat}
|
{updateChat}
|
||||||
{editMessage}
|
{editMessage}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
export let chatId;
|
export let chatId;
|
||||||
export let history;
|
export let history;
|
||||||
export let messageId;
|
export let messageId;
|
||||||
|
export let selectedModels = [];
|
||||||
|
|
||||||
export let isLastMessage;
|
export let isLastMessage;
|
||||||
export let readOnly = false;
|
export let readOnly = false;
|
||||||
|
|
@ -252,6 +253,7 @@
|
||||||
{chatId}
|
{chatId}
|
||||||
{history}
|
{history}
|
||||||
messageId={_messageId}
|
messageId={_messageId}
|
||||||
|
{selectedModels}
|
||||||
isLastMessage={true}
|
isLastMessage={true}
|
||||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||||
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||||
import { config, models } from '$lib/stores';
|
import { config, models } from '$lib/stores';
|
||||||
import Tags from '$lib/components/common/Tags.svelte';
|
import Tags from '$lib/components/common/Tags.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -123,16 +124,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-4'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
export let history;
|
export let history;
|
||||||
export let messageId;
|
export let messageId;
|
||||||
|
export let selectedModels = [];
|
||||||
|
|
||||||
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
|
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||||
$: if (history.messages) {
|
$: if (history.messages) {
|
||||||
|
|
@ -601,7 +602,7 @@
|
||||||
id="message-{message.id}"
|
id="message-{message.id}"
|
||||||
dir={$settings.chatDirection}
|
dir={$settings.chatDirection}
|
||||||
>
|
>
|
||||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex `}>
|
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
src={model?.info?.meta?.profile_image_url ??
|
src={model?.info?.meta?.profile_image_url ??
|
||||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||||
|
|
@ -609,7 +610,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-auto w-0 pl-1 relative -translate-y-0.5">
|
<div class="flex-auto w-0 pl-1 relative">
|
||||||
<Name>
|
<Name>
|
||||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||||
<span class="line-clamp-1 text-black dark:text-white">
|
<span class="line-clamp-1 text-black dark:text-white">
|
||||||
|
|
@ -795,6 +796,7 @@
|
||||||
<ContentRenderer
|
<ContentRenderer
|
||||||
id={message.id}
|
id={message.id}
|
||||||
{history}
|
{history}
|
||||||
|
{selectedModels}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
sources={message.sources}
|
sources={message.sources}
|
||||||
floatingButtons={message?.done && !readOnly}
|
floatingButtons={message?.done && !readOnly}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 Search from '$lib/components/icons/Search.svelte';
|
||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
|
|
||||||
export let status = { urls: [], query: '' };
|
export let status = { urls: [], query: '' };
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<MagnifyingGlass />
|
<Search />
|
||||||
|
|
||||||
<div class=" line-clamp-1">
|
<div class=" line-clamp-1">
|
||||||
{status.query}
|
{status.query}
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,29 @@
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full mt-2 mb-2">
|
<span class="relative flex {size === 'md' ? 'size-3 my-2' : 'size-2 my-1'} mx-1">
|
||||||
<div class="animate-pulse flex w-full">
|
<span
|
||||||
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
|
class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
|
||||||
<div
|
></span>
|
||||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14"
|
<span
|
||||||
/>
|
class="relative inline-flex {size === 'md'
|
||||||
|
? 'size-3'
|
||||||
|
: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<style>
|
||||||
<div
|
@keyframes size {
|
||||||
class="{size === 'md'
|
0%,
|
||||||
? 'h-2'
|
100% {
|
||||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
transform: scale(1);
|
||||||
/>
|
}
|
||||||
<div
|
50% {
|
||||||
class="{size === 'md'
|
transform: scale(1.25);
|
||||||
? 'h-2'
|
}
|
||||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-4 gap-4">
|
|
||||||
<div
|
|
||||||
class="{size === 'md'
|
|
||||||
? 'h-2'
|
|
||||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="{size === 'md'
|
|
||||||
? 'h-2'
|
|
||||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="{size === 'md'
|
|
||||||
? 'h-2'
|
|
||||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1 mr-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
|
.animate-size {
|
||||||
</div>
|
animation: size 1.5s ease-in-out infinite;
|
||||||
</div>
|
}
|
||||||
</div>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
id="message-{message.id}"
|
id="message-{message.id}"
|
||||||
>
|
>
|
||||||
{#if !($settings?.chatBubble ?? true)}
|
{#if !($settings?.chatBubble ?? true)}
|
||||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
|
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
src={message.user
|
src={message.user
|
||||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
{#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">
|
||||||
<div class="mr-1 max-w-full">
|
<div class="max-w-full {($settings?.highContrastMode ?? false) ? 'm-1' : 'mr-1'}">
|
||||||
<Selector
|
<Selector
|
||||||
id={`${selectedModelIdx}`}
|
id={`${selectedModelIdx}`}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
@ -345,12 +346,17 @@
|
||||||
closeFocus={false}
|
closeFocus={false}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
class="relative w-full font-primary"
|
class="relative w-full font-primary {($settings?.highContrastMode ?? false)
|
||||||
|
? ''
|
||||||
|
: 'outline-hidden focus:outline-hidden'}"
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
id="model-selector-{id}-button"
|
id="model-selector-{id}-button"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
class="flex w-full text-left px-0.5 outline-hidden bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-hidden"
|
class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ??
|
||||||
|
false)
|
||||||
|
? 'dark:placeholder-gray-100 placeholder-gray-800'
|
||||||
|
: 'placeholder-gray-400'}"
|
||||||
on:mouseenter={async () => {
|
on:mouseenter={async () => {
|
||||||
models.set(
|
models.set(
|
||||||
await getModels(
|
await getModels(
|
||||||
|
|
@ -359,7 +365,6 @@
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
{#if selectedModel}
|
{#if selectedModel}
|
||||||
{selectedModel.label}
|
{selectedModel.label}
|
||||||
|
|
@ -367,7 +372,7 @@
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{/if}
|
{/if}
|
||||||
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
||||||
</button>
|
</div>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
|
|
@ -550,29 +555,7 @@
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||||
<svg
|
<Spinner />
|
||||||
class="size-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col self-start">
|
<div class="flex flex-col self-start">
|
||||||
|
|
|
||||||
|
|
@ -173,19 +173,19 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 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"
|
|
||||||
>
|
>
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
|
<span class="sr-only">{$i18n.t('User menu')}</span>
|
||||||
<img
|
<img
|
||||||
src={$user?.profile_image_url}
|
src={$user?.profile_image_url}
|
||||||
class="size-6 object-cover rounded-full"
|
class="size-6 object-cover rounded-full"
|
||||||
alt="User profile"
|
alt=""
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</UserMenu>
|
</UserMenu>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,10 @@
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
aria-hidden={models.length <= 1}
|
||||||
|
aria-label={$i18n.t('Get information on {{name}} in the UI', {
|
||||||
|
name: models[modelIdx]?.name
|
||||||
|
})}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedModelIdx = modelIdx;
|
selectedModelIdx = modelIdx;
|
||||||
}}
|
}}
|
||||||
|
|
@ -129,7 +133,7 @@
|
||||||
? `/doge.png`
|
? `/doge.png`
|
||||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||||
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||||
alt="logo"
|
aria-hidden="true"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($config?.features?.enable_version_update_check) {
|
||||||
checkForVersionUpdates();
|
checkForVersionUpdates();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -58,6 +60,7 @@
|
||||||
v{WEBUI_VERSION}
|
v{WEBUI_VERSION}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_version_update_check}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -68,6 +71,7 @@
|
||||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||||
: $i18n.t('(latest)')}
|
: $i18n.t('(latest)')}
|
||||||
</a>
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -80,6 +84,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_version_update_check}
|
||||||
<button
|
<button
|
||||||
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -88,6 +93,7 @@
|
||||||
>
|
>
|
||||||
{$i18n.t('Check for updates')}
|
{$i18n.t('Check for updates')}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,10 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
archiveAllChats,
|
archiveAllChats,
|
||||||
createNewChat,
|
|
||||||
deleteAllChats,
|
deleteAllChats,
|
||||||
getAllChats,
|
getAllChats,
|
||||||
getAllUserChats,
|
getChatList,
|
||||||
getChatList
|
importChat
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
@ -58,9 +57,18 @@
|
||||||
console.log(chat);
|
console.log(chat);
|
||||||
|
|
||||||
if (chat.chat) {
|
if (chat.chat) {
|
||||||
await createNewChat(localStorage.token, chat.chat);
|
await importChat(
|
||||||
|
localStorage.token,
|
||||||
|
chat.chat,
|
||||||
|
chat.meta ?? {},
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
chat?.created_at ?? null,
|
||||||
|
chat?.updated_at ?? null
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await createNewChat(localStorage.token, chat);
|
// Legacy format
|
||||||
|
await importChat(localStorage.token, chat, {}, false, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +109,7 @@
|
||||||
const handleArchivedChatsChange = async () => {
|
const handleArchivedChatsChange = async () => {
|
||||||
currentChatPage.set(1);
|
currentChatPage.set(1);
|
||||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||||
|
|
||||||
scrollPaginationEnabled.set(true);
|
scrollPaginationEnabled.set(true);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
|
import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
|
@ -46,16 +48,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -93,29 +86,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
import { updateMemoryById } from '$lib/apis/memories';
|
import { updateMemoryById } from '$lib/apis/memories';
|
||||||
|
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.svelte';
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
|
@ -56,16 +58,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -103,29 +96,7 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="ml-2 self-center">
|
<div class="ml-2 self-center">
|
||||||
<svg
|
<Spinner />
|
||||||
class=" w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><style>
|
|
||||||
.spinner_ajPY {
|
|
||||||
transform-origin: center;
|
|
||||||
animation: spinner_AtaB 0.75s infinite linear;
|
|
||||||
}
|
|
||||||
@keyframes spinner_AtaB {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style><path
|
|
||||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
|
||||||
opacity=".25"
|
|
||||||
/><path
|
|
||||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
|
||||||
class="spinner_ajPY"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Link from '../icons/Link.svelte';
|
import Link from '../icons/Link.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let chatId;
|
export let chatId;
|
||||||
|
|
||||||
|
|
@ -90,16 +91,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -18,16 +19,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,10 @@
|
||||||
|
|
||||||
<div class="h-40 w-full">
|
<div class="h-40 w-full">
|
||||||
{#if filteredPrompts.length > 0}
|
{#if filteredPrompts.length > 0}
|
||||||
<div class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
<div role="list" class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||||
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
|
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
|
||||||
<button
|
<button
|
||||||
|
role="listitem"
|
||||||
class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
|
class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
|
||||||
px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
|
px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
|
||||||
dark:hover:bg-white/5 transition group"
|
dark:hover:bg-white/5 transition group"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
import Link from '../icons/Link.svelte';
|
import Link from '../icons/Link.svelte';
|
||||||
import Collapsible from '../common/Collapsible.svelte';
|
import Collapsible from '../common/Collapsible.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
|
|
@ -30,16 +31,7 @@
|
||||||
show = false;
|
show = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<XMark className={'size-5'} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
|
|
||||||
|
console.log('Banner mounted:', banner);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -82,9 +84,8 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-60 overflow-y-auto">
|
||||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-20 overflow-y-auto">
|
{@html marked.parse(DOMPurify.sanitize((banner?.content ?? '').replace(/\n/g, '<br>')))}
|
||||||
{@html marked.parse(DOMPurify.sanitize(banner.content))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue